Bind history to the recipient session and channel, not the name (closes #148)
check / check (push) Waiting to run

History was selected by msg_to, so whoever next held a released nick, or
created a deleted channel again under its old name, could read the
earlier holder's messages. Each message row now records the session a
DM or INVITE was sent to (to_session_id) and the channel a channel
message was sent to (to_channel_id), and GET /api/v1/history selects by
the one that applies, using its index. A session's DM history now also
follows it across nick changes. A DM or channel message whose recipient
is deleted before it is stored gets the unknown nick or channel error.

Model: opus-5-5
This commit was merged in pull request #161.
This commit is contained in:
2026-10-20 19:48:35 +02:00
parent 5100b9a665
commit 164fc93a48
11 changed files with 569 additions and 135 deletions
+33 -23
View File
@@ -376,9 +376,10 @@ balancer, and CDN handles it correctly.
- DMs are **fan-out to both sender and recipient** — the sender sees their own
DM echoed back in their message queue, enabling multi-client consistency (your
laptop sees DMs you sent from your phone).
- DM history is stored in the `messages` table with the recipient nick as the
`msg_to` field. This means DM history is queryable per-nick, but if a user
changes their nick, old DMs are associated with the old nick.
- DM history is stored in the `messages` table with the recipient's session as
the `to_session_id` field. A user's DM history therefore follows them across
nick changes, and a user who later takes a released nick sees none of the DMs
sent to its previous holder.
- DMs are **not stored long-term** by default — they follow the same rotation
policy as channel messages.
@@ -1839,18 +1840,21 @@ Fetch historical messages for a channel. Returns messages in chronological order
**Query Parameters:**
| Param | Type | Default | Description |
| -------- | ------- | ---------- | ------------------------------------------------------------------------------------------------ |
| `target` | string | (required) | Channel name (e.g., `#general`) |
| `before` | string | (none) | The `id` of a message sent to `target`; return only messages older than it. Omit for the latest. |
| `limit` | integer | `50` | Maximum messages to return, 1–500; any other value gives 50. |
| Param | Type | Default | Description |
| -------- | ------- | ---------- | ----------------------------------------------------------------------------------------------- |
| `target` | string | (required) | Channel name (e.g., `#general`) |
| `before` | string | (none) | The `id` of a message in this history; return only messages older than it. Omit for the latest. |
| `limit` | integer | `50` | Maximum messages to return, 1–500; any other value gives 50. |
To page back, pass the `id` of the first (oldest) message of a page as `before`
for the next; an empty page means there is nothing older.
`target` may also be your own nick, which returns the DMs sent to you. Errors:
`400` `target required`, `400` `unknown before id` when no message with that
`id` was sent to `target`, `404` `channel not found`, `403`
`target` may also be your own nick, which returns the DMs sent to your session,
including those sent under its earlier nicks. A channel created again after its
last member left starts with no history, and a user who takes a released nick
gets none of the DMs sent to its previous holder. Errors: `400`
`target required`, `400` `unknown before id` when no message with that `id` is
in this history, `404` `channel not found`, `403`
`not a member of this channel`, and `403` `forbidden` for any other nick.
**Response:** `200 OK`
@@ -2508,19 +2512,25 @@ Unique constraint on `(channel_id, session_id)`.
#### `messages`
| Column | Type | Description |
| ------------ | -------- | --------------------------------------------------------------- |
| `id` | INTEGER | Primary key (auto-increment). Internal ID for queue references. |
| `uuid` | TEXT | UUID v4, exposed to clients as the message `id` |
| `command` | TEXT | IRC command (`PRIVMSG`, `JOIN`, etc.) |
| `msg_from` | TEXT | Sender nick |
| `msg_to` | TEXT | Target (`#channel` or nick) |
| `params` | TEXT | JSON-encoded IRC-style positional parameters |
| `body` | TEXT | JSON-encoded body (array or object) |
| `meta` | TEXT | JSON-encoded metadata |
| `created_at` | DATETIME | Server timestamp |
| Column | Type | Description |
| --------------- | -------- | ----------------------------------------------------------------------------------- |
| `id` | INTEGER | Primary key (auto-increment). Internal ID for queue references. |
| `uuid` | TEXT | UUID v4, exposed to clients as the message `id` |
| `command` | TEXT | IRC command (`PRIVMSG`, `JOIN`, etc.) |
| `msg_from` | TEXT | Sender nick |
| `msg_to` | TEXT | Target (`#channel` or nick) |
| `to_session_id` | INTEGER | FK → sessions.id (set to null on delete): the session a DM or `INVITE` was sent to |
| `to_channel_id` | INTEGER | FK → channels.id (set to null on delete): the channel a channel message was sent to |
| `params` | TEXT | JSON-encoded IRC-style positional parameters |
| `body` | TEXT | JSON-encoded body (array or object) |
| `meta` | TEXT | JSON-encoded metadata |
| `created_at` | DATETIME | Server timestamp |
Indexes on `(msg_to, id)` and `(created_at)`.
Indexes on `(to_session_id, id)`, `(to_channel_id, id)` and `(created_at)`.
`to_session_id` and `to_channel_id` are null on a message sent to neither, such
as `QUIT`. `GET /api/v1/history` selects by one of these two columns rather than
by `msg_to`, because nicks and channel names are reused once released, and
session and channel ids are not.
#### `spent_hashcash`