Промежуточный этап синхронизации
This commit is contained in:
@@ -31,7 +31,7 @@ export function ChatHeader() {
|
|||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const [blocked, blockUser, unblockUser] = useBlacklist(dialog);
|
const [blocked, blockUser, unblockUser] = useBlacklist(dialog);
|
||||||
const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog);
|
const [opponent, ___, forceUpdateUserInformation] = useUserInformation(dialog);
|
||||||
const protocolState = useProtocolState();
|
const [protocolState] = useProtocolState();
|
||||||
const [userTypeing, setUserTypeing] = useState(false);
|
const [userTypeing, setUserTypeing] = useState(false);
|
||||||
const timeoutRef = useRef<NodeJS.Timeout>(undefined);
|
const timeoutRef = useRef<NodeJS.Timeout>(undefined);
|
||||||
const avatars = useAvatars(dialog);
|
const avatars = useAvatars(dialog);
|
||||||
|
|||||||
39
app/components/DialogHeaderText/DialogHeaderText.tsx
Normal file
39
app/components/DialogHeaderText/DialogHeaderText.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { useRosettaColors } from "@/app/hooks/useRosettaColors";
|
||||||
|
import { ProtocolState } from "@/app/providers/ProtocolProvider/ProtocolProvider";
|
||||||
|
import { useProtocolState } from "@/app/providers/ProtocolProvider/useProtocolState";
|
||||||
|
import { Flex, Loader, Text } from "@mantine/core";
|
||||||
|
|
||||||
|
export function DialogHeaderText() {
|
||||||
|
const [protocolState] = useProtocolState();
|
||||||
|
const colors = useRosettaColors();
|
||||||
|
|
||||||
|
const headerType = () => {
|
||||||
|
switch(protocolState){
|
||||||
|
case ProtocolState.SYNCHRONIZATION:
|
||||||
|
return (<>
|
||||||
|
<Loader size={12} color={colors.chevrons.active}></Loader>
|
||||||
|
<Text fw={500} style={{
|
||||||
|
userSelect: 'none'
|
||||||
|
}} size={'sm'}>Updating...</Text>
|
||||||
|
</>);
|
||||||
|
case ProtocolState.CONNECTED:
|
||||||
|
return (<>
|
||||||
|
<Text fw={500} style={{
|
||||||
|
userSelect: 'none'
|
||||||
|
}} size={'sm'}>Chats</Text>
|
||||||
|
</>);
|
||||||
|
default:
|
||||||
|
return (<>
|
||||||
|
<Text fw={500} style={{
|
||||||
|
userSelect: 'none'
|
||||||
|
}} size={'sm'}>Chats</Text>
|
||||||
|
</>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex direction={'row'} align={'center'} gap={'xs'}>
|
||||||
|
{headerType()}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import { useLogout } from "@/app/providers/AccountProvider/useLogout";
|
|||||||
import { useHotkeys } from "@mantine/hooks";
|
import { useHotkeys } from "@mantine/hooks";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
|
import { usePublicKey } from "@/app/providers/AccountProvider/usePublicKey";
|
||||||
|
import { DialogHeaderText } from "../DialogHeaderText/DialogHeaderText";
|
||||||
|
|
||||||
export function DialogsPanelHeader() {
|
export function DialogsPanelHeader() {
|
||||||
const colors = useRosettaColors();
|
const colors = useRosettaColors();
|
||||||
@@ -66,9 +67,7 @@ export function DialogsPanelHeader() {
|
|||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Text fw={500} style={{
|
<DialogHeaderText></DialogHeaderText>
|
||||||
userSelect: 'none'
|
|
||||||
}} size={'sm'}>Chats</Text>
|
|
||||||
<Menu withArrow width={150} shadow="md">
|
<Menu withArrow width={150} shadow="md">
|
||||||
<Menu.Target>
|
<Menu.Target>
|
||||||
<IconEdit style={{
|
<IconEdit style={{
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export function GroupHeader() {
|
|||||||
const {deleteMessages, dialog} = useDialog();
|
const {deleteMessages, dialog} = useDialog();
|
||||||
const theme = useMantineTheme();
|
const theme = useMantineTheme();
|
||||||
const {groupInfo} = useGroupInformation(dialog);
|
const {groupInfo} = useGroupInformation(dialog);
|
||||||
const protocolState = useProtocolState();
|
const [protocolState] = useProtocolState();
|
||||||
const [usersTypeing, setUsersTypeing] = useState<{
|
const [usersTypeing, setUsersTypeing] = useState<{
|
||||||
timeout: NodeJS.Timeout | null,
|
timeout: NodeJS.Timeout | null,
|
||||||
fromPublicKey: string
|
fromPublicKey: string
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { MacFrameButtons } from "../MacFrameButtons/MacFrameButtons";
|
|||||||
|
|
||||||
export function Topbar() {
|
export function Topbar() {
|
||||||
const colors = useRosettaColors();
|
const colors = useRosettaColors();
|
||||||
const protocolState = useProtocolState();
|
const [protocolState] = useProtocolState();
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -16,14 +16,14 @@ export function Topbar() {
|
|||||||
{window.platform == 'win32' && <WindowsFrameButtons></WindowsFrameButtons>}
|
{window.platform == 'win32' && <WindowsFrameButtons></WindowsFrameButtons>}
|
||||||
{window.platform == 'darwin' && <MacFrameButtons></MacFrameButtons>}
|
{window.platform == 'darwin' && <MacFrameButtons></MacFrameButtons>}
|
||||||
{window.platform == 'linux' && <WindowsFrameButtons></WindowsFrameButtons>}
|
{window.platform == 'linux' && <WindowsFrameButtons></WindowsFrameButtons>}
|
||||||
{(protocolState == ProtocolState.CONNECTED || !window.location.hash.includes("main")) &&
|
{(protocolState == ProtocolState.CONNECTED || protocolState == ProtocolState.SYNCHRONIZATION || !window.location.hash.includes("main")) &&
|
||||||
<Flex align={'center'} justify={'center'}>
|
<Flex align={'center'} justify={'center'}>
|
||||||
<Text fw={'bolder'} fz={13} c={'gray'}>
|
<Text fw={'bolder'} fz={13} c={'gray'}>
|
||||||
Rosetta Messenger
|
Rosetta Messenger
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
}
|
}
|
||||||
{(protocolState != ProtocolState.CONNECTED && protocolState != ProtocolState.DEVICE_VERIFICATION_REQUIRED && window.location.hash.includes("main")) &&
|
{(protocolState != ProtocolState.CONNECTED && protocolState != ProtocolState.SYNCHRONIZATION && protocolState != ProtocolState.DEVICE_VERIFICATION_REQUIRED && window.location.hash.includes("main")) &&
|
||||||
<Flex align={'center'} gap={5} justify={'center'}>
|
<Flex align={'center'} gap={5} justify={'center'}>
|
||||||
<Loader size={12} color={colors.chevrons.active}></Loader>
|
<Loader size={12} color={colors.chevrons.active}></Loader>
|
||||||
<Text fw={'bolder'} fz={13} c={'gray'}>
|
<Text fw={'bolder'} fz={13} c={'gray'}>
|
||||||
|
|||||||
12
app/providers/DialogProvider/dialogQueue.ts
Normal file
12
app/providers/DialogProvider/dialogQueue.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
let tail: Promise<void> = Promise.resolve();
|
||||||
|
|
||||||
|
export const runTaskInQueue = (fn: () => Promise<void>) => {
|
||||||
|
tail = tail.then(fn).catch((e) => {
|
||||||
|
console.error("Dialog queue error", e);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ждет, пока все пакеты попадающие в очередь не будут обработаны
|
||||||
|
*/
|
||||||
|
export const whenFinish = () => tail;
|
||||||
@@ -44,7 +44,7 @@ export function useDialog() : {
|
|||||||
const privateKey = usePrivateKeyHash();
|
const privateKey = usePrivateKeyHash();
|
||||||
const privatePlain = usePrivatePlain();
|
const privatePlain = usePrivatePlain();
|
||||||
const {writeFile} = useFileStorage();
|
const {writeFile} = useFileStorage();
|
||||||
const protocolState = useProtocolState();
|
const [protocolState] = useProtocolState();
|
||||||
const {hasGroup, getGroupKey} = useGroups();
|
const {hasGroup, getGroupKey} = useGroups();
|
||||||
const {warn} = useConsoleLogger('useDialog');
|
const {warn} = useConsoleLogger('useDialog');
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { useGroups } from "./useGroups";
|
|||||||
import { useDialogState } from "../DialogStateProvider.tsx/useDialogState";
|
import { useDialogState } from "../DialogStateProvider.tsx/useDialogState";
|
||||||
import { useUserInformation } from "../InformationProvider/useUserInformation";
|
import { useUserInformation } from "../InformationProvider/useUserInformation";
|
||||||
import { useMentions } from "../DialogStateProvider.tsx/useMentions";
|
import { useMentions } from "../DialogStateProvider.tsx/useMentions";
|
||||||
|
import { runTaskInQueue } from "./dialogQueue";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* При вызове будет запущен "фоновый" обработчик
|
* При вызове будет запущен "фоновый" обработчик
|
||||||
@@ -41,14 +42,14 @@ export function useDialogFiber() {
|
|||||||
const notify = useNotification();
|
const notify = useNotification();
|
||||||
const focused = useWindowFocus();
|
const focused = useWindowFocus();
|
||||||
const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache();
|
const { getDialogCache, addOrUpdateDialogCache } = useDialogsCache();
|
||||||
const {info, error} = useConsoleLogger('useDialogFiber');
|
const { info, error } = useConsoleLogger('useDialogFiber');
|
||||||
const [viewState] = useViewPanelsState();
|
const [viewState] = useViewPanelsState();
|
||||||
const {writeFile} = useFileStorage();
|
const { writeFile } = useFileStorage();
|
||||||
const {updateDialog} = useDialogsList();
|
const { updateDialog } = useDialogsList();
|
||||||
const {hasGroup, getGroupKey, normalize} = useGroups();
|
const { hasGroup, getGroupKey, normalize } = useGroups();
|
||||||
const {muted} = useDialogState();
|
const { muted } = useDialogState();
|
||||||
const [userInfo] = useUserInformation(publicKey);
|
const [userInfo] = useUserInformation(publicKey);
|
||||||
const {pushMention} = useMentions();
|
const { pushMention } = useMentions();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Лог
|
* Лог
|
||||||
@@ -64,486 +65,501 @@ export function useDialogFiber() {
|
|||||||
* Метод нужен для синхронизации своих сообщений
|
* Метод нужен для синхронизации своих сообщений
|
||||||
*/
|
*/
|
||||||
usePacket(0x06, async (packet: PacketMessage) => {
|
usePacket(0x06, async (packet: PacketMessage) => {
|
||||||
const fromPublicKey = packet.getFromPublicKey();
|
runTaskInQueue(async () => {
|
||||||
const toPublicKey = packet.getToPublicKey();
|
const fromPublicKey = packet.getFromPublicKey();
|
||||||
const aesChachaKey = packet.getAesChachaKey();
|
const toPublicKey = packet.getToPublicKey();
|
||||||
const content = packet.getContent();
|
const aesChachaKey = packet.getAesChachaKey();
|
||||||
const timestamp = packet.getTimestamp();
|
const content = packet.getContent();
|
||||||
const messageId = packet.getMessageId();
|
const timestamp = packet.getTimestamp();
|
||||||
|
const messageId = packet.getMessageId();
|
||||||
|
|
||||||
|
|
||||||
if(fromPublicKey != publicKey){
|
if (fromPublicKey != publicKey) {
|
||||||
/**
|
|
||||||
* Игнорируем если это не сообщение от нас
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary");
|
|
||||||
const key = chachaDecryptedKey.slice(0, 32);
|
|
||||||
const nonce = chachaDecryptedKey.slice(32);
|
|
||||||
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
|
|
||||||
|
|
||||||
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);
|
return;
|
||||||
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`,
|
}
|
||||||
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary'));
|
const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary");
|
||||||
messageAttachments[nextLength - 1].blob = decryptedBlob;
|
const key = chachaDecryptedKey.slice(0, 32);
|
||||||
|
const nonce = chachaDecryptedKey.slice(32);
|
||||||
|
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
|
||||||
|
|
||||||
|
let attachmentsMeta: any[] = [];
|
||||||
|
let messageAttachments: Attachment[] = [];
|
||||||
|
for (let i = 0; i < packet.getAttachments().length; i++) {
|
||||||
|
const attachment = packet.getAttachments()[i];
|
||||||
|
log("Attachment received id " + attachment.id + " type " + attachment.type);
|
||||||
|
|
||||||
|
let nextLength = messageAttachments.push({
|
||||||
|
...attachment,
|
||||||
|
blob: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attachment.type == AttachmentType.MESSAGES) {
|
||||||
|
/**
|
||||||
|
* Этот тип вложения приходит сразу в blob и не нуждается
|
||||||
|
* в последующем скачивании
|
||||||
|
*/
|
||||||
|
const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob);
|
||||||
|
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`,
|
||||||
|
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary'));
|
||||||
|
messageAttachments[nextLength - 1].blob = decryptedBlob;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsMeta.push({
|
||||||
|
id: attachment.id,
|
||||||
|
type: attachment.type,
|
||||||
|
preview: attachment.preview
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentsMeta.push({
|
const newMessage: Message = {
|
||||||
id: attachment.id,
|
from_public_key: fromPublicKey,
|
||||||
type: attachment.type,
|
to_public_key: toPublicKey,
|
||||||
preview: attachment.preview
|
content: content,
|
||||||
});
|
timestamp: timestamp,
|
||||||
}
|
readed: 1, //сообщение прочитано
|
||||||
|
chacha_key: chachaDecryptedKey.toString('utf-8'),
|
||||||
|
from_me: 1, //сообщение от нас
|
||||||
|
plain_message: (decryptedContent as string),
|
||||||
|
delivered: DeliveredMessageState.DELIVERED,
|
||||||
|
message_id: messageId,
|
||||||
|
attachments: messageAttachments
|
||||||
|
};
|
||||||
|
|
||||||
const newMessage: Message = {
|
await runQuery(`
|
||||||
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
|
INSERT INTO messages
|
||||||
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, [fromPublicKey,
|
`, [fromPublicKey,
|
||||||
toPublicKey,
|
toPublicKey,
|
||||||
content,
|
content,
|
||||||
timestamp,
|
timestamp,
|
||||||
0, //по умолчанию не прочитаны
|
0, //по умолчанию не прочитаны
|
||||||
'',
|
'',
|
||||||
1, //Свои же сообщения всегда от нас
|
1, //Свои же сообщения всегда от нас
|
||||||
await encodeWithPassword(privatePlain, decryptedContent),
|
await encodeWithPassword(privatePlain, decryptedContent),
|
||||||
publicKey,
|
publicKey,
|
||||||
messageId,
|
messageId,
|
||||||
DeliveredMessageState.DELIVERED,
|
DeliveredMessageState.DELIVERED,
|
||||||
JSON.stringify(attachmentsMeta)]);
|
JSON.stringify(attachmentsMeta)]);
|
||||||
|
|
||||||
updateDialog(toPublicKey);
|
updateDialog(toPublicKey);
|
||||||
|
|
||||||
let dialogCache = getDialogCache(toPublicKey);
|
let dialogCache = getDialogCache(toPublicKey);
|
||||||
if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) {
|
if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) {
|
||||||
addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
|
addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}, [privatePlain, currentDialogPublicKeyView]);
|
}, [privatePlain, currentDialogPublicKeyView]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обработчик сообщений для группы
|
* Обработчик сообщений для группы
|
||||||
*/
|
*/
|
||||||
usePacket(0x06, async (packet: PacketMessage) => {
|
usePacket(0x06, async (packet: PacketMessage) => {
|
||||||
const fromPublicKey = packet.getFromPublicKey();
|
runTaskInQueue(async () => {
|
||||||
const toPublicKey = packet.getToPublicKey();
|
const fromPublicKey = packet.getFromPublicKey();
|
||||||
const content = packet.getContent();
|
const toPublicKey = packet.getToPublicKey();
|
||||||
const timestamp = packet.getTimestamp();
|
const content = packet.getContent();
|
||||||
const messageId = packet.getMessageId();
|
const timestamp = packet.getTimestamp();
|
||||||
if(!hasGroup(toPublicKey)){
|
const messageId = packet.getMessageId();
|
||||||
/**
|
if (!hasGroup(toPublicKey)) {
|
||||||
* Если это личное сообщение, то игнорируем его здесь
|
|
||||||
* для него есть отдельный слушатель usePacket (снизу)
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(fromPublicKey == publicKey){
|
|
||||||
/**
|
|
||||||
* Игнорируем свои же сообщения,
|
|
||||||
* такое получается при пакете синхронизации
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const groupKey = await getGroupKey(toPublicKey);
|
|
||||||
if(!groupKey){
|
|
||||||
log("Group key not found for group " + toPublicKey);
|
|
||||||
error("Message dropped because group key not found for group " + toPublicKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
info("New group message packet received from " + fromPublicKey);
|
|
||||||
|
|
||||||
let decryptedContent = '';
|
|
||||||
|
|
||||||
try{
|
|
||||||
decryptedContent = await decodeWithPassword(groupKey, content);
|
|
||||||
}catch(e) {
|
|
||||||
decryptedContent = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
let attachmentsMeta: any[] = [];
|
|
||||||
let messageAttachments: Attachment[] = [];
|
|
||||||
for (let i = 0; i < packet.getAttachments().length; i++) {
|
|
||||||
const attachment = packet.getAttachments()[i];
|
|
||||||
log("Attachment received id " + attachment.id + " type " + attachment.type);
|
|
||||||
|
|
||||||
let nextLength = messageAttachments.push({
|
|
||||||
...attachment,
|
|
||||||
blob: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
if(attachment.type == AttachmentType.MESSAGES){
|
|
||||||
/**
|
/**
|
||||||
* Этот тип вложения приходит сразу в blob и не нуждается
|
* Если это личное сообщение, то игнорируем его здесь
|
||||||
* в последующем скачивании
|
* для него есть отдельный слушатель usePacket (снизу)
|
||||||
*/
|
*/
|
||||||
const decryptedBlob = await decodeWithPassword(groupKey, attachment.blob);
|
return;
|
||||||
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`,
|
}
|
||||||
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary'));
|
if (fromPublicKey == publicKey) {
|
||||||
messageAttachments[nextLength - 1].blob = decryptedBlob;
|
/**
|
||||||
|
* Игнорируем свои же сообщения,
|
||||||
|
* такое получается при пакете синхронизации
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const groupKey = await getGroupKey(toPublicKey);
|
||||||
|
if (!groupKey) {
|
||||||
|
log("Group key not found for group " + toPublicKey);
|
||||||
|
error("Message dropped because group key not found for group " + toPublicKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info("New group message packet received from " + fromPublicKey);
|
||||||
|
|
||||||
|
let decryptedContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
decryptedContent = await decodeWithPassword(groupKey, content);
|
||||||
|
} catch (e) {
|
||||||
|
decryptedContent = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentsMeta.push({
|
let attachmentsMeta: any[] = [];
|
||||||
id: attachment.id,
|
let messageAttachments: Attachment[] = [];
|
||||||
type: attachment.type,
|
for (let i = 0; i < packet.getAttachments().length; i++) {
|
||||||
preview: attachment.preview
|
const attachment = packet.getAttachments()[i];
|
||||||
});
|
log("Attachment received id " + attachment.id + " type " + attachment.type);
|
||||||
}
|
|
||||||
|
|
||||||
const newMessage: Message = {
|
let nextLength = messageAttachments.push({
|
||||||
from_public_key: fromPublicKey,
|
...attachment,
|
||||||
to_public_key: toPublicKey,
|
blob: ""
|
||||||
content: content,
|
});
|
||||||
timestamp: timestamp,
|
|
||||||
readed: idle ? 0 : 1,
|
|
||||||
chacha_key: groupKey,
|
|
||||||
from_me: fromPublicKey == publicKey ? 1 : 0,
|
|
||||||
plain_message: decryptedContent,
|
|
||||||
delivered: DeliveredMessageState.DELIVERED,
|
|
||||||
message_id: messageId,
|
|
||||||
attachments: messageAttachments
|
|
||||||
};
|
|
||||||
|
|
||||||
await runQuery(`
|
if (attachment.type == AttachmentType.MESSAGES) {
|
||||||
|
/**
|
||||||
|
* Этот тип вложения приходит сразу в blob и не нуждается
|
||||||
|
* в последующем скачивании
|
||||||
|
*/
|
||||||
|
const decryptedBlob = await decodeWithPassword(groupKey, attachment.blob);
|
||||||
|
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`,
|
||||||
|
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary'));
|
||||||
|
messageAttachments[nextLength - 1].blob = decryptedBlob;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsMeta.push({
|
||||||
|
id: attachment.id,
|
||||||
|
type: attachment.type,
|
||||||
|
preview: attachment.preview
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMessage: Message = {
|
||||||
|
from_public_key: fromPublicKey,
|
||||||
|
to_public_key: toPublicKey,
|
||||||
|
content: content,
|
||||||
|
timestamp: timestamp,
|
||||||
|
readed: idle ? 0 : 1,
|
||||||
|
chacha_key: groupKey,
|
||||||
|
from_me: fromPublicKey == publicKey ? 1 : 0,
|
||||||
|
plain_message: decryptedContent,
|
||||||
|
delivered: DeliveredMessageState.DELIVERED,
|
||||||
|
message_id: messageId,
|
||||||
|
attachments: messageAttachments
|
||||||
|
};
|
||||||
|
|
||||||
|
await runQuery(`
|
||||||
INSERT INTO messages
|
INSERT INTO messages
|
||||||
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, [fromPublicKey,
|
`, [fromPublicKey,
|
||||||
toPublicKey,
|
toPublicKey,
|
||||||
content,
|
content,
|
||||||
timestamp,
|
timestamp,
|
||||||
/**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */
|
/**если текущий открытый диалог == беседе (которая приходит в toPublicKey) */
|
||||||
(currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0,
|
(currentDialogPublicKeyView == toPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0,
|
||||||
'',
|
'',
|
||||||
0,
|
0,
|
||||||
await encodeWithPassword(privatePlain, decryptedContent),
|
await encodeWithPassword(privatePlain, decryptedContent),
|
||||||
publicKey,
|
publicKey,
|
||||||
messageId,
|
messageId,
|
||||||
DeliveredMessageState.DELIVERED,
|
DeliveredMessageState.DELIVERED,
|
||||||
JSON.stringify(attachmentsMeta)]);
|
JSON.stringify(attachmentsMeta)]);
|
||||||
|
|
||||||
/**
|
|
||||||
* Так как у нас в toPublicKey приходит ID группы,
|
|
||||||
* то обновляем диалог по этому ID, а не по fromPublicKey
|
|
||||||
* как это сделано в личных сообщениях
|
|
||||||
*/
|
|
||||||
updateDialog(toPublicKey);
|
|
||||||
|
|
||||||
if (((normalize(currentDialogPublicKeyView) !== normalize(toPublicKey) || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) &&
|
|
||||||
(timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) {
|
|
||||||
/**
|
/**
|
||||||
* Условие со временем нужно для того,
|
* Так как у нас в toPublicKey приходит ID группы,
|
||||||
* чтобы когда приходит пачка сообщений с сервера в момент того как
|
* то обновляем диалог по этому ID, а не по fromPublicKey
|
||||||
* пользователь был неактивен, не слать уведомления по всем этим сообщениям
|
* как это сделано в личных сообщениях
|
||||||
*/
|
*/
|
||||||
let mentionFlag = false;
|
updateDialog(toPublicKey);
|
||||||
if((newMessage.from_public_key != publicKey) && (decryptedContent.includes(`@${userInfo.username}`) || decryptedContent.includes(`@all`))){
|
|
||||||
/**
|
|
||||||
* Если в сообщении есть упоминание текущего пользователя или @all,
|
|
||||||
* при этом сообщение отправляли не мы,
|
|
||||||
* то добавляем упоминание в состояние диалога.
|
|
||||||
*
|
|
||||||
* TODO: сделать чтобы all работал только для админов группы
|
|
||||||
*/
|
|
||||||
mentionFlag = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!muted.includes(toPublicKey) || mentionFlag){
|
if (((normalize(currentDialogPublicKeyView) !== normalize(toPublicKey) || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) &&
|
||||||
|
(timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) {
|
||||||
/**
|
/**
|
||||||
* Если группа не в мутие или есть упоминание - отправляем уведомление
|
* Условие со временем нужно для того,
|
||||||
|
* чтобы когда приходит пачка сообщений с сервера в момент того как
|
||||||
|
* пользователь был неактивен, не слать уведомления по всем этим сообщениям
|
||||||
*/
|
*/
|
||||||
notify("New message", "You have a new message");
|
let mentionFlag = false;
|
||||||
|
if ((newMessage.from_public_key != publicKey) && (decryptedContent.includes(`@${userInfo.username}`) || decryptedContent.includes(`@all`))) {
|
||||||
|
/**
|
||||||
|
* Если в сообщении есть упоминание текущего пользователя или @all,
|
||||||
|
* при этом сообщение отправляли не мы,
|
||||||
|
* то добавляем упоминание в состояние диалога.
|
||||||
|
*
|
||||||
|
* TODO: сделать чтобы all работал только для админов группы
|
||||||
|
*/
|
||||||
|
mentionFlag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!muted.includes(toPublicKey) || mentionFlag) {
|
||||||
|
/**
|
||||||
|
* Если группа не в мутие или есть упоминание - отправляем уведомление
|
||||||
|
*/
|
||||||
|
notify("New message", "You have a new message");
|
||||||
|
}
|
||||||
|
if (mentionFlag) {
|
||||||
|
/**
|
||||||
|
* Если в сообщении есть упоминание текущего пользователя или @all,
|
||||||
|
* то добавляем упоминание в состояние диалога
|
||||||
|
*
|
||||||
|
* TODO: сделать чтобы all работал только для админов группы
|
||||||
|
*/
|
||||||
|
pushMention({
|
||||||
|
dialog_id: toPublicKey,
|
||||||
|
message_id: messageId
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if(mentionFlag){
|
let dialogCache = getDialogCache(toPublicKey);
|
||||||
/**
|
if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) {
|
||||||
* Если в сообщении есть упоминание текущего пользователя или @all,
|
addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
|
||||||
* то добавляем упоминание в состояние диалога
|
|
||||||
*
|
|
||||||
* TODO: сделать чтобы all работал только для админов группы
|
|
||||||
*/
|
|
||||||
pushMention({
|
|
||||||
dialog_id: toPublicKey,
|
|
||||||
message_id: messageId
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
let dialogCache = getDialogCache(toPublicKey);
|
|
||||||
if (currentDialogPublicKeyView !== toPublicKey && dialogCache.length > 0) {
|
|
||||||
addOrUpdateDialogCache(toPublicKey, [...dialogCache, newMessage].slice(-MESSAGE_MAX_LOADED));
|
|
||||||
}
|
|
||||||
}, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]);
|
}, [blocked, muted, updateDialog, focused, currentDialogPublicKeyView, viewState, idle]);
|
||||||
/**
|
/**
|
||||||
* Обработчик личных сообщений
|
* Обработчик личных сообщений
|
||||||
*/
|
*/
|
||||||
usePacket(0x06, async (packet: PacketMessage) => {
|
usePacket(0x06, async (packet: PacketMessage) => {
|
||||||
const fromPublicKey = packet.getFromPublicKey();
|
runTaskInQueue(async () => {
|
||||||
if(fromPublicKey == publicKey){
|
const fromPublicKey = packet.getFromPublicKey();
|
||||||
/**
|
if (fromPublicKey == publicKey) {
|
||||||
* Игнорируем свои же сообщения,
|
|
||||||
* такое получается при пакете синхронизации
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const toPublicKey = packet.getToPublicKey();
|
|
||||||
const content = packet.getContent();
|
|
||||||
const chachaKey = packet.getChachaKey();
|
|
||||||
const timestamp = packet.getTimestamp();
|
|
||||||
const messageId = packet.getMessageId();
|
|
||||||
if(hasGroup(toPublicKey)){
|
|
||||||
/**
|
|
||||||
* Если это групповое сообщение, то игнорируем его здесь
|
|
||||||
* для него есть отдельный слушатель usePacket
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
info("New message packet received from " + fromPublicKey);
|
|
||||||
if (blocked.includes(fromPublicKey)) {
|
|
||||||
/**
|
|
||||||
* Если пользователь заблокирован и это не групповое сообщение,
|
|
||||||
* то игнорируем сообщение
|
|
||||||
*/
|
|
||||||
log("Message from blocked user, ignore " + fromPublicKey);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (privatePlain == "") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chachaDecryptedKey = Buffer.from(await decrypt(chachaKey, privatePlain), "binary");
|
|
||||||
const key = chachaDecryptedKey.slice(0, 32);
|
|
||||||
const nonce = chachaDecryptedKey.slice(32);
|
|
||||||
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
|
|
||||||
|
|
||||||
let attachmentsMeta: any[] = [];
|
|
||||||
let messageAttachments: Attachment[] = [];
|
|
||||||
for (let i = 0; i < packet.getAttachments().length; i++) {
|
|
||||||
const attachment = packet.getAttachments()[i];
|
|
||||||
log("Attachment received id " + attachment.id + " type " + attachment.type);
|
|
||||||
|
|
||||||
let nextLength = messageAttachments.push({
|
|
||||||
...attachment,
|
|
||||||
blob: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
if(attachment.type == AttachmentType.MESSAGES){
|
|
||||||
/**
|
/**
|
||||||
* Этот тип вложения приходит сразу в blob и не нуждается
|
* Игнорируем свои же сообщения,
|
||||||
* в последующем скачивании
|
* такое получается при пакете синхронизации
|
||||||
*/
|
*/
|
||||||
const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob);
|
return;
|
||||||
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`,
|
}
|
||||||
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary'));
|
const toPublicKey = packet.getToPublicKey();
|
||||||
messageAttachments[nextLength - 1].blob = decryptedBlob;
|
const content = packet.getContent();
|
||||||
|
const chachaKey = packet.getChachaKey();
|
||||||
|
const timestamp = packet.getTimestamp();
|
||||||
|
const messageId = packet.getMessageId();
|
||||||
|
if (hasGroup(toPublicKey)) {
|
||||||
|
/**
|
||||||
|
* Если это групповое сообщение, то игнорируем его здесь
|
||||||
|
* для него есть отдельный слушатель usePacket
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info("New message packet received from " + fromPublicKey);
|
||||||
|
if (blocked.includes(fromPublicKey)) {
|
||||||
|
/**
|
||||||
|
* Если пользователь заблокирован и это не групповое сообщение,
|
||||||
|
* то игнорируем сообщение
|
||||||
|
*/
|
||||||
|
log("Message from blocked user, ignore " + fromPublicKey);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentsMeta.push({
|
if (privatePlain == "") {
|
||||||
id: attachment.id,
|
return;
|
||||||
type: attachment.type,
|
}
|
||||||
preview: attachment.preview
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMessage: Message = {
|
const chachaDecryptedKey = Buffer.from(await decrypt(chachaKey, privatePlain), "binary");
|
||||||
from_public_key: fromPublicKey,
|
const key = chachaDecryptedKey.slice(0, 32);
|
||||||
to_public_key: toPublicKey,
|
const nonce = chachaDecryptedKey.slice(32);
|
||||||
content: content,
|
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
|
||||||
timestamp: timestamp,
|
|
||||||
readed: idle ? 0 : 1,
|
let attachmentsMeta: any[] = [];
|
||||||
chacha_key: chachaDecryptedKey.toString('utf-8'),
|
let messageAttachments: Attachment[] = [];
|
||||||
from_me: fromPublicKey == publicKey ? 1 : 0,
|
for (let i = 0; i < packet.getAttachments().length; i++) {
|
||||||
plain_message: (decryptedContent as string),
|
const attachment = packet.getAttachments()[i];
|
||||||
delivered: DeliveredMessageState.DELIVERED,
|
log("Attachment received id " + attachment.id + " type " + attachment.type);
|
||||||
message_id: messageId,
|
|
||||||
attachments: messageAttachments
|
let nextLength = messageAttachments.push({
|
||||||
};
|
...attachment,
|
||||||
|
blob: ""
|
||||||
|
});
|
||||||
|
|
||||||
|
if (attachment.type == AttachmentType.MESSAGES) {
|
||||||
|
/**
|
||||||
|
* Этот тип вложения приходит сразу в blob и не нуждается
|
||||||
|
* в последующем скачивании
|
||||||
|
*/
|
||||||
|
const decryptedBlob = await decodeWithPassword(chachaDecryptedKey.toString('utf-8'), attachment.blob);
|
||||||
|
writeFile(`m/${await generateMd5(attachment.id + publicKey)}`,
|
||||||
|
Buffer.from(await encodeWithPassword(privatePlain, decryptedBlob)).toString('binary'));
|
||||||
|
messageAttachments[nextLength - 1].blob = decryptedBlob;
|
||||||
|
}
|
||||||
|
|
||||||
|
attachmentsMeta.push({
|
||||||
|
id: attachment.id,
|
||||||
|
type: attachment.type,
|
||||||
|
preview: attachment.preview
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMessage: Message = {
|
||||||
|
from_public_key: fromPublicKey,
|
||||||
|
to_public_key: toPublicKey,
|
||||||
|
content: content,
|
||||||
|
timestamp: timestamp,
|
||||||
|
readed: idle ? 0 : 1,
|
||||||
|
chacha_key: chachaDecryptedKey.toString('utf-8'),
|
||||||
|
from_me: fromPublicKey == publicKey ? 1 : 0,
|
||||||
|
plain_message: (decryptedContent as string),
|
||||||
|
delivered: DeliveredMessageState.DELIVERED,
|
||||||
|
message_id: messageId,
|
||||||
|
attachments: messageAttachments
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
await runQuery(`
|
await runQuery(`
|
||||||
INSERT INTO messages
|
INSERT INTO messages
|
||||||
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
(from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
`, [fromPublicKey,
|
`, [fromPublicKey,
|
||||||
toPublicKey,
|
toPublicKey,
|
||||||
content,
|
content,
|
||||||
timestamp,
|
timestamp,
|
||||||
(currentDialogPublicKeyView == fromPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0,
|
(currentDialogPublicKeyView == fromPublicKey && !idle && viewState != ViewPanelsState.DIALOGS_PANEL_ONLY) ? 1 : 0,
|
||||||
chachaKey,
|
chachaKey,
|
||||||
0,
|
0,
|
||||||
await encodeWithPassword(privatePlain, decryptedContent),
|
await encodeWithPassword(privatePlain, decryptedContent),
|
||||||
publicKey,
|
publicKey,
|
||||||
messageId,
|
messageId,
|
||||||
DeliveredMessageState.DELIVERED,
|
DeliveredMessageState.DELIVERED,
|
||||||
JSON.stringify(attachmentsMeta)]);
|
JSON.stringify(attachmentsMeta)]);
|
||||||
|
|
||||||
log("New message received from " + fromPublicKey);
|
log("New message received from " + fromPublicKey);
|
||||||
|
|
||||||
updateDialog(fromPublicKey);
|
updateDialog(fromPublicKey);
|
||||||
if (((currentDialogPublicKeyView !== fromPublicKey || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) &&
|
if (((currentDialogPublicKeyView !== fromPublicKey || viewState == ViewPanelsState.DIALOGS_PANEL_ONLY) &&
|
||||||
(timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) {
|
(timestamp + TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD) > (Date.now() / 1000)) || !focused) {
|
||||||
/**
|
|
||||||
* Условие со временем нужно для того,
|
|
||||||
* чтобы когда приходит пачка сообщений с сервера в момент того как
|
|
||||||
* пользователь был неактивен, не слать уведомления по всем этим сообщениям
|
|
||||||
*/
|
|
||||||
if(!muted.includes(fromPublicKey)){
|
|
||||||
/**
|
/**
|
||||||
* Если пользователь в муте - не отправляем уведомление
|
* Условие со временем нужно для того,
|
||||||
|
* чтобы когда приходит пачка сообщений с сервера в момент того как
|
||||||
|
* пользователь был неактивен, не слать уведомления по всем этим сообщениям
|
||||||
*/
|
*/
|
||||||
notify("New message", "You have a new message");
|
if (!muted.includes(fromPublicKey)) {
|
||||||
|
/**
|
||||||
|
* Если пользователь в муте - не отправляем уведомление
|
||||||
|
*/
|
||||||
|
notify("New message", "You have a new message");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
let dialogCache = getDialogCache(fromPublicKey);
|
||||||
let dialogCache = getDialogCache(fromPublicKey);
|
if (currentDialogPublicKeyView !== fromPublicKey && dialogCache.length > 0) {
|
||||||
if (currentDialogPublicKeyView !== fromPublicKey && dialogCache.length > 0) {
|
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]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обработчик синхронизации прочтения личных сообщений
|
* Обработчик синхронизации прочтения личных сообщений
|
||||||
*/
|
*/
|
||||||
usePacket(0x07, async (packet: PacketRead) => {
|
usePacket(0x07, async (packet: PacketRead) => {
|
||||||
if(hasGroup(packet.getToPublicKey())){
|
runTaskInQueue(async () => {
|
||||||
/**
|
if (hasGroup(packet.getToPublicKey())) {
|
||||||
* Если это относится к группам, то игнорируем здесь,
|
/**
|
||||||
* для этого есть отдельный слушатель usePacket ниже
|
* Если это относится к группам, то игнорируем здесь,
|
||||||
*/
|
* для этого есть отдельный слушатель usePacket ниже
|
||||||
return;
|
*/
|
||||||
}
|
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;
|
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]);
|
}, [updateDialog, publicKey]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Обработчик прочтения личных сообщений
|
* Обработчик прочтения личных сообщений
|
||||||
*/
|
*/
|
||||||
usePacket(0x07, async (packet: PacketRead) => {
|
usePacket(0x07, async (packet: PacketRead) => {
|
||||||
if(hasGroup(packet.getToPublicKey())){
|
runTaskInQueue(async () => {
|
||||||
/**
|
if (hasGroup(packet.getToPublicKey())) {
|
||||||
* Если это относится к группам, то игнорируем здесь,
|
/**
|
||||||
* для этого есть отдельный слушатель usePacket ниже
|
* Если это относится к группам, то игнорируем здесь,
|
||||||
*/
|
* для этого есть отдельный слушатель usePacket ниже
|
||||||
return;
|
*/
|
||||||
}
|
return;
|
||||||
const fromPublicKey = packet.getFromPublicKey();
|
|
||||||
const toPublicKey = packet.getToPublicKey();
|
|
||||||
if(fromPublicKey == publicKey){
|
|
||||||
/**
|
|
||||||
* Игнорируем если это наше прочтение
|
|
||||||
* которое получается при синхронизации
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.info("PACKED_READ_IM");
|
|
||||||
await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]);
|
|
||||||
updateDialog(fromPublicKey);
|
|
||||||
log("Read packet received from " + fromPublicKey + " for " + toPublicKey);
|
|
||||||
addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => {
|
|
||||||
if (message.from_public_key == toPublicKey && !message.readed) {
|
|
||||||
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
|
|
||||||
console.info({fromPublicKey, toPublicKey});
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
readed: 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return message;
|
const fromPublicKey = packet.getFromPublicKey();
|
||||||
}));
|
const toPublicKey = packet.getToPublicKey();
|
||||||
|
if (fromPublicKey == publicKey) {
|
||||||
|
/**
|
||||||
|
* Игнорируем если это наше прочтение
|
||||||
|
* которое получается при синхронизации
|
||||||
|
*/
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.info("PACKED_READ_IM");
|
||||||
|
await runQuery(`UPDATE messages SET read = 1 WHERE from_public_key = ? AND to_public_key = ? AND account = ?`, [toPublicKey, fromPublicKey, publicKey]);
|
||||||
|
console.info("read im with params ", [fromPublicKey, toPublicKey, publicKey]);
|
||||||
|
updateDialog(fromPublicKey);
|
||||||
|
log("Read packet received from " + fromPublicKey + " for " + toPublicKey);
|
||||||
|
addOrUpdateDialogCache(fromPublicKey, getDialogCache(fromPublicKey).map((message) => {
|
||||||
|
if (message.from_public_key == toPublicKey && !message.readed) {
|
||||||
|
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
|
||||||
|
console.info({ fromPublicKey, toPublicKey });
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
readed: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}));
|
||||||
|
});
|
||||||
}, [updateDialog, publicKey]);
|
}, [updateDialog, publicKey]);
|
||||||
/**
|
/**
|
||||||
* Обработчик прочтения групповых сообщений
|
* Обработчик прочтения групповых сообщений
|
||||||
*/
|
*/
|
||||||
usePacket(0x07, async (packet: PacketRead) => {
|
usePacket(0x07, async (packet: PacketRead) => {
|
||||||
if(!hasGroup(packet.getToPublicKey())){
|
runTaskInQueue(async () => {
|
||||||
/**
|
if (!hasGroup(packet.getToPublicKey())) {
|
||||||
* Если это не относится к группам, то игнорируем здесь,
|
/**
|
||||||
* для этого есть отдельный слушатель usePacket выше
|
* Если это не относится к группам, то игнорируем здесь,
|
||||||
*/
|
* для этого есть отдельный слушатель usePacket выше
|
||||||
return;
|
*/
|
||||||
}
|
return;
|
||||||
const fromPublicKey = packet.getFromPublicKey();
|
|
||||||
const toPublicKey = packet.getToPublicKey();
|
|
||||||
await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]);
|
|
||||||
updateDialog(toPublicKey);
|
|
||||||
addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => {
|
|
||||||
if (!message.readed) {
|
|
||||||
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
|
|
||||||
console.info({fromPublicKey, toPublicKey});
|
|
||||||
return {
|
|
||||||
...message,
|
|
||||||
readed: 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return message;
|
const fromPublicKey = packet.getFromPublicKey();
|
||||||
}));
|
const toPublicKey = packet.getToPublicKey();
|
||||||
|
await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND from_public_key = ? AND account = ?`, [toPublicKey, publicKey, publicKey]);
|
||||||
|
updateDialog(toPublicKey);
|
||||||
|
addOrUpdateDialogCache(toPublicKey, getDialogCache(toPublicKey).map((message) => {
|
||||||
|
if (!message.readed) {
|
||||||
|
console.info("Marking message as read in cache for dialog with " + fromPublicKey);
|
||||||
|
console.info({ fromPublicKey, toPublicKey });
|
||||||
|
return {
|
||||||
|
...message,
|
||||||
|
readed: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}));
|
||||||
|
});
|
||||||
}, [updateDialog]);
|
}, [updateDialog]);
|
||||||
/**
|
/**
|
||||||
* Обработчик доставки сообщений
|
* Обработчик доставки сообщений
|
||||||
*/
|
*/
|
||||||
usePacket(0x08, async (packet: PacketDelivery) => {
|
usePacket(0x08, async (packet: PacketDelivery) => {
|
||||||
const messageId = packet.getMessageId();
|
runTaskInQueue(async () => {
|
||||||
await runQuery(`UPDATE messages SET delivered = ?, timestamp = ? WHERE message_id = ? AND account = ?`, [DeliveredMessageState.DELIVERED, Date.now(), messageId, publicKey]);
|
const messageId = packet.getMessageId();
|
||||||
updateDialog(packet.getToPublicKey());
|
await runQuery(`UPDATE messages SET delivered = ?, timestamp = ? WHERE message_id = ? AND account = ?`, [DeliveredMessageState.DELIVERED, Date.now(), messageId, publicKey]);
|
||||||
log("Delivery packet received msg id " + messageId);
|
updateDialog(packet.getToPublicKey());
|
||||||
addOrUpdateDialogCache(packet.getToPublicKey(), getDialogCache(packet.getToPublicKey()).map((message) => {
|
log("Delivery packet received msg id " + messageId);
|
||||||
if (message.message_id == messageId) {
|
addOrUpdateDialogCache(packet.getToPublicKey(), getDialogCache(packet.getToPublicKey()).map((message) => {
|
||||||
return {
|
if (message.message_id == messageId) {
|
||||||
...message,
|
return {
|
||||||
delivered: DeliveredMessageState.DELIVERED,
|
...message,
|
||||||
timestamp: Date.now()
|
delivered: DeliveredMessageState.DELIVERED,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return message;
|
||||||
return message;
|
}));
|
||||||
}));
|
});
|
||||||
}, [updateDialog]);
|
}, [updateDialog]);
|
||||||
}
|
}
|
||||||
57
app/providers/DialogProvider/useSynchronize.ts
Normal file
57
app/providers/DialogProvider/useSynchronize.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { useProtocolState } from "../ProtocolProvider/useProtocolState";
|
||||||
|
import { ProtocolState } from "../ProtocolProvider/ProtocolProvider";
|
||||||
|
import { useDatabase } from "../DatabaseProvider/useDatabase";
|
||||||
|
import { usePublicKey } from "../AccountProvider/usePublicKey";
|
||||||
|
import { PacketSync, SyncStatus } from "../ProtocolProvider/protocol/packets/packet.sync";
|
||||||
|
import { useSender } from "../ProtocolProvider/useSender";
|
||||||
|
import { usePacket } from "../ProtocolProvider/usePacket";
|
||||||
|
import { whenFinish } from "./dialogQueue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Хук отвечает за синхронизацию сообщений, запрос синхронизации
|
||||||
|
* при подключении
|
||||||
|
*/
|
||||||
|
export function useSynchronize() {
|
||||||
|
const [protocolState, setProtocolState] = useProtocolState();
|
||||||
|
const {getQuery} = useDatabase();
|
||||||
|
const publicKey = usePublicKey();
|
||||||
|
const send = useSender();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(protocolState == ProtocolState.CONNECTED){
|
||||||
|
trySync();
|
||||||
|
setProtocolState(ProtocolState.SYNCHRONIZATION);
|
||||||
|
}
|
||||||
|
}, [protocolState]);
|
||||||
|
|
||||||
|
const trySync = async () => {
|
||||||
|
const lastMessage = await getQuery("SELECT timestamp FROM messages WHERE account = ? ORDER BY timestamp DESC LIMIT 1", [publicKey]);
|
||||||
|
if(!lastMessage){
|
||||||
|
sendSynchronize(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sendSynchronize(lastMessage.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sendSynchronize = (timestamp: number) => {
|
||||||
|
const packet = new PacketSync();
|
||||||
|
packet.setStatus(0);
|
||||||
|
packet.setTimestamp(timestamp);
|
||||||
|
send(packet);
|
||||||
|
}
|
||||||
|
|
||||||
|
usePacket(25, async (packet: PacketSync) => {
|
||||||
|
const status = packet.getStatus();
|
||||||
|
if(status == SyncStatus.BATCH_END){
|
||||||
|
await whenFinish();
|
||||||
|
trySync();
|
||||||
|
}
|
||||||
|
if(status == SyncStatus.NOT_NEEDED){
|
||||||
|
/**
|
||||||
|
* Синхронизация не нужна, все данные актуальны
|
||||||
|
*/
|
||||||
|
setProtocolState(ProtocolState.CONNECTED);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -12,10 +12,13 @@ export enum ProtocolState {
|
|||||||
HANDSHAKE_EXCHANGE,
|
HANDSHAKE_EXCHANGE,
|
||||||
DISCONNECTED,
|
DISCONNECTED,
|
||||||
RECONNECTING,
|
RECONNECTING,
|
||||||
DEVICE_VERIFICATION_REQUIRED
|
DEVICE_VERIFICATION_REQUIRED,
|
||||||
|
SYNCHRONIZATION
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProtocolContext = createContext<[Protocol|null, ProtocolState]>([null, ProtocolState.DISCONNECTED]);
|
export type ProtocolContextType = [Protocol|null, ProtocolState, (state: ProtocolState) => void];
|
||||||
|
|
||||||
|
export const ProtocolContext = createContext<ProtocolContextType>([null, ProtocolState.DISCONNECTED, () => {}]);
|
||||||
|
|
||||||
interface ProtocolProviderProps {
|
interface ProtocolProviderProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -91,7 +94,7 @@ export function ProtocolProvider(props : ProtocolProviderProps) {
|
|||||||
}, [publicKey, privateKey, systemInfo.id]);
|
}, [publicKey, privateKey, systemInfo.id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProtocolContext.Provider value={[protocol, connect]}>
|
<ProtocolContext.Provider value={[protocol, connect, setConnect]}>
|
||||||
{props.children}
|
{props.children}
|
||||||
</ProtocolContext.Provider>
|
</ProtocolContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import Packet from "../packet";
|
||||||
|
import Stream from "../stream";
|
||||||
|
|
||||||
|
export enum SyncStatus {
|
||||||
|
NOT_NEEDED,
|
||||||
|
BATCH_START,
|
||||||
|
BATCH_END
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PacketSync extends Packet {
|
||||||
|
|
||||||
|
private status : SyncStatus = SyncStatus.NOT_NEEDED;
|
||||||
|
private timestamp : number = 0;
|
||||||
|
|
||||||
|
public getPacketId(): number {
|
||||||
|
return 25; //0x19
|
||||||
|
}
|
||||||
|
|
||||||
|
public _receive(stream: Stream): void {
|
||||||
|
this.status = stream.readInt8() as SyncStatus;
|
||||||
|
this.timestamp = stream.readInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
public _send(): Promise<Stream> | Stream {
|
||||||
|
let stream = new Stream();
|
||||||
|
stream.writeInt16(this.getPacketId());
|
||||||
|
stream.writeInt8(this.status);
|
||||||
|
stream.writeInt64(this.timestamp);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStatus() : SyncStatus {
|
||||||
|
return this.status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setStatus(status: SyncStatus) {
|
||||||
|
this.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTimestamp() : number {
|
||||||
|
return this.timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setTimestamp(timestamp: number) {
|
||||||
|
this.timestamp = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import { PacketGroupBan } from "./packets/packet.group.ban";
|
|||||||
import { PacketDeviceNew } from "./packets/packet.device.new";
|
import { PacketDeviceNew } from "./packets/packet.device.new";
|
||||||
import { PacketDeviceList } from "./packets/packet.device.list";
|
import { PacketDeviceList } from "./packets/packet.device.list";
|
||||||
import { PacketDeviceResolve } from "./packets/packet.device.resolve";
|
import { PacketDeviceResolve } from "./packets/packet.device.resolve";
|
||||||
|
import { PacketSync } from "./packets/packet.sync";
|
||||||
|
|
||||||
export default class Protocol extends EventEmitter {
|
export default class Protocol extends EventEmitter {
|
||||||
private serverAddress: string;
|
private serverAddress: string;
|
||||||
@@ -123,6 +124,7 @@ export default class Protocol extends EventEmitter {
|
|||||||
this._supportedPackets.set(0x16, new PacketGroupBan());
|
this._supportedPackets.set(0x16, new PacketGroupBan());
|
||||||
this._supportedPackets.set(0x17, new PacketDeviceList());
|
this._supportedPackets.set(0x17, new PacketDeviceList());
|
||||||
this._supportedPackets.set(0x18, new PacketDeviceResolve());
|
this._supportedPackets.set(0x18, new PacketDeviceResolve());
|
||||||
|
this._supportedPackets.set(25, new PacketSync());
|
||||||
}
|
}
|
||||||
|
|
||||||
private _findWaiters(packetId: number): ((packet: Packet) => void)[] {
|
private _findWaiters(packetId: number): ((packet: Packet) => void)[] {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { ProtocolContext } from "./ProtocolProvider";
|
import { ProtocolContext, ProtocolContextType, ProtocolState } from "./ProtocolProvider";
|
||||||
|
|
||||||
export const useProtocolState = () => {
|
export const useProtocolState = () => {
|
||||||
const [context, connect] = useContext(ProtocolContext);
|
const context : ProtocolContextType = useContext(ProtocolContext);
|
||||||
|
|
||||||
if(!context){
|
if(!context){
|
||||||
throw new Error("useProtocol must be used within a ProtocolProvider");
|
throw new Error("useProtocol must be used within a ProtocolProvider");
|
||||||
}
|
}
|
||||||
|
|
||||||
return connect;
|
return [context[1], context[2]] as [ProtocolState, (state: ProtocolState) => void];
|
||||||
};
|
};
|
||||||
@@ -12,7 +12,7 @@ import { usePacket } from "@/app/providers/ProtocolProvider/usePacket";
|
|||||||
import { PacketDeviceResolve, Solution } from "@/app/providers/ProtocolProvider/protocol/packets/packet.device.resolve";
|
import { PacketDeviceResolve, Solution } from "@/app/providers/ProtocolProvider/protocol/packets/packet.device.resolve";
|
||||||
|
|
||||||
export function DeviceConfirm() {
|
export function DeviceConfirm() {
|
||||||
const protocolState = useProtocolState();
|
const [protocolState] = useProtocolState();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const logout = useLogout();
|
const logout = useLogout();
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { useLogout } from "@/app/providers/AccountProvider/useLogout";
|
|||||||
import { useUpdateMessage } from "@/app/hooks/useUpdateMessage";
|
import { useUpdateMessage } from "@/app/hooks/useUpdateMessage";
|
||||||
import { useDeviceMessage } from "@/app/hooks/useDeviceMessage";
|
import { useDeviceMessage } from "@/app/hooks/useDeviceMessage";
|
||||||
import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider";
|
import { UpdateProvider } from "@/app/providers/UpdateProvider/UpdateProvider";
|
||||||
|
import { useSynchronize } from "@/app/providers/DialogProvider/useSynchronize";
|
||||||
|
|
||||||
export function Main() {
|
export function Main() {
|
||||||
const { mainColor, borderColor } = useRosettaColors();
|
const { mainColor, borderColor } = useRosettaColors();
|
||||||
@@ -54,6 +55,11 @@ export function Main() {
|
|||||||
*/
|
*/
|
||||||
useDeviceMessage();
|
useDeviceMessage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Синхронизируем сообщения при подключении
|
||||||
|
*/
|
||||||
|
useSynchronize();
|
||||||
|
|
||||||
const { setSize, setResizeble } = useWindow();
|
const { setSize, setResizeble } = useWindow();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user