Files
rosetta 83f38dc63f 'init'
2026-01-30 05:01:05 +02:00

278 lines
12 KiB
TypeScript
Raw Permalink 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 { 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<string>;
/**
* Получает строку для приглашения в группу
* @param groupId ид группы
* @param title заголовок
* @param encryptKey ключ шифрования
* @param description описание
* @returns строка, которая нужна для приглашения в группу
*/
constructGroupString: (groupId: string, title: string, encryptKey: string, description?: string) => Promise<string>;
/**
* Функция, обратная 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<void>;
/**
* Зайдет в группу по строке приглашения
* @param groupString строка приглашение
* @returns
*/
joinGroup: (groupString: string) => Promise<void>;
/**
* Покидает группу
* @param groupId ид группы
* @returns
*/
leaveGroup: (groupId: string) => Promise<void>;
/**
*
* @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<boolean>(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
}
}