Files
desktop/app/providers/DialogProvider/useDialog.ts
rosetta 83f38dc63f 'init'
2026-01-30 05:01:05 +02:00

242 lines
11 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 { 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<void>;
deleteMessages: () => Promise<void>;
loadMessagesToTop: (count?: number) => Promise<void>;
deleteMessageById: (messageId: string) => Promise<void>;
loading: boolean;
deleteSelectedMessages: (messageIds: string[]) => Promise<void>;
dialog: string;
loadMessagesToMessageId: (messageId: string) => Promise<void>;
updateAttachmentsInMessagesByAttachmentId: (attachmentId: string, blob: string) => Promise<void>;
} {
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,
};
}