Files
desktop/app/providers/DialogProvider/DialogProvider.tsx

954 lines
42 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { chacha20Decrypt, decodeWithPassword, decrypt, encodeWithPassword, generateMd5 } from '@/app/crypto/crypto';
import { useDatabase } from '@/app/providers/DatabaseProvider/useDatabase';
import { createContext, useEffect, useRef, useState } from 'react';
import { Attachment, AttachmentType, PacketMessage } from '@/app/providers/ProtocolProvider/protocol/packets/packet.message';
import { usePrivatePlain } from '../AccountProvider/usePrivatePlain';
import { usePublicKey } from '../AccountProvider/usePublicKey';
import { PacketRead } from '@/app/providers/ProtocolProvider/protocol/packets/packet.read';
import { usePrivateKeyHash } from '../AccountProvider/usePrivateKeyHash';
import { useMemory } from '../MemoryProvider/useMemory';
import { useBlacklist } from '../BlacklistProvider/useBlacklist';
import { useLogger } from '@/app/hooks/useLogger';
import { useSender } from '../ProtocolProvider/useSender';
import { usePacket } from '../ProtocolProvider/usePacket';
import { MAX_MESSAGES_LOAD, MESSAGE_MAX_LOADED, MESSAGE_MAX_TIME_TO_DELEVERED_S, TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD } from '@/app/constants';
import { PacketDelivery } from '@/app/providers/ProtocolProvider/protocol/packets/packet.delivery';
import { useIdle } from '@mantine/hooks';
import { useWindowFocus } from '@/app/hooks/useWindowFocus';
import { useDialogsCache } from './useDialogsCache';
import { useConsoleLogger } from '@/app/hooks/useConsoleLogger';
import { useViewPanelsState, ViewPanelsState } from '@/app/hooks/useViewPanelsState';
import { MessageReply } from './useReplyMessages';
import { useTransport } from '../TransportProvider/useTransport';
import { useFileStorage } from '@/app/hooks/useFileStorage';
import { useSystemAccounts } from '../SystemAccountsProvider/useSystemAccounts';
import { useDialogsList } from '../DialogListProvider/useDialogsList';
import { useGroups } from './useGroups';
import { useMentions } from '../DialogStateProvider.tsx/useMentions';
export interface DialogContextValue {
loading: boolean;
messages: Message[];
setMessages: (messages: React.SetStateAction<Message[]>) => void;
dialog: string;
clearDialogCache: () => void;
prepareAttachmentsToSend: (password: string, attachments: Attachment[]) => Promise<Attachment[]>;
loadMessagesToTop: () => Promise<void>;
loadMessagesToMessageId: (messageId: string) => Promise<void>;
}
export const DialogContext = createContext<DialogContextValue | null>(null);
export enum DeliveredMessageState {
WAITING,
DELIVERED,
ERROR
}
export interface AttachmentMeta {
id: string;
type: AttachmentType;
preview: string;
}
export interface Message {
from_public_key: string;
to_public_key: string;
content: string;
timestamp: number;
readed: number;
chacha_key: string;
from_me: number;
plain_message: string;
delivered: DeliveredMessageState;
message_id: string;
attachments: Attachment[];
}
interface DialogProviderProps {
children: React.ReactNode;
dialog: string;
}
export function DialogProvider(props: DialogProviderProps) {
const [messages, setMessages] = useState<Message[]>([]);
const {allQuery, runQuery} = useDatabase();
const privatePlain = usePrivatePlain();
const publicKey = usePublicKey();
const privateKey = usePrivateKeyHash();
const send = useSender();
const [__, setCurrentDialogPublicKeyView] = useMemory("current-dialog-public-key-view", "", true);
const log = useLogger('DialogProvider');
const [blocked] = useBlacklist(props.dialog)
const lastMessageTimeRef = useRef(0);
const idle = useIdle(TIME_TO_INACTIVE_FOR_MESSAGES_UNREAD * 1000);
const focus = useWindowFocus();
const [loading, setLoading] = useState(false);
const {getDialogCache, addOrUpdateDialogCache, dialogsCache, setDialogsCache} = useDialogsCache();
const {info, warn, error} = useConsoleLogger('DialogProvider');
const [viewState] = useViewPanelsState();
const {uploadFile} = useTransport();
const {readFile} = useFileStorage();
const intervalsRef = useRef<NodeJS.Timeout>(null);
const systemAccounts = useSystemAccounts();
const {updateDialog} = useDialogsList();
const {hasGroup, getGroupKey} = useGroups();
const {popMention, isMentioned} = useMentions();
useEffect(() => {
setCurrentDialogPublicKeyView(props.dialog);
return () => {
setCurrentDialogPublicKeyView("");
}
}, [props.dialog]);
useEffect(() => {
if(props.dialog == "demo"){
return;
}
if(idle){
return;
}
(async () => {
if(hasGroup(props.dialog)){
await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND account = ? AND read != 1 AND from_public_key != ?`, [props.dialog, publicKey, publicKey]);
}else{
await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]);
}
updateDialog(props.dialog);
})();
}, [idle, props.dialog]);
useEffect(() => {
if(props.dialog == "demo"){
return;
}
setMessages([]);
if(props.dialog == ""
|| privatePlain == "") {
return;
}
(async () => {
let dialogCacheEntry = getDialogCache(props.dialog);
if(dialogCacheEntry.length > 0){
const messagesToLoadFromCache = dialogCacheEntry.slice(-MESSAGE_MAX_LOADED);
setMessages(messagesToLoadFromCache);
info("Loading messages for " + props.dialog + " from cache");
setLoading(false);
if(hasGroup(props.dialog)){
await runQuery(`UPDATE messages SET read = 1 WHERE to_public_key = ? AND account = ? AND read != 1 AND from_public_key != ?`, [props.dialog, publicKey, publicKey]);
}else{
await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]);
}
if(isMentioned(props.dialog)){
/**
* Удаляем упоминания потому что мы только что загрузили
* диалог из кэша, может быть в нем есть упоминания
*/
for(let i = 0; i < messagesToLoadFromCache.length; i++){
const message = messagesToLoadFromCache[i];
popMention({
dialog_id: props.dialog,
message_id: message.message_id
});
}
}
updateDialog(props.dialog);
return;
}
info("Loading messages for " + props.dialog + " from database");
setLoading(true);
let result: any[] = [];
if (props.dialog != publicKey) {
if(hasGroup(props.dialog)){
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [props.dialog, publicKey, MAX_MESSAGES_LOAD]);
}else{
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [props.dialog, props.dialog, publicKey, publicKey, publicKey, MAX_MESSAGES_LOAD]);
}
} else {
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [publicKey, publicKey, publicKey, MAX_MESSAGES_LOAD]);
}
await runQuery(`UPDATE messages SET read = 1 WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? AND read != 1 AND from_me = 0`, [props.dialog, publicKey, publicKey, props.dialog, publicKey]);
const finalMessages : Message[] = [];
let readUpdated = false;
for(let i = 0; i < result.length; i++){
const message = result[i];
if(message.read != 1 && !readUpdated){
readUpdated = true;
}
let decryptKey = '';
if(message.from_me){
/**
* Если сообщение от меня, то ключ расшифровки для вложений
* не нужен, передаем пустую строку, так как под капотом
* в MessageAttachment.tsx при расшифровке вложений используется
* локальный ключ, а не тот что в сообщении, так как файл и так находится
* у нас локально
*/
decryptKey = '';
}
if(hasGroup(props.dialog)){
/**
* Если это групповое сообщение, то получаем ключ группы
*/
decryptKey = await getGroupKey(props.dialog);
}
if(!message.from_me && !hasGroup(props.dialog)){
/**
* Если сообщение не от меня и не групповое,
* расшифровываем ключ чачи своим приватным ключом
*/
decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8');
}
finalMessages.push({
from_public_key: message.from_public_key,
to_public_key: message.to_public_key,
content: "__ENCRYPTED__",
timestamp: message.timestamp,
readed: message.read || message.from_public_key == message.to_public_key,
chacha_key: decryptKey,
from_me: message.from_me,
plain_message: await loadMessage(message.plain_message),
delivered: message.delivered,
message_id: message.message_id,
attachments: await loadAttachments(message.attachments)
});
if(isMentioned(props.dialog)){
/**
* Если мы были упомянуты в этом диалоге, то убираем упоминание,
* так как мы только что загрузили это сообщение
*/
popMention({
dialog_id: props.dialog,
message_id: message.message_id
});
}
}
if(readUpdated){
updateDialog(props.dialog);
}
setMessages(finalMessages);
setLoading(false);
})();
}, [props.dialog]);
useEffect(() => {
if(props.dialog == "demo"){
return;
}
if(!messages || messages.length == 0){
return;
}
addOrUpdateDialogCache(props.dialog, messages);
}, [props.dialog, messages])
useEffect(() => {
if(props.dialog == publicKey || messages.length == 0
|| blocked
|| idle
|| lastMessageTimeRef.current == messages[messages.length - 1].timestamp
|| !focus){
return;
}
if(viewState == ViewPanelsState.DIALOGS_PANEL_ONLY){
/**
* Если мы сейчас видим только диалоги
* то сообщение мы не читаем
*/
return;
}
if(systemAccounts.find(acc => acc.publicKey == props.dialog)){
/**
* Системные аккаунты не отмечаем как прочитанные
*/
return;
}
const readPacket = new PacketRead();
readPacket.setFromPublicKey(publicKey);
readPacket.setToPublicKey(props.dialog);
readPacket.setPrivateKey(privateKey);
send(readPacket);
log("Send read packet to " + props.dialog);
info("Send read packet");
lastMessageTimeRef.current = messages[messages.length - 1].timestamp;
}, [props.dialog, viewState, focus, messages, blocked, idle]);
/**
* Обработчик чтения для личных сообщений
*/
usePacket(0x07, async (packet : PacketRead) => {
info("Read packet received in dialog provider");
const fromPublicKey = packet.getFromPublicKey();
if(fromPublicKey == publicKey){
/**
* Если это пакет синхронизации прочтения то игнорируем его здесь, для него есть другой обработчик
*/
return;
}
if(hasGroup(props.dialog)){
/**
* Для групп обработка чтения есть ниже
*/
return;
}
if(fromPublicKey != props.dialog && !idle){
return;
}
setMessages((prev) => prev.map((msg) => {
if(msg.from_public_key == publicKey && !msg.readed){
return {
...msg,
readed: 1
}
}
return msg;
}));
//updateDialog(props.dialog);
}, [idle, props.dialog]);
/**
* Обработчик чтения групповых сообщений
*/
usePacket(0x07, async (packet : PacketRead) => {
info("Read packet received in dialog provider");
const fromPublicKey = packet.getFromPublicKey();
if(fromPublicKey == publicKey){
/**
* Если это пакет синхронизации прочтения то игнорируем его здесь, для него есть другой обработчик
*/
return;
}
const toPublicKey = packet.getToPublicKey();
if(!hasGroup(props.dialog)){
/**
* Для личных сообщений обработка чтения выше
*/
return;
}
if(toPublicKey != props.dialog && !idle){
return;
}
setMessages((prev) => prev.map((msg) => {
if(msg.from_public_key == publicKey && !msg.readed){
return {
...msg,
readed: 1
}
}
return msg;
}));
//updateDialog(props.dialog);
}, [idle, props.dialog]);
/**
* Обработчик синхронизации чтения
*/
usePacket(0x07, async (packet : PacketRead) => {
const fromPublicKey = packet.getFromPublicKey();
const toPublicKey = packet.getToPublicKey();
if(fromPublicKey != publicKey){
/**
* Это не пакет синхронизации, игнорируем
*/
return;
}
if(toPublicKey != props.dialog){
/**
* Относится не к этому диалогу
*/
return;
}
setMessages((prev) => prev.map((msg) => {
if(msg.from_public_key == toPublicKey && !msg.readed){
return {
...msg,
readed: 1
}
}
return msg;
}));
}, [publicKey]);
usePacket(0x08, async (packet : PacketDelivery) => {
info("Delivery packet received in dialog provider");
const fromPublicKey = packet.getToPublicKey();
const messageId = packet.getMessageId();
if(fromPublicKey != props.dialog){
return;
}
setMessages((prev) => prev.map((msg) => {
if(msg.message_id == messageId && msg.delivered != DeliveredMessageState.DELIVERED){
return {
...msg,
delivered: DeliveredMessageState.DELIVERED,
timestamp: Date.now()
}
}
return msg;
}));
}, [props.dialog]);
/**
* Нам приходят сообщения от себя самих же при синхронизации
* нужно обрабатывать их особым образом соотвественно
*
* Метод нужен для синхронизации своих сообщений
*/
usePacket(0x06, async (packet: PacketMessage) => {
const fromPublicKey = packet.getFromPublicKey();
const toPublicKey = packet.getToPublicKey();
const aesChachaKey = packet.getAesChachaKey();
const content = packet.getContent();
const timestamp = packet.getTimestamp();
const messageId = packet.getMessageId();
if(fromPublicKey != publicKey){
/**
* Игнорируем если это не сообщение от нас
*/
return;
}
const chachaDecryptedKey = Buffer.from(await decodeWithPassword(privatePlain, aesChachaKey), "binary");
const key = chachaDecryptedKey.slice(0, 32);
const nonce = chachaDecryptedKey.slice(32);
const decryptedContent = await chacha20Decrypt(content, nonce.toString('hex'), key.toString('hex'));
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(chachaDecryptedKey.toString('utf-8'), attachment.blob) : ""
});
}
const newMessage: Message = {
from_public_key: fromPublicKey,
to_public_key: toPublicKey,
content: content,
timestamp: timestamp,
readed: 0, //сообщение прочитано
chacha_key: chachaDecryptedKey.toString('utf-8'),
from_me: 1, //сообщение от нас
plain_message: (decryptedContent as string),
delivered: DeliveredMessageState.DELIVERED,
message_id: messageId,
attachments: attachments
};
setMessages((prev) => ([...prev, newMessage]));
}, [privatePlain]);
/**
* Обработчик для личных сообщений
*/
usePacket(0x06, async (packet: PacketMessage) => {
const fromPublicKey = packet.getFromPublicKey();
const toPublicKey = packet.getToPublicKey();
if(hasGroup(props.dialog)){
/**
* Если это групповое сообщение, то для него есть
* другой обработчик ниже
*/
return;
}
if(fromPublicKey != props.dialog || toPublicKey != publicKey){
console.info("From " + fromPublicKey + " to " + props.dialog + " ignore");
return;
}
if(blocked){
warn("Message from blocked user, ignore " + fromPublicKey);
log("Message from blocked user, ignore " + fromPublicKey);
return;
}
const content = packet.getContent();
const chachaKey = packet.getChachaKey();
const timestamp = packet.getTimestamp();
/**
* Генерация рандомного ID сообщения по SEED нужна для того,
* чтобы сообщение записанное здесь в стек сообщений совпадало
* с тем что записывается в БД в файле useDialogFiber.ts
*/
const messageId = packet.getMessageId();
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 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(chachaDecryptedKey.toString('utf-8'), attachment.blob) : ""
});
}
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: attachments
};
setMessages((prev) => ([...prev, newMessage]));
}, [blocked, messages, idle, props.dialog]);
/**
* Обработчик для групповых сообщений
*/
usePacket(0x06, async (packet: PacketMessage) => {
const fromPublicKey = packet.getFromPublicKey();
const toPublicKey = packet.getToPublicKey();
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: idle ? 0 : 1,
chacha_key: groupKey,
from_me: fromPublicKey == publicKey ? 1 : 0,
plain_message: decryptedContent,
delivered: DeliveredMessageState.DELIVERED,
message_id: messageId,
attachments: attachments
};
setMessages((prev) => ([...prev, newMessage]));
}, [messages, idle, props.dialog]);
/**
* Расшифровывает сообщение
* @param message Зашифрованное сообщение
* @returns Расшифрованное сообщение
*/
const loadMessage = async (message : string) => {
try{
return await decodeWithPassword(privatePlain, message);
}catch(e){
return "";
}
}
/**
* Загружает часть диалога где есть определенный message_id
*/
const loadMessagesToMessageId = async (messageId: string) => {
warn("Load messages to message ID " + messageId + " for " + props.dialog);
if(props.dialog == "DELETED"
|| privatePlain == "") {
return;
}
let result : any[] = [];
if(props.dialog != publicKey){
if(hasGroup(props.dialog)){
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [props.dialog, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]);
}else{
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [props.dialog, props.dialog, publicKey, publicKey, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]);
}
}else{
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? AND timestamp <= (SELECT timestamp FROM messages WHERE message_id = ? AND account = ?) ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [publicKey, publicKey, publicKey, messageId, publicKey, MAX_MESSAGES_LOAD]);
}
const finalMessages : Message[] = [];
for(let i = 0; i < result.length; i++){
const message = result[i];
let decryptKey = '';
if(message.from_me){
/**
* Если сообщение от меня, то ключ расшифровки для вложений
* не нужен, передаем пустую строку, так как под капотом
* в MessageAttachment.tsx при расшифровке вложений используется
* локальный ключ, а не тот что в сообщении, так как файл и так находится
* у нас локально
*/
decryptKey = '';
}
if(hasGroup(props.dialog)){
/**
* Если это групповое сообщение, то получаем ключ группы
*/
decryptKey = await getGroupKey(props.dialog);
}
if(!message.from_me && !hasGroup(props.dialog)){
/**
* Если сообщение не от меня и не групповое,
* расшифровываем ключ чачи своим приватным ключом
*/
decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8');
}
finalMessages.push({
from_public_key: message.from_public_key,
to_public_key: message.to_public_key,
content: "__ENCRYPTED__",
timestamp: message.timestamp,
readed: message.read || message.from_public_key == message.to_public_key || !message.from_me,
chacha_key: decryptKey,
from_me: message.from_me,
plain_message: await loadMessage(message.plain_message),
delivered: message.delivered,
message_id: message.message_id,
attachments: await loadAttachments(message.attachments)
});
if(isMentioned(props.dialog)){
/**
* Если мы были упомянуты в этом диалоге, то убираем упоминание,
* так как мы только что загрузили это сообщение
*/
popMention({
dialog_id: props.dialog,
message_id: message.message_id
});
}
}
if(finalMessages.length == 0) {
return;
}
setMessages([...finalMessages]);
}
/**
* Загружает сообщения в верх диалога, когда пользователь
* скроллит вверх и доскроллил до конца
* @returns
*/
const loadMessagesToTop = async () => {
warn("Load messages to top for " + props.dialog);
if(props.dialog == "DELETED"
|| privatePlain == "") {
return;
}
let result : any[] = [];
if(props.dialog != publicKey){
if(hasGroup(props.dialog)){
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE (to_public_key = ?) AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [props.dialog, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]);
}else{
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE (from_public_key = ? OR to_public_key = ?) AND (from_public_key = ? OR to_public_key = ?) AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [props.dialog, props.dialog, publicKey, publicKey, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]);
}
}else{
result = await allQuery(`
SELECT * FROM (SELECT * FROM messages WHERE from_public_key = ? AND to_public_key = ? AND account = ? AND timestamp < ? ORDER BY timestamp DESC LIMIT ?) ORDER BY timestamp ASC
`, [publicKey, publicKey, publicKey, messages.length > 0 ? messages[0].timestamp : Math.floor(Date.now() / 1000), MAX_MESSAGES_LOAD]);
}
const finalMessages : Message[] = [];
for(let i = 0; i < result.length; i++){
const message = result[i];
let decryptKey = '';
if(message.from_me){
/**
* Если сообщение от меня, то ключ расшифровки для вложений
* не нужен, передаем пустую строку, так как под капотом
* в MessageAttachment.tsx при расшифровке вложений используется
* локальный ключ, а не тот что в сообщении, так как файл и так находится
* у нас локально
*/
decryptKey = '';
}
if(hasGroup(props.dialog)){
/**
* Если это групповое сообщение, то получаем ключ группы
*/
decryptKey = await getGroupKey(props.dialog);
}
if(!message.from_me && !hasGroup(props.dialog)){
/**
* Если сообщение не от меня и не групповое,
* расшифровываем ключ чачи своим приватным ключом
*/
decryptKey = Buffer.from(await decrypt(message.chacha_key, privatePlain), "binary").toString('utf-8');
}
finalMessages.push({
from_public_key: message.from_public_key,
to_public_key: message.to_public_key,
content: "__ENCRYPTED__",
timestamp: message.timestamp,
readed: message.read || message.from_public_key == message.to_public_key || !message.from_me,
chacha_key: decryptKey,
from_me: message.from_me,
plain_message: await loadMessage(message.plain_message),
delivered: message.delivered,
message_id: message.message_id,
attachments: await loadAttachments(message.attachments)
});
if(isMentioned(props.dialog)){
/**
* Если мы были упомянуты в этом диалоге, то убираем упоминание,
* так как мы только что загрузили это сообщение
*/
popMention({
dialog_id: props.dialog,
message_id: message.message_id
});
}
}
if(finalMessages.length == 0) {
return;
}
setMessages([...finalMessages, ...messages]);
}
/**
* Загружает вложения из JSON строки.
* Если вложение не было загружено (то есть его нет на диске),
* то в blob кладется пустая строка, далее в useAttachment
* это отработается (загрузится превью вложение и появится возможность скачать и тд)
* @param jsonAttachments JSON вложений AttachmentMeta формат
* @returns Вложения
*/
const loadAttachments = async (jsonAttachments : string) : Promise<Attachment[]> => {
if(jsonAttachments == "[]") {
return [];
}
try {
const attachmentsMeta : AttachmentMeta[] = JSON.parse(jsonAttachments);
const attachments : Attachment[] = [];
for(const meta of attachmentsMeta) {
if(meta.type == AttachmentType.FILE){
/**
* Все кроме файлов декодируем заранее
*/
attachments.push({
...meta,
blob: ""
});
continue;
}
const fileData = await readFile(`m/${await generateMd5(meta.id + publicKey)}`);
if(!fileData) {
attachments.push({
...meta,
blob: ""
});
continue;
}
const decrypted = await decodeWithPassword(privatePlain, Buffer.from(fileData, 'binary').toString());
attachments.push({
id: meta.id,
blob: decrypted,
type: meta.type,
preview: meta.preview
});
}
return attachments;
}catch(e) {
error("Failed to parse attachments");
}
return [];
}
/**
* Обновляет временную метку в сообщении, пока вложения отправляются,
* потому что если этого не делать, то сообщение может быть помечено как
* не доставленное из-за таймаута доставки
* @param attachments Вложения
*/
const doTimestampUpdateImMessageWhileAttachmentsSend = (attachments : Attachment[]) => {
if(intervalsRef.current){
clearInterval(intervalsRef.current);
}
intervalsRef.current = setInterval(() => {
//update timestamp in message to keep message marked as error
updateDialog(props.dialog);
setMessages((prev) => {
return prev.map((value) => {
if(value.attachments.length <= 0){
return value;
}
if(value.attachments[0].id != attachments[0].id){
return value;
}
runQuery("UPDATE messages SET timestamp = ? WHERE message_id = ?", [Date.now(), value.message_id]);
return {
...value,
timestamp: Date.now()
};
})
});
}, (MESSAGE_MAX_TIME_TO_DELEVERED_S / 2) * 1000);
}
/**
* Удаляет старый тег если вложения были подготовлены заново
* например при пересылке сообщений
*/
const removeOldTagIfAttachemtnsRePreapred = (preview : string) => {
if(preview.indexOf("::") == -1){
return preview;
}
let parts = preview.split("::");
return parts.slice(1).join("::");
}
/**
* Подготавливает вложения для отправки. Подготовка
* состоит в загрузке файлов на транспортный сервер, мы не делаем
* это через WebSocket из-за ограничений по размеру сообщений,
* а так же из-за надежности доставки файлов через HTTP
* @param attachments Attachments to prepare for sending
*/
const prepareAttachmentsToSend = async (password: string, attachments : Attachment[], rePrepared : boolean = false) : Promise<Attachment[]> => {
if(attachments.length <= 0){
return [];
}
let prepared : Attachment[] = [];
try{
for(let i = 0; i < attachments.length; i++){
const attachment : Attachment = attachments[i];
if(attachment.type == AttachmentType.MESSAGES){
let reply : MessageReply[] = JSON.parse(attachment.blob)
for(let j = 0; j < reply.length; j++){
reply[j].attachments = await prepareAttachmentsToSend(password, reply[j].attachments, true);
}
prepared.push({
...attachment,
blob: await encodeWithPassword(password, JSON.stringify(reply))
});
continue;
}
doTimestampUpdateImMessageWhileAttachmentsSend(attachments);
const content = await encodeWithPassword(password, attachment.blob);
const upid = attachment.id;
info(`Uploading attachment with upid: ${upid}`);
info(`Attachment content length: ${content.length}`);
let tag = await uploadFile(upid, content);
info(`Uploaded attachment with upid: ${upid}, received tag: ${tag}`);
if(intervalsRef.current != null){
clearInterval(intervalsRef.current);
}
prepared.push({
...attachment,
preview: tag + "::" + (rePrepared ? removeOldTagIfAttachemtnsRePreapred(attachment.preview) : attachment.preview),
blob: ""
});
}
return prepared;
}catch(e){
return prepared;
}
}
/**
* Дедубликация сообщений по message_id, так как может возникать ситуация, что одно и то же сообщение
* может загрузиться несколько раз при накладках сети, отставании часов, при синхронизации
* @param messages массив сообщений
* @returns массив уникальных сообщений
*/
const deduplicate = (messages: Message[]) => {
const map = new Map<string, Message>();
for(let i = 0; i < messages.length; i++){
const message = messages[i];
if(map.has(message.message_id)){
continue;
}
map.set(message.message_id, message);
}
return Array.from(map.values());
}
return (
<DialogContext.Provider value={{
loading,
messages: deduplicate(messages),
setMessages,
clearDialogCache: () => {
setDialogsCache(dialogsCache.filter((cache) => cache.publicKey != props.dialog));
},
dialog: props.dialog,
prepareAttachmentsToSend,
loadMessagesToTop,
loadMessagesToMessageId
}}>
{props.children}
</DialogContext.Provider>
)
}