From 8b906169ce08fabd77454ea788e6d31b2252b43d Mon Sep 17 00:00:00 2001 From: RoyceDa Date: Sun, 15 Feb 2026 14:56:44 +0200 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=BE=D0=BC=D0=B5=D0=B6=D1=83?= =?UTF-8?q?=D1=82=D0=BE=D1=87=D0=BD=D1=8B=D0=B9=20=D1=8D=D1=82=D0=B0=D0=BF?= =?UTF-8?q?=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/ChatHeader/ChatHeader.tsx | 2 +- .../DialogHeaderText/DialogHeaderText.tsx | 39 + .../DialogsPanelHeader/DialogsPanelHeader.tsx | 5 +- app/components/GroupHeader/GroupHeader.tsx | 2 +- app/components/Topbar/Topbar.tsx | 6 +- app/providers/DialogProvider/dialogQueue.ts | 12 + app/providers/DialogProvider/useDialog.ts | 2 +- .../DialogProvider/useDialogFiber.ts | 838 +++++++++--------- .../DialogProvider/useSynchronize.ts | 57 ++ .../ProtocolProvider/ProtocolProvider.tsx | 9 +- .../protocol/packets/packet.sync.ts | 48 + .../ProtocolProvider/protocol/protocol.ts | 2 + .../ProtocolProvider/useProtocolState.ts | 6 +- app/views/DeviceConfirm/DeviceConfirm.tsx | 2 +- app/views/Main/Main.tsx | 6 + 15 files changed, 609 insertions(+), 427 deletions(-) create mode 100644 app/components/DialogHeaderText/DialogHeaderText.tsx create mode 100644 app/providers/DialogProvider/dialogQueue.ts create mode 100644 app/providers/DialogProvider/useSynchronize.ts create mode 100644 app/providers/ProtocolProvider/protocol/packets/packet.sync.ts diff --git a/app/components/ChatHeader/ChatHeader.tsx b/app/components/ChatHeader/ChatHeader.tsx index df15bb6..0b13363 100644 --- a/app/components/ChatHeader/ChatHeader.tsx +++ b/app/components/ChatHeader/ChatHeader.tsx @@ -31,7 +31,7 @@ export function ChatHeader() { const theme = useMantineTheme(); const [blocked, blockUser, unblockUser] = useBlacklist(dialog); const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog); - const protocolState = useProtocolState(); + const [protocolState] = useProtocolState(); const [userTypeing, setUserTypeing] = useState(false); const timeoutRef = useRef(undefined); const avatars = useAvatars(dialog); diff --git a/app/components/DialogHeaderText/DialogHeaderText.tsx b/app/components/DialogHeaderText/DialogHeaderText.tsx new file mode 100644 index 0000000..868df71 --- /dev/null +++ b/app/components/DialogHeaderText/DialogHeaderText.tsx @@ -0,0 +1,39 @@ +import { useRosettaColors } from "@/app/hooks/useRosettaColors"; +import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider"; +import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState"; +import { Flex, Loader, Text } from "@mantine/core"; + +export function DialogHeaderText() { + const [protocolState] = useProtocolState(); + const colors = useRosettaColors(); + + const headerType = () => { + switch(protocolState){ + case ProtocolState.SYNCHRONIZATION: + return (<> + + Updating... + ); + case ProtocolState.CONNECTED: + return (<> + Chats + ); + default: + return (<> + Chats + ); + } + } + + return ( + + {headerType()} + + ) +} \ No newline at end of file diff --git a/app/components/DialogsPanelHeader/DialogsPanelHeader.tsx b/app/components/DialogsPanelHeader/DialogsPanelHeader.tsx index bd33cbb..04553c9 100644 --- a/app/components/DialogsPanelHeader/DialogsPanelHeader.tsx +++ b/app/components/DialogsPanelHeader/DialogsPanelHeader.tsx @@ -6,6 +6,7 @@ import { useLogout } from "@/app/providers/AccountProvider/useLogout"; import { useHotkeys } from "@mantine/hooks"; import { useNavigate } from "react-router-dom"; import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey"; +import { DialogHeaderText } from "../DialogHeaderText/DialogHeaderText"; export function DialogsPanelHeader() { const colors = useRosettaColors(); @@ -66,9 +67,7 @@ export function DialogsPanelHeader() { - Chats + } {window.platform == 'darwin' && } {window.platform == 'linux' && } - {(protocolState == ProtocolState.CONNECTED || !window.location.hash.includes("main")) && + {(protocolState == ProtocolState.CONNECTED || protocolState == ProtocolState.SYNCHRONIZATION || !window.location.hash.includes("main")) && Rosetta Messenger } - {(protocolState != ProtocolState.CONNECTED && protocolState != ProtocolState.DEVICE_VERIFICATION_REQUIRED && window.location.hash.includes("main")) && + {(protocolState != ProtocolState.CONNECTED && protocolState != ProtocolState.SYNCHRONIZATION && protocolState != ProtocolState.DEVICE_VERIFICATION_REQUIRED && window.location.hash.includes("main")) && diff --git a/app/providers/DialogProvider/dialogQueue.ts b/app/providers/DialogProvider/dialogQueue.ts new file mode 100644 index 0000000..7256a77 --- /dev/null +++ b/app/providers/DialogProvider/dialogQueue.ts @@ -0,0 +1,12 @@ +let tail: Promise = Promise.resolve(); + +export const runTaskInQueue = (fn: () => Promise) => { + tail = tail.then(fn).catch((e) => { + console.error("Dialog queue error", e); + }); +}; + +/** + * Ждет, пока все пакеты попадающие в очередь не будут обработаны + */ +export const whenFinish = () => tail; \ No newline at end of file diff --git a/app/providers/DialogProvider/useDialog.ts b/app/providers/DialogProvider/useDialog.ts index ea6db23..5ebfcd5 100644 --- a/app/providers/DialogProvider/useDialog.ts +++ b/app/providers/DialogProvider/useDialog.ts @@ -44,7 +44,7 @@ export function useDialog() : { const privateKey = usePrivateKeyHash(); const privatePlain = usePrivatePlain(); const {writeFile} = useFileStorage(); - const protocolState = useProtocolState(); + const [protocolState] = useProtocolState(); const {hasGroup, getGroupKey} = useGroups(); const {warn} = useConsoleLogger('useDialog'); diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index ce312fe..f9bb42b 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -24,6 +24,7 @@ import { useGroups } from "./useGroups"; import { useDialogState } from "../DialogStateProvider.tsx/useDialogState"; import { useUserInformation } from "../InformationProvider/useUserInformation"; import { useMentions } from "../DialogStateProvider.tsx/useMentions"; +import { runTaskInQueue } from "./dialogQueue"; /** * При вызове будет запущен "фоновый" обработчик @@ -41,14 +42,14 @@ export function useDialogFiber() { const notify = useNotification(); const focused = useWindowFocus(); const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache(); - const {info, error} = useConsoleLogger('useDialogFiber'); + const { info, error } = useConsoleLogger('useDialogFiber'); const [viewState] = useViewPanelsState(); - const {writeFile} = useFileStorage(); - const {updateDialog} = useDialogsList(); - const {hasGroup, getGroupKey, normalize} = useGroups(); - const {muted} = useDialogState(); + const { writeFile } = useFileStorage(); + const { updateDialog } = useDialogsList(); + const { hasGroup, getGroupKey, normalize } = useGroups(); + const { muted } = useDialogState(); const [userInfo] = useUserInformation(publicKey); - const {pushMention} = useMentions(); + const { pushMention } = useMentions(); /** * Лог @@ -64,486 +65,501 @@ export function useDialogFiber() { * Метод нужен для синхронизации своих сообщений */ usePacket(0x06, async (packet: PacketMessage) => { - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); - const aesChachaKey = packet.getAesChachaKey(); - const content = packet.getContent(); - const timestamp = packet.getTimestamp(); - const messageId = packet.getMessageId(); - - - if(fromPublicKey != publicKey){ - /** - * Игнорируем если это не сообщение от нас - */ - return; - } - const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary"); - const key = chachaDecryptedKey.slice(0, 32); - const nonce = chachaDecryptedKey.slice(32); - const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); + runTaskInQueue(async () => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + const aesChachaKey = packet.getAesChachaKey(); + const content = packet.getContent(); + const timestamp = packet.getTimestamp(); + const messageId = packet.getMessageId(); - let attachmentsMeta: any[] = []; - let messageAttachments: Attachment[] = []; - for (let i = 0; i < packet.getAttachments().length; i++) { - const attachment = packet.getAttachments()[i]; - log("Attachment received id " + attachment.id + " type " + attachment.type); - let nextLength = messageAttachments.push({ - ...attachment, - blob: "" - }); - - if(attachment.type == AttachmentType.MESSAGES){ + if (fromPublicKey != publicKey) { /** - * Этот тип вложения приходит сразу в blob и не нуждается - * в последующем скачивании + * Игнорируем если это не сообщение от нас */ - const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob); - writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, - Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); - messageAttachments[nextLength - 1].blob = decryptedBlob; + return; + } + const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary"); + const key = chachaDecryptedKey.slice(0, 32); + const nonce = chachaDecryptedKey.slice(32); + const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); + + let attachmentsMeta: any[] = []; + let messageAttachments: Attachment[] = []; + for (let i = 0; i < packet.getAttachments().length; i++) { + const attachment = packet.getAttachments()[i]; + log("Attachment received id " + attachment.id + " type " + attachment.type); + + let nextLength = messageAttachments.push({ + ...attachment, + blob: "" + }); + + if (attachment.type == AttachmentType.MESSAGES) { + /** + * Этот тип вложения приходит сразу в blob и не нуждается + * в последующем скачивании + */ + const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob); + writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, + Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); + messageAttachments[nextLength - 1].blob = decryptedBlob; + } + + attachmentsMeta.push({ + id: attachment.id, + type: attachment.type, + preview: attachment.preview + }); } - attachmentsMeta.push({ - id: attachment.id, - type: attachment.type, - preview: attachment.preview - }); - } + const newMessage: Message = { + from_public_key: fromPublicKey, + to_public_key: toPublicKey, + content: content, + timestamp: timestamp, + readed: 1, //сообщение прочитано + chacha_key: chachaDecryptedKey.toString('utf-8'), + from_me: 1, //сообщение от нас + plain_message: (decryptedContent as string), + delivered: DeliveredMessageState.DELIVERED, + message_id: messageId, + attachments: messageAttachments + }; - const newMessage: Message = { - from_public_key: fromPublicKey, - to_public_key: toPublicKey, - content: content, - timestamp: timestamp, - readed: 1, //сообщение прочитано - chacha_key: chachaDecryptedKey.toString('utf-8'), - from_me: 1, //сообщение от нас - plain_message: (decryptedContent as string), - delivered: DeliveredMessageState.DELIVERED, - message_id: messageId, - attachments: messageAttachments - }; - - await runQuery(` + await runQuery(` INSERT INTO messages (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [fromPublicKey, - toPublicKey, - content, - timestamp, - 0, //по умолчанию не прочитаны - '', - 1, //Свои же сообщения всегда от нас - await encodeWithPassword(privatePlain, decryptedContent), - publicKey, - messageId, - DeliveredMessageState.DELIVERED, - JSON.stringify(attachmentsMeta)]); + `, [fromPublicKey, + toPublicKey, + content, + timestamp, + 0, //по умолчанию не прочитаны + '', + 1, //Свои же сообщения всегда от нас + await encodeWithPassword(privatePlain, decryptedContent), + publicKey, + messageId, + DeliveredMessageState.DELIVERED, + JSON.stringify(attachmentsMeta)]); - updateDialog(toPublicKey); + updateDialog(toPublicKey); - let dialogCache = getDialogCache(toPublicKey); - if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { - addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); - } + let dialogCache = getDialogCache(toPublicKey); + if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { + addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); + } + }); }, [privatePlain, currentDialogPublicKeyView]); /** * Обработчик сообщений для группы */ usePacket(0x06, async (packet: PacketMessage) => { - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); - const content = packet.getContent(); - const timestamp = packet.getTimestamp(); - const messageId = packet.getMessageId(); - if(!hasGroup(toPublicKey)){ - /** - * Если это личное сообщение, то игнорируем его здесь - * для него есть отдельный слушатель usePacket (снизу) - */ - return; - } - if(fromPublicKey == publicKey){ - /** - * Игнорируем свои же сообщения, - * такое получается при пакете синхронизации - */ - return; - } - const groupKey = await getGroupKey(toPublicKey); - if(!groupKey){ - log("Group key not found for group " + toPublicKey); - error("Message dropped because group key not found for group " + toPublicKey); - return; - } - info("New group message packet received from " + fromPublicKey); - - let decryptedContent = ''; - - try{ - decryptedContent = await decodeWithPassword(groupKey, content); - }catch(e) { - decryptedContent = ''; - } - - let attachmentsMeta: any[] = []; - let messageAttachments: Attachment[] = []; - for (let i = 0; i < packet.getAttachments().length; i++) { - const attachment = packet.getAttachments()[i]; - log("Attachment received id " + attachment.id + " type " + attachment.type); - - let nextLength = messageAttachments.push({ - ...attachment, - blob: "" - }); - - if(attachment.type == AttachmentType.MESSAGES){ + runTaskInQueue(async () => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + const content = packet.getContent(); + const timestamp = packet.getTimestamp(); + const messageId = packet.getMessageId(); + if (!hasGroup(toPublicKey)) { /** - * Этот тип вложения приходит сразу в blob и не нуждается - * в последующем скачивании + * Если это личное сообщение, то игнорируем его здесь + * для него есть отдельный слушатель usePacket (снизу) */ - const decryptedBlob = await decodeWithPassword(groupKey, attachment.blob); - writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, - Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); - messageAttachments[nextLength - 1].blob = decryptedBlob; + return; + } + if (fromPublicKey == publicKey) { + /** + * Игнорируем свои же сообщения, + * такое получается при пакете синхронизации + */ + return; + } + const groupKey = await getGroupKey(toPublicKey); + if (!groupKey) { + log("Group key not found for group " + toPublicKey); + error("Message dropped because group key not found for group " + toPublicKey); + return; + } + info("New group message packet received from " + fromPublicKey); + + let decryptedContent = ''; + + try { + decryptedContent = await decodeWithPassword(groupKey, content); + } catch (e) { + decryptedContent = ''; } - attachmentsMeta.push({ - id: attachment.id, - type: attachment.type, - preview: attachment.preview - }); - } + let attachmentsMeta: any[] = []; + let messageAttachments: Attachment[] = []; + for (let i = 0; i < packet.getAttachments().length; i++) { + const attachment = packet.getAttachments()[i]; + log("Attachment received id " + attachment.id + " type " + attachment.type); - const newMessage: Message = { - from_public_key: fromPublicKey, - to_public_key: toPublicKey, - content: content, - timestamp: timestamp, - readed: idle ? 0 : 1, - chacha_key: groupKey, - from_me: fromPublicKey == publicKey ? 1 : 0, - plain_message: decryptedContent, - delivered: DeliveredMessageState.DELIVERED, - message_id: messageId, - attachments: messageAttachments - }; + let nextLength = messageAttachments.push({ + ...attachment, + blob: "" + }); - await runQuery(` + if (attachment.type == AttachmentType.MESSAGES) { + /** + * Этот тип вложения приходит сразу в blob и не нуждается + * в последующем скачивании + */ + const decryptedBlob = await decodeWithPassword(groupKey, attachment.blob); + writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, + Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); + messageAttachments[nextLength - 1].blob = decryptedBlob; + } + + attachmentsMeta.push({ + id: attachment.id, + type: attachment.type, + preview: attachment.preview + }); + } + + const newMessage: Message = { + from_public_key: fromPublicKey, + to_public_key: toPublicKey, + content: content, + timestamp: timestamp, + readed: idle ? 0 : 1, + chacha_key: groupKey, + from_me: fromPublicKey == publicKey ? 1 : 0, + plain_message: decryptedContent, + delivered: DeliveredMessageState.DELIVERED, + message_id: messageId, + attachments: messageAttachments + }; + + await runQuery(` INSERT INTO messages (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [fromPublicKey, - toPublicKey, - content, - timestamp, - /**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */ - (currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, - '', - 0, - await encodeWithPassword(privatePlain, decryptedContent), - publicKey, - messageId, - DeliveredMessageState.DELIVERED, - JSON.stringify(attachmentsMeta)]); + `, [fromPublicKey, + toPublicKey, + content, + timestamp, + /**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */ + (currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, + '', + 0, + await encodeWithPassword(privatePlain, decryptedContent), + publicKey, + messageId, + DeliveredMessageState.DELIVERED, + JSON.stringify(attachmentsMeta)]); - /** - * Так как у нас в toPublicKey приходит ID группы, - * то обновляем диалог по этому ID, а не по fromPublicKey - * как это сделано в личных сообщениях - */ - updateDialog(toPublicKey); - - if (((normalize(currentDialogPublicKeyView) !== normalize(toPublicKey) || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) && - (timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) { /** - * Условие со временем нужно для того, - * чтобы когда приходит пачка сообщений с сервера в момент того как - * пользователь был неактивен, не слать уведомления по всем этим сообщениям + * Так как у нас в toPublicKey приходит ID группы, + * то обновляем диалог по этому ID, а не по fromPublicKey + * как это сделано в личных сообщениях */ - let mentionFlag = false; - if((newMessage.from_public_key != publicKey) && (decryptedContent.includes(`@${userInfo.username}`) || decryptedContent.includes(`@all`))){ - /** - * Если в сообщении есть упоминание текущего пользователя или @all, - * при этом сообщение отправляли не мы, - * то добавляем упоминание в состояние диалога. - * - * TODO: сделать чтобы all работал только для админов группы - */ - mentionFlag = true; - } + updateDialog(toPublicKey); - if(!muted.includes(toPublicKey) || mentionFlag){ + if (((normalize(currentDialogPublicKeyView) !== normalize(toPublicKey) || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) && + (timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) { /** - * Если группа не в мутие или есть упоминание - отправляем уведомление + * Условие со временем нужно для того, + * чтобы когда приходит пачка сообщений с сервера в момент того как + * пользователь был неактивен, не слать уведомления по всем этим сообщениям */ - notify("New message", "You have a new message"); + let mentionFlag = false; + if ((newMessage.from_public_key != publicKey) && (decryptedContent.includes(`@${userInfo.username}`) || decryptedContent.includes(`@all`))) { + /** + * Если в сообщении есть упоминание текущего пользователя или @all, + * при этом сообщение отправляли не мы, + * то добавляем упоминание в состояние диалога. + * + * TODO: сделать чтобы all работал только для админов группы + */ + mentionFlag = true; + } + + if (!muted.includes(toPublicKey) || mentionFlag) { + /** + * Если группа не в мутие или есть упоминание - отправляем уведомление + */ + notify("New message", "You have a new message"); + } + if (mentionFlag) { + /** + * Если в сообщении есть упоминание текущего пользователя или @all, + * то добавляем упоминание в состояние диалога + * + * TODO: сделать чтобы all работал только для админов группы + */ + pushMention({ + dialog_id: toPublicKey, + message_id: messageId + }); + } } - if(mentionFlag){ - /** - * Если в сообщении есть упоминание текущего пользователя или @all, - * то добавляем упоминание в состояние диалога - * - * TODO: сделать чтобы all работал только для админов группы - */ - pushMention({ - dialog_id: toPublicKey, - message_id: messageId - }); - } - } - let dialogCache = getDialogCache(toPublicKey); - if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { - addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); - } + let dialogCache = getDialogCache(toPublicKey); + if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { + addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); + } + }); }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); /** * Обработчик личных сообщений */ usePacket(0x06, async (packet: PacketMessage) => { - const fromPublicKey = packet.getFromPublicKey(); - if(fromPublicKey == publicKey){ - /** - * Игнорируем свои же сообщения, - * такое получается при пакете синхронизации - */ - return; - } - const toPublicKey = packet.getToPublicKey(); - const content = packet.getContent(); - const chachaKey = packet.getChachaKey(); - const timestamp = packet.getTimestamp(); - const messageId = packet.getMessageId(); - if(hasGroup(toPublicKey)){ - /** - * Если это групповое сообщение, то игнорируем его здесь - * для него есть отдельный слушатель usePacket - */ - return; - } - info("New message packet received from " + fromPublicKey); - if (blocked.includes(fromPublicKey)) { - /** - * Если пользователь заблокирован и это не групповое сообщение, - * то игнорируем сообщение - */ - log("Message from blocked user, ignore " + fromPublicKey); - return; - } - - if (privatePlain == "") { - return; - } - - const chachaDecryptedKey = Buffer.from(await decrypt(chachaKey, privatePlain), "binary"); - const key = chachaDecryptedKey.slice(0, 32); - const nonce = chachaDecryptedKey.slice(32); - const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); - - let attachmentsMeta: any[] = []; - let messageAttachments: Attachment[] = []; - for (let i = 0; i < packet.getAttachments().length; i++) { - const attachment = packet.getAttachments()[i]; - log("Attachment received id " + attachment.id + " type " + attachment.type); - - let nextLength = messageAttachments.push({ - ...attachment, - blob: "" - }); - - if(attachment.type == AttachmentType.MESSAGES){ + runTaskInQueue(async () => { + const fromPublicKey = packet.getFromPublicKey(); + if (fromPublicKey == publicKey) { /** - * Этот тип вложения приходит сразу в blob и не нуждается - * в последующем скачивании + * Игнорируем свои же сообщения, + * такое получается при пакете синхронизации */ - const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob); - writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, - Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); - messageAttachments[nextLength - 1].blob = decryptedBlob; + return; + } + const toPublicKey = packet.getToPublicKey(); + const content = packet.getContent(); + const chachaKey = packet.getChachaKey(); + const timestamp = packet.getTimestamp(); + const messageId = packet.getMessageId(); + if (hasGroup(toPublicKey)) { + /** + * Если это групповое сообщение, то игнорируем его здесь + * для него есть отдельный слушатель usePacket + */ + return; + } + info("New message packet received from " + fromPublicKey); + if (blocked.includes(fromPublicKey)) { + /** + * Если пользователь заблокирован и это не групповое сообщение, + * то игнорируем сообщение + */ + log("Message from blocked user, ignore " + fromPublicKey); + return; } - attachmentsMeta.push({ - id: attachment.id, - type: attachment.type, - preview: attachment.preview - }); - } + if (privatePlain == "") { + return; + } - const newMessage: Message = { - from_public_key: fromPublicKey, - to_public_key: toPublicKey, - content: content, - timestamp: timestamp, - readed: idle ? 0 : 1, - chacha_key: chachaDecryptedKey.toString('utf-8'), - from_me: fromPublicKey == publicKey ? 1 : 0, - plain_message: (decryptedContent as string), - delivered: DeliveredMessageState.DELIVERED, - message_id: messageId, - attachments: messageAttachments - }; + const chachaDecryptedKey = Buffer.from(await decrypt(chachaKey, privatePlain), "binary"); + const key = chachaDecryptedKey.slice(0, 32); + const nonce = chachaDecryptedKey.slice(32); + const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); + + let attachmentsMeta: any[] = []; + let messageAttachments: Attachment[] = []; + for (let i = 0; i < packet.getAttachments().length; i++) { + const attachment = packet.getAttachments()[i]; + log("Attachment received id " + attachment.id + " type " + attachment.type); + + let nextLength = messageAttachments.push({ + ...attachment, + blob: "" + }); + + if (attachment.type == AttachmentType.MESSAGES) { + /** + * Этот тип вложения приходит сразу в blob и не нуждается + * в последующем скачивании + */ + const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob); + writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, + Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary')); + messageAttachments[nextLength - 1].blob = decryptedBlob; + } + + attachmentsMeta.push({ + id: attachment.id, + type: attachment.type, + preview: attachment.preview + }); + } + + const newMessage: Message = { + from_public_key: fromPublicKey, + to_public_key: toPublicKey, + content: content, + timestamp: timestamp, + readed: idle ? 0 : 1, + chacha_key: chachaDecryptedKey.toString('utf-8'), + from_me: fromPublicKey == publicKey ? 1 : 0, + plain_message: (decryptedContent as string), + delivered: DeliveredMessageState.DELIVERED, + message_id: messageId, + attachments: messageAttachments + }; - await runQuery(` + await runQuery(` INSERT INTO messages (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [fromPublicKey, - toPublicKey, - content, - timestamp, - (currentDialogPublicKeyView == fromPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, - chachaKey, - 0, - await encodeWithPassword(privatePlain, decryptedContent), - publicKey, - messageId, - DeliveredMessageState.DELIVERED, - JSON.stringify(attachmentsMeta)]); + `, [fromPublicKey, + toPublicKey, + content, + timestamp, + (currentDialogPublicKeyView == fromPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0, + chachaKey, + 0, + await encodeWithPassword(privatePlain, decryptedContent), + publicKey, + messageId, + DeliveredMessageState.DELIVERED, + JSON.stringify(attachmentsMeta)]); - log("New message received from " + fromPublicKey); + log("New message received from " + fromPublicKey); - updateDialog(fromPublicKey); - if (((currentDialogPublicKeyView !== fromPublicKey || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) && - (timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) { - /** - * Условие со временем нужно для того, - * чтобы когда приходит пачка сообщений с сервера в момент того как - * пользователь был неактивен, не слать уведомления по всем этим сообщениям - */ - if(!muted.includes(fromPublicKey)){ + updateDialog(fromPublicKey); + if (((currentDialogPublicKeyView !== fromPublicKey || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) && + (timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) { /** - * Если пользователь в муте - не отправляем уведомление + * Условие со временем нужно для того, + * чтобы когда приходит пачка сообщений с сервера в момент того как + * пользователь был неактивен, не слать уведомления по всем этим сообщениям */ - notify("New message", "You have a new message"); + if (!muted.includes(fromPublicKey)) { + /** + * Если пользователь в муте - не отправляем уведомление + */ + notify("New message", "You have a new message"); + } } - } - let dialogCache = getDialogCache(fromPublicKey); - if (currentDialogPublicKeyView !== fromPublicKey && dialogCache.length > 0) { - addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); - } + let dialogCache = getDialogCache(fromPublicKey); + if (currentDialogPublicKeyView !== fromPublicKey && dialogCache.length > 0) { + addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); + } + }); }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); /** * Обработчик синхронизации прочтения личных сообщений */ usePacket(0x07, async (packet: PacketRead) => { - if(hasGroup(packet.getToPublicKey())){ - /** - * Если это относится к группам, то игнорируем здесь, - * для этого есть отдельный слушатель usePacket ниже - */ - return; - } - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); - if(fromPublicKey != publicKey){ - /** - * Игнорируем если это не синхронизация нашего прочтения - */ - return; - } - console.info("PACKED_READ_SYNC"); - await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, - [toPublicKey, fromPublicKey, publicKey]); - - console.info("updating with params ", [fromPublicKey, toPublicKey, publicKey]); - updateDialog(toPublicKey); - log("Read sync packet from other device"); - addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { - if (message.from_public_key == toPublicKey && !message.readed) { - console.info("Marking message as read in cache for dialog with " + fromPublicKey); - console.info({fromPublicKey, toPublicKey}); - return { - ...message, - readed: 1 - } + runTaskInQueue(async () => { + if (hasGroup(packet.getToPublicKey())) { + /** + * Если это относится к группам, то игнорируем здесь, + * для этого есть отдельный слушатель usePacket ниже + */ + return; } - return message; - })); + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (fromPublicKey != publicKey) { + /** + * Игнорируем если это не синхронизация нашего прочтения + */ + return; + } + console.info("PACKED_READ_SYNC"); + await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, + [toPublicKey, fromPublicKey, publicKey]); + + console.info("updating with params ", [fromPublicKey, toPublicKey, publicKey]); + updateDialog(toPublicKey); + log("Read sync packet from other device"); + addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { + if (message.from_public_key == toPublicKey && !message.readed) { + console.info("Marking message as read in cache for dialog with " + fromPublicKey); + console.info({ fromPublicKey, toPublicKey }); + return { + ...message, + readed: 1 + } + } + return message; + })); + }); }, [updateDialog, publicKey]); /** * Обработчик прочтения личных сообщений */ usePacket(0x07, async (packet: PacketRead) => { - if(hasGroup(packet.getToPublicKey())){ - /** - * Если это относится к группам, то игнорируем здесь, - * для этого есть отдельный слушатель usePacket ниже - */ - return; - } - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); - if(fromPublicKey == publicKey){ - /** - * Игнорируем если это наше прочтение - * которое получается при синхронизации - */ - return; - } - console.info("PACKED_READ_IM"); - await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]); - updateDialog(fromPublicKey); - log("Read packet received from " + fromPublicKey + " for " + toPublicKey); - addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { - if (message.from_public_key == toPublicKey && !message.readed) { - console.info("Marking message as read in cache for dialog with " + fromPublicKey); - console.info({fromPublicKey, toPublicKey}); - return { - ...message, - readed: 1 - } + runTaskInQueue(async () => { + if (hasGroup(packet.getToPublicKey())) { + /** + * Если это относится к группам, то игнорируем здесь, + * для этого есть отдельный слушатель usePacket ниже + */ + return; } - return message; - })); + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (fromPublicKey == publicKey) { + /** + * Игнорируем если это наше прочтение + * которое получается при синхронизации + */ + return; + } + console.info("PACKED_READ_IM"); + await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]); + console.info("read im with params ", [fromPublicKey, toPublicKey, publicKey]); + updateDialog(fromPublicKey); + log("Read packet received from " + fromPublicKey + " for " + toPublicKey); + addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { + if (message.from_public_key == toPublicKey && !message.readed) { + console.info("Marking message as read in cache for dialog with " + fromPublicKey); + console.info({ fromPublicKey, toPublicKey }); + return { + ...message, + readed: 1 + } + } + return message; + })); + }); }, [updateDialog, publicKey]); /** * Обработчик прочтения групповых сообщений */ usePacket(0x07, async (packet: PacketRead) => { - if(!hasGroup(packet.getToPublicKey())){ - /** - * Если это не относится к группам, то игнорируем здесь, - * для этого есть отдельный слушатель usePacket выше - */ - return; - } - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); - await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]); - updateDialog(toPublicKey); - addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => { - if (!message.readed) { - console.info("Marking message as read in cache for dialog with " + fromPublicKey); - console.info({fromPublicKey, toPublicKey}); - return { - ...message, - readed: 1 - } + runTaskInQueue(async () => { + if (!hasGroup(packet.getToPublicKey())) { + /** + * Если это не относится к группам, то игнорируем здесь, + * для этого есть отдельный слушатель usePacket выше + */ + return; } - return message; - })); - }, [updateDialog]); + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]); + updateDialog(toPublicKey); + addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => { + if (!message.readed) { + console.info("Marking message as read in cache for dialog with " + fromPublicKey); + console.info({ fromPublicKey, toPublicKey }); + return { + ...message, + readed: 1 + } + } + return message; + })); + }); + }, [updateDialog]); /** * Обработчик доставки сообщений */ usePacket(0x08, async (packet: PacketDelivery) => { - const messageId = packet.getMessageId(); - await runQuery(`UPDATE messages SET delivered = ?, timestamp = ? WHERE message_id = ? AND account = ?`, [DeliveredMessageState.DELIVERED, Date.now(), messageId, publicKey]); - updateDialog(packet.getToPublicKey()); - log("Delivery packet received msg id " + messageId); - addOrUpdateDialogCache(packet.getToPublicKey(), getDialogCache(packet.getToPublicKey()).map((message) => { - if (message.message_id == messageId) { - return { - ...message, - delivered: DeliveredMessageState.DELIVERED, - timestamp: Date.now() + runTaskInQueue(async () => { + const messageId = packet.getMessageId(); + await runQuery(`UPDATE messages SET delivered = ?, timestamp = ? WHERE message_id = ? AND account = ?`, [DeliveredMessageState.DELIVERED, Date.now(), messageId, publicKey]); + updateDialog(packet.getToPublicKey()); + log("Delivery packet received msg id " + messageId); + addOrUpdateDialogCache(packet.getToPublicKey(), getDialogCache(packet.getToPublicKey()).map((message) => { + if (message.message_id == messageId) { + return { + ...message, + delivered: DeliveredMessageState.DELIVERED, + timestamp: Date.now() + } } - } - return message; - })); + return message; + })); + }); }, [updateDialog]); } \ No newline at end of file diff --git a/app/providers/DialogProvider/useSynchronize.ts b/app/providers/DialogProvider/useSynchronize.ts new file mode 100644 index 0000000..873aeaf --- /dev/null +++ b/app/providers/DialogProvider/useSynchronize.ts @@ -0,0 +1,57 @@ +import { useEffect } from "react"; +import { useProtocolState } from "../ProtocolProvider/useProtocolState"; +import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; +import { useDatabase } from "../DatabaseProvider/useDatabase"; +import { usePublicKey } from "../AccountProvider/usePublicKey"; +import { PacketSync, SyncStatus } from "../ProtocolProvider/protocol/packets/packet.sync"; +import { useSender } from "../ProtocolProvider/useSender"; +import { usePacket } from "../ProtocolProvider/usePacket"; +import { whenFinish } from "./dialogQueue"; + +/** + * Хук отвечает за синхронизацию сообщений, запрос синхронизации + * при подключении + */ +export function useSynchronize() { + const [protocolState, setProtocolState] = useProtocolState(); + const {getQuery} = useDatabase(); + const publicKey = usePublicKey(); + const send = useSender(); + + useEffect(() => { + if(protocolState == ProtocolState.CONNECTED){ + trySync(); + setProtocolState(ProtocolState.SYNCHRONIZATION); + } + }, [protocolState]); + + const trySync = async () => { + const lastMessage = await getQuery("SELECT timestamp FROM messages WHERE account = ? ORDER BY timestamp DESC LIMIT 1", [publicKey]); + if(!lastMessage){ + sendSynchronize(0); + return; + } + sendSynchronize(lastMessage.timestamp); + } + + const sendSynchronize = (timestamp: number) => { + const packet = new PacketSync(); + packet.setStatus(0); + packet.setTimestamp(timestamp); + send(packet); + } + + usePacket(25, async (packet: PacketSync) => { + const status = packet.getStatus(); + if(status == SyncStatus.BATCH_END){ + await whenFinish(); + trySync(); + } + if(status == SyncStatus.NOT_NEEDED){ + /** + * Синхронизация не нужна, все данные актуальны + */ + setProtocolState(ProtocolState.CONNECTED); + } + }); +} \ No newline at end of file diff --git a/app/providers/ProtocolProvider/ProtocolProvider.tsx b/app/providers/ProtocolProvider/ProtocolProvider.tsx index 926ef70..4c70347 100644 --- a/app/providers/ProtocolProvider/ProtocolProvider.tsx +++ b/app/providers/ProtocolProvider/ProtocolProvider.tsx @@ -12,10 +12,13 @@ export enum ProtocolState { HANDSHAKE_EXCHANGE, DISCONNECTED, RECONNECTING, - DEVICE_VERIFICATION_REQUIRED + DEVICE_VERIFICATION_REQUIRED, + SYNCHRONIZATION } -export const ProtocolContext = createContext<[Protocol|null, ProtocolState]>([null, ProtocolState.DISCONNECTED]); +export type ProtocolContextType = [Protocol|null, ProtocolState, (state: ProtocolState) => void]; + +export const ProtocolContext = createContext([null, ProtocolState.DISCONNECTED, () => {}]); interface ProtocolProviderProps { children: React.ReactNode; @@ -91,7 +94,7 @@ export function ProtocolProvider(props : ProtocolProviderProps) { }, [publicKey, privateKey, systemInfo.id]); return ( - + {props.children} ); diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.sync.ts b/app/providers/ProtocolProvider/protocol/packets/packet.sync.ts new file mode 100644 index 0000000..b235991 --- /dev/null +++ b/app/providers/ProtocolProvider/protocol/packets/packet.sync.ts @@ -0,0 +1,48 @@ +import Packet from "../packet"; +import Stream from "../stream"; + +export enum SyncStatus { + NOT_NEEDED, + BATCH_START, + BATCH_END +} + +export class PacketSync extends Packet { + + private status : SyncStatus = SyncStatus.NOT_NEEDED; + private timestamp : number = 0; + + public getPacketId(): number { + return 25; //0x19 + } + + public _receive(stream: Stream): void { + this.status = stream.readInt8() as SyncStatus; + this.timestamp = stream.readInt64(); + } + + public _send(): Promise | Stream { + let stream = new Stream(); + stream.writeInt16(this.getPacketId()); + stream.writeInt8(this.status); + stream.writeInt64(this.timestamp); + return stream; + } + + public getStatus() : SyncStatus { + return this.status; + } + + public setStatus(status: SyncStatus) { + this.status = status; + } + + public getTimestamp() : number { + return this.timestamp; + } + + public setTimestamp(timestamp: number) { + this.timestamp = timestamp; + } + +} \ No newline at end of file diff --git a/app/providers/ProtocolProvider/protocol/protocol.ts b/app/providers/ProtocolProvider/protocol/protocol.ts index 8afba70..1fe6486 100644 --- a/app/providers/ProtocolProvider/protocol/protocol.ts +++ b/app/providers/ProtocolProvider/protocol/protocol.ts @@ -24,6 +24,7 @@ import { PacketGroupBan } from "./packets/packet.group.ban"; import { PacketDeviceNew } from "./packets/packet.device.new"; import { PacketDeviceList } from "./packets/packet.device.list"; import { PacketDeviceResolve } from "./packets/packet.device.resolve"; +import { PacketSync } from "./packets/packet.sync"; export default class Protocol extends EventEmitter { private serverAddress: string; @@ -123,6 +124,7 @@ export default class Protocol extends EventEmitter { this._supportedPackets.set(0x16, new PacketGroupBan()); this._supportedPackets.set(0x17, new PacketDeviceList()); this._supportedPackets.set(0x18, new PacketDeviceResolve()); + this._supportedPackets.set(25, new PacketSync()); } private _findWaiters(packetId: number): ((packet: Packet) => void)[] { diff --git a/app/providers/ProtocolProvider/useProtocolState.ts b/app/providers/ProtocolProvider/useProtocolState.ts index 4d22f59..2e9ef6e 100644 --- a/app/providers/ProtocolProvider/useProtocolState.ts +++ b/app/providers/ProtocolProvider/useProtocolState.ts @@ -1,12 +1,12 @@ import { useContext } from "react"; -import { ProtocolContext } from "./ProtocolProvider"; +import { ProtocolContext, ProtocolContextType, ProtocolState } from "./ProtocolProvider"; export const useProtocolState = () => { - const [context, connect] = useContext(ProtocolContext); + const context : ProtocolContextType = useContext(ProtocolContext); if(!context){ throw new Error("useProtocol must be used within a ProtocolProvider"); } - return connect; + return [context[1], context[2]] as [ProtocolState, (state: ProtocolState) => void]; }; \ No newline at end of file diff --git a/app/views/DeviceConfirm/DeviceConfirm.tsx b/app/views/DeviceConfirm/DeviceConfirm.tsx index e17a113..3fae8f2 100644 --- a/app/views/DeviceConfirm/DeviceConfirm.tsx +++ b/app/views/DeviceConfirm/DeviceConfirm.tsx @@ -12,7 +12,7 @@ import { usePacket } from "@/app/providers/ProtocolProvider/usePacket"; import { PacketDeviceResolve, Solution } from "@/app/providers/ProtocolProvider/protocol/packets/packet.device.resolve"; export function DeviceConfirm() { - const protocolState = useProtocolState(); + const [protocolState] = useProtocolState(); const navigate = useNavigate(); const logout = useLogout(); diff --git a/app/views/Main/Main.tsx b/app/views/Main/Main.tsx index 5f7ff6d..fdb45d1 100644 --- a/app/views/Main/Main.tsx +++ b/app/views/Main/Main.tsx @@ -30,6 +30,7 @@ import { useLogout } from "@/app/providers/AccountProvider/useLogout"; import { useUpdateMessage } from "@/app/hooks/useUpdateMessage"; import { useDeviceMessage } from "@/app/hooks/useDeviceMessage"; import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider"; +import { useSynchronize } from "@/app/providers/DialogProvider/useSynchronize"; export function Main() { const { mainColor, borderColor } = useRosettaColors(); @@ -54,6 +55,11 @@ export function Main() { */ useDeviceMessage(); + /** + * Синхронизируем сообщения при подключении + */ + useSynchronize(); + const { setSize, setResizeble } = useWindow(); /**