diff --git a/app/components/Dialog/Dialog.tsx b/app/components/Dialog/Dialog.tsx index d83ed15..9c4137c 100644 --- a/app/components/Dialog/Dialog.tsx +++ b/app/components/Dialog/Dialog.tsx @@ -18,6 +18,8 @@ import { useDialogInfo } from "@/app/providers/DialogListProvider/useDialogInfo" import { useDialogContextMenu } from "@/app/hooks/useDialogContextMenu"; import { useDialogPin } from "@/app/providers/DialogStateProvider.tsx/useDialogPin"; import { useDialogMute } from "@/app/providers/DialogStateProvider.tsx/useDialogMute"; +import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState"; +import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider"; export interface DialogProps extends DialogRow { onClickDialog: (dialog: string) => void; @@ -51,6 +53,7 @@ export function Dialog(props : DialogProps) { const isInCurrentDialog = props.dialog_id == сurrentDialogPublicKeyView; const currentDialogColor = computedTheme == 'dark' ? '#2a6292' :'#438fd1'; + const [protocolState] = useProtocolState(); usePacket(0x0B, (packet : PacketTyping) => { if(packet.getFromPublicKey() == opponent && packet.getToPublicKey() == publicKey && !fromMe){ @@ -153,7 +156,7 @@ export function Dialog(props : DialogProps) { {!loading && (lastMessage.delivered == DeliveredMessageState.ERROR || (!isMessageDeliveredByTime(lastMessage.timestamp, lastMessage.attachments.length) && lastMessage.delivered != DeliveredMessageState.DELIVERED)) && ( )} - {unreaded > 0 && !lastMessageFromMe && 0 && !lastMessageFromMe && protocolState != ProtocolState.SYNCHRONIZATION && {unreaded > 99 ? '99+' : unreaded}} diff --git a/app/providers/DialogProvider/DialogProvider.tsx b/app/providers/DialogProvider/DialogProvider.tsx index 38affed..adfdc44 100644 --- a/app/providers/DialogProvider/DialogProvider.tsx +++ b/app/providers/DialogProvider/DialogProvider.tsx @@ -420,13 +420,18 @@ export function DialogProvider(props: DialogProviderProps) { const timestamp = packet.getTimestamp(); const messageId = packet.getMessageId(); - if(fromPublicKey != publicKey){ /** * Игнорируем если это не сообщение от нас */ return; } + if(hasGroup(toPublicKey)){ + /** + * Есть другой обработчик для синхронизации групп + */ + return; + } if(toPublicKey != props.dialog) { /** * Игнорируем если это не сообщение для этого диалога @@ -466,6 +471,87 @@ export function DialogProvider(props: DialogProviderProps) { setMessages((prev) => ([...prev, newMessage])); }, [privatePlain]); + /** + * Обработчик сообщений для синхронизации своих же сообщений в группе + */ + usePacket(0x06, async (packet: PacketMessage) => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + + if(fromPublicKey != publicKey){ + /** + * Это не синхронизация, игнорируем ее в этом обработчике + */ + return; + } + + if(toPublicKey != props.dialog){ + /** + * Исправление кросс диалогового сообщения + */ + return; + } + + if(!hasGroup(props.dialog)){ + /** + * Если это не групповое сообщение, то для него есть + * другой обработчик выше + */ + return; + } + const content = packet.getContent(); + const timestamp = packet.getTimestamp(); + /** + * Генерация рандомного ID сообщения по SEED нужна для того, + * чтобы сообщение записанное здесь в стек сообщений совпадало + * с тем что записывается в БД в файле useDialogFiber.ts + */ + const messageId = packet.getMessageId(); + + 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 attachments: Attachment[] = []; + for(let i = 0; i < packet.getAttachments().length; i++) { + const attachment = packet.getAttachments()[i]; + attachments.push({ + id: attachment.id, + preview: attachment.preview, + type: attachment.type, + blob: attachment.type == AttachmentType.MESSAGES ? await decodeWithPassword(groupKey, attachment.blob) : "" + }); + } + + const newMessage : Message = { + from_public_key: fromPublicKey, + to_public_key: toPublicKey, + content: content, + timestamp: timestamp, + readed: 0, + chacha_key: groupKey, + from_me: 1, + plain_message: decryptedContent, + delivered: DeliveredMessageState.DELIVERED, + message_id: messageId, + attachments: attachments + }; + + setMessages((prev) => ([...prev, newMessage])); + }, [messages, idle, props.dialog]); + /** * Обработчик для личных сообщений */ diff --git a/app/providers/DialogProvider/useDialogFiber.ts b/app/providers/DialogProvider/useDialogFiber.ts index b3710b3..c2399ce 100644 --- a/app/providers/DialogProvider/useDialogFiber.ts +++ b/app/providers/DialogProvider/useDialogFiber.ts @@ -27,6 +27,7 @@ import { useMentions } from "../DialogStateProvider.tsx/useMentions"; import { runTaskInQueue } from "./dialogQueue"; import { useProtocolState } from "../ProtocolProvider/useProtocolState"; import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; +import { useUpdateSyncTime } from "./useUpdateSyncTime"; /** * При вызове будет запущен "фоновый" обработчик @@ -53,27 +54,7 @@ export function useDialogFiber() { const [userInfo] = useUserInformation(publicKey); const { pushMention } = useMentions(); const [protocolState] = useProtocolState(); - - /** - * Обновляет время последней синхронизации для аккаунта - * @param timestamp время - */ - const updateSyncTime = async (timestamp: number) => { - if(protocolState == ProtocolState.SYNCHRONIZATION){ - /** - * Если сейчас идет синхронизация то чтобы при синхронизации - * не создавать нагрузку на базу данных - * по постоянному обновлению, обновляем базу один раз - когда - * приходит пакет о том что синхронизация закончилась - */ - return; - } - await runQuery( - "INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " + - "ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?", - [publicKey, timestamp, timestamp, publicKey] - ); - }; + const updateSyncTime = useUpdateSyncTime(); /** * Лог @@ -82,101 +63,6 @@ export function useDialogFiber() { info("Starting passive fiber for dialog packets"); }, []); - /** - * Нам приходят сообщения от себя самих же при синхронизации - * нужно обрабатывать их особым образом соотвественно - * - * Метод нужен для синхронизации своих сообщений - */ - usePacket(0x06, async (packet: PacketMessage) => { - 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(); - if (fromPublicKey != publicKey) { - /** - * Игнорируем если это не сообщение от нас - */ - return; - } - - const chachaKey = await decodeWithPassword(privatePlain, aesChachaKey); - const chachaDecryptedKey = Buffer.from(chachaKey, "binary"); - const key = chachaDecryptedKey.slice(0, 32); - const nonce = chachaDecryptedKey.slice(32); - const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); - await updateSyncTime(timestamp); - 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: 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(` - 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, //по умолчанию не прочитаны - "sync:" + aesChachaKey, - 1, //Свои же сообщения всегда от нас - await encodeWithPassword(privatePlain, decryptedContent), - publicKey, - messageId, - DeliveredMessageState.DELIVERED, - JSON.stringify(attachmentsMeta)]); - - updateDialog(toPublicKey); - - let dialogCache = getDialogCache(toPublicKey); - if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { - addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); - } - }); - }, [privatePlain, currentDialogPublicKeyView]); - /** * Обработчик сообщений для группы */ @@ -328,7 +214,7 @@ export function useDialogFiber() { addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); } }); - }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); + }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]); /** * Обработчик личных сообщений */ @@ -445,9 +331,9 @@ export function useDialogFiber() { * чтобы когда приходит пачка сообщений с сервера в момент того как * пользователь был неактивен, не слать уведомления по всем этим сообщениям */ - if (!muted.includes(fromPublicKey) || protocolState != ProtocolState.SYNCHRONIZATION) { + if (!muted.includes(fromPublicKey) && protocolState != ProtocolState.SYNCHRONIZATION) { /** - * Если пользователь в муте или сейчас идет синхронизация - не отправляем уведомление + * Если пользователь в муте И сейчас не идет синхронизация, то не отправляем уведомление */ notify("New message", "You have a new message"); } @@ -457,48 +343,7 @@ export function useDialogFiber() { addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); } }); - }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); - - /** - * Обработчик синхронизации прочтения личных сообщений - */ - usePacket(0x07, async (packet: PacketRead) => { - runTaskInQueue(async () => { - 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 - } - } - return message; - })); - }); - }, [updateDialog, publicKey]); + }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]); /** * Обработчик прочтения личных сообщений @@ -540,20 +385,28 @@ export function useDialogFiber() { })); }); }, [updateDialog, publicKey]); + /** * Обработчик прочтения групповых сообщений */ usePacket(0x07, async (packet: PacketRead) => { runTaskInQueue(async () => { - if (!hasGroup(packet.getToPublicKey())) { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (!hasGroup(toPublicKey)) { /** * Если это не относится к группам, то игнорируем здесь, * для этого есть отдельный слушатель usePacket выше */ return; } - const fromPublicKey = packet.getFromPublicKey(); - const toPublicKey = packet.getToPublicKey(); + if(fromPublicKey == publicKey){ + /** + * Игнорируем если это наше прочтение + * которое получается при синхронизации + */ + return; + } await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]); await updateSyncTime(Date.now()); updateDialog(toPublicKey); diff --git a/app/providers/DialogProvider/useGroups.ts b/app/providers/DialogProvider/useGroups.ts index 78499d6..1935cdc 100644 --- a/app/providers/DialogProvider/useGroups.ts +++ b/app/providers/DialogProvider/useGroups.ts @@ -162,26 +162,10 @@ export function useGroups() : { const groupId = packet.getGroupId(); info(`Creating group with id ${groupId}`); const encryptKey = generateRandomKey(64); - const secureKey = await encodeWithPassword(privatePlain, encryptKey); - let content = await encodeWithPassword(encryptKey, `$a=Group created`); - let plainMessage = await encodeWithPassword(privatePlain, `$a=Group created`); - await runQuery(` - INSERT INTO groups (account, group_id, title, description, key) VALUES (?, ?, ?, ?, ?) - `, [publicKey, groupId, title, description, secureKey]); - 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `, [publicKey, "#group:" + groupId, content, Date.now(), 1, "", 1, plainMessage, publicKey, generateRandomKey(16), - DeliveredMessageState.DELIVERED - , '[]']); - updateDialog("#group:" + groupId); - updateGroupInformation({ - groupId: groupId, - title: title, - description: description - }); - setLoading(false); - navigate(`/main/chat/${prepareForRoute(groupId)}`); + /** + * После создания группы в нее необходимо зайти, в соотвествии с новым протоколом + */ + joinGroup(await constructGroupString(groupId, title, encryptKey, description)); }); } @@ -201,9 +185,11 @@ export function useGroups() : { const groupId = parsed.groupId; const title = parsed.title; const description = parsed.description; + const encodedGroupString = await encodeWithPassword(privatePlain, groupString); const packet = new PacketGroupJoin(); packet.setGroupId(parsed.groupId); + packet.setGroupString(encodedGroupString); send(packet); setLoading(true); diff --git a/app/providers/DialogProvider/useSynchronize.ts b/app/providers/DialogProvider/useSynchronize.ts index 0456a64..82c5bef 100644 --- a/app/providers/DialogProvider/useSynchronize.ts +++ b/app/providers/DialogProvider/useSynchronize.ts @@ -6,20 +6,56 @@ 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"; +import { runTaskInQueue, whenFinish } from "./dialogQueue"; import { useProtocol } from "../ProtocolProvider/useProtocol"; +import { PacketGroupJoin } from "../ProtocolProvider/protocol/packets/packet.group.join"; +import { useGroups } from "./useGroups"; +import { chacha20Decrypt, decodeWithPassword, encodeWithPassword, generateMd5 } from "@/app/workers/crypto/crypto"; +import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; +import { GroupStatus } from "../ProtocolProvider/protocol/packets/packet.group.invite.info"; +import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; +import { useDialogsList } from "../DialogListProvider/useDialogsList"; +import { useUpdateGroupInformation } from "../InformationProvider/useUpdateGroupInformation"; +import { useGroupInviteStatus } from "./useGroupInviteStatus"; +import { Attachment, AttachmentType, PacketMessage } from "../ProtocolProvider/protocol/packets/packet.message"; +import { useUpdateSyncTime } from "./useUpdateSyncTime"; +import { useFileStorage } from "@/app/hooks/useFileStorage"; +import { DeliveredMessageState, Message } from "./DialogProvider"; +import { MESSAGE_MAX_LOADED, TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD } from "@/app/constants"; +import { useMemory } from "../MemoryProvider/useMemory"; +import { useDialogsCache } from "./useDialogsCache"; +import { PacketRead } from "../ProtocolProvider/protocol/packets/packet.read"; +import { useLogger } from "@/app/hooks/useLogger"; +import { useIdle } from "@mantine/hooks"; +import { useViewPanelsState } from "@/app/hooks/useViewPanelsState"; +import { useWindowFocus } from "@/app/hooks/useWindowFocus"; /** * Хук отвечает за синхронизацию сообщений, запрос синхронизации * при подключении */ export function useSynchronize() { - const [_, setProtocolState] = useProtocolState(); + const [protocolState, setProtocolState] = useProtocolState(); const {getQuery, runQuery} = useDatabase(); const publicKey = usePublicKey(); const send = useSender(); const {protocol} = useProtocol(); + const {parseGroupString, hasGroup, getGroupKey} = useGroups(); + const privatePlain = usePrivatePlain(); + const {error, info} = useConsoleLogger('useSynchronize'); + const log = useLogger('useSynchronize'); + const {setInviteStatusByGroupId} = useGroupInviteStatus(''); + const updateGroupInformation = useUpdateGroupInformation(); + const {updateDialog} = useDialogsList(); + const idle = useIdle(TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD * 1000); + const updateSyncTime = useUpdateSyncTime(); + const {writeFile} = useFileStorage(); + const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache(); + const [currentDialogPublicKeyView, __] = useMemory("current-dialog-public-key-view", "", true); + const [viewState] = useViewPanelsState(); + const focused = useWindowFocus(); + useEffect(() => { if(protocol.handshakeExchangeComplete){ trySync(); @@ -38,15 +74,47 @@ export function useSynchronize() { send(packet); } + /** + * Пакет приходит либо при входе в группу (но там используется слушатель once), либо при + * синхронизации. В данном случае этот пакет прийдет только при синхронизации + */ + usePacket(20, async (packet: PacketGroupJoin) => { + const decryptedGroupString = await decodeWithPassword(privatePlain, packet.getGroupString()); + const parsed = await parseGroupString(decryptedGroupString); + if(!parsed){ + error("Received invalid group string, skipping"); + return; + } + const groupStatus = packet.getGroupStatus(); + if(groupStatus != GroupStatus.JOINED){ + error("Cannot sync group that is not joined, skipping"); + return; + } + const secureKey = await encodeWithPassword(privatePlain, parsed.encryptKey); + await runQuery(` + INSERT INTO groups (account, group_id, title, description, key) VALUES (?, ?, ?, ?, ?) + `, [publicKey, parsed.groupId, parsed.title, parsed.description, secureKey]); + updateDialog("#group:" + parsed.groupId); + setInviteStatusByGroupId(parsed.groupId, GroupStatus.JOINED); + updateGroupInformation({ + groupId: parsed.groupId, + title: parsed.title, + description: parsed.description + }); + info("Group synchronized " + parsed.groupId); + }, [publicKey]); + usePacket(25, async (packet: PacketSync) => { const status = packet.getStatus(); if(status == SyncStatus.BATCH_START){ setProtocolState(ProtocolState.SYNCHRONIZATION); } if(status == SyncStatus.BATCH_END){ - console.info("Batch start"); + /** + * Этот Promise ждет пока все сообщения синхронизируются и обработаются, только + * после этого + */ await whenFinish(); - console.info("Batch finished"); await runQuery( "INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " + "ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?", @@ -62,4 +130,293 @@ export function useSynchronize() { setProtocolState(ProtocolState.CONNECTED); } }, [publicKey]); + + /** + * Нам приходят сообщения от себя самих же при синхронизации + * нужно обрабатывать их особым образом соотвественно + * + * Метод нужен для синхронизации своих сообщений + */ + usePacket(0x06, async (packet: PacketMessage) => { + 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(); + if(hasGroup(toPublicKey)){ + /** + * Игнорируем если это сообщение для группы, для них есть отдельный слушатель usePacket ниже + */ + return; + } + + if (fromPublicKey != publicKey) { + /** + * Игнорируем если это не сообщение от нас + */ + return; + } + + const chachaKey = await decodeWithPassword(privatePlain, aesChachaKey); + const chachaDecryptedKey = Buffer.from(chachaKey, "binary"); + const key = chachaDecryptedKey.slice(0, 32); + const nonce = chachaDecryptedKey.slice(32); + const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex')); + await updateSyncTime(timestamp); + let attachmentsMeta: any[] = []; + let messageAttachments: Attachment[] = []; + for (let i = 0; i < packet.getAttachments().length; i++) { + const attachment = packet.getAttachments()[i]; + + + 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: 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(` + 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, //по умолчанию не прочитаны + "sync:" + aesChachaKey, + 1, //Свои же сообщения всегда от нас + await encodeWithPassword(privatePlain, decryptedContent), + publicKey, + messageId, + DeliveredMessageState.DELIVERED, + JSON.stringify(attachmentsMeta)]); + + updateDialog(toPublicKey); + + let dialogCache = getDialogCache(toPublicKey); + if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { + addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); + } + }); + }, [privatePlain, currentDialogPublicKeyView]); + + /** + * Обработчик синхронизации прочтения личных сообщений + */ + usePacket(0x07, async (packet: PacketRead) => { + runTaskInQueue(async () => { + if (hasGroup(packet.getToPublicKey())) { + /** + * Если это относится к группам, то игнорируем здесь, + * для этого есть отдельный слушатель usePacket ниже + */ + return; + } + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (fromPublicKey != publicKey) { + /** + * Игнорируем если это не синхронизация нашего прочтения + */ + return; + } + await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, + [toPublicKey, fromPublicKey, publicKey]); + + updateDialog(toPublicKey); + addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => { + if (message.from_public_key == toPublicKey && !message.readed) { + console.info({ fromPublicKey, toPublicKey }); + return { + ...message, + readed: 1 + } + } + return message; + })); + }); + }, [updateDialog, publicKey]); + + /** + * Обработчик синхронизации прочтения групповых сообщений + */ + usePacket(0x07, async (packet: PacketRead) => { + runTaskInQueue(async () => { + const fromPublicKey = packet.getFromPublicKey(); + const toPublicKey = packet.getToPublicKey(); + if (!hasGroup(toPublicKey)) { + /** + * Если это не относится к группам, то игнорируем здесь, + * для этого есть отдельный слушатель usePacket выше + */ + return; + } + if(fromPublicKey != publicKey){ + /** + * Игнорируем если это наше прочтение + * которое получается при синхронизации + */ + return; + } + await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key != ? AND account = ?`, + [toPublicKey, publicKey, publicKey]); + await updateSyncTime(Date.now()); + updateDialog(toPublicKey); + addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => { + if (!message.readed && message.from_public_key != publicKey) { + return { + ...message, + readed: 1 + } + } + return message; + })); + }); + }, [updateDialog]); + + /** + * Обработчик сообщений для синхронизации своих же сообщений в группе + */ + usePacket(0x06, async (packet: PacketMessage) => { + 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)) { + /** + * Если это личное сообщение, то игнорируем его здесь + * для него есть отдельный слушатель usePacket (снизу) + */ + return; + } + if (fromPublicKey != publicKey) { + /** + * Игнорируем если это сообщения не от нас + */ + return; + } + await updateSyncTime(timestamp); + 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) { + /** + * Этот тип вложения приходит сразу в 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: 0, + chacha_key: groupKey, + from_me: 1, + 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, + 0, //по умолчанию не прочитаны + "", + 1, //Свои же сообщения всегда от нас + await encodeWithPassword(privatePlain, decryptedContent), + publicKey, + messageId, + DeliveredMessageState.DELIVERED, + JSON.stringify(attachmentsMeta)]); + + /** + * Так как у нас в toPublicKey приходит ID группы, + * то обновляем диалог по этому ID, а не по fromPublicKey + * как это сделано в личных сообщениях + */ + updateDialog(toPublicKey); + + let dialogCache = getDialogCache(toPublicKey); + if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) { + addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); + } + }); + }, [updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]); } \ No newline at end of file diff --git a/app/providers/DialogProvider/useUpdateSyncTime.ts b/app/providers/DialogProvider/useUpdateSyncTime.ts new file mode 100644 index 0000000..45ad845 --- /dev/null +++ b/app/providers/DialogProvider/useUpdateSyncTime.ts @@ -0,0 +1,33 @@ +import { usePublicKey } from "../AccountProvider/usePublicKey"; +import { useDatabase } from "../DatabaseProvider/useDatabase"; +import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; +import { useProtocolState } from "../ProtocolProvider/useProtocolState"; + +export function useUpdateSyncTime() : (timestamp: number) => Promise { + const [protocolState] = useProtocolState(); + const {runQuery} = useDatabase(); + const publicKey = usePublicKey(); + + /** + * Обновляет время последней синхронизации для аккаунта + * @param timestamp время + */ + const updateSyncTime = async (timestamp: number) => { + if(protocolState == ProtocolState.SYNCHRONIZATION){ + /** + * Если сейчас идет синхронизация то чтобы при синхронизации + * не создавать нагрузку на базу данных + * по постоянному обновлению, обновляем базу один раз - когда + * приходит пакет о том что синхронизация закончилась + */ + return; + } + await runQuery( + "INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " + + "ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?", + [publicKey, timestamp, timestamp, publicKey] + ); + }; + + return updateSyncTime; +} \ No newline at end of file diff --git a/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts b/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts index 89c271c..2f9f71c 100644 --- a/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts +++ b/app/providers/ProtocolProvider/protocol/packets/packet.group.join.ts @@ -12,6 +12,14 @@ export class PacketGroupJoin extends Packet { private groupId: string = ""; private groupStatus: GroupStatus = GroupStatus.NOT_JOINED; + /** + * Строка группы, которая содержит информацию о группе, такую как ее название, описание и ключ + * Строка зашифрована обратимым шифрованием, где ключом выступает - реальный приватный ключ + * входящего в группу клиента. Нужно это для будущей синхронзации, так как клиенту на его другом + * устройстве нужно получить ключ группы и ее информацию. Сервер расшифровать эту строку не может. Эту + * строку может расшифровать только клиент, так как она зашифрована его приватным ключом + */ + private groupString: string = ""; public getPacketId(): number { return 0x14; @@ -20,6 +28,7 @@ export class PacketGroupJoin extends Packet { public _receive(stream: Stream): void { this.groupId = stream.readString(); this.groupStatus = stream.readInt8(); + this.groupString = stream.readString(); } public _send(): Promise | Stream { @@ -27,6 +36,7 @@ export class PacketGroupJoin extends Packet { stream.writeInt16(this.getPacketId()); stream.writeString(this.groupId); stream.writeInt8(this.groupStatus); + stream.writeString(this.groupString); return stream; } @@ -45,5 +55,13 @@ export class PacketGroupJoin extends Packet { public getGroupStatus(): GroupStatus { return this.groupStatus; } + + public setGroupString(groupString: string) { + this.groupString = groupString; + } + + public getGroupString(): string { + return this.groupString; + } } \ No newline at end of file diff --git a/app/version.ts b/app/version.ts index 9ff6ed3..bc70765 100644 --- a/app/version.ts +++ b/app/version.ts @@ -1,8 +1,12 @@ -export const APP_VERSION = "1.0.6"; +export const APP_VERSION = "1.0.7"; export const CORE_MIN_REQUIRED_VERSION = "1.5.0"; export const RELEASE_NOTICE = ` -**Обновление v1.0.6** :emoji_1f631: -- Исправлена очистка сообщения при нажатии ESC -- При клике на текст в сообщении теперь сообщение не уходит в ответ +**Обновление v1.0.7** :emoji_1f631: +- Фикс уведомлений при синхронизации сообщений +- Защищенная синхронизация ключей в группах +- Синхронизация сообщений в группах +- Синхронизация вложений в группах +- Синхронизация индикаторов прочтения +- Улучшенная организация кода и оптимизации `; \ No newline at end of file