You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
201 lines
9.2 KiB
201 lines
9.2 KiB
Архитектура чата (группового) |
|
|
|
База данных: 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 не добавлены — можно добавить позже при необходимости.
|
|
|