import { useContext } from "react"; import { useDatabase } from "../DatabaseProvider/useDatabase"; import { chacha20Encrypt, encodeWithPassword, encrypt, generateMd5} from "../../crypto/crypto"; import { AttachmentMeta, DeliveredMessageState, DialogContext, Message } from "./DialogProvider"; import { Attachment, AttachmentType, PacketMessage } from "@/app/providers/ProtocolProvider/protocol/packets/packet.message"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; import { usePrivateKeyHash } from "../AccountProvider/usePrivateKeyHash"; import { useSender } from "../ProtocolProvider/useSender"; import { generateRandomKey } from "@/app/utils/utils"; import { useFileStorage } from "@/app/hooks/useFileStorage"; import { useDialogsList } from "../DialogListProvider/useDialogsList"; import { useProtocolState } from "../ProtocolProvider/useProtocolState"; import { ProtocolState } from "../ProtocolProvider/ProtocolProvider"; import { useGroups } from "./useGroups"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; export function useDialog() : { messages: Message[]; sendMessage: (message: string, attachemnts : Attachment[]) => Promise; deleteMessages: () => Promise; loadMessagesToTop: (count?: number) => Promise; deleteMessageById: (messageId: string) => Promise; loading: boolean; deleteSelectedMessages: (messageIds: string[]) => Promise; dialog: string; loadMessagesToMessageId: (messageId: string) => Promise; updateAttachmentsInMessagesByAttachmentId: (attachmentId: string, blob: string) => Promise; } { const {runQuery} = useDatabase(); const send = useSender(); const context = useContext(DialogContext); if(!context) { throw new Error("useDialog must be used within a DialogProvider"); } const {loading, messages, prepareAttachmentsToSend, clearDialogCache, setMessages, dialog, loadMessagesToTop, loadMessagesToMessageId} = context; const {updateDialog} = useDialogsList(); const publicKey = usePublicKey(); const privateKey = usePrivateKeyHash(); const privatePlain = usePrivatePlain(); const {writeFile} = useFileStorage(); const [protocolState] = useProtocolState(); const {hasGroup, getGroupKey} = useGroups(); const {warn} = useConsoleLogger('useDialog'); /** * Отправка сообщения в диалог * @param message Сообщение * @param attachemnts Вложения */ const sendMessage = async (message: string, attachemnts : Attachment[]) => { const messageId = generateRandomKey(16); let cahchaEncrypted = {ciphertext: "", key: "", nonce: ""} as any; let key = Buffer.from(""); let encryptedKey = ""; let plainMessage = ""; let content = ""; if(!hasGroup(dialog)){ cahchaEncrypted = (await chacha20Encrypt(message.trim()) as any); key = Buffer.concat([ Buffer.from(cahchaEncrypted.key, "hex"), Buffer.from(cahchaEncrypted.nonce, "hex")]); encryptedKey = await encrypt(key.toString('binary'), dialog); plainMessage = await encodeWithPassword(privatePlain, message.trim()); content = cahchaEncrypted.ciphertext; }else{ /** * Это группа, там шифрование устроено иначе * для групп используется один общий ключ, который * есть только у участников группы, сам ключ при этом никак * не отправляется по сети (ведь ID у группы общий и у каждого * и так есть этот ключ) */ const groupKey = await getGroupKey(dialog); if(!groupKey){ warn("Group key not found for dialog " + dialog); return; } content = await encodeWithPassword(groupKey, message.trim()); plainMessage = await encodeWithPassword(privatePlain, message.trim()); encryptedKey = ""; // В группах не нужен зашифрованный ключ key = Buffer.from(groupKey); } /** * Нужно зашифровать ключ еще и нашим ключом, * чтобы в последствии мы могли расшифровать этот ключ у своих * же сообщений (смотреть problem_sync.md) */ const aesChachaKey = await encodeWithPassword(privatePlain, key.toString('binary')); setMessages((prev : Message[]) => ([...prev, { from_public_key: publicKey, to_public_key: dialog, content: content, timestamp: Date.now(), readed: publicKey == dialog ? 1 : 0, chacha_key: "", from_me: 1, plain_message: message, delivered: publicKey == dialog ? DeliveredMessageState.DELIVERED : DeliveredMessageState.WAITING, message_id: messageId, attachments: attachemnts }])); let attachmentsMeta : AttachmentMeta[] = []; for(let i = 0; i < attachemnts.length; i++) { const attachment = attachemnts[i]; attachmentsMeta.push({ id: attachment.id, type: attachment.type, preview: attachment.preview }); if(attachment.type == AttachmentType.FILE){ /** * Обычно вложения дублируются на диск. Так происходит со всем. * Кроме файлов. Если дублировать файл весом в 2гб на диск отправка будет * занимать очень много времени. * К тому же, это приведет к созданию ненужной копии у отправителя */ continue; } writeFile(`m/${await generateMd5(attachment.id + publicKey)}`, Buffer.from(await encodeWithPassword(privatePlain, attachment.blob)).toString('binary')); } await runQuery(` INSERT INTO messages (from_public_key, to_public_key, content, timestamp, read, chacha_key, from_me, plain_message, account, message_id, delivered, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [publicKey, dialog, content, Date.now(), publicKey == dialog ? 1 : 0, encryptedKey, 1, plainMessage, publicKey, messageId, publicKey == dialog ? DeliveredMessageState.DELIVERED : ( protocolState != ProtocolState.CONNECTED ? DeliveredMessageState.ERROR : DeliveredMessageState.WAITING ), JSON.stringify(attachmentsMeta)]); updateDialog(dialog); if(publicKey == "" || dialog == "" || publicKey == dialog) { return; } //98acbbc68f4b2449daf0a39d1b3eab9a3056da5d45b811bbc903e214c21d39643394980231e1a89c811830d870f3354184319665327ca8bd console.info("Sending key for message ", key.toString('hex')); let preparedToNetworkSendAttachements : Attachment[] = await prepareAttachmentsToSend(key.toString('utf-8'), attachemnts); if(attachemnts.length <= 0 && message.trim() == ""){ runQuery("UPDATE messages SET delivered = ? WHERE message_id = ?", [DeliveredMessageState.ERROR, messageId]); updateDialog(dialog); return; } const packet = new PacketMessage(); packet.setFromPublicKey(publicKey); packet.setToPublicKey(dialog); packet.setContent(content); packet.setChachaKey(encryptedKey); packet.setPrivateKey(privateKey); packet.setMessageId(messageId); packet.setTimestamp(Date.now()); packet.setAttachments(preparedToNetworkSendAttachements); packet.setAesChachaKey(aesChachaKey); send(packet); } const deleteMessages = async () => { if(!hasGroup(dialog)){ await runQuery(` DELETE FROM messages WHERE ((from_public_key = ? AND to_public_key = ?) OR (from_public_key = ? AND to_public_key = ?)) AND account = ? `, [dialog, publicKey, publicKey, dialog, publicKey]); }else{ await runQuery(` DELETE FROM messages WHERE to_public_key = ? AND account = ? `, [dialog, publicKey]); } setMessages([]); updateDialog(dialog); clearDialogCache(); } const deleteMessageById = async (messageId: string) => { await runQuery(` DELETE FROM messages WHERE message_id = ? AND account = ? `, [messageId, publicKey]); setMessages((prev) => prev.filter((msg) => msg.message_id !== messageId)); updateDialog(dialog); } const deleteSelectedMessages = async (messageIds: string[]) => { if(messageIds.length == 0){ return; } /** * Old messages support, ignore empty IDs * @since 0.1.7 all messages have IDs */ let idsNotEmpty = messageIds.filter(v => v.trim() != ""); if(idsNotEmpty.length == 0){ return; } const placeholders = idsNotEmpty.map(() => '?').join(','); await runQuery(` DELETE FROM messages WHERE message_id IN (` +placeholders+ `) AND account = ? `, [...idsNotEmpty, publicKey]); setMessages((prev) => prev.filter((msg) => !messageIds.includes(msg.message_id))); updateDialog(dialog); } const updateAttachmentsInMessagesByAttachmentId = async (attachmentId: string, blob: string) => { setMessages((prevMessages) => { return prevMessages.map((msg) => { let updated = false; const updatedAttachments = msg.attachments.map((attachment) => { if (attachment.id === attachmentId) { updated = true; return { ...attachment, blob: blob }; } return attachment; }); if (updated) { return { ...msg, attachments: updatedAttachments, }; } return msg; }); }); } return { messages, sendMessage, updateAttachmentsInMessagesByAttachmentId, deleteMessages, loadMessagesToTop, loadMessagesToMessageId, deleteMessageById, loading, deleteSelectedMessages, dialog, }; }