﻿Архитектура чата (группового)

База данных: SQLite3 (WAL mode), chats.db

================================================================================
Схема БД (10 таблиц)
================================================================================

-- 1. local_identity — текущий локальный узел (одна строка, приватный ключ здесь)
--    приватный ключ получаем от utun через MSG_RSP_PRIVKEY (только localhost)
CREATE TABLE local_identity (
    id              INTEGER PRIMARY KEY CHECK (id = 1),
    node_id         INTEGER NOT NULL UNIQUE,
    name            TEXT NOT NULL,
    x25519_pubkey   BLOB NOT NULL,             -- 32 bytes
    x25519_privkey  BLOB,                      -- 32 bytes (только на этом устройстве)
    ed25519_pubkey  BLOB,                      -- 32 bytes (derive из x25519 privkey)
    created_at      INTEGER DEFAULT (unixepoch()),
    updated_at      INTEGER DEFAULT (unixepoch())
);


-- 2. my_nodes — другие наши ноды (другие устройства, нет приватного ключа)
CREATE TABLE my_nodes (
    node_id         INTEGER PRIMARY KEY,
    name            TEXT NOT NULL,
    x25519_pubkey   BLOB NOT NULL,             -- 32 bytes
    ed25519_pubkey  BLOB,                      -- 32 bytes
    created_at      INTEGER DEFAULT (unixepoch()),
    updated_at      INTEGER DEFAULT (unixepoch())
);


-- 3. nodes — известные удалённые узлы (получаем через utun)
CREATE TABLE nodes (
    node_id         INTEGER PRIMARY KEY,
    name            TEXT,
    x25519_pubkey   BLOB NOT NULL,             -- 32 bytes
    ed25519_pubkey  BLOB,                      -- 32 bytes (для проверки подписи пира)
    is_online       INTEGER DEFAULT 0,
    last_seen_at    INTEGER DEFAULT (unixepoch()),
    created_at      INTEGER DEFAULT (unixepoch()),
    updated_at      INTEGER DEFAULT (unixepoch())
);


-- 3a. accounts — UI-информация об узлах для отображения сообщений
CREATE TABLE accounts (
    node_id         INTEGER PRIMARY KEY REFERENCES nodes(node_id),
    display_name    TEXT NOT NULL,              -- "Alice"
    avatar_color    TEXT DEFAULT '#4A90E2',     -- hex RGB
    avatar_letter   TEXT NOT NULL,              -- "A"
    is_contact      INTEGER DEFAULT 1,          -- показывать в боковой панели
    created_at      INTEGER DEFAULT (unixepoch())
);


-- 4. channels — каналы (DM и групповые)
CREATE TABLE channels (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_id      TEXT NOT NULL UNIQUE,       -- "dm:nodeA_nodeB" (A<B) или "group:hash"
    name            TEXT NOT NULL,
    owner_node_id   INTEGER,                   -- создатель (NULL для DM)
    is_dm           INTEGER DEFAULT 0,
    last_message    TEXT,
    last_msg_at     INTEGER,                   -- unix timestamp ms
    created_at      INTEGER DEFAULT (unixepoch())
);
CREATE UNIQUE INDEX idx_channel_cid ON channels(channel_id);


-- 5. channel_members — участники канала
CREATE TABLE channel_members (
    channel_id      TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
    node_id         INTEGER NOT NULL,
    joined_at       INTEGER DEFAULT (unixepoch()),
    PRIMARY KEY (channel_id, node_id)
);
CREATE INDEX idx_cm_node ON channel_members(node_id);


-- 6. messages — сообщения чата
--    signature = Ed25519 отправителя над (channel_id, author_node_id, content, timestamp)
CREATE TABLE messages (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    channel_id      TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
    author_node_id  INTEGER NOT NULL,
    content         TEXT NOT NULL CHECK(length(content) <= 4096),
    content_type    TEXT DEFAULT 'text/plain',
    timestamp       INTEGER NOT NULL,           -- unix milliseconds
    signature       BLOB NOT NULL CHECK(length(signature) == 64),
    is_outgoing     INTEGER DEFAULT 0,
    is_read         INTEGER DEFAULT 0,
    created_at      INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_msg_channel_time ON messages(channel_id, timestamp);
CREATE INDEX idx_msg_author     ON messages(author_node_id);
-- дедупликация: один автор не может отправить два сообщения в один ms в один канал
CREATE UNIQUE INDEX idx_msg_dedup ON messages(channel_id, author_node_id, timestamp);


-- 7. reactions — реакции на сообщения
--    signature = Ed25519 отправителя над (message_id, node_id, emoji)
CREATE TABLE reactions (
    message_id      INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    node_id         INTEGER NOT NULL,
    emoji           TEXT NOT NULL,              -- unicode codepoint или :shortcode:
    signature       BLOB NOT NULL CHECK(length(signature) == 64),
    created_at      INTEGER DEFAULT (unixepoch()),
    PRIMARY KEY (message_id, node_id, emoji)
);


-- 8. attachments — вложения к сообщениям
--    signature = Ed25519 отправителя над (message_id, filename, content_type, size, sha256)
CREATE TABLE attachments (
    id              INTEGER PRIMARY KEY AUTOINCREMENT,
    message_id      INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
    filename        TEXT NOT NULL,
    content_type    TEXT NOT NULL,              -- MIME type
    size            INTEGER NOT NULL,           -- bytes
    sha256          BLOB NOT NULL,              -- 32 bytes
    file_path       TEXT,                       -- локальный путь к сохранённому файлу
    signature       BLOB NOT NULL CHECK(length(signature) == 64),
    is_upload       INTEGER DEFAULT 0,          -- 0=скачано, 1=наше
    created_at      INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_att_msg ON attachments(message_id);


-- 9. ui_state — сохранение состояния UI (последний канал, позиция скролла и т.д.)
CREATE TABLE ui_state (
    key   TEXT PRIMARY KEY,
    value TEXT
);================================================================================
Основные запросы
================================================================================

-- Текущая нода (приватный ключ — только на этом устройстве)
SELECT node_id, name, x25519_pubkey, x25519_privkey, ed25519_pubkey
FROM local_identity WHERE id = 1;

-- Другие наши ноды (без приватных ключей)
SELECT node_id, name, x25519_pubkey, ed25519_pubkey FROM my_nodes;

-- Список каналов с последним сообщением
SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at
FROM channels c
JOIN channel_members cm ON c.channel_id = cm.channel_id
WHERE cm.node_id = (SELECT node_id FROM local_identity WHERE id = 1)
ORDER BY c.last_msg_at DESC;

-- Сообщения канала (последние 50)
SELECT id, author_node_id, content, content_type, timestamp,
       signature, is_outgoing, is_read
FROM messages
WHERE channel_id = ?
ORDER BY timestamp DESC
LIMIT 50;

-- Кто в канале
SELECT cm.node_id, n.name
FROM channel_members cm LEFT JOIN nodes n ON cm.node_id = n.node_id
WHERE cm.channel_id = ?;

-- DM между двумя узлами
-- channel_id = "dm:" || min(a,b) || "_" || max(a,b)

-- Реакции сообщения
SELECT node_id, emoji, signature FROM reactions WHERE message_id = ? ORDER BY created_at;

-- Вложения сообщения
SELECT id, filename, content_type, size, sha256, file_path
FROM attachments WHERE message_id = ?;


================================================================================
Примечания
================================================================================

- local_identity — одна строка (id=1), содержит x25519_privkey.
  Приватный ключ получаем от utun через MSG_RSP_PRIVKEY (доступен только с localhost).

- my_nodes — другие наши устройства, только pubkey-информация (нет приватных ключей).

- Адреса и подсети (node_addresses, node_subnets) — зона ответственности utun.
  Чат не занимается сетевым уровнем.

- Все timestamp: INTEGER (unix epoch). messages.timestamp — миллисекунды,
  created_at/updated_at/joined_at — секунды.

- signature везде NOT NULL Ed25519 (64 bytes) — подпись автора контента:
    messages   : sign(edPriv, channel_id || author_node_id || content || timestamp)
    reactions  : sign(edPriv, message_id || node_id || emoji)
    attachments: sign(edPriv, message_id || filename || content_type || size || sha256)

- channel_id для DM: "dm:nodeA_nodeB" где A < B (канонический порядок).

- channel_id для групп: "group:" + SHA256(owner_node_id || name || timestamp).

- deleted_at / soft delete не добавлены — можно добавить позже при необходимости.
