Files
IdP/apps/frontend/hooks/use-e2e-chat.ts
2026-06-30 09:39:35 +03:00

148 lines
4.5 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.
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useAuth } from '@/components/id/auth-provider';
import { ChatMessage, ChatRoom, fetchUserE2EPublicKey, setUserE2EPublicKey } from '@/lib/api';
import { ensureE2EKeyPair } from '@/lib/e2e-crypto';
import {
decryptE2EPayload,
encryptE2EPayload,
readableE2EPayload,
type E2EMessagePayload
} from '@/lib/e2e-chat';
export function resolveChatPeerUserId(room: ChatRoom | null | undefined, userId: string | undefined) {
if (!room || !userId) return null;
const fromMembers = room.members.find((member) => member.userId !== userId)?.userId;
if (fromMembers) return fromMembers;
if (room.peerUserId && room.peerUserId !== userId) return room.peerUserId;
return null;
}
export function getE2EMessageText(options: {
message: ChatMessage;
isE2E: boolean;
peerE2eKey: string | null;
peerKeyLoading: boolean;
e2ePayload?: E2EMessagePayload | null;
e2eError?: string;
}) {
const { message, isE2E, peerE2eKey, peerKeyLoading, e2ePayload, e2eError } = options;
if (!message.isEncrypted) {
return message.content ?? '';
}
if (e2ePayload) {
return readableE2EPayload(e2ePayload);
}
if (e2eError) {
return e2eError;
}
if (isE2E && peerKeyLoading) {
return '🔒 Расшифровка...';
}
if (isE2E && !peerE2eKey) {
return '🔒 Собеседник не опубликовал ключ шифрования';
}
return '🔒 Расшифровка...';
}
export function useE2EChat(options: {
room: ChatRoom | null | undefined;
messages: ChatMessage[];
userId?: string;
token?: string | null;
}) {
const { isLoading, isApiReady } = useAuth();
const isE2E = options.room?.type === 'E2E';
const peerUserId = useMemo(
() => resolveChatPeerUserId(options.room, options.userId),
[options.room, options.userId]
);
const [peerE2eKey, setPeerE2eKey] = useState<string | null>(null);
const [peerKeyLoading, setPeerKeyLoading] = useState(false);
const [e2ePayloads, setE2ePayloads] = useState<Record<string, E2EMessagePayload | null>>({});
const [e2eErrors, setE2eErrors] = useState<Record<string, string>>({});
useEffect(() => {
if (!options.userId || !options.token || isLoading || !isApiReady) return;
void (async () => {
try {
const { publicKey } = await ensureE2EKeyPair(options.userId!);
await setUserE2EPublicKey(options.userId!, publicKey, options.token);
} catch {
// фоновая инициализация E2E
}
})();
}, [isApiReady, isLoading, options.token, options.userId]);
useEffect(() => {
if (!isE2E || !peerUserId || !options.token) {
setPeerE2eKey(null);
setPeerKeyLoading(false);
return;
}
setPeerKeyLoading(true);
void fetchUserE2EPublicKey(peerUserId, options.token)
.then((response) => setPeerE2eKey(response.e2ePublicKey ?? null))
.catch(() => setPeerE2eKey(null))
.finally(() => setPeerKeyLoading(false));
}, [isE2E, options.token, peerUserId]);
useEffect(() => {
if (!isE2E || !peerE2eKey || !options.userId) {
setE2ePayloads({});
setE2eErrors({});
return;
}
let cancelled = false;
void (async () => {
const nextPayloads: Record<string, E2EMessagePayload | null> = {};
const nextErrors: Record<string, string> = {};
for (const message of options.messages) {
if (!message.isEncrypted || !message.content) continue;
const payload = await decryptE2EPayload(options.userId!, peerE2eKey, message.content);
if (payload) {
nextPayloads[message.id] = payload;
} else {
nextErrors[message.id] = '🔒 Не удалось расшифровать сообщение';
}
}
if (!cancelled) {
setE2ePayloads(nextPayloads);
setE2eErrors(nextErrors);
}
})();
return () => {
cancelled = true;
};
}, [isE2E, options.messages, options.userId, peerE2eKey]);
const encryptOutgoing = useCallback(
async (payload: Parameters<typeof encryptE2EPayload>[2]) => {
if (!isE2E || !options.userId) {
throw new Error('E2E недоступен');
}
if (!peerE2eKey) {
throw new Error('Собеседник ещё не опубликовал ключ шифрования');
}
return encryptE2EPayload(options.userId, peerE2eKey, payload);
},
[isE2E, options.userId, peerE2eKey]
);
return {
isE2E,
peerUserId,
peerE2eKey,
peerKeyLoading,
e2ePayloads,
e2eErrors,
encryptOutgoing
};
}