'init'
This commit is contained in:
836
app/providers/DialogProvider/DialogProvider.tsx
Normal file
836
app/providers/DialogProvider/DialogProvider.tsx
Normal file
@@ -0,0 +1,836 @@
|
||||
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 { generateRandomKeyFormSeed } from '@/app/utils/utils';
|
||||
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)){
|
||||
/**
|
||||
* Если сообщение не от меня и не групповое,
|
||||
* расшифровываем ключ чачи своим приватным ключом
|
||||
*/
|
||||
console.info("Decrypting chacha key for message");
|
||||
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 = generateRandomKeyFormSeed(16, fromPublicKey + toPublicKey + timestamp.toString());
|
||||
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 = generateRandomKeyFormSeed(16, fromPublicKey + toPublicKey + timestamp.toString());
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user