import { useDatabase } from "@/app/providers/DatabaseProvider/useDatabase"; import { usePrivatePlain } from "../AccountProvider/usePrivatePlain"; import { decodeWithPassword, encodeWithPassword } from "@/app/crypto/crypto"; import { generateRandomKey } from "@/app/utils/utils"; import { useDialogsList } from "../DialogListProvider/useDialogsList"; import { usePublicKey } from "../AccountProvider/usePublicKey"; import { DeliveredMessageState } from "./DialogProvider"; import { useSender } from "../ProtocolProvider/useSender"; import { useState } from "react"; import { PacketCreateGroup } from "../ProtocolProvider/protocol/packets/packet.create.group"; import { useProtocol } from "../ProtocolProvider/useProtocol"; import { useConsoleLogger } from "@/app/hooks/useConsoleLogger"; import { GroupStatus, PacketGroupJoin } from "../ProtocolProvider/protocol/packets/packet.group.join"; import { useGroupInviteStatus } from "./useGroupInviteStatus"; import { useNavigate } from "react-router-dom"; import { useUpdateGroupInformation } from "../InformationProvider/useUpdateGroupInformation"; import { PacketGroupLeave } from "../ProtocolProvider/protocol/packets/packet.group.leave"; import { PacketGroupBan } from "../ProtocolProvider/protocol/packets/packet.group.ban"; export function useGroups() : { /** * Получаем ключ шифрования из базы данных по ид группы * @param groupId ид группы * @returns ключ шифрования */ getGroupKey: (groupId: string) => Promise; /** * Получает строку для приглашения в группу * @param groupId ид группы * @param title заголовок * @param encryptKey ключ шифрования * @param description описание * @returns строка, которая нужна для приглашения в группу */ constructGroupString: (groupId: string, title: string, encryptKey: string, description?: string) => Promise; /** * Функция, обратная constructGroupString, парсит строку приглашения в группу * @param groupString строка приглашения в группу * @returns объект с информацией о группе или null, если строка некорректна */ parseGroupString: (groupString: string) => Promise<{ groupId: string; title: string; encryptKey: string; description: string; } | null>; /** * Проверяет, является ли диалог группой * @param dialog ид диалога * @returns вернет true, если это группа и false если это пользователь */ hasGroup: (dialog: string) => boolean; /** * Возвращает подготовленный для роута groupId * @param groupId подготавливает groupId для роута * @returns заменяет символы которые может не обрабатывать роутер */ prepareForRoute: (groupId: string) => string; /** * Создает группу * @param title заголовок * @param description описание * @returns */ createGroup: (title: string, description: string) => Promise; /** * Зайдет в группу по строке приглашения * @param groupString строка приглашение * @returns */ joinGroup: (groupString: string) => Promise; /** * Покидает группу * @param groupId ид группы * @returns */ leaveGroup: (groupId: string) => Promise; /** * * @param str * @returns */ normalize: (str: string) => string; banUserOnGroup: (userPublicKey: string, groupId: string) => void; getPrefix: () => string; loading: boolean; } { const {allQuery, runQuery} = useDatabase(); const privatePlain = usePrivatePlain(); const {updateDialog} = useDialogsList(); const publicKey = usePublicKey(); const [loading, setLoading] = useState(false); const send = useSender(); const {protocol} = useProtocol(); const {info} = useConsoleLogger('useGroups'); const {setInviteStatusByGroupId} = useGroupInviteStatus(''); const navigate = useNavigate(); const updateGroupInformation = useUpdateGroupInformation(); const constructGroupString = async (groupId: string, title: string, encryptKey: string, description?: string) => { let groupString = `${groupId}:${title}:${encryptKey}`; if (description && description.trim().length > 0) { groupString += `:${description}`; } let encodedPayload = await encodeWithPassword('rosetta_group', groupString); return `#group:${encodedPayload}`; } const hasGroup = (dialog: string) => { return dialog.startsWith('#group:'); } const getPrefix = () => { return '#group:'; } const parseGroupString = async (groupString: string) => { try{ if (!groupString.startsWith('#group:')) { return null; } let encodedPayload = groupString.substring(7); let decodedPayload = await decodeWithPassword('rosetta_group', encodedPayload); let parts = decodedPayload.split(':'); return { groupId: parts[0], title: parts[1], encryptKey: parts[2], description: parts[3] || '' } }catch(e) { return null; } } const getGroupKey = async (groupId: string) => { const query = `SELECT key FROM groups WHERE group_id = ? AND account = ? LIMIT 1`; const result = await allQuery(query, [normalize(groupId), publicKey]); if(result.length > 0) { let key = result[0].key; return await decodeWithPassword(privatePlain, key); } return ""; }; const prepareForRoute = (groupId: string) => { return `#group:${groupId}`.replace('#', '%23'); } const normalize = (str: string) => { return str.replace('#group:', '').trim(); } const createGroup = async (title: string, description: string) => { if(title.trim().length === 0){ return; } setLoading(true); const packet = new PacketCreateGroup(); send(packet); protocol.waitPacketOnce(0x11, async (packet : PacketCreateGroup) => { const groupId = packet.getGroupId(); info(`Creating group with id ${groupId}`); const encryptKey = generateRandomKey(64); const secureKey = await encodeWithPassword(privatePlain, encryptKey); let content = await encodeWithPassword(encryptKey, `$a=Group created`); let plainMessage = await encodeWithPassword(privatePlain, `$a=Group created`); await runQuery(` INSERT INTO groups (account, group_id, title, description, key) VALUES (?, ?, ?, ?, ?) `, [publicKey, groupId, title, description, secureKey]); 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, "#group:" + groupId, content, Date.now(), 1, "", 1, plainMessage, publicKey, generateRandomKey(16), DeliveredMessageState.DELIVERED , '[]']); updateDialog("#group:" + groupId); updateGroupInformation({ groupId: groupId, title: title, description: description }); setLoading(false); navigate(`/main/chat/${prepareForRoute(groupId)}`); }); } const banUserOnGroup = (userPublicKey: string, groupId: string) => { const packet = new PacketGroupBan(); packet.setGroupId(groupId); packet.setPublicKey(userPublicKey); send(packet); } const joinGroup = async (groupString: string) => { const parsed = await parseGroupString(groupString); if (!parsed) { return; } const encryptKey = parsed.encryptKey; const groupId = parsed.groupId; const title = parsed.title; const description = parsed.description; const packet = new PacketGroupJoin(); packet.setGroupId(parsed.groupId); send(packet); setLoading(true); protocol.waitPacketOnce(0x14, async (packet: PacketGroupJoin) => { console.info(`Received group join response for group ${parsed.groupId}`); const groupStatus = packet.getGroupStatus(); if(groupStatus != GroupStatus.JOINED){ info(`Cannot join group ${parsed.groupId}, banned`); setInviteStatusByGroupId(parsed.groupId, groupStatus); setLoading(false); return; } const secureKey = await encodeWithPassword(privatePlain, encryptKey); let content = await encodeWithPassword(encryptKey, `$a=Group joined`); let plainMessage = await encodeWithPassword(privatePlain, `$a=Group joined`); await runQuery(` INSERT INTO groups (account, group_id, title, description, key) VALUES (?, ?, ?, ?, ?) `, [publicKey, groupId, title, description, secureKey]); 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, "#group:" + groupId, content, Date.now(), 1, "", 1, plainMessage, publicKey, generateRandomKey(16), DeliveredMessageState.DELIVERED , '[]']); updateDialog("#group:" + groupId); setInviteStatusByGroupId(groupId, GroupStatus.JOINED); setLoading(false); updateGroupInformation({ groupId: groupId, title: title, description: description }); navigate(`/main/chat/${prepareForRoute(groupId)}`); }); } const leaveGroup = async (groupId: string) => { const packet = new PacketGroupLeave(); packet.setGroupId(groupId); send(packet); setLoading(true); protocol.waitPacketOnce(0x15, async (packet: PacketGroupLeave) => { if(packet.getGroupId() != groupId){ return; } await runQuery(` DELETE FROM groups WHERE group_id = ? AND account = ? `, [groupId, publicKey]); await runQuery(` DELETE FROM messages WHERE to_public_key = ? AND account = ? `, ["#group:" + normalize(groupId), publicKey]); updateDialog("#group:" + normalize(groupId)); setLoading(false); navigate(`/main`); }); } return { getGroupKey, constructGroupString, parseGroupString, hasGroup, prepareForRoute, createGroup, joinGroup, leaveGroup, getPrefix, banUserOnGroup, normalize, loading } }