From bb92cb6b2b4a5391b56fb77a899f04e8a6d20eacd159d6c566ae56712216f4ef Mon Sep 17 00:00:00 2001 From: AidarKC Date: Fri, 10 Jul 2026 11:47:39 +0400 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20DM=20=D0=BF=D0=BE=D1=81?= =?UTF-8?q?=D0=BB=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BB=D0=BE=D0=B3=D0=B8?= =?UTF-8?q?=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/Net_CreateAuthSession__Handler.java | 3 +- .../auth/Net_SessionLogin_Handler.java | 3 +- .../JSON/messages/SignedMessagesRealtime.java | 24 +++++ VERSION.properties | 4 +- shine-UI/js/app.js | 89 +++++++++++++++---- shine-UI/js/state.js | 27 ++++++ 6 files changed, 130 insertions(+), 20 deletions(-) diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java index 4086c0e..2e963c7 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_CreateAuthSession__Handler.java @@ -51,6 +51,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { private static final Logger log = LoggerFactory.getLogger(Net_CreateAuthSession__Handler.class); private static final SecureRandom RANDOM = new SecureRandom(); private static final long CLOSE_AFTER_ERROR_DELAY_MS = 75L; + private static final long SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS = 250L; public static final long ALLOWED_SKEW_MS = 30_000L; @@ -423,7 +424,7 @@ public class Net_CreateAuthSession__Handler implements JsonMessageHandler { ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ActiveConnectionsRegistry.getInstance().register(ctx); - SignedMessagesRealtime.dispatchPendingForSession(ctx); + SignedMessagesRealtime.dispatchPendingForSessionAsync(ctx, SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS); // --- формируем ответ --- Net_CreateAuthSession_Response resp = new Net_CreateAuthSession_Response(); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java index 7859c3e..bc4685e 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/auth/Net_SessionLogin_Handler.java @@ -44,6 +44,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler { private static final Logger log = LoggerFactory.getLogger(Net_SessionLogin_Handler.class); private static final long ALLOWED_SKEW_MS = 30_000L; + private static final long SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS = 250L; @Override public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception { @@ -301,7 +302,7 @@ public class Net_SessionLogin_Handler implements JsonMessageHandler { ctx.setAuthenticationStatus(ConnectionContext.AUTH_STATUS_USER); ActiveConnectionsRegistry.getInstance().register(ctx); - SignedMessagesRealtime.dispatchPendingForSession(ctx); + SignedMessagesRealtime.dispatchPendingForSessionAsync(ctx, SIGNED_DM_BACKLOG_AFTER_AUTH_DELAY_MS); // ответ Net_SessionLogin_Response resp = new Net_SessionLogin_Response(); diff --git a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesRealtime.java b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesRealtime.java index 67e187c..b37804a 100644 --- a/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesRealtime.java +++ b/SHiNE-server/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/messages/SignedMessagesRealtime.java @@ -18,10 +18,22 @@ import java.util.Base64; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; public final class SignedMessagesRealtime { private static final Logger log = LoggerFactory.getLogger(SignedMessagesRealtime.class); private static final ObjectMapper MAPPER = new ObjectMapper(); + private static final ScheduledExecutorService BACKLOG_EXECUTOR = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, "signed-messages-backlog"); + thread.setDaemon(true); + return thread; + } + }); private SignedMessagesRealtime() {} @@ -63,7 +75,19 @@ public final class SignedMessagesRealtime { String login = ctx.getLogin(); String sessionId = ctx.getSessionId(); if (isBlank(login) || isBlank(sessionId)) return; + dispatchPendingForSession(login, sessionId); + } + public static void dispatchPendingForSessionAsync(ConnectionContext ctx, long delayMs) { + if (ctx == null || !ctx.isAuthenticatedUser()) return; + String login = safeLower(ctx.getLogin()); + String sessionId = String.valueOf(ctx.getSessionId() == null ? "" : ctx.getSessionId()).trim(); + if (isBlank(login) || isBlank(sessionId)) return; + long safeDelayMs = Math.max(0L, delayMs); + BACKLOG_EXECUTOR.schedule(() -> dispatchPendingForSession(login, sessionId), safeDelayMs, TimeUnit.MILLISECONDS); + } + + private static void dispatchPendingForSession(String login, String sessionId) { try { List pending = SignedMessagesV2DAO.getInstance() .listPendingForSession(login, sessionId); diff --git a/VERSION.properties b/VERSION.properties index d5f05a2..1db2cef 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.301 -server.version=1.2.280 +client.version=1.2.302 +server.version=1.2.281 diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 7dd17db..334113d 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -135,6 +135,8 @@ const toolbarEl = document.getElementById('toolbar-slot'); const appShellEl = document.querySelector('.app-shell'); const CONNECTION_CHECK_INTERVAL_MS = 20 * 1000; +const SIGNED_DM_DECRYPT_CONTEXT_WAIT_MS = 2000; +const SIGNED_DM_DECRYPT_CONTEXT_POLL_MS = 50; const UI_VERSION_PERIODIC_CHECK_MS = 5 * 60 * 1000; const CURRENT_BUILD_HASH = String(window.__SHINE_BUILD_HASH__ || '').trim(); const UI_BUILD_HASH_PATTERN = /window\.__SHINE_BUILD_HASH__\s*=\s*'([^']+)'/; @@ -641,6 +643,44 @@ function wsIsOpen() { return !!(ws && ws.readyState === WebSocket.OPEN); } +function sleep(ms) { + return new Promise((resolve) => { + window.setTimeout(resolve, ms); + }); +} + +async function waitForSignedDmDecryptContext(timeoutMs = SIGNED_DM_DECRYPT_CONTEXT_WAIT_MS) { + const startedAtMs = Date.now(); + while ((Date.now() - startedAtMs) < timeoutMs) { + const login = String(state.session?.login || '').trim(); + const storagePwd = String(state.session?.storagePwdInMemory || '').trim(); + if (login && storagePwd) { + return { login, storagePwd }; + } + await sleep(SIGNED_DM_DECRYPT_CONTEXT_POLL_MS); + } + return { + login: String(state.session?.login || '').trim(), + storagePwd: String(state.session?.storagePwdInMemory || '').trim(), + }; +} + +function resolveSignedDmChatId({ parsed, login }) { + const parsedBlock = parsed || {}; + const messageType = Number(parsedBlock.messageType || 0); + const fromLogin = String(parsedBlock.fromLogin || '').trim(); + const toLogin = String(parsedBlock.toLogin || '').trim(); + const currentLogin = String(login || '').trim().toLowerCase(); + + if (messageType === 1) return normalizeDmChatId(fromLogin); + if (messageType === 2) return normalizeDmChatId(toLogin); + + if (currentLogin && currentLogin === fromLogin.toLowerCase()) { + return normalizeDmChatId(toLogin); + } + return normalizeDmChatId(fromLogin); +} + async function ensureSessionAfterWsReconnect() { if (!state.session.isAuthorized) return true; if (wsSessionRestoreInFlight) return wsSessionRestoreInFlight; @@ -1020,29 +1060,44 @@ async function init() { const fromLogin = parsed.fromLogin || ''; const toLogin = parsed.toLogin || ''; const messageType = Number(parsed.messageType || 0); - const currentLogin = String(state.session?.login || '').trim().toLowerCase(); - const chatPeerLogin = currentLogin && currentLogin === String(fromLogin || '').trim().toLowerCase() - ? toLogin - : fromLogin; - const chatId = normalizeDmChatId(chatPeerLogin); + let shouldAckSessionDelivery = true; let text = ''; + let sessionLoginForRouting = String(state.session?.login || '').trim(); if (messageType === 1 || messageType === 2) { + const decryptContext = await waitForSignedDmDecryptContext(); + if (!decryptContext.login || !decryptContext.storagePwd) { + shouldAckSessionDelivery = false; + addAppLogEntry({ + level: 'warn', + source: 'signed-dm', + message: 'Отложена обработка DM: decrypt-контекст сессии ещё не готов', + details: { messageKey, baseKey: parsed.baseKey, messageType }, + }); + return; + } + sessionLoginForRouting = decryptContext.login; try { const decrypted = await authService.decryptSignedMessageContent({ parsed, - login: state.session?.login || '', - storagePwd: state.session?.storagePwdInMemory || '', + login: decryptContext.login, + storagePwd: decryptContext.storagePwd, }); text = String(decrypted?.text || ''); } catch (error) { + shouldAckSessionDelivery = false; addAppLogEntry({ level: 'warn', source: 'signed-dm', message: 'Не удалось расшифровать DM', details: { messageKey, baseKey: parsed.baseKey, error: error?.message || 'unknown' }, }); + return; } } + const chatId = resolveSignedDmChatId({ + parsed, + login: sessionLoginForRouting, + }); let shouldRefreshToolbarUnread = false; @@ -1125,15 +1180,17 @@ async function init() { }); } - try { - await authService.ackSessionDelivery(messageKey); - } catch (error) { - addAppLogEntry({ - level: 'warn', - source: 'signed-dm', - message: 'Не удалось отправить ACK доставки по сессии', - details: { messageKey, error: error?.message || 'unknown' }, - }); + if (shouldAckSessionDelivery) { + try { + await authService.ackSessionDelivery(messageKey); + } catch (error) { + addAppLogEntry({ + level: 'warn', + source: 'signed-dm', + message: 'Не удалось отправить ACK доставки по сессии', + details: { messageKey, error: error?.message || 'unknown' }, + }); + } } const pageId = getRoute().pageId || ''; diff --git a/shine-UI/js/state.js b/shine-UI/js/state.js index 999a90f..42008d4 100644 --- a/shine-UI/js/state.js +++ b/shine-UI/js/state.js @@ -252,6 +252,8 @@ function createInitialState({ withStoredSession = true } = {}) { appLog: [], incomingDedup: {}, knownMessageKeys: {}, + pendingOutgoingReadByBaseKey: {}, + pendingIncomingReadByBaseKey: {}, outgoingTempSeq: 1, notificationsTab: 'replies', pageLabelCollapsed: false, @@ -558,32 +560,46 @@ export function markOutgoingSent(tempId, { messageKey = '', baseKey = '' } = {}) export function markOutgoingReadByBaseKey(baseKey) { if (!baseKey) return; const keys = Object.keys(state.chats || {}); + let matched = false; keys.forEach((chatId) => { const list = getChatMessages(chatId); list.forEach((row) => { if (row?.from !== 'out') return; if (row.baseKey === baseKey) { + matched = true; row.secondTick = true; persistMessageRecord(chatId, row); } }); }); + if (matched) { + delete state.pendingOutgoingReadByBaseKey[baseKey]; + } else { + state.pendingOutgoingReadByBaseKey[baseKey] = true; + } } export function markIncomingReadByBaseKey(baseKey) { if (!baseKey) return; const keys = Object.keys(state.chats || {}); + let matched = false; keys.forEach((chatId) => { const list = getChatMessages(chatId); list.forEach((row) => { if (row?.from !== 'in') return; if (row.baseKey === baseKey) { + matched = true; row.unread = false; row.readReceiptSent = true; persistMessageRecord(chatId, row); } }); }); + if (matched) { + delete state.pendingIncomingReadByBaseKey[baseKey]; + } else { + state.pendingIncomingReadByBaseKey[baseKey] = true; + } } export function markReadReceiptSentByBaseKey(baseKey) { @@ -651,6 +667,15 @@ export function addSignedMessageToChat({ row.firstTick = row.from === 'out'; row.secondTick = Boolean(existing?.secondTick); row.readReceiptSent = Boolean(existing?.readReceiptSent); + if (row.baseKey && row.from === 'out' && state.pendingOutgoingReadByBaseKey[row.baseKey]) { + row.secondTick = true; + delete state.pendingOutgoingReadByBaseKey[row.baseKey]; + } + if (row.baseKey && row.from === 'in' && state.pendingIncomingReadByBaseKey[row.baseKey]) { + row.unread = false; + row.readReceiptSent = true; + delete state.pendingIncomingReadByBaseKey[row.baseKey]; + } if (existingIndex < 0) { list.push(row); @@ -872,6 +897,8 @@ function resetStateForSignedOut() { state.appLog = next.appLog; state.incomingDedup = next.incomingDedup; state.knownMessageKeys = next.knownMessageKeys; + state.pendingOutgoingReadByBaseKey = next.pendingOutgoingReadByBaseKey; + state.pendingIncomingReadByBaseKey = next.pendingIncomingReadByBaseKey; state.outgoingTempSeq = next.outgoingTempSeq; state.notificationsTab = next.notificationsTab; state.pageLabelCollapsed = next.pageLabelCollapsed;