diff --git a/Dev_Docs/Pending_Features/2026-06-19_1905_call-push-target-session-fix.md b/Dev_Docs/Pending_Features/2026-06-19_1905_call-push-target-session-fix.md new file mode 100644 index 0000000..05a2f13 --- /dev/null +++ b/Dev_Docs/Pending_Features/2026-06-19_1905_call-push-target-session-fix.md @@ -0,0 +1,16 @@ +# Фикс привязки call push к целевой sessionId + +- краткое описание: + - push-события `incoming_call` и `stop_call` теперь помечаются целевой `sessionId`; + - UI и service worker обрабатывают call push только для своей целевой сессии; + - `stop_call` для лишних сессий закрывает локальный экран тихо, без обратных сигналов и без лишних тех-сообщений. +- что проверять: + - держать несколько сессий одного пользователя в одном браузере/на одном origin; + - позвонить этому пользователю и убедиться, что входящий экран закрывается корректно только на целевых сессиях; + - после `ACCEPT` одной сессии остальные должны тихо убрать экран вызова и не ломать выбранную пару; + - после отмены входящей сессией исходящая сессия должна централизованно завершить сценарий. +- ожидаемый результат: + - push одного session endpoint больше не влияет на чужие сессии этого же origin; + - исчезают ложные `stop_call_push:accepted_on_other_device` и `terminal_call_signal_150` на неправильных сессиях. +- статус: + - pending diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java index d478ad3..a1023a2 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallInviteBroadcast_Handler.java @@ -89,6 +89,7 @@ public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler { + ",\"text\":\"Вам звонит " + jsonEscape(from) + "\"" + ",\"fromLogin\":\"" + jsonEscape(from) + "\"" + ",\"fromSessionId\":\"" + jsonEscape(ctx.getSessionId()) + "\"" + + ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\"" + ",\"toLogin\":\"" + jsonEscape(to) + "\"" + ",\"callId\":\"" + jsonEscape(callId) + "\"" + ",\"sentAtMs\":" + timeMs diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallSignalToSession_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallSignalToSession_Handler.java index 036d819..d065f7e 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallSignalToSession_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/Net_CallSignalToSession_Handler.java @@ -164,6 +164,7 @@ public class Net_CallSignalToSession_Handler implements JsonMessageHandler { + ",\"reason\":\"" + jsonEscape(reason) + "\"" + ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\"" + ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\"" + + ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\"" + ",\"toLogin\":\"" + jsonEscape(targetLogin) + "\"" + ",\"sentAtMs\":" + sentAtMs + "}"; diff --git a/VERSION.properties b/VERSION.properties index ee18127..ca65ab9 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.217 -server.version=1.2.205 +client.version=1.2.218 +server.version=1.2.206 diff --git a/shine-UI/firebase-messaging-sw.js b/shine-UI/firebase-messaging-sw.js index 57759af..a07c946 100644 --- a/shine-UI/firebase-messaging-sw.js +++ b/shine-UI/firebase-messaging-sw.js @@ -100,6 +100,7 @@ self.addEventListener('push', (event) => { const json = decodePushJson(rawText); const callId = String(json.callId || '').trim(); const fromSessionId = String(json.fromSessionId || '').trim(); + const targetSessionId = String(json.targetSessionId || '').trim(); const toLogin = String(json.toLogin || '').trim(); const reason = String(json.reason || '').trim(); const sentAtMs = Number(json.sentAtMs || 0); @@ -139,6 +140,7 @@ self.addEventListener('push', (event) => { callId, fromLogin, fromSessionId, + targetSessionId, toLogin, sentAtMs, expiresAtMs, @@ -165,6 +167,7 @@ self.addEventListener('push', (event) => { body, fromLogin, fromSessionId, + targetSessionId, toLogin, callId, sentAtMs, @@ -186,6 +189,7 @@ self.addEventListener('notificationclick', (event) => { callId: String(data.callId || '').trim(), fromLogin: String(data.fromLogin || '').trim(), fromSessionId: String(data.fromSessionId || '').trim(), + targetSessionId: String(data.targetSessionId || '').trim(), toLogin: String(data.toLogin || '').trim(), sentAtMs: Number(data.sentAtMs || 0), expiresAtMs: Number(data.expiresAtMs || 0), diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 31aaea4..8e851ee 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -269,6 +269,13 @@ function savePendingCallPushAction(action, payload = {}) { } } +function isCallPushTargetForCurrentSession(payload = {}) { + const targetSessionId = String(payload?.targetSessionId || '').trim(); + if (!targetSessionId) return true; + const currentSessionId = String(state?.session?.sessionId || '').trim(); + return Boolean(currentSessionId) && currentSessionId === targetSessionId; +} + function loadPendingCallPushAction() { try { const raw = localStorage.getItem(CALL_PUSH_PENDING_ACTION_KEY); @@ -322,6 +329,7 @@ async function processPendingCallPushActionIfPossible() { if (!state.session.isAuthorized) return; const pending = loadPendingCallPushAction(); if (!pending) return; + if (!isCallPushTargetForCurrentSession(pending.payload || {})) return; clearPendingCallPushAction(); try { await handleCallPushAction(pending.action, pending.payload || {}); @@ -827,6 +835,7 @@ async function init() { const action = String(data.action || '').trim().toLowerCase(); const payload = data.payload || {}; if (action === 'accept' || action === 'decline') { + if (!isCallPushTargetForCurrentSession(payload)) return; savePendingCallPushAction(action, payload); void processPendingCallPushActionIfPossible(); } @@ -835,6 +844,7 @@ async function init() { if (data.type !== 'SHINE_WEB_PUSH_EVENT') return; const payload = data.payload || {}; + if (!isCallPushTargetForCurrentSession(payload)) return; const kind = String(payload.kind || '').trim(); const now = Date.now(); try { diff --git a/shine-UI/js/services/call-service.js b/shine-UI/js/services/call-service.js index 7534b7e..3303f96 100644 --- a/shine-UI/js/services/call-service.js +++ b/shine-UI/js/services/call-service.js @@ -831,6 +831,9 @@ async function finalizeCall(call, { localReasonCode = 'error', debugReason = '', notifyRemoteHangup = false, + suppressRemoteSignal = false, + suppressReports = false, + suppressSummary = false, } = {}) { if (!call) return; const diagnosticsBeforeClose = getCallDiagnosticsContext(call); @@ -839,7 +842,8 @@ async function finalizeCall(call, { stopTone(); const shouldNotifyRemoteFailure = - !notifyRemoteHangup + !suppressRemoteSignal + && !notifyRemoteHangup && Boolean(call.remoteSessionId) && String(localReasonCode || '') !== 'completed' && String(debugReason || '') !== 'remote_hangup'; @@ -868,7 +872,7 @@ async function finalizeCall(call, { } const reasonText = debugReason || localReasonCode; - if (String(localReasonCode || '') !== 'completed') { + if (!suppressReports && String(localReasonCode || '') !== 'completed') { const failureStage = call.phase || ''; const failureContext = { failureStage, @@ -890,7 +894,9 @@ async function finalizeCall(call, { } } - pushCallSummary(call, localReasonCode); + if (!suppressSummary) { + pushCallSummary(call, localReasonCode); + } call.phase = 'ended'; call.statusText = 'Звонок завершён'; @@ -1128,6 +1134,13 @@ function isIncomingCallPushFresh(payload) { return true; } +function isCallPushForCurrentSession(payload = {}) { + const targetSessionId = String(payload?.targetSessionId || '').trim(); + if (!targetSessionId) return true; + const currentSessionId = String(state?.session?.sessionId || '').trim(); + return Boolean(currentSessionId) && currentSessionId === targetSessionId; +} + async function handleIncomingInvitePayload(payload, { source = 'ws' } = {}) { const callId = String(payload?.callId || '').trim(); const fromLogin = String(payload?.fromLogin || '').trim(); @@ -1627,11 +1640,13 @@ export async function hangupActiveCall() { } export async function handleIncomingCallPush(payload = {}) { + if (!isCallPushForCurrentSession(payload)) return; if (!isIncomingCallPushFresh(payload)) return; await handleIncomingInvitePayload(payload, { source: 'push' }); } export async function handleStopCallPush(payload = {}) { + if (!isCallPushForCurrentSession(payload)) return; const callId = String(payload?.callId || '').trim(); if (!callId) return; const call = getCall(callId); @@ -1646,10 +1661,14 @@ export async function handleStopCallPush(payload = {}) { await finalizeCall(call, { localReasonCode: call.connectedAtMs ? 'completed' : 'no_answer', debugReason: `stop_call_push:${reason}`, + suppressRemoteSignal: true, + suppressReports: true, + suppressSummary: true, }); } export async function handleCallPushAction(action, payload = {}) { + if (!isCallPushForCurrentSession(payload)) return; const normalized = String(action || '').trim().toLowerCase(); if (normalized !== 'accept' && normalized !== 'decline') return; if (!isIncomingCallPushFresh(payload)) return;