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