mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
* feat(auth): add revocable API tokens (bot/service auth) Add long-lived, revocable API tokens so headless clients (the introspection MCP tool, bots, CI) can authenticate without a password. Presented as "Authorization: Bearer <token>", a token authenticates as a specific user, inheriting that user's role and permissions. - migration 018 + dedicated api_tokens table (kept separate from sessions so bulk logout and the per-user session cap never touch these); only the SHA-256 hash is stored, raw token shown once at creation - auth.ResolveTokenHash: one shared bearer resolver that both AuthMiddleware and adminAuthMiddleware now call. Sessions are matched first so existing login behavior is unchanged; API tokens are a fallback only on session miss. A DB outage is returned wrapped, never mistaken for a bad token. - `server token create|list|revoke` CLI: mints directly against the DB with no HTTP and no login — the password-free bootstrap path - tests: resolver (8 cases incl. outage-not-fallthrough), db queries (6), api middleware integration (valid + revoked token) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tools): add owncord-introspect MCP server A local MCP dev tool that lets Claude Code introspect a running OwnCord instance: read its logs, query any REST endpoint, and tail the desktop client's log file. It is a thin wrapper over the existing API plus the client log — no new product surface. - tools/mcp-introspect/index.mjs (Node/ESM, one dep: @modelcontextprotocol/sdk) exposes api_request (full read-write passthrough), server_logs (admin SSE ring-buffer stream), client_logs (reads the desktop log file) - authenticates with an API token (OWNCORD_API_TOKEN); pins the self-signed cert and skips hostname checks (the cert has no SAN) - registered in .mcp.json (secret-free ${OWNCORD_API_TOKEN}) - un-ignore tools/mcp-introspect/ so this shared dev tool is committed, while tools/livekit-server.exe and node_modules stay ignored - docs/mcp-introspect.md: how it works, tool reference, setup, troubleshooting Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(dependencies): update and add various crate versions in Cargo.lock * feat(admin): manage API tokens from the admin panel Add Owner-gated HTTP endpoints and a UI card to create, list, and revoke API tokens from the web admin panel. Previously only the `server token` CLI could manage them, which requires shell access to the host. - POST|GET|DELETE /admin/api/tokens in admin/handlers_tokens.go, wired in admin/api.go. All three are Owner-only (ownerOnlyMiddleware, like backups/updates): an HTTP token-mint endpoint is a network-reachable credential-minting surface, and API tokens deliberately survive password change + bulk logout, so a hijacked admin session must not mint one. - Reuses the same db.*APIToken calls as the CLI; create sources the actor from request context (audits who clicked, not the bound user); the raw token is returned once in the 201 body, never stored. - Add json tags to db.APITokenListItem for snake_case wire consistency. - Admin panel: "API Tokens" nav item + create modal, show-once reveal, revoke confirm in admin/static/index.html. - Tests: 7 in admin/api_test.go (+api_tokens table in the in-memory schema). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: modernize to Go 1.26 idioms + enable modernize linter Apply `golangci-lint modernize` autofixes across the server and enable the linter in .golangci.yml so these stop re-accumulating (they built up only because modernize was never in the config). Production code: slices.Contains for hand-rolled membership loops (api router, ws origin, db/account, plugin manifest); strings.SplitSeq for allocation-free line/segment iteration (db/migrate, updater, livekit_proxy); strings.Cut (config); fmt.Appendf (dm_handler); min() (event_pruner); any (ws client). Tests: range-over-int, t.Context(), WaitGroup.Go, slices.Sort, maps.Copy, new(expr), interface{}->any. - plugin/manifest.go parent-traversal check applied by hand: modernize skipped it (two conflicting rewrites); used the slices.Contains form. - Removed the now-dead ptr() test helper after newexpr inlined its callers. - Dropped dangling sort imports left by the sort.Slice->slices.Sort rewrite. No behavior change. All four tag variants build, full test suite is green, and golangci-lint (with modernize enabled) reports 0 issues. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
597 lines
19 KiB
Markdown
597 lines
19 KiB
Markdown
# Database Schema Reference
|
|
|
|
OwnCord uses a single SQLite database file (`data/chatserver.db`) with the pure-Go driver `modernc.org/sqlite` (no CGO). Migrations run automatically on startup.
|
|
|
|
> **Data-access layers:** queries currently run as hand-written SQL in
|
|
> `Server/db`; an sqlc-generated layer (`Server/db/dbgen`, from
|
|
> `Server/db/queries/`) exists and is slated to become the real query layer
|
|
> per decision D2 in
|
|
> [plans/audit-2026-07-19-decisions.md](plans/audit-2026-07-19-decisions.md).
|
|
> See [architecture/data-model.md](architecture/data-model.md) for the full
|
|
> picture.
|
|
|
|
---
|
|
|
|
## Database Configuration
|
|
|
|
| PRAGMA | Value | Purpose |
|
|
|--------|-------|---------|
|
|
| `journal_mode` | `WAL` | Write-Ahead Logging for concurrent readers |
|
|
| `foreign_keys` | `ON` | Enforces all `REFERENCES` constraints |
|
|
| `busy_timeout` | `5000` | Waits up to 5 seconds for the write lock |
|
|
| `synchronous` | `NORMAL` | Safe with WAL mode, reduces fsync calls |
|
|
| `temp_store` | `MEMORY` | Temporary tables stored in RAM |
|
|
| `mmap_size` | `268435456` | 256 MB memory-mapped I/O |
|
|
| `cache_size` | `-64000` | 64 MB page cache |
|
|
|
|
SQLite only allows one writer at a time. The connection pool is pinned to a single connection.
|
|
|
|
---
|
|
|
|
## Migration System
|
|
|
|
Migrations are embedded `.sql` files applied in lexicographic order. Each migration runs in a transaction and is tracked in `schema_versions`.
|
|
|
|
```sql
|
|
CREATE TABLE IF NOT EXISTS schema_versions (
|
|
version TEXT PRIMARY KEY,
|
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
```
|
|
|
|
### Migration History
|
|
|
|
| File | Description |
|
|
|------|-------------|
|
|
| `001_initial_schema.sql` | All core tables, default roles and settings |
|
|
| `002_voice_states.sql` | Adds `voice_states` table |
|
|
| `003_audit_log.sql` | Recreates `audit_log` with canonical column names (via a transient `audit_log_v6` rename) |
|
|
| `004_voice_optimization.sql` | Adds `camera`, `screenshare` to voice_states; voice settings to channels |
|
|
| `005_fix_member_permissions.sql` | Fixes Member role permissions |
|
|
| `006_channel_overrides_index.sql` | Adds composite index on channel_overrides |
|
|
| `007_member_video_permissions.sql` | Adds USE_VIDEO and SHARE_SCREEN to Member role |
|
|
| `008_attachment_dimensions.sql` | Adds `width` and `height` to attachments |
|
|
| `009_dm_tables.sql` | Adds `dm_participants` and `dm_open_state` tables |
|
|
| `010_attachment_uploader.sql` | Adds `attachments.uploader_id` + index for upload-ownership checks |
|
|
| `011_rate_lockouts.sql` | Adds `rate_lockouts` so rate-limit lockouts survive restarts |
|
|
| `012_user_blocks.sql` | Adds `user_blocks` (blocks DM creation/messaging between users) |
|
|
| `013_channel_type_constraint.sql` | INSERT/UPDATE triggers restricting `channels.type` to `text`/`voice`/`dm` |
|
|
| `014_events_table.sql` | Adds `events` — persistent broadcast log for reconnect cold-tier replay |
|
|
| `015_plugins.sql` | Adds `plugins` and `plugin_kv` for the WASM plugin runtime |
|
|
| `016_announcement_channel_type.sql` | Recreates the channel-type triggers to allow `announcement` |
|
|
| `017_user_identity_key.sql` | Adds `users.identity_public_key` (long-term E2EE identity key for voice TOFU) |
|
|
| `018_api_tokens.sql` | Adds `api_tokens` — long-lived, revocable bearer tokens for headless clients (bot/service auth) |
|
|
|
|
---
|
|
|
|
## Tables
|
|
|
|
### roles
|
|
|
|
Defines permission tiers.
|
|
|
|
```sql
|
|
CREATE TABLE roles (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
color TEXT,
|
|
permissions INTEGER NOT NULL DEFAULT 0,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
is_default INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
```
|
|
|
|
**Default roles:**
|
|
|
|
| id | name | color | permissions | position | Notes |
|
|
|----|------|-------|-------------|----------|-------|
|
|
| 1 | Owner | `#E74C3C` | `0x7FFFFFFF` | 100 | All 31 permission bits set |
|
|
| 2 | Admin | `#F39C12` | `0x3FFFFFFF` | 80 | Everything except ADMINISTRATOR |
|
|
| 3 | Moderator | `#3498DB` | `0x000FFFFF` | 60 | All message + voice + moderation |
|
|
| 4 | Member | NULL | `0x1E63` | 40 | Send, read, attach, react, voice, video, screen share |
|
|
|
|
---
|
|
|
|
### users
|
|
|
|
```sql
|
|
CREATE TABLE users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
password TEXT NOT NULL,
|
|
avatar TEXT,
|
|
role_id INTEGER NOT NULL DEFAULT 4 REFERENCES roles(id),
|
|
totp_secret TEXT,
|
|
status TEXT NOT NULL DEFAULT 'offline',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
last_seen TEXT,
|
|
banned INTEGER NOT NULL DEFAULT 0,
|
|
ban_reason TEXT,
|
|
ban_expires TEXT,
|
|
identity_public_key TEXT
|
|
);
|
|
```
|
|
|
|
Valid status values: `online`, `idle`, `dnd`, `offline`. All statuses are reset to `offline` on server startup.
|
|
|
|
`identity_public_key` (added in migration 017) is the user's long-term E2EE
|
|
identity public key (base64 ECDSA P-256) used for TOFU pinning of voice E2EE
|
|
announces; `NULL` = not published (legacy client).
|
|
|
|
---
|
|
|
|
### sessions
|
|
|
|
```sql
|
|
CREATE TABLE sessions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token TEXT NOT NULL UNIQUE,
|
|
device TEXT,
|
|
ip_address TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
last_used TEXT NOT NULL DEFAULT (datetime('now')),
|
|
expires_at TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
Session TTL: 30 days. Token is stored as SHA-256 hash.
|
|
|
|
---
|
|
|
|
### api_tokens
|
|
|
|
```sql
|
|
CREATE TABLE api_tokens (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
label TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
last_used_at TEXT,
|
|
expires_at TEXT,
|
|
revoked_at TEXT
|
|
);
|
|
```
|
|
|
|
Long-lived, revocable bearer tokens for headless clients (bots, CI, the introspection
|
|
MCP tool). A token authenticates as `user_id`, inheriting that user's role/permissions,
|
|
and is resolved by the same middleware as sessions (see `auth.ResolveTokenHash`). Only the
|
|
SHA-256 hash is stored; the raw token is shown once at creation. `expires_at` NULL = never
|
|
expires; `revoked_at` NULL = active. Mint/list/revoke via `server token …`. Separate from
|
|
`sessions` so bulk logout and the per-user session cap never affect these.
|
|
|
|
---
|
|
|
|
### channels
|
|
|
|
```sql
|
|
CREATE TABLE channels (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
type TEXT NOT NULL DEFAULT 'text',
|
|
category TEXT,
|
|
topic TEXT,
|
|
position INTEGER NOT NULL DEFAULT 0,
|
|
slow_mode INTEGER NOT NULL DEFAULT 0,
|
|
archived INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
voice_max_users INTEGER NOT NULL DEFAULT 0,
|
|
voice_quality TEXT,
|
|
mixing_threshold INTEGER,
|
|
voice_max_video INTEGER NOT NULL DEFAULT 25
|
|
);
|
|
```
|
|
|
|
Channel types: `text`, `voice`, `announcement`, `dm`. Migration 013 installs
|
|
INSERT/UPDATE triggers restricting the value to this set (migration 016 added
|
|
`announcement`). Announcement channels are readable like text channels but
|
|
posting is restricted to users with `MANAGE_MESSAGES` (enforced in the service
|
|
layer, `Server/service/message.go`).
|
|
|
|
---
|
|
|
|
### channel_overrides
|
|
|
|
Per-channel permission overrides for specific roles.
|
|
|
|
```sql
|
|
CREATE TABLE channel_overrides (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
|
allow INTEGER NOT NULL DEFAULT 0,
|
|
deny INTEGER NOT NULL DEFAULT 0,
|
|
UNIQUE(channel_id, role_id)
|
|
);
|
|
```
|
|
|
|
Effective permission calculation: `effective = (base_permissions & ~deny) | allow`
|
|
|
|
---
|
|
|
|
### messages
|
|
|
|
```sql
|
|
CREATE TABLE messages (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id),
|
|
content TEXT NOT NULL,
|
|
reply_to INTEGER REFERENCES messages(id) ON DELETE SET NULL,
|
|
edited_at TEXT,
|
|
deleted INTEGER NOT NULL DEFAULT 0,
|
|
pinned INTEGER NOT NULL DEFAULT 0,
|
|
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
```
|
|
|
|
Messages are soft-deleted (`deleted = 1`), never physically removed by user action.
|
|
|
|
---
|
|
|
|
### messages_fts (FTS5 Virtual Table)
|
|
|
|
Full-text search index synchronized via triggers.
|
|
|
|
```sql
|
|
CREATE VIRTUAL TABLE messages_fts USING fts5(
|
|
content,
|
|
content='messages',
|
|
content_rowid='id'
|
|
);
|
|
```
|
|
|
|
Supports FTS5 query syntax: simple terms, phrase queries, prefix queries, boolean operators (`AND`, `OR`, `NOT`).
|
|
|
|
---
|
|
|
|
### attachments
|
|
|
|
```sql
|
|
CREATE TABLE attachments (
|
|
id TEXT PRIMARY KEY,
|
|
message_id INTEGER REFERENCES messages(id) ON DELETE CASCADE,
|
|
filename TEXT NOT NULL,
|
|
stored_as TEXT NOT NULL,
|
|
mime_type TEXT NOT NULL,
|
|
size INTEGER NOT NULL,
|
|
uploaded_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
width INTEGER,
|
|
height INTEGER,
|
|
uploader_id INTEGER REFERENCES users(id)
|
|
);
|
|
```
|
|
|
|
Uses UUID primary keys. `message_id` is NULL during upload, linked when the
|
|
message is sent. `uploader_id` (added by migration 010) records who uploaded
|
|
the file and backs the ownership check when attaching an upload to a message.
|
|
|
|
---
|
|
|
|
### reactions
|
|
|
|
```sql
|
|
CREATE TABLE reactions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
emoji TEXT NOT NULL,
|
|
UNIQUE(message_id, user_id, emoji)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### invites
|
|
|
|
```sql
|
|
CREATE TABLE invites (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
code TEXT NOT NULL UNIQUE,
|
|
created_by INTEGER NOT NULL REFERENCES users(id),
|
|
redeemed_by INTEGER REFERENCES users(id),
|
|
max_uses INTEGER,
|
|
use_count INTEGER NOT NULL DEFAULT 0,
|
|
expires_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
revoked INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
```
|
|
|
|
Invite codes are 8 random bytes encoded as hex. Uses are validated and incremented atomically.
|
|
|
|
---
|
|
|
|
### read_states
|
|
|
|
```sql
|
|
CREATE TABLE read_states (
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
last_message_id INTEGER NOT NULL DEFAULT 0,
|
|
mention_count INTEGER NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (user_id, channel_id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### audit_log
|
|
|
|
```sql
|
|
CREATE TABLE audit_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
actor_id INTEGER NOT NULL DEFAULT 0,
|
|
action TEXT NOT NULL,
|
|
target_type TEXT NOT NULL DEFAULT '',
|
|
target_id INTEGER NOT NULL DEFAULT 0,
|
|
detail TEXT NOT NULL DEFAULT '',
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### voice_states
|
|
|
|
Ephemeral -- all rows deleted on server startup.
|
|
|
|
```sql
|
|
CREATE TABLE voice_states (
|
|
user_id INTEGER PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
muted INTEGER NOT NULL DEFAULT 0,
|
|
deafened INTEGER NOT NULL DEFAULT 0,
|
|
speaking INTEGER NOT NULL DEFAULT 0,
|
|
camera INTEGER NOT NULL DEFAULT 0,
|
|
screenshare INTEGER NOT NULL DEFAULT 0,
|
|
joined_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### dm_participants
|
|
|
|
```sql
|
|
CREATE TABLE dm_participants (
|
|
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
PRIMARY KEY (channel_id, user_id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### dm_open_state
|
|
|
|
```sql
|
|
CREATE TABLE dm_open_state (
|
|
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
channel_id INTEGER NOT NULL REFERENCES channels(id) ON DELETE CASCADE,
|
|
opened_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (user_id, channel_id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### login_attempts
|
|
|
|
Login attempt log used for IP-based rate limiting and lockouts.
|
|
|
|
```sql
|
|
CREATE TABLE login_attempts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
ip_address TEXT NOT NULL,
|
|
username TEXT,
|
|
success INTEGER NOT NULL DEFAULT 0,
|
|
timestamp TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### settings
|
|
|
|
Generic key/value store for server settings (`server_name`, `motd`,
|
|
`registration_open`, …). Written by the admin API; read by the REST layer and
|
|
the WebSocket hub (cached with a short TTL).
|
|
|
|
```sql
|
|
CREATE TABLE settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### emoji
|
|
|
|
Custom emoji metadata.
|
|
|
|
```sql
|
|
CREATE TABLE emoji (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
shortcode TEXT NOT NULL UNIQUE,
|
|
filename TEXT NOT NULL,
|
|
uploaded_by INTEGER NOT NULL REFERENCES users(id),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### sounds
|
|
|
|
**Dead schema.** Created by the initial schema for the soundboard feature,
|
|
which has since been removed; the table remains but nothing reads or writes it.
|
|
Slated for a cleanup migration (audit A-2026-07-13).
|
|
|
|
```sql
|
|
CREATE TABLE sounds (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL,
|
|
filename TEXT NOT NULL,
|
|
duration_ms INTEGER NOT NULL,
|
|
uploaded_by INTEGER NOT NULL REFERENCES users(id),
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### rate_lockouts
|
|
|
|
Persists rate-limiter lockouts (e.g. repeated failed logins) so they survive
|
|
server restarts. Sliding-window counters themselves stay in memory.
|
|
|
|
```sql
|
|
CREATE TABLE rate_lockouts (
|
|
key TEXT PRIMARY KEY,
|
|
expires_at TEXT NOT NULL
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### user_blocks
|
|
|
|
User blocking (added by migration 012): a block prevents DM creation and
|
|
messaging between the two users.
|
|
|
|
```sql
|
|
CREATE TABLE user_blocks (
|
|
blocker_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
blocked_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
PRIMARY KEY (blocker_id, blocked_id),
|
|
CHECK (blocker_id != blocked_id)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### events
|
|
|
|
Persistent broadcast log (migration 014) — the cold tier of the reconnect
|
|
replay pipeline (see [protocol.md](protocol.md)). Written asynchronously by
|
|
the event persister, pruned by retention (configurable, default 24h). The
|
|
hub's in-memory sequence counter is seeded from `MAX(events.seq)` at startup
|
|
so sequence numbers stay monotonic across restarts.
|
|
|
|
```sql
|
|
CREATE TABLE events (
|
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
event_type TEXT NOT NULL,
|
|
payload BLOB NOT NULL,
|
|
channel_id INTEGER NOT NULL DEFAULT 0,
|
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
### plugins / plugin_kv
|
|
|
|
Plugin registry and per-plugin key/value storage (migration 015). `plugin_kv`
|
|
is namespaced per plugin via the composite primary key.
|
|
|
|
```sql
|
|
CREATE TABLE plugins (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
version TEXT NOT NULL,
|
|
enabled INTEGER NOT NULL DEFAULT 0,
|
|
manifest_json TEXT NOT NULL,
|
|
installed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
|
|
CREATE TABLE plugin_kv (
|
|
plugin_id INTEGER NOT NULL REFERENCES plugins(id) ON DELETE CASCADE,
|
|
key TEXT NOT NULL,
|
|
value BLOB NOT NULL,
|
|
PRIMARY KEY (plugin_id, key)
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## Indexes
|
|
|
|
| Index Name | Table | Columns | Purpose |
|
|
|------------|-------|---------|---------|
|
|
| `idx_sessions_token` | sessions | `(token)` | Fast session lookup by token hash |
|
|
| `idx_sessions_user` | sessions | `(user_id)` | Fast deletion of all sessions for a user |
|
|
| `idx_messages_channel` | messages | `(channel_id, id DESC)` | Latest messages in channel query |
|
|
| `idx_messages_user` | messages | `(user_id)` | Filter by author |
|
|
| `idx_invites_code` | invites | `(code)` | Fast invite validation |
|
|
| `idx_audit_timestamp` | audit_log | `(created_at DESC)` | Pagination of audit log |
|
|
| `idx_audit_log_actor` | audit_log | `(actor_id)` | Filter by actor |
|
|
| `idx_login_ip` | login_attempts | `(ip_address, timestamp)` | Rate limiting queries |
|
|
| `idx_voice_states_channel` | voice_states | `(channel_id)` | All users in a voice channel |
|
|
| `idx_channel_overrides_channel_role` | channel_overrides | `(channel_id, role_id)` | Permission lookup |
|
|
| `idx_dm_participants_user` | dm_participants | `(user_id)` | DM channel lookup |
|
|
| `idx_attachments_uploader` | attachments | `(uploader_id)` | Upload-ownership checks |
|
|
| `idx_user_blocks_blocked` | user_blocks | `(blocked_id, blocker_id)` | Reverse block lookup |
|
|
| `idx_events_channel_seq` | events | `(channel_id, seq)` | Cold-tier replay per channel |
|
|
| `idx_events_created_at` | events | `(created_at)` | Retention pruning |
|
|
|
|
---
|
|
|
|
## Permission Bitfield System
|
|
|
|
Permissions are stored as an integer bitfield (31 bits used) in `roles.permissions`, `channel_overrides.allow`, and `channel_overrides.deny`.
|
|
|
|
### Bit Map
|
|
|
|
| Bit | Hex | Name | Description |
|
|
|-----|-----|------|-------------|
|
|
| 0 | `0x1` | `SEND_MESSAGES` | Post messages in text channels |
|
|
| 1 | `0x2` | `READ_MESSAGES` | View messages in text channels |
|
|
| 5 | `0x20` | `ATTACH_FILES` | Upload file attachments |
|
|
| 6 | `0x40` | `ADD_REACTIONS` | Add emoji reactions |
|
|
| 9 | `0x200` | `CONNECT_VOICE` | Join voice channels |
|
|
| 10 | `0x400` | `SPEAK_VOICE` | Transmit audio in voice channels |
|
|
| 11 | `0x800` | `USE_VIDEO` | Enable camera in voice channels |
|
|
| 12 | `0x1000` | `SHARE_SCREEN` | Share screen in voice channels |
|
|
| 16 | `0x10000` | `MANAGE_MESSAGES` | Delete others' messages, pin/unpin |
|
|
| 17 | `0x20000` | `MANAGE_CHANNELS` | Create, edit, delete channels |
|
|
| 18 | `0x40000` | `KICK_MEMBERS` | Kick users |
|
|
| 19 | `0x80000` | `BAN_MEMBERS` | Ban/unban users |
|
|
| 20 | `0x100000` | `MUTE_MEMBERS` | Server-side mute/deafen in voice |
|
|
| 24 | `0x1000000` | `MANAGE_ROLES` | Create, edit, delete roles |
|
|
| 25 | `0x2000000` | `MANAGE_SERVER` | Modify server settings |
|
|
| 26 | `0x4000000` | `MANAGE_INVITES` | Create and revoke invite codes |
|
|
| 27 | `0x8000000` | `VIEW_AUDIT_LOG` | View the audit log |
|
|
| 30 | `0x40000000` | `ADMINISTRATOR` | Bypasses ALL permission checks |
|
|
|
|
Bits 2-4, 7, 13-15, 21-23, 28-29, 31 are reserved.
|
|
|
|
### Permission Checking Logic
|
|
|
|
```
|
|
1. Get the user's role -> role.Permissions (base)
|
|
2. If (base & ADMINISTRATOR) != 0 -> ALLOW everything
|
|
3. Get channel_overrides for (channel_id, role_id) -> allow, deny
|
|
4. effective = (base & ~deny) | allow
|
|
5. Check: (effective & required_permission) != 0
|
|
```
|
|
|
|
Deny is applied first (strips bits), then allow (adds bits), so allow wins
|
|
when both target the same bit — matching Discord's channel-override semantics
|
|
(`permissions.EffectivePerms`).
|
|
|
|
DM channels bypass role permissions entirely and use participant-based authorization instead.
|
|
|
|
### Default Role Permission Values
|
|
|
|
| Role | Hex | Permissions |
|
|
|------|-----|-------------|
|
|
| Owner | `0x7FFFFFFF` | Everything including ADMINISTRATOR |
|
|
| Admin | `0x3FFFFFFF` | Everything except ADMINISTRATOR |
|
|
| Moderator | `0x000FFFFF` | All message + voice + moderation |
|
|
| Member | `0x1E63` | Send, read, attach, react, voice, video, screen share |
|