The E2EE direct-message types (MessageUser/Message/Conversation/MyKeyBundle/ KeySetup) + the ~9 messaging methods move into a self-contained api/messaging.ts exporting a `messagingApi` slice (imports only core's req). api.ts spreads the slice and re-exports the types. api.ts 882 -> 819 lines; no runtime change. Gate green: typecheck, eslint, prettier, 74 vitest.
73 lines
2.6 KiB
TypeScript
73 lines
2.6 KiB
TypeScript
// Direct-messaging domain: the E2EE key bundle setup/reset, the conversation list, threads, and
|
|
// send/unread. Self-contained — types reference only each other, methods only touch core's req.
|
|
// (The WebCrypto layer that encrypts/decrypts message bodies lives in e2ee.ts, not here.)
|
|
import { req } from "./core";
|
|
|
|
export interface MessageUser {
|
|
id: number;
|
|
name: string;
|
|
avatar_url: string | null;
|
|
}
|
|
|
|
export interface Message {
|
|
id: number;
|
|
kind: "user" | "system";
|
|
sender_id: number | null;
|
|
recipient_id: number;
|
|
body: string | null; // plaintext, system messages only
|
|
ciphertext: string | null; // base64, E2EE user messages only
|
|
iv: string | null;
|
|
read_at: string | null;
|
|
created_at: string | null;
|
|
}
|
|
|
|
export interface Conversation {
|
|
partner: MessageUser;
|
|
last_message: Message;
|
|
unread: number;
|
|
}
|
|
|
|
// My own E2EE key bundle. When configured, the wrapped blob + params let a device unlock.
|
|
export interface MyKeyBundle {
|
|
configured: boolean;
|
|
public_key?: string;
|
|
wrapped_private_key?: string;
|
|
salt?: string;
|
|
wrap_iv?: string;
|
|
key_check?: string;
|
|
}
|
|
|
|
export interface KeySetup {
|
|
public_key: string;
|
|
wrapped_private_key: string;
|
|
salt: string;
|
|
wrap_iv: string;
|
|
key_check: string;
|
|
}
|
|
|
|
export const messagingApi = {
|
|
messageMyKey: (): Promise<MyKeyBundle> => req("/api/messages/keys/me"),
|
|
setupMessageKey: (k: KeySetup): Promise<{ configured: boolean }> =>
|
|
req("/api/messages/keys", { method: "POST", body: JSON.stringify(k) }),
|
|
// Forgotten-passphrase reset: drops the server key (and the now-undecryptable history) so a new
|
|
// passphrase can be set. Password accounts must pass their current password; errors shown inline.
|
|
resetMessageKey: (currentPassword?: string): Promise<{ configured: boolean }> =>
|
|
req(
|
|
"/api/messages/keys/reset",
|
|
{ method: "POST", body: JSON.stringify({ current_password: currentPassword ?? null }) },
|
|
{ quiet: true },
|
|
),
|
|
messagePublicKey: (userId: number): Promise<{ user_id: number; public_key: string }> =>
|
|
req(`/api/messages/keys/${userId}`),
|
|
messageUsers: (): Promise<MessageUser[]> => req("/api/messages/users"),
|
|
conversations: (): Promise<{ items: Conversation[] }> => req("/api/messages/conversations"),
|
|
thread: (partnerId: number): Promise<{ partner: MessageUser; items: Message[] }> =>
|
|
req(`/api/messages/conversations/${partnerId}`),
|
|
sendMessage: (recipientId: number, ciphertext: string, iv: string): Promise<Message> =>
|
|
req("/api/messages", {
|
|
method: "POST",
|
|
body: JSON.stringify({ recipient_id: recipientId, ciphertext, iv }),
|
|
}),
|
|
messageUnreadCount: (): Promise<{ count: number }> => req("/api/messages/unread_count"),
|
|
};
|