242 lines
11 KiB
TypeScript
242 lines
11 KiB
TypeScript
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,
|
||
};
|
||
} |