Исправить маршрутизацию call push по sessionId

This commit is contained in:
AidarKC 2026-06-19 19:18:16 +04:00
parent 47574100f9
commit cc074a941f
7 changed files with 56 additions and 5 deletions

View File

@ -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

View File

@ -89,6 +89,7 @@ public class Net_CallInviteBroadcast_Handler implements JsonMessageHandler {
+ ",\"text\":\"Вам звонит " + jsonEscape(from) + "\"" + ",\"text\":\"Вам звонит " + jsonEscape(from) + "\""
+ ",\"fromLogin\":\"" + jsonEscape(from) + "\"" + ",\"fromLogin\":\"" + jsonEscape(from) + "\""
+ ",\"fromSessionId\":\"" + jsonEscape(ctx.getSessionId()) + "\"" + ",\"fromSessionId\":\"" + jsonEscape(ctx.getSessionId()) + "\""
+ ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\""
+ ",\"toLogin\":\"" + jsonEscape(to) + "\"" + ",\"toLogin\":\"" + jsonEscape(to) + "\""
+ ",\"callId\":\"" + jsonEscape(callId) + "\"" + ",\"callId\":\"" + jsonEscape(callId) + "\""
+ ",\"sentAtMs\":" + timeMs + ",\"sentAtMs\":" + timeMs

View File

@ -164,6 +164,7 @@ public class Net_CallSignalToSession_Handler implements JsonMessageHandler {
+ ",\"reason\":\"" + jsonEscape(reason) + "\"" + ",\"reason\":\"" + jsonEscape(reason) + "\""
+ ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\"" + ",\"fromLogin\":\"" + jsonEscape(fromLogin) + "\""
+ ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\"" + ",\"fromSessionId\":\"" + jsonEscape(fromSessionId) + "\""
+ ",\"targetSessionId\":\"" + jsonEscape(sessionId) + "\""
+ ",\"toLogin\":\"" + jsonEscape(targetLogin) + "\"" + ",\"toLogin\":\"" + jsonEscape(targetLogin) + "\""
+ ",\"sentAtMs\":" + sentAtMs + ",\"sentAtMs\":" + sentAtMs
+ "}"; + "}";

View File

@ -1,2 +1,2 @@
client.version=1.2.217 client.version=1.2.218
server.version=1.2.205 server.version=1.2.206

View File

@ -100,6 +100,7 @@ self.addEventListener('push', (event) => {
const json = decodePushJson(rawText); const json = decodePushJson(rawText);
const callId = String(json.callId || '').trim(); const callId = String(json.callId || '').trim();
const fromSessionId = String(json.fromSessionId || '').trim(); const fromSessionId = String(json.fromSessionId || '').trim();
const targetSessionId = String(json.targetSessionId || '').trim();
const toLogin = String(json.toLogin || '').trim(); const toLogin = String(json.toLogin || '').trim();
const reason = String(json.reason || '').trim(); const reason = String(json.reason || '').trim();
const sentAtMs = Number(json.sentAtMs || 0); const sentAtMs = Number(json.sentAtMs || 0);
@ -139,6 +140,7 @@ self.addEventListener('push', (event) => {
callId, callId,
fromLogin, fromLogin,
fromSessionId, fromSessionId,
targetSessionId,
toLogin, toLogin,
sentAtMs, sentAtMs,
expiresAtMs, expiresAtMs,
@ -165,6 +167,7 @@ self.addEventListener('push', (event) => {
body, body,
fromLogin, fromLogin,
fromSessionId, fromSessionId,
targetSessionId,
toLogin, toLogin,
callId, callId,
sentAtMs, sentAtMs,
@ -186,6 +189,7 @@ self.addEventListener('notificationclick', (event) => {
callId: String(data.callId || '').trim(), callId: String(data.callId || '').trim(),
fromLogin: String(data.fromLogin || '').trim(), fromLogin: String(data.fromLogin || '').trim(),
fromSessionId: String(data.fromSessionId || '').trim(), fromSessionId: String(data.fromSessionId || '').trim(),
targetSessionId: String(data.targetSessionId || '').trim(),
toLogin: String(data.toLogin || '').trim(), toLogin: String(data.toLogin || '').trim(),
sentAtMs: Number(data.sentAtMs || 0), sentAtMs: Number(data.sentAtMs || 0),
expiresAtMs: Number(data.expiresAtMs || 0), expiresAtMs: Number(data.expiresAtMs || 0),

View File

@ -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() { function loadPendingCallPushAction() {
try { try {
const raw = localStorage.getItem(CALL_PUSH_PENDING_ACTION_KEY); const raw = localStorage.getItem(CALL_PUSH_PENDING_ACTION_KEY);
@ -322,6 +329,7 @@ async function processPendingCallPushActionIfPossible() {
if (!state.session.isAuthorized) return; if (!state.session.isAuthorized) return;
const pending = loadPendingCallPushAction(); const pending = loadPendingCallPushAction();
if (!pending) return; if (!pending) return;
if (!isCallPushTargetForCurrentSession(pending.payload || {})) return;
clearPendingCallPushAction(); clearPendingCallPushAction();
try { try {
await handleCallPushAction(pending.action, pending.payload || {}); await handleCallPushAction(pending.action, pending.payload || {});
@ -827,6 +835,7 @@ async function init() {
const action = String(data.action || '').trim().toLowerCase(); const action = String(data.action || '').trim().toLowerCase();
const payload = data.payload || {}; const payload = data.payload || {};
if (action === 'accept' || action === 'decline') { if (action === 'accept' || action === 'decline') {
if (!isCallPushTargetForCurrentSession(payload)) return;
savePendingCallPushAction(action, payload); savePendingCallPushAction(action, payload);
void processPendingCallPushActionIfPossible(); void processPendingCallPushActionIfPossible();
} }
@ -835,6 +844,7 @@ async function init() {
if (data.type !== 'SHINE_WEB_PUSH_EVENT') return; if (data.type !== 'SHINE_WEB_PUSH_EVENT') return;
const payload = data.payload || {}; const payload = data.payload || {};
if (!isCallPushTargetForCurrentSession(payload)) return;
const kind = String(payload.kind || '').trim(); const kind = String(payload.kind || '').trim();
const now = Date.now(); const now = Date.now();
try { try {

View File

@ -831,6 +831,9 @@ async function finalizeCall(call, {
localReasonCode = 'error', localReasonCode = 'error',
debugReason = '', debugReason = '',
notifyRemoteHangup = false, notifyRemoteHangup = false,
suppressRemoteSignal = false,
suppressReports = false,
suppressSummary = false,
} = {}) { } = {}) {
if (!call) return; if (!call) return;
const diagnosticsBeforeClose = getCallDiagnosticsContext(call); const diagnosticsBeforeClose = getCallDiagnosticsContext(call);
@ -839,7 +842,8 @@ async function finalizeCall(call, {
stopTone(); stopTone();
const shouldNotifyRemoteFailure = const shouldNotifyRemoteFailure =
!notifyRemoteHangup !suppressRemoteSignal
&& !notifyRemoteHangup
&& Boolean(call.remoteSessionId) && Boolean(call.remoteSessionId)
&& String(localReasonCode || '') !== 'completed' && String(localReasonCode || '') !== 'completed'
&& String(debugReason || '') !== 'remote_hangup'; && String(debugReason || '') !== 'remote_hangup';
@ -868,7 +872,7 @@ async function finalizeCall(call, {
} }
const reasonText = debugReason || localReasonCode; const reasonText = debugReason || localReasonCode;
if (String(localReasonCode || '') !== 'completed') { if (!suppressReports && String(localReasonCode || '') !== 'completed') {
const failureStage = call.phase || ''; const failureStage = call.phase || '';
const failureContext = { const failureContext = {
failureStage, failureStage,
@ -890,7 +894,9 @@ async function finalizeCall(call, {
} }
} }
if (!suppressSummary) {
pushCallSummary(call, localReasonCode); pushCallSummary(call, localReasonCode);
}
call.phase = 'ended'; call.phase = 'ended';
call.statusText = 'Звонок завершён'; call.statusText = 'Звонок завершён';
@ -1128,6 +1134,13 @@ function isIncomingCallPushFresh(payload) {
return true; 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' } = {}) { async function handleIncomingInvitePayload(payload, { source = 'ws' } = {}) {
const callId = String(payload?.callId || '').trim(); const callId = String(payload?.callId || '').trim();
const fromLogin = String(payload?.fromLogin || '').trim(); const fromLogin = String(payload?.fromLogin || '').trim();
@ -1627,11 +1640,13 @@ export async function hangupActiveCall() {
} }
export async function handleIncomingCallPush(payload = {}) { export async function handleIncomingCallPush(payload = {}) {
if (!isCallPushForCurrentSession(payload)) return;
if (!isIncomingCallPushFresh(payload)) return; if (!isIncomingCallPushFresh(payload)) return;
await handleIncomingInvitePayload(payload, { source: 'push' }); await handleIncomingInvitePayload(payload, { source: 'push' });
} }
export async function handleStopCallPush(payload = {}) { export async function handleStopCallPush(payload = {}) {
if (!isCallPushForCurrentSession(payload)) return;
const callId = String(payload?.callId || '').trim(); const callId = String(payload?.callId || '').trim();
if (!callId) return; if (!callId) return;
const call = getCall(callId); const call = getCall(callId);
@ -1646,10 +1661,14 @@ export async function handleStopCallPush(payload = {}) {
await finalizeCall(call, { await finalizeCall(call, {
localReasonCode: call.connectedAtMs ? 'completed' : 'no_answer', localReasonCode: call.connectedAtMs ? 'completed' : 'no_answer',
debugReason: `stop_call_push:${reason}`, debugReason: `stop_call_push:${reason}`,
suppressRemoteSignal: true,
suppressReports: true,
suppressSummary: true,
}); });
} }
export async function handleCallPushAction(action, payload = {}) { export async function handleCallPushAction(action, payload = {}) {
if (!isCallPushForCurrentSession(payload)) return;
const normalized = String(action || '').trim().toLowerCase(); const normalized = String(action || '').trim().toLowerCase();
if (normalized !== 'accept' && normalized !== 'decline') return; if (normalized !== 'accept' && normalized !== 'decline') return;
if (!isIncomingCallPushFresh(payload)) return; if (!isIncomingCallPushFresh(payload)) return;