From bff403ea04551d5e77c1d4bc1c3e7667068cb6653ecc4f48395d038e00e5c585 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 1 May 2026 15:09:20 +0300 Subject: [PATCH] =?UTF-8?q?CallDeliveryReport:=20=D1=83=D0=BD=D0=B8=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D1=81=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D1=84?= =?UTF-8?q?=D0=BE=D1=80=D0=BC=D0=B0=D1=82=20type/value=20=D0=B8=20=D1=80?= =?UTF-8?q?=D0=B0=D1=81=D1=88=D0=B8=D1=80=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?=D0=BE=D1=82=D1=87=D1=91=D1=82=D1=8B=20=D0=BF=D0=BE=20=D0=B7?= =?UTF-8?q?=D0=B2=D0=BE=D0=BD=D0=BA=D0=B0=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 4 +- shine-UI/js/services/auth-service.js | 23 +-- shine-UI/js/services/call-service.js | 147 ++++++++++++++++-- .../ws_protocol/JSON/JsonHandlerRegistry.java | 8 +- .../Net_CallDeliveryReport_Handler.java | 96 ++++++++++++ .../Net_CallDeliveryReport_Request.java | 13 ++ .../Net_CallDeliveryReport_Response.java | 14 ++ 7 files changed, 281 insertions(+), 24 deletions(-) create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Response.java diff --git a/VERSION.properties b/VERSION.properties index fb1480e..799a7f6 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.21 -server.version=1.2.21 +client.version=1.2.22 +server.version=1.2.22 diff --git a/shine-UI/js/services/auth-service.js b/shine-UI/js/services/auth-service.js index 6c9917c..98e4d85 100644 --- a/shine-UI/js/services/auth-service.js +++ b/shine-UI/js/services/auth-service.js @@ -1171,8 +1171,6 @@ export class AuthService { return this.runWriteLocked(key, async () => { const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd); const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim(); - const userLastGlobalNumber = Number(user?.serverLastGlobalNumber); - const userLastGlobalHash = normalizeHex32(user?.serverLastGlobalHash, ZERO64); const ownerBlockchainName = owner; const lineCode = root; @@ -1182,17 +1180,14 @@ export class AuthService { if (ownerBlockchainName !== blockchainName) { throw new Error('Posting is allowed only to your own channels'); } + // Канал 0 оставляем как технический root-поток. + // Контент-публикации в него временно отключены (пишем только в именованные каналы). + if (lineCode === 0) { + throw new Error('Публикации в канал 0 временно отключены. Создайте отдельный канал.'); + } let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64); - if (lineCode === 0) { - rootHashHex = ( - Number.isFinite(userLastGlobalNumber) && - userLastGlobalNumber === 0 && - userLastGlobalHash !== ZERO64 - ) - ? userLastGlobalHash - : await this.resolveHeaderHashForBlockchain(blockchainName); - } else if (rootHashHex === ZERO64) { + if (rootHashHex === ZERO64) { const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName); const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode); if (!rootChannel) throw new Error('Channel root not found'); @@ -1431,6 +1426,12 @@ export class AuthService { return response.payload || {}; } + async sendCallDeliveryReport(payload = {}) { + const response = await this.ws.request('CallDeliveryReport', payload); + if (response.status !== 200) throw opError('CallDeliveryReport', response); + return response.payload || {}; + } + async listContacts() { const response = await this.ws.request('ListContacts', {}); if (response.status !== 200) throw opError('ListContacts', response); diff --git a/shine-UI/js/services/call-service.js b/shine-UI/js/services/call-service.js index 6691517..b2cad83 100644 --- a/shine-UI/js/services/call-service.js +++ b/shine-UI/js/services/call-service.js @@ -93,12 +93,25 @@ async function resolveIceServers(call) { const turnUrls = uniqueUrls(parseIceUrls(payload?.turnUrls)); const turnUsername = String(payload?.turnUsername || '').trim(); const turnPassword = String(payload?.turnPassword || '').trim(); + const turnServers = Array.isArray(payload?.turnServers) ? payload.turnServers : []; const iceServers = []; if (stunUrls.length > 0) { iceServers.push({ urls: stunUrls.length === 1 ? stunUrls[0] : stunUrls }); } - if (turnUrls.length > 0 && turnUsername && turnPassword) { + if (turnServers.length > 0) { + turnServers.forEach((item) => { + const urls = uniqueUrls(parseIceUrls(item?.urls)); + const username = String(item?.username || '').trim(); + const password = String(item?.password || '').trim(); + if (urls.length === 0 || !username || !password) return; + iceServers.push({ + urls: urls.length === 1 ? urls[0] : urls, + username, + credential: password, + }); + }); + } else if (turnUrls.length > 0 && turnUsername && turnPassword) { iceServers.push({ urls: turnUrls.length === 1 ? turnUrls[0] : turnUrls, username: turnUsername, @@ -110,7 +123,7 @@ async function resolveIceServers(call) { await emitDebug(call, 'warn', 'call_ice_empty_from_server', 'using_default_stun'); return cloneDefaultIceServers(); } - await emitDebug(call, 'info', 'call_ice_loaded_from_server', `stun=${stunUrls.length}; turn=${turnUrls.length}`); + await emitDebug(call, 'info', 'call_ice_loaded_from_server', `stun=${stunUrls.length}; turnEntries=${Math.max(0, iceServers.length - (stunUrls.length > 0 ? 1 : 0))}`); return iceServers; } catch (error) { await emitDebug(call, 'warn', 'call_ice_load_failed', toErrorText(error)); @@ -236,6 +249,59 @@ function setActiveStatus(call) { setStatus(call, buildActiveStatusText(call), 'active'); } +function toIsoTs(ts) { + const n = Number(ts || 0); + if (!Number.isFinite(n) || n <= 0) return ''; + try { return new Date(n).toISOString(); } catch { return ''; } +} + +function buildCallFactsJson(call, extra = {}) { + const pc = call?.pc || null; + const facts = { + callId: call?.callId || '', + peerLogin: call?.peerLogin || '', + remoteSessionId: call?.remoteSessionId || '', + direction: call?.direction || '', + phase: call?.phase || '', + statusText: call?.statusText || '', + startedAtMs: Number(call?.startedAtMs || 0), + startedAtIso: toIsoTs(call?.startedAtMs), + connectedAtMs: Number(call?.connectedAtMs || 0), + connectedAtIso: toIsoTs(call?.connectedAtMs), + routeLabel: call?.connectionRouteLabel || '', + routeDetails: call?.connectionRouteDetails || '', + pcConnectionState: pc?.connectionState || '', + pcIceConnectionState: pc?.iceConnectionState || '', + pcSignalingState: pc?.signalingState || '', + hasLocalStream: Boolean(call?.localStream), + localAudioTracksCount: call?.localStream?.getAudioTracks?.()?.length || 0, + ...extra, + }; + try { + return JSON.stringify(facts); + } catch { + return JSON.stringify({ callId: call?.callId || '', serializeError: true }); + } +} + +async function sendCallDeliveryReport(call, eventType, eventCode, reason = '', extraFacts = {}) { + if (!call || !authService || typeof authService.sendCallDeliveryReport !== 'function') return; + try { + const valueJson = buildCallFactsJson(call, { + eventType: String(eventType || '').trim(), + eventCode: String(eventCode || '').trim(), + reason: String(reason || '').trim(), + reportedAtMs: nowMs(), + reportedAtIso: toIsoTs(nowMs()), + ...extraFacts, + }); + await authService.sendCallDeliveryReport({ + type: String(eventType || '').trim(), + value: valueJson, + }); + } catch {} +} + function cleanupTimers(call) { if (call.timers?.ack10s) clearTimeout(call.timers.ack10s); if (call.timers?.total35s) clearTimeout(call.timers.total35s); @@ -251,6 +317,7 @@ async function closeMedia(call) { call.localStream = null; call.audioSenders = []; call.connectionRouteLabel = ''; + call.connectionRouteDetails = ''; } function stopReconnectFlow(call) { @@ -269,7 +336,9 @@ function stopReconnectFlow(call) { async function detectConnectionRoute(call) { const pc = call?.pc; - if (!pc || typeof pc.getStats !== 'function') return { label: '', details: '' }; + if (!pc || typeof pc.getStats !== 'function') { + return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' }; + } try { const stats = await pc.getStats(); let selectedPair = null; @@ -298,7 +367,7 @@ async function detectConnectionRoute(call) { }); } - if (!selectedPair) return { label: '', details: '' }; + if (!selectedPair) return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' }; const local = selectedPair.localCandidateId && typeof stats.get === 'function' ? stats.get(selectedPair.localCandidateId) @@ -309,17 +378,39 @@ async function detectConnectionRoute(call) { const localType = String(local?.candidateType || '').trim().toLowerCase(); const remoteType = String(remote?.candidateType || '').trim().toLowerCase(); - const details = `local=${localType || '-'}; remote=${remoteType || '-'}`; + const localIp = String(local?.ip || local?.address || ''); + const remoteIp = String(remote?.ip || remote?.address || ''); + const localPort = String(local?.port || ''); + const remotePort = String(remote?.port || ''); + const relayProto = String(local?.relayProtocol || remote?.relayProtocol || ''); + const turnCandidateAddress = localType === 'relay' + ? `${localIp}${localPort ? `:${localPort}` : ''}` + : (remoteType === 'relay' ? `${remoteIp}${remotePort ? `:${remotePort}` : ''}` : ''); + const details = `local=${localType || '-'}(${localIp || '-'}${localPort ? `:${localPort}` : ''}); remote=${remoteType || '-'}(${remoteIp || '-'}${remotePort ? `:${remotePort}` : ''})`; if (localType === 'relay' || remoteType === 'relay') { - return { label: 'через TURN', details }; + const label = turnCandidateAddress + ? `через TURN (${turnCandidateAddress})` + : (relayProto ? `через TURN (${relayProto})` : 'через TURN'); + return { label, details, localIp, remoteIp, turnCandidateAddress }; } if (localType || remoteType) { - return { label: 'прямое', details }; + const sameLan = localIp && remoteIp && ( + (localIp.startsWith('10.') && remoteIp.startsWith('10.')) + || (localIp.startsWith('192.168.') && remoteIp.startsWith('192.168.')) + || (localIp.startsWith('172.16.') && remoteIp.startsWith('172.16.')) + ); + return { + label: sameLan ? 'напрямую в локальной сети' : 'напрямую через интернет', + details, + localIp, + remoteIp, + turnCandidateAddress: '', + }; } - return { label: '', details }; + return { label: '', details, localIp, remoteIp, turnCandidateAddress: '' }; } catch { - return { label: '', details: '' }; + return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' }; } } @@ -336,6 +427,7 @@ function startTransportProbe(call) { const route = await detectConnectionRoute(call); if (!route.label || route.label === call.connectionRouteLabel) return; call.connectionRouteLabel = route.label; + call.connectionRouteDetails = route.details || ''; setActiveStatus(call); await emitDebug(call, 'info', 'peer_connection_route', route.details || route.label); }; @@ -475,6 +567,18 @@ async function finalizeCall(call, { await emitDebug(call, 'info', 'call_finalize', `${localReasonCode}:${debugReason}`); } + const reasonText = debugReason || localReasonCode; + if (String(localReasonCode || '') !== 'completed') { + if (call.direction === 'out') { + await sendCallDeliveryReport(call, 'outgoing_failed', `outgoing_${localReasonCode}`, reasonText); + } else if (call.direction === 'in') { + await sendCallDeliveryReport(call, 'incoming_failed', `incoming_${localReasonCode}`, reasonText); + } + if (String(localReasonCode || '') === 'error') { + await sendCallDeliveryReport(call, 'unknown_error', 'call_unknown_error', reasonText); + } + } + pushCallSummary(call, localReasonCode); call.phase = 'ended'; @@ -565,6 +669,30 @@ async function ensurePeerConnection(call) { setActiveStatus(call); startTransportProbe(call); void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`); + if (call.direction === 'out' && !call.connectionSuccessReported) { + call.connectionSuccessReported = true; + void (async () => { + const route = await detectConnectionRoute(call); + if (route?.label) { + call.connectionRouteLabel = route.label; + } + call.connectionRouteDetails = route?.details || ''; + await sendCallDeliveryReport( + call, + 'call_connected', + 'call_connected_success', + `connected:${route?.label || 'unknown_route'}`, + { + reportBy: 'initiator', + routeLabel: route?.label || '', + routeDetails: route?.details || '', + localIp: route?.localIp || '', + remoteIp: route?.remoteIp || '', + turnCandidateAddress: route?.turnCandidateAddress || '', + }, + ); + })(); + } return; } if (state === 'failed') { @@ -599,6 +727,7 @@ async function ensurePeerConnection(call) { call.localStream = stream; call.audioSenders = []; call.connectionRouteLabel = ''; + call.connectionRouteDetails = ''; stream.getTracks().forEach((track) => { track.enabled = track.kind === 'audio' ? !call.muted : true; const sender = pc.addTrack(track, stream); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index e65138c..dff4723 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -84,7 +84,9 @@ import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_GetCallIceConfig_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_ClientDebugLog_Handler; +import server.logic.ws_protocol.JSON.handlers.system.Net_CallDeliveryReport_Handler; import server.logic.ws_protocol.JSON.handlers.system.Net_Ping_Handler; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_CallDeliveryReport_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientErrorLog_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_Request; import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_GetCallIceConfig_Request; @@ -152,7 +154,8 @@ public final class JsonHandlerRegistry { Map.entry("GetServerInfo", new Net_GetServerInfo_Handler()), Map.entry("GetCallIceConfig", new Net_GetCallIceConfig_Handler()), Map.entry("ClientErrorLog", new Net_ClientErrorLog_Handler()), - Map.entry("ClientDebugLog", new Net_ClientDebugLog_Handler()) + Map.entry("ClientDebugLog", new Net_ClientDebugLog_Handler()), + Map.entry("CallDeliveryReport", new Net_CallDeliveryReport_Handler()) // --- subscriptions --- // Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler()) @@ -207,7 +210,8 @@ public final class JsonHandlerRegistry { Map.entry("GetServerInfo", Net_GetServerInfo_Request.class), Map.entry("GetCallIceConfig", Net_GetCallIceConfig_Request.class), Map.entry("ClientErrorLog", Net_ClientErrorLog_Request.class), - Map.entry("ClientDebugLog", Net_ClientDebugLog_Request.class) + Map.entry("ClientDebugLog", Net_ClientDebugLog_Request.class), + Map.entry("CallDeliveryReport", Net_CallDeliveryReport_Request.class) ); private JsonHandlerRegistry() { } diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java new file mode 100644 index 0000000..5fa4fb7 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/Net_CallDeliveryReport_Handler.java @@ -0,0 +1,96 @@ +package server.logic.ws_protocol.JSON.handlers.system; + +import org.eclipse.jetty.websocket.api.Session; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_CallDeliveryReport_Request; +import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_CallDeliveryReport_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.Instant; +import java.util.Locale; +import java.util.concurrent.locks.ReentrantLock; + +public class Net_CallDeliveryReport_Handler implements JsonMessageHandler { + private static final ReentrantLock FILE_LOCK = new ReentrantLock(true); + private static final Path LOG_FILE = Path.of("logs", "call-delivery-events.log"); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_CallDeliveryReport_Request req = (Net_CallDeliveryReport_Request) baseRequest; + if (safe(req.getType()).isEmpty()) { + return NetExceptionResponseFactory.error( + req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "Поле type обязательно" + ); + } + + long serverTs = System.currentTimeMillis(); + String line = String.format( + Locale.ROOT, + "%s | type=%s | login=%s | remote=%s | value=%s%n", + Instant.ofEpochMilli(serverTs), + clip(req.getType(), 80), + clip(ctx != null ? ctx.getLogin() : "", 80), + clip(remoteAddress(ctx), 200), + clip(req.getValue(), 8000) + ); + + appendLine(line); + + Net_CallDeliveryReport_Response resp = new Net_CallDeliveryReport_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + resp.setAccepted(true); + resp.setServerTs(serverTs); + return resp; + } + + private static void appendLine(String line) { + FILE_LOCK.lock(); + try { + Path parent = LOG_FILE.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.writeString( + LOG_FILE, + line, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND + ); + } catch (IOException e) { + throw new IllegalStateException("Не удалось записать call-delivery-report", e); + } finally { + FILE_LOCK.unlock(); + } + } + + private static String remoteAddress(ConnectionContext ctx) { + if (ctx == null) return ""; + Session ws = ctx.getWsSession(); + if (ws == null) return ""; + SocketAddress remote = ws.getRemoteAddress(); + return remote != null ? remote.toString() : ""; + } + + private static String safe(String value) { + return value == null ? "" : value.trim(); + } + + private static String clip(String value, int maxLen) { + String cleaned = safe(value).replace('\n', ' ').replace('\r', ' '); + if (cleaned.length() <= maxLen) return cleaned; + return cleaned.substring(0, Math.max(0, maxLen - 3)) + "..."; + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Request.java new file mode 100644 index 0000000..1a1f6b3 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Request.java @@ -0,0 +1,13 @@ +package server.logic.ws_protocol.JSON.handlers.system.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +public class Net_CallDeliveryReport_Request extends Net_Request { + private String type; + private String value; + + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Response.java new file mode 100644 index 0000000..ee87fad --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/system/entyties/Net_CallDeliveryReport_Response.java @@ -0,0 +1,14 @@ +package server.logic.ws_protocol.JSON.handlers.system.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +public class Net_CallDeliveryReport_Response extends Net_Response { + private long serverTs; + private boolean accepted; + + public long getServerTs() { return serverTs; } + public void setServerTs(long serverTs) { this.serverTs = serverTs; } + public boolean isAccepted() { return accepted; } + public void setAccepted(boolean accepted) { this.accepted = accepted; } +} +