feat(chat): direct chats and groups between users in the pet's window

Server mode: a rail beside the pet's chat lists the pet, then the user's
direct chats (one per pair) and groups with unread counts, and + for a new
chat. A room shows its messages (group senders named), loads older pages,
and sends text, pasted / dropped / attached files (<= 10 x 25 MB; images
inline). A group's creator renames it and adds or removes people; anyone may
leave. A blue dot on the pet counts unread messages.

Only a room's members read or write it, an admin included (roomFor → 404).
Files are stored under server-made names in <data dir>/chat/<room>/ and
served to members only under a sandbox CSP + nosniff. chat_message /
chat_room frames reach exactly the ids in `to` (scope.exactAudience).
New tables chat_rooms / chat_members / chat_messages / chat_files (additive).
This commit is contained in:
2026-11-03 09:33:33 +07:00
parent 85edc88a6d
commit 21a042dc2b
19 changed files with 2527 additions and 66 deletions
+41
View File
@@ -1912,6 +1912,47 @@ db.exec(`
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
`);
// Chat between users (server mode, server/lib/chat.js): a direct chat (`dm`,
// one per pair — `dm_key` is "<low id>:<high id>") or a group. Only a room's
// members read it, an admin included. Files live under <data dir>/chat/.
db.exec(`
CREATE TABLE IF NOT EXISTS chat_rooms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL CHECK (kind IN ('dm', 'group')),
name TEXT NOT NULL DEFAULT '',
dm_key TEXT UNIQUE,
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
last_message_at TEXT
);
CREATE TABLE IF NOT EXISTS chat_members (
room_id INTEGER NOT NULL REFERENCES chat_rooms(id) ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
joined_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
last_read_id INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (room_id, user_id)
);
CREATE INDEX IF NOT EXISTS idx_chat_members_user ON chat_members(user_id);
CREATE TABLE IF NOT EXISTS chat_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
room_id INTEGER NOT NULL REFERENCES chat_rooms(id) ON DELETE CASCADE,
user_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
kind TEXT NOT NULL DEFAULT 'text' CHECK (kind IN ('text', 'system')),
body TEXT NOT NULL DEFAULT '',
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
);
CREATE INDEX IF NOT EXISTS idx_chat_messages_room ON chat_messages(room_id, id);
CREATE TABLE IF NOT EXISTS chat_files (
id TEXT PRIMARY KEY,
message_id INTEGER NOT NULL REFERENCES chat_messages(id) ON DELETE CASCADE,
room_id INTEGER NOT NULL REFERENCES chat_rooms(id) ON DELETE CASCADE,
name TEXT NOT NULL,
type TEXT NOT NULL,
size INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_chat_files_message ON chat_files(message_id);
`);
// An account's own agent instructions, put before every workspace's in each
// pane it owns (server/lib/lane-sources.js, issue #67). Additive.
try {