Skip to content

Latest commit

 

History

History
185 lines (146 loc) · 7.92 KB

File metadata and controls

185 lines (146 loc) · 7.92 KB

Direct messages

Status: implemented standalone behavior and recipient privacy, pre-alpha

The IRC adapter has two ordinary direct-message paths plus one live-only DCC negotiation path. This keeps ordinary IRC clients useful without pretending that a disposable nickname or expiring host/port offer is a safe authorization key for retained private history.

Live nickname addressing

Any registered client can send to a currently connected nickname:

PRIVMSG Bob :hello

The same route accepts a notice:

NOTICE Bob :quiet heads-up

If both connections authenticated with SASL, the server commits the message to the single durable conversation for those two account IDs before delivery. It then fans the event out to every registered connection on the sender and recipient accounts. The source keeps the nickname and username presentation used when the message was sent; each receiving account device sees its own current nickname as the incoming target.

If either endpoint is anonymous, the message goes only to the addressed live connection, plus a negotiated sender echo. It is not persisted. Other devices do not receive it, and reconnecting or taking the same nickname does not expose it.

Notices obey those same live-only or durable account-pair rules. Their command type is retained in history, so replay emits NOTICE rather than PRIVMSG. The server sends no automatic error or away reply in response to a notice, including when the target is invalid or unavailable.

Live DCC negotiation

An exact CTCP DCC ... PRIVMSG is always connection-scoped, even when both users authenticated. It must target one currently connected nickname; the experimental @account form is rejected because it could fan an expiring offer out to several devices or persist it for an offline recipient. The frame crosses the recipient's ordinary inbound DM policy, goes only to that exact connection plus a negotiated sender echo, and is never inserted into the durable account-pair thread. Reconnect, history, and another device cannot replay it.

This preserves ordinary user-to-user DCC negotiation without making Telex a file relay. The two clients still establish their own data connection, disclose the addresses contained in that negotiation to one another, and own any encryption or file-safety decision. Registered XDCC providers use the same legacy CTCP presentation but a separately bounded server/provider control path described in external content references.

Stable account addressing

An authenticated client can send to an existing local account even when none of its devices are connected:

PRIVMSG @alice :this will be waiting for you
NOTICE @alice :this will also be waiting for you

Account names are case-insensitive. @account is an experimental adapter syntax, not an IRCv3 capability or a valid IRC nickname. Existing clients that cannot enter it as a message target may need a raw-command input; a future reference client can expose accounts directly.

The stable target also works for retained history and both cursor types:

CHATHISTORY LATEST @alice * 100
MARKREAD @alice timestamp=2026-07-17T12:34:56.789Z
CHATHISTORY RESUME @alice * 100
MARKDELIVERED @alice msgid=019...

These commands require the corresponding negotiated capabilities. A nickname can also name an online authenticated peer in these commands, but @account is the reliable form across disconnects and nick changes.

Authorization and privacy boundary

Each durable thread is keyed by an unordered pair of stable account IDs. The requester's authenticated account is always one member; clients never supply a conversation ID. History references and read or delivery acknowledgements must name events in exactly that pair's conversation. A third account using a valid message ID from another pair receives the same invalid-reference failure as an unknown ID.

Anonymous sessions cannot access durable direct history, and anonymous DMs do not create it. Nick ownership is only a live routing fact, so nick reuse carries no private-message authority.

Recipient-owned privacy

Every account owns one inbound policy:

  • open admits authenticated and anonymous senders unless an authenticated account has an explicit block;
  • allowlist admits only authenticated accounts with an explicit allow; and
  • closed admits nobody, even when an allow rule is retained.

open is the compatibility-preserving default for new and migrated accounts. Changing policy does not erase explicit rules, so a user can close DMs temporarily and later return to the previous allowlist. Each account may keep at most 256 case-insensitive allow/block entries. Rules are private recipient preferences: channel membership, invitations, prior messages, and replies never create them implicitly.

Rules accept any syntactically valid canonical account name without checking whether it currently exists. This permits preemptive blocks and, importantly, keeps the management command from becoming an offline account-enumeration oracle. The current account lifecycle does not delete and reassign names.

An authenticated IRC client can inspect or update only its own settings:

DMCONTROL
DMCONTROL LIST
DMCONTROL POLICY open
DMCONTROL POLICY allowlist
DMCONTROL POLICY closed
DMCONTROL ALLOW alice
DMCONTROL BLOCK bob
DMCONTROL REMOVE bob

The server advertises the vendor command as the DMCONTROL ISUPPORT token. A successful query or mutation returns the complete current state, bounded by the same rule limit:

:irc.example DMCONTROL Alice POLICY allowlist
:irc.example DMCONTROL Alice ALLOW bob
:irc.example DMCONTROL Alice BLOCK mallory
:irc.example DMCONTROL Alice END

The response composes with labeled-response; a multi-line state response is placed in the normal labeled batch. The shared JavaScript session exposes query/set/allow/block/remove helpers and emits dmsettings after a complete response. The terminal client wraps them with /dm, /dm allowlist, and /dm allow|block|remove account. This command is a local extension, not an IRCv3 standard.

Opaque rejection

Policy is evaluated before a direct conversation is created and before an event is inserted. Blocked, closed, missing, disabled, self-account, and otherwise undisclosable targets all produce the same wire result. With standard-replies, a rejected private message receives:

FAIL PRIVMSG DM_UNAVAILABLE target :Direct messages are unavailable

Older clients receive generic numeric 401 text. Rejected notices remain silent, as IRC requires. In every case the purported recipient receives no message, notification, away interaction, or attempt record. The server creates no empty direct thread, message event, or authority-audit row. Ordinary CTCP content crosses the same check and follows the normal message path; exact DCC negotiation additionally uses the live-only rule above. IRCv3 TAGMSG, including +typing, crosses the same recipient decision before any live delivery. It is ephemeral: it neither creates a direct conversation nor appends an event, and an offline recipient has nothing to replay. Only active recipient devices that negotiated message-tags receive it. A blocked or unavailable target gets the same opaque DM_UNAVAILABLE result as PRIVMSG; no purported recipient learns that an attempt occurred.

For authenticated delivery, recipient resolution, policy evaluation, direct thread creation, and event insertion share one immediate SQLite transaction. This prevents a policy change from racing between authorization and persistence. Anonymous-to-authenticated delivery uses the same recipient decision but stays live-only when admitted.

This is transport encryption and server-side authorization, not end-to-end encryption. The standalone server operator can read message content from the running service or database and can inspect stored privacy preferences. TLS protects traffic in transit; local database, backup, and client-storage protection remain deployment responsibilities.