835 lines
37 KiB
TypeScript
835 lines
37 KiB
TypeScript
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(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();
|
||
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(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();
|
||
|
||
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;
|
||
}
|
||
}
|
||
|
||
return (
|
||
<DialogContext.Provider value={{
|
||
loading,
|
||
messages,
|
||
setMessages,
|
||
clearDialogCache: () => {
|
||
setDialogsCache(dialogsCache.filter((cache) => cache.publicKey != props.dialog));
|
||
},
|
||
dialog: props.dialog,
|
||
prepareAttachmentsToSend,
|
||
loadMessagesToTop,
|
||
loadMessagesToMessageId
|
||
}}>
|
||
{props.children}
|
||
</DialogContext.Provider>
|
||
)
|
||
} |