1.0.7-1.5.0
All checks were successful
SP Builds / build (push) Successful in 5m34s

Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-02-24 16:46:36 +00:00
8 changed files with 534 additions and 194 deletions

View File

@@ -18,6 +18,8 @@ import { useDialogInfo } from "@/app/providers/DialogListProvider/useDialogInfo"
import { useDialogContextMenu } from "@/app/hooks/useDialogContextMenu"; import { useDialogContextMenu } from "@/app/hooks/useDialogContextMenu";
import { useDialogPin } from "@/app/providers/DialogStateProvider.tsx/useDialogPin"; import { useDialogPin } from "@/app/providers/DialogStateProvider.tsx/useDialogPin";
import { useDialogMute } from "@/app/providers/DialogStateProvider.tsx/useDialogMute"; 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 { export interface DialogProps extends DialogRow {
onClickDialog: (dialog: string) => void; onClickDialog: (dialog: string) => void;
@@ -51,6 +53,7 @@ export function Dialog(props : DialogProps) {
const isInCurrentDialog = props.dialog_id == сurrentDialogPublicKeyView; const isInCurrentDialog = props.dialog_id == сurrentDialogPublicKeyView;
const currentDialogColor = computedTheme == 'dark' ? '#2a6292' :'#438fd1'; const currentDialogColor = computedTheme == 'dark' ? '#2a6292' :'#438fd1';
const [protocolState] = useProtocolState();
usePacket(0x0B, (packet : PacketTyping) => { usePacket(0x0B, (packet : PacketTyping) => {
if(packet.getFromPublicKey() == opponent && packet.getToPublicKey() == publicKey && !fromMe){ 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)) && ( {!loading && (lastMessage.delivered == DeliveredMessageState.ERROR || (!isMessageDeliveredByTime(lastMessage.timestamp, lastMessage.attachments.length) && lastMessage.delivered != DeliveredMessageState.DELIVERED)) && (
<IconAlertCircle stroke={3} size={15} color={colors.error}></IconAlertCircle> <IconAlertCircle stroke={3} size={15} color={colors.error}></IconAlertCircle>
)} )}
{unreaded > 0 && !lastMessageFromMe && <Badge {unreaded > 0 && !lastMessageFromMe && protocolState != ProtocolState.SYNCHRONIZATION && <Badge
color={isInCurrentDialog ? 'white' : (isMuted ? colors.chevrons.active : colors.brandColor)} color={isInCurrentDialog ? 'white' : (isMuted ? colors.chevrons.active : colors.brandColor)}
c={isInCurrentDialog ? colors.brandColor : 'white'} c={isInCurrentDialog ? colors.brandColor : 'white'}
size={'sm'} circle={unreaded < 10}>{unreaded > 99 ? '99+' : unreaded}</Badge>} size={'sm'} circle={unreaded < 10}>{unreaded > 99 ? '99+' : unreaded}</Badge>}

View File

@@ -420,13 +420,18 @@ export function DialogProvider(props: DialogProviderProps) {
const timestamp = packet.getTimestamp(); const timestamp = packet.getTimestamp();
const messageId = packet.getMessageId(); const messageId = packet.getMessageId();
if(fromPublicKey != publicKey){ if(fromPublicKey != publicKey){
/** /**
* Игнорируем если это не сообщение от нас * Игнорируем если это не сообщение от нас
*/ */
return; return;
} }
if(hasGroup(toPublicKey)){
/**
* Есть другой обработчик для синхронизации групп
*/
return;
}
if(toPublicKey != props.dialog) { if(toPublicKey != props.dialog) {
/** /**
* Игнорируем если это не сообщение для этого диалога * Игнорируем если это не сообщение для этого диалога
@@ -466,6 +471,87 @@ export function DialogProvider(props: DialogProviderProps) {
setMessages((prev) => ([...prev, newMessage])); setMessages((prev) => ([...prev, newMessage]));
}, [privatePlain]); }, [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]);
/** /**
* Обработчик для личных сообщений * Обработчик для личных сообщений
*/ */

View File

@@ -27,6 +27,7 @@ import { useMentions } from "../DialogStateProvider.tsx/useMentions";
import { runTaskInQueue } from "./dialogQueue"; import { runTaskInQueue } from "./dialogQueue";
import { useProtocolState } from "../ProtocolProvider/useProtocolState"; import { useProtocolState } from "../ProtocolProvider/useProtocolState";
import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; import { ProtocolState } from "../ProtocolProvider/ProtocolProvider";
import { useUpdateSyncTime } from "./useUpdateSyncTime";
/** /**
* При вызове будет запущен "фоновый" обработчик * При вызове будет запущен "фоновый" обработчик
@@ -53,27 +54,7 @@ export function useDialogFiber() {
const [userInfo] = useUserInformation(publicKey); const [userInfo] = useUserInformation(publicKey);
const { pushMention } = useMentions(); const { pushMention } = useMentions();
const [protocolState] = useProtocolState(); const [protocolState] = useProtocolState();
const updateSyncTime = useUpdateSyncTime();
/**
* Обновляет время последней синхронизации для аккаунта
* @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]
);
};
/** /**
* Лог * Лог
@@ -82,101 +63,6 @@ export function useDialogFiber() {
info("Starting passive fiber for dialog packets"); 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)); 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"); notify("New message", "You have a new message");
} }
@@ -457,48 +343,7 @@ export function useDialogFiber() {
addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED)); addOrUpdateDialogCache(fromPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
} }
}); });
}, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]); }, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle, protocolState]);
/**
* Обработчик синхронизации прочтения личных сообщений
*/
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]);
/** /**
* Обработчик прочтения личных сообщений * Обработчик прочтения личных сообщений
@@ -540,20 +385,28 @@ export function useDialogFiber() {
})); }));
}); });
}, [updateDialog, publicKey]); }, [updateDialog, publicKey]);
/** /**
* Обработчик прочтения групповых сообщений * Обработчик прочтения групповых сообщений
*/ */
usePacket(0x07, async (packet: PacketRead) => { usePacket(0x07, async (packet: PacketRead) => {
runTaskInQueue(async () => { runTaskInQueue(async () => {
if (!hasGroup(packet.getToPublicKey())) { const fromPublicKey = packet.getFromPublicKey();
const toPublicKey = packet.getToPublicKey();
if (!hasGroup(toPublicKey)) {
/** /**
* Если это не относится к группам, то игнорируем здесь, * Если это не относится к группам, то игнорируем здесь,
* для этого есть отдельный слушатель usePacket выше * для этого есть отдельный слушатель usePacket выше
*/ */
return; return;
} }
const fromPublicKey = packet.getFromPublicKey(); if(fromPublicKey == publicKey){
const toPublicKey = packet.getToPublicKey(); /**
* Игнорируем если это наше прочтение
* которое получается при синхронизации
*/
return;
}
await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]); await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]);
await updateSyncTime(Date.now()); await updateSyncTime(Date.now());
updateDialog(toPublicKey); updateDialog(toPublicKey);

View File

@@ -162,26 +162,10 @@ export function useGroups() : {
const groupId = packet.getGroupId(); const groupId = packet.getGroupId();
info(`Creating group with id ${groupId}`); info(`Creating group with id ${groupId}`);
const encryptKey = generateRandomKey(64); 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(` joinGroup(await constructGroupString(groupId, title, encryptKey, description));
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)}`);
}); });
} }
@@ -201,9 +185,11 @@ export function useGroups() : {
const groupId = parsed.groupId; const groupId = parsed.groupId;
const title = parsed.title; const title = parsed.title;
const description = parsed.description; const description = parsed.description;
const encodedGroupString = await encodeWithPassword(privatePlain, groupString);
const packet = new PacketGroupJoin(); const packet = new PacketGroupJoin();
packet.setGroupId(parsed.groupId); packet.setGroupId(parsed.groupId);
packet.setGroupString(encodedGroupString);
send(packet); send(packet);
setLoading(true); setLoading(true);

View File

@@ -6,20 +6,56 @@ import { usePublicKey } from "../AccountProvider/usePublicKey";
import { PacketSync, SyncStatus } from "../ProtocolProvider/protocol/packets/packet.sync"; import { PacketSync, SyncStatus } from "../ProtocolProvider/protocol/packets/packet.sync";
import { useSender } from "../ProtocolProvider/useSender"; import { useSender } from "../ProtocolProvider/useSender";
import { usePacket } from "../ProtocolProvider/usePacket"; import { usePacket } from "../ProtocolProvider/usePacket";
import { whenFinish } from "./dialogQueue"; import { runTaskInQueue, whenFinish } from "./dialogQueue";
import { useProtocol } from "../ProtocolProvider/useProtocol"; 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() { export function useSynchronize() {
const [_, setProtocolState] = useProtocolState(); const [protocolState, setProtocolState] = useProtocolState();
const {getQuery, runQuery} = useDatabase(); const {getQuery, runQuery} = useDatabase();
const publicKey = usePublicKey(); const publicKey = usePublicKey();
const send = useSender(); const send = useSender();
const {protocol} = useProtocol(); 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(() => { useEffect(() => {
if(protocol.handshakeExchangeComplete){ if(protocol.handshakeExchangeComplete){
trySync(); trySync();
@@ -38,15 +74,47 @@ export function useSynchronize() {
send(packet); 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) => { usePacket(25, async (packet: PacketSync) => {
const status = packet.getStatus(); const status = packet.getStatus();
if(status == SyncStatus.BATCH_START){ if(status == SyncStatus.BATCH_START){
setProtocolState(ProtocolState.SYNCHRONIZATION); setProtocolState(ProtocolState.SYNCHRONIZATION);
} }
if(status == SyncStatus.BATCH_END){ if(status == SyncStatus.BATCH_END){
console.info("Batch start"); /**
* Этот Promise ждет пока все сообщения синхронизируются и обработаются, только
* после этого
*/
await whenFinish(); await whenFinish();
console.info("Batch finished");
await runQuery( await runQuery(
"INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " + "INSERT INTO accounts_sync_times (account, last_sync) VALUES (?, ?) " +
"ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?", "ON CONFLICT(account) DO UPDATE SET last_sync = ? WHERE account = ?",
@@ -62,4 +130,293 @@ export function useSynchronize() {
setProtocolState(ProtocolState.CONNECTED); setProtocolState(ProtocolState.CONNECTED);
} }
}, [publicKey]); }, [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]);
} }

View File

@@ -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<void> {
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;
}

View File

@@ -12,6 +12,14 @@ export class PacketGroupJoin extends Packet {
private groupId: string = ""; private groupId: string = "";
private groupStatus: GroupStatus = GroupStatus.NOT_JOINED; private groupStatus: GroupStatus = GroupStatus.NOT_JOINED;
/**
* Строка группы, которая содержит информацию о группе, такую как ее название, описание и ключ
* Строка зашифрована обратимым шифрованием, где ключом выступает - реальный приватный ключ
* входящего в группу клиента. Нужно это для будущей синхронзации, так как клиенту на его другом
* устройстве нужно получить ключ группы и ее информацию. Сервер расшифровать эту строку не может. Эту
* строку может расшифровать только клиент, так как она зашифрована его приватным ключом
*/
private groupString: string = "";
public getPacketId(): number { public getPacketId(): number {
return 0x14; return 0x14;
@@ -20,6 +28,7 @@ export class PacketGroupJoin extends Packet {
public _receive(stream: Stream): void { public _receive(stream: Stream): void {
this.groupId = stream.readString(); this.groupId = stream.readString();
this.groupStatus = stream.readInt8(); this.groupStatus = stream.readInt8();
this.groupString = stream.readString();
} }
public _send(): Promise<Stream> | Stream { public _send(): Promise<Stream> | Stream {
@@ -27,6 +36,7 @@ export class PacketGroupJoin extends Packet {
stream.writeInt16(this.getPacketId()); stream.writeInt16(this.getPacketId());
stream.writeString(this.groupId); stream.writeString(this.groupId);
stream.writeInt8(this.groupStatus); stream.writeInt8(this.groupStatus);
stream.writeString(this.groupString);
return stream; return stream;
} }
@@ -45,5 +55,13 @@ export class PacketGroupJoin extends Packet {
public getGroupStatus(): GroupStatus { public getGroupStatus(): GroupStatus {
return this.groupStatus; return this.groupStatus;
} }
public setGroupString(groupString: string) {
this.groupString = groupString;
}
public getGroupString(): string {
return this.groupString;
}
} }

View File

@@ -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 CORE_MIN_REQUIRED_VERSION = "1.5.0";
export const RELEASE_NOTICE = ` export const RELEASE_NOTICE = `
**Обновление v1.0.6** :emoji_1f631: **Обновление v1.0.7** :emoji_1f631:
- Исправлена очистка сообщения при нажатии ESC - Фикс уведомлений при синхронизации сообщений
- При клике на текст в сообщении теперь сообщение не уходит в ответ - Защищенная синхронизация ключей в группах
- Синхронизация сообщений в группах
- Синхронизация вложений в группах
- Синхронизация индикаторов прочтения
- Улучшенная организация кода и оптимизации
`; `;