CallDeliveryReport: универсальный формат type/value и расширенные отчёты по звонкам
This commit is contained in:
parent
3061bf3d1e
commit
bff403ea04
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.21
|
client.version=1.2.22
|
||||||
server.version=1.2.21
|
server.version=1.2.22
|
||||||
|
|||||||
@ -1171,8 +1171,6 @@ export class AuthService {
|
|||||||
return this.runWriteLocked(key, async () => {
|
return this.runWriteLocked(key, async () => {
|
||||||
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
const user = await this.ensureChainInitializedForLineOps(cleanLogin, storagePwd);
|
||||||
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
const blockchainName = String(user?.blockchainName || `${cleanLogin}-${BCH_SUFFIX}`).trim();
|
||||||
const userLastGlobalNumber = Number(user?.serverLastGlobalNumber);
|
|
||||||
const userLastGlobalHash = normalizeHex32(user?.serverLastGlobalHash, ZERO64);
|
|
||||||
|
|
||||||
const ownerBlockchainName = owner;
|
const ownerBlockchainName = owner;
|
||||||
const lineCode = root;
|
const lineCode = root;
|
||||||
@ -1182,17 +1180,14 @@ export class AuthService {
|
|||||||
if (ownerBlockchainName !== blockchainName) {
|
if (ownerBlockchainName !== blockchainName) {
|
||||||
throw new Error('Posting is allowed only to your own channels');
|
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);
|
let rootHashHex = normalizeHex32(selector?.channelRootBlockHash, ZERO64);
|
||||||
if (lineCode === 0) {
|
if (rootHashHex === ZERO64) {
|
||||||
rootHashHex = (
|
|
||||||
Number.isFinite(userLastGlobalNumber) &&
|
|
||||||
userLastGlobalNumber === 0 &&
|
|
||||||
userLastGlobalHash !== ZERO64
|
|
||||||
)
|
|
||||||
? userLastGlobalHash
|
|
||||||
: await this.resolveHeaderHashForBlockchain(blockchainName);
|
|
||||||
} else if (rootHashHex === ZERO64) {
|
|
||||||
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
const ownChannels = await this.listOwnChannelsForBlockchain(cleanLogin, blockchainName);
|
||||||
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
|
const rootChannel = ownChannels.find((item) => item.rootBlockNumber === lineCode);
|
||||||
if (!rootChannel) throw new Error('Channel root not found');
|
if (!rootChannel) throw new Error('Channel root not found');
|
||||||
@ -1431,6 +1426,12 @@ export class AuthService {
|
|||||||
return response.payload || {};
|
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() {
|
async listContacts() {
|
||||||
const response = await this.ws.request('ListContacts', {});
|
const response = await this.ws.request('ListContacts', {});
|
||||||
if (response.status !== 200) throw opError('ListContacts', response);
|
if (response.status !== 200) throw opError('ListContacts', response);
|
||||||
|
|||||||
@ -93,12 +93,25 @@ async function resolveIceServers(call) {
|
|||||||
const turnUrls = uniqueUrls(parseIceUrls(payload?.turnUrls));
|
const turnUrls = uniqueUrls(parseIceUrls(payload?.turnUrls));
|
||||||
const turnUsername = String(payload?.turnUsername || '').trim();
|
const turnUsername = String(payload?.turnUsername || '').trim();
|
||||||
const turnPassword = String(payload?.turnPassword || '').trim();
|
const turnPassword = String(payload?.turnPassword || '').trim();
|
||||||
|
const turnServers = Array.isArray(payload?.turnServers) ? payload.turnServers : [];
|
||||||
|
|
||||||
const iceServers = [];
|
const iceServers = [];
|
||||||
if (stunUrls.length > 0) {
|
if (stunUrls.length > 0) {
|
||||||
iceServers.push({ urls: stunUrls.length === 1 ? stunUrls[0] : stunUrls });
|
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({
|
iceServers.push({
|
||||||
urls: turnUrls.length === 1 ? turnUrls[0] : turnUrls,
|
urls: turnUrls.length === 1 ? turnUrls[0] : turnUrls,
|
||||||
username: turnUsername,
|
username: turnUsername,
|
||||||
@ -110,7 +123,7 @@ async function resolveIceServers(call) {
|
|||||||
await emitDebug(call, 'warn', 'call_ice_empty_from_server', 'using_default_stun');
|
await emitDebug(call, 'warn', 'call_ice_empty_from_server', 'using_default_stun');
|
||||||
return cloneDefaultIceServers();
|
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;
|
return iceServers;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await emitDebug(call, 'warn', 'call_ice_load_failed', toErrorText(error));
|
await emitDebug(call, 'warn', 'call_ice_load_failed', toErrorText(error));
|
||||||
@ -236,6 +249,59 @@ function setActiveStatus(call) {
|
|||||||
setStatus(call, buildActiveStatusText(call), 'active');
|
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) {
|
function cleanupTimers(call) {
|
||||||
if (call.timers?.ack10s) clearTimeout(call.timers.ack10s);
|
if (call.timers?.ack10s) clearTimeout(call.timers.ack10s);
|
||||||
if (call.timers?.total35s) clearTimeout(call.timers.total35s);
|
if (call.timers?.total35s) clearTimeout(call.timers.total35s);
|
||||||
@ -251,6 +317,7 @@ async function closeMedia(call) {
|
|||||||
call.localStream = null;
|
call.localStream = null;
|
||||||
call.audioSenders = [];
|
call.audioSenders = [];
|
||||||
call.connectionRouteLabel = '';
|
call.connectionRouteLabel = '';
|
||||||
|
call.connectionRouteDetails = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopReconnectFlow(call) {
|
function stopReconnectFlow(call) {
|
||||||
@ -269,7 +336,9 @@ function stopReconnectFlow(call) {
|
|||||||
|
|
||||||
async function detectConnectionRoute(call) {
|
async function detectConnectionRoute(call) {
|
||||||
const pc = call?.pc;
|
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 {
|
try {
|
||||||
const stats = await pc.getStats();
|
const stats = await pc.getStats();
|
||||||
let selectedPair = null;
|
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'
|
const local = selectedPair.localCandidateId && typeof stats.get === 'function'
|
||||||
? stats.get(selectedPair.localCandidateId)
|
? stats.get(selectedPair.localCandidateId)
|
||||||
@ -309,17 +378,39 @@ async function detectConnectionRoute(call) {
|
|||||||
|
|
||||||
const localType = String(local?.candidateType || '').trim().toLowerCase();
|
const localType = String(local?.candidateType || '').trim().toLowerCase();
|
||||||
const remoteType = String(remote?.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') {
|
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) {
|
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 {
|
} catch {
|
||||||
return { label: '', details: '' };
|
return { label: '', details: '', localIp: '', remoteIp: '', turnCandidateAddress: '' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,6 +427,7 @@ function startTransportProbe(call) {
|
|||||||
const route = await detectConnectionRoute(call);
|
const route = await detectConnectionRoute(call);
|
||||||
if (!route.label || route.label === call.connectionRouteLabel) return;
|
if (!route.label || route.label === call.connectionRouteLabel) return;
|
||||||
call.connectionRouteLabel = route.label;
|
call.connectionRouteLabel = route.label;
|
||||||
|
call.connectionRouteDetails = route.details || '';
|
||||||
setActiveStatus(call);
|
setActiveStatus(call);
|
||||||
await emitDebug(call, 'info', 'peer_connection_route', route.details || route.label);
|
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}`);
|
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);
|
pushCallSummary(call, localReasonCode);
|
||||||
|
|
||||||
call.phase = 'ended';
|
call.phase = 'ended';
|
||||||
@ -565,6 +669,30 @@ async function ensurePeerConnection(call) {
|
|||||||
setActiveStatus(call);
|
setActiveStatus(call);
|
||||||
startTransportProbe(call);
|
startTransportProbe(call);
|
||||||
void emitDebug(call, 'info', 'peer_connection_connected', `callId=${call.callId}`);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (state === 'failed') {
|
if (state === 'failed') {
|
||||||
@ -599,6 +727,7 @@ async function ensurePeerConnection(call) {
|
|||||||
call.localStream = stream;
|
call.localStream = stream;
|
||||||
call.audioSenders = [];
|
call.audioSenders = [];
|
||||||
call.connectionRouteLabel = '';
|
call.connectionRouteLabel = '';
|
||||||
|
call.connectionRouteDetails = '';
|
||||||
stream.getTracks().forEach((track) => {
|
stream.getTracks().forEach((track) => {
|
||||||
track.enabled = track.kind === 'audio' ? !call.muted : true;
|
track.enabled = track.kind === 'audio' ? !call.muted : true;
|
||||||
const sender = pc.addTrack(track, stream);
|
const sender = pc.addTrack(track, stream);
|
||||||
|
|||||||
@ -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_GetCallIceConfig_Handler;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.Net_ClientErrorLog_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_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.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_ClientErrorLog_Request;
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.entyties.Net_ClientDebugLog_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;
|
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("GetServerInfo", new Net_GetServerInfo_Handler()),
|
||||||
Map.entry("GetCallIceConfig", new Net_GetCallIceConfig_Handler()),
|
Map.entry("GetCallIceConfig", new Net_GetCallIceConfig_Handler()),
|
||||||
Map.entry("ClientErrorLog", new Net_ClientErrorLog_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 ---
|
// --- subscriptions ---
|
||||||
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
// Map.entry("ListSubscribedChannels", new Net_GetSubscribedChannels_Handler())
|
||||||
@ -207,7 +210,8 @@ public final class JsonHandlerRegistry {
|
|||||||
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
|
Map.entry("GetServerInfo", Net_GetServerInfo_Request.class),
|
||||||
Map.entry("GetCallIceConfig", Net_GetCallIceConfig_Request.class),
|
Map.entry("GetCallIceConfig", Net_GetCallIceConfig_Request.class),
|
||||||
Map.entry("ClientErrorLog", Net_ClientErrorLog_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() { }
|
private JsonHandlerRegistry() { }
|
||||||
|
|||||||
@ -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)) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user