Discord: check our safeguards against the ones Dissent uses to avoid third-party-client flags #338

Open
opened 2026-10-21 22:52:00 -04:00 by Salastil · 1 comment
Owner

Discord flags accounts that use an unofficial client (forced logout, phone verification, a "spam" violation, sometimes removed again within minutes). Our account has been hit twice, both times within seconds of its first message in a server (see the notes under Where we are). Dissent (a GTK4 Discord client, Go, built on arikawa + ningen) has had the same reports (dissent#343, still open: "possible account suspension") and has put several safeguards in. This is the list of what its code actually does, so we can go through ours and verify each one.

Read at: Dissent 6ff6182 (2026-08-09), arikawa b430932 (2026-05-18), ningen 5a08d3a (2026-03-06). Local clones in /1tb/git/dissent, /1tb/git/arikawa, /1tb/git/ningen.

What is and is not known. Nobody, including Dissent's maintainer, has pinned the flagging to one cause; Discord gives no reason. These are the mitigations the project adopted or reasoned its way to, not proven fixes. Hypotheses from the thread: the first message in a newly created DM; images (oversized uploads, an image straight after a text message); a fresh token on a new device that then sends several messages at once; connections cut mid-request. Several reported violations were removed within minutes.

How to use this: tick an item when our code has the equivalent (or when we decide on purpose not to). Each item says where Dissent does it and what to look for in ours.

A. Identity: how the client presents itself

  • A1. A stable, honest User-Agent. Dissent: api.UserAgent = "Dissent (https://libdb.so/dissent)", set once (internal/gtkcord/state.go). It does not copy a browser or Discord's app. Check: one fixed UA on every REST call, naming the client and a version/URL; no per-request variation.
  • A2. A stable gateway identity that does not change between runs. Dissent: IDENTIFY properties = os (real runtime.GOOS), device = "Arikawa" (fixed), browser = "Dissent on " + hostname (gtkcord/state.go, gateway.DefaultIdentity). The one varying part is the machine's own hostname, so the same install always looks like the same device. Check: our properties never change between launches or between accounts on one install.
  • A3. capabilities sent in IDENTIFY. Dissent (ningen New): LazyUserNotes | VersionedReadStates | VersionedUserGuildSettings | DedupeUserObjects | PrioritizedReadyPayload | MultipleGuildExperimentPopulations | NonChannelReadStates. This is what a modern user client declares, and it changes the shape of READY (read states, deduplicated users, prioritized payload). Check: whether we send any; if we adopt it, READY handling has to read the new shapes.
  • A4. Default IDENTIFY parameters. Dissent (arikawa DefaultIdentifyCommand): compress: true, large_threshold: 50, a default presence, no intents (user accounts must not send them). Check ours match what a user client sends.
  • A5. One connection per login. Dissent keeps one gateway session and one in-memory state per login; nothing opens extra sessions. Check: no second IDENTIFY for the same account while one is live (the audit that led to our login limit: 77 fresh IDENTIFYs in a week).

B. Request volume: reading from the gateway instead of asking over REST

README: Dissent "tries its best to not use the REST API at all unless necessary to reduce the risk of abuse".

  • B1. Offline-first state. Dissent (ningen State.Offline()): UI code reads through a state whose context is already cancelled, so any REST fallback fails immediately. It uses Online() only for deliberate user actions (opening a channel's history, sending, reacting, acking, deleting). Check: every read the UI makes is served from what READY and the gateway gave us, with REST only on an explicit action.
  • B2. Guilds, channels, roles, members, emoji, DM list, read states, relationships, notes and mutes all come from READY/gateway events. The DM list is Cabinet.PrivateChannels() (READY), not GET /users/@me/channels. Check: what we still ask over REST at startup (we do GET /users/@me/channels, a per-DM catch-up, and own-member lookups).
  • B3. A small history fetch per channel opened. Dissent: 15 messages when a channel is opened (FetchBacklog), only when it is opened; "load more" is 50 and is answered from the cached messages first, REST only when the cache runs out. No catch-up of channels that are not on screen, no prefetch. Check: nothing fetches history for channels nobody opened.
  • B4. Member lists by gateway subscription, once per guild. Dissent (ningen MemberState.Subscribe): one guild-subscribe command (typing + threads + activities) the first time a guild is opened, deduplicated and retried only on failure. Check our op 14 usage (per channel, [[0,99]]) and that it is not repeated.
  • B5. Member search by gateway, throttled and deduplicated. Dissent: RequestGuildMembers (op 8), at most one per 600 ms per guild, limit 50, empty queries skipped; RequestMember skips a member already in the state or already requested. Check: no REST member lookups where the gateway can answer, and a minimum gap between searches.
  • B6. Images, avatars and emoji from the public CDN, never through authenticated API calls. Check: nothing fetches a picture with the account's token.

C. Read state (acks)

  • C1. Ack only what the person has actually read. Dissent (internal/messages/view.go): MarkRead runs when the window gains focus or the list is scrolled to its bottom, and only if the view is active (window focused and the view mapped). Check: we do not ack in the background or for a channel that is not on screen.
  • C2. Never ack twice, never ack your own. Dissent (ningen markRead): returns if the channel is already read to that message with no mentions; no ack for a message the account authored; no ack for a message not in the cache; a MESSAGE_ACK from another client updates state without sending another ack. Check each (we de-duplicate per channel and message since 1.3.0; verify the rest).
  • C3. Unread and mention counts kept locally from events. Dissent updates read state from MESSAGE_CREATE itself rather than refetching. Check: no polling of read state.

D. Sending

  • D1. No outgoing typing indicator. Dissent never calls the typing endpoint; it only displays other people's typing. Check: we send one (throttled to one per 8 s per channel since 1.3.0): decide whether to keep it, and whether to send it at all.
  • D2. A nonce on every message, random per message, also used to match the gateway's echo to the pending message (messages/key.go, view.go). Check: every message-creating request carries one (added to text, files, voice, stickers and forwards in 1.3.1).
  • D3. allowed_mentions sent explicitly (parse users/roles/everyone, replied_user set from the reply toggle). Check: present on every send (added in 1.3.1).
  • D4. Length limit enforced before sending: 2000 characters, 4000 with Nitro (composer.go); an over-limit message is refused with a notice and never sent. Check: we refuse over-limit text before the request.
  • D5. Empty messages are not sent; a bare "+emoji" is a reaction to a valid target, not a message. Check: nothing posts an empty body.
  • D6. Hard refusal to speak first in a DM. Dissent (commit ffdff32, 2024-09-19): if a DM has no messages it refuses to open it: "please send a message via the official client first", because messaging a newly added person immediately triggers a warning. Check: we lock empty DMs (1.3.1) and also servers where the account has not spoken since joining.
  • D7. Uploads refused before they start when over the limit. Dissent (commit 5b447f4, 2025-07-03, fixing #350: "we suspect this was causing account warnings"): limit from arikawa DetermineUploadSize = 10 MiB, 50 MiB (Nitro Basic/Classic), 500 MiB (Nitro), or the guild's boost tier (50 MiB tier 2, 100 MiB tier 3), whichever is higher; a file over it is not added to the tray and an alert says so. Check: our limits match, and the check happens before any bytes go out (added in 1.3.1 with the same figures).
  • D8. Never cut a request off halfway. The maintainer noted that arikawa used to cut the connection when an upload reached a size, and judged that "might cause Discord to trip off alerts". Dissent runs important writes on a background context so switching channel cannot abort them (DeleteMessage: "ensure it goes through even if the user switches away"; sends and uploads run to completion). Check: a navigation, a window close or a timeout does not abandon a send or an upload in flight.
  • D9. Pace between a text message and a file. Not in Dissent. From the thread: a user was flagged right after pasting an image straight after a message, and suggested a cooldown. Check: we wait before a file following a message (5 s) and before a message following a file (3 s) since 1.3.1, plus a 400 ms gap between any two writes.

E. Connection management

  • E1. Backoff on reconnect. Dissent (arikawa DefaultGatewayOpts): wait 4 s + 2 s * attempt between attempts, with no cap on the attempt count. Check our delays and that a refused login does not retry quickly.
  • E2. Stop on authentication failure. Close code 4004 (and 4010-4014) are fatal: no retry, the person is sent back to log in. Check: we stop on 4004, and treat 4003/4007/4009 as a lost session (new login, not a retry loop).
  • E3. Resume instead of re-identifying. Dissent resumes while the last heartbeat echo is under 15 minutes old (deadbeatDuration), otherwise identifies; op 9 and close 4007 invalidate the session and reconnect. Check: ours resumes (we use Discord's own 3-minute rule, 150 s, and persist the session across restarts since 1.3.0).
  • E4. A reconnect must not look like a fresh login each time, and a flapping connection must not turn into a burst of IDENTIFYs. Check: the login limit (3 an hour, 8 a day) and "Connect anyway" still hold, and nothing counts a resume.
  • E5. Closes the gateway on exit. Dissent calls state.Close() on shutdown (AlwaysCloseGracefully: true: a proper close frame, which invalidates the session). Check what ours does on quit and whether we want the session kept for a quick restart instead.

F. REST rate limiting

  • F1. Per-bucket and global limiter honouring Discord's headers, plus 250 ms of extra delay on every bucket wait (arikawa api/rate), and retries on 429 that wait as long as Discord asked. Check: we obey Retry-After/X-RateLimit-*, wait rather than fail, and cap how long we will wait.

G. People and documentation

  • G1. Be plain with the user about the risk. Dissent's README: using an unofficial client at all is against Discord's terms and may get the account banned; username/password login is "strongly discouraged"; token login is recommended. Check what we tell people when they add a Discord account.

Considered and not adopted by Dissent

  • Spoofing a browser/app identity (headers, X-Super-Properties, IDENTIFY properties). Suggested in the #343 thread (pointing at discordo's headers.go and props.go); Dissent does not do it, and neither do we. Recorded so the decision stays a decision.

Where we are (context, not a checklist)

  • Both flags on our account followed the first message ever sent from moho in chasm server (2026-10-20 23:04 and 2026-10-21 20:03, each ~12-27 s after sending, then the session was revoked). Plain text, single messages, none in DMs, none in the older server we have spoken in for a year.
  • Already in 1.3.0/1.3.1: login limit and "Connect anyway", session resume across restarts, trimmed startup REST, typing and ack throttles, install id/locale/timezone headers, nonce and allowed_mentions, upload limit check, send pacing, first-word lock for servers and empty DMs.
Discord flags accounts that use an unofficial client (forced logout, phone verification, a "spam" violation, sometimes removed again within minutes). Our account has been hit twice, both times within seconds of its first message in a server (see the notes under *Where we are*). [Dissent](https://github.com/diamondburned/dissent) (a GTK4 Discord client, Go, built on `arikawa` + `ningen`) has had the same reports ([dissent#343](https://github.com/diamondburned/dissent/issues/343), still open: "possible account suspension") and has put several safeguards in. This is the list of what its code actually does, so we can go through ours and verify each one. Read at: Dissent `6ff6182` (2026-08-09), arikawa `b430932` (2026-05-18), ningen `5a08d3a` (2026-03-06). Local clones in `/1tb/git/dissent`, `/1tb/git/arikawa`, `/1tb/git/ningen`. **What is and is not known.** Nobody, including Dissent's maintainer, has pinned the flagging to one cause; Discord gives no reason. These are the mitigations the project adopted or reasoned its way to, not proven fixes. Hypotheses from the thread: the first message in a newly created DM; images (oversized uploads, an image straight after a text message); a fresh token on a new device that then sends several messages at once; connections cut mid-request. Several reported violations were removed within minutes. How to use this: tick an item when our code has the equivalent (or when we decide on purpose not to). Each item says where Dissent does it and what to look for in ours. ## A. Identity: how the client presents itself - [x] **A1. A stable, honest User-Agent.** Dissent: `api.UserAgent = "Dissent (https://libdb.so/dissent)"`, set once (`internal/gtkcord/state.go`). It does not copy a browser or Discord's app. Check: one fixed UA on every REST call, naming the client and a version/URL; no per-request variation. - [x] **A2. A stable gateway identity that does not change between runs.** Dissent: IDENTIFY `properties` = `os` (real `runtime.GOOS`), `device` = `"Arikawa"` (fixed), `browser` = `"Dissent on " + hostname` (`gtkcord/state.go`, `gateway.DefaultIdentity`). The one varying part is the machine's own hostname, so the same install always looks like the same device. Check: our `properties` never change between launches or between accounts on one install. - [ ] **A3. `capabilities` sent in IDENTIFY.** Dissent (ningen `New`): `LazyUserNotes | VersionedReadStates | VersionedUserGuildSettings | DedupeUserObjects | PrioritizedReadyPayload | MultipleGuildExperimentPopulations | NonChannelReadStates`. This is what a modern user client declares, and it changes the shape of READY (read states, deduplicated users, prioritized payload). Check: whether we send any; if we adopt it, READY handling has to read the new shapes. - [ ] **A4. Default IDENTIFY parameters.** Dissent (arikawa `DefaultIdentifyCommand`): `compress: true`, `large_threshold: 50`, a default presence, no `intents` (user accounts must not send them). Check ours match what a user client sends. - [x] **A5. One connection per login.** Dissent keeps one gateway session and one in-memory state per login; nothing opens extra sessions. Check: no second IDENTIFY for the same account while one is live (the audit that led to our login limit: 77 fresh IDENTIFYs in a week). ## B. Request volume: reading from the gateway instead of asking over REST README: Dissent "tries its best to not use the REST API at all unless necessary to reduce the risk of abuse". - [x] **B1. Offline-first state.** Dissent (ningen `State.Offline()`): UI code reads through a state whose context is already cancelled, so any REST fallback fails immediately. It uses `Online()` only for deliberate user actions (opening a channel's history, sending, reacting, acking, deleting). Check: every read the UI makes is served from what READY and the gateway gave us, with REST only on an explicit action. - [ ] **B2. Guilds, channels, roles, members, emoji, DM list, read states, relationships, notes and mutes all come from READY/gateway events.** The DM list is `Cabinet.PrivateChannels()` (READY), not `GET /users/@me/channels`. Check: what we still ask over REST at startup (we do `GET /users/@me/channels`, a per-DM catch-up, and own-member lookups). - [x] **B3. A small history fetch per channel opened.** Dissent: 15 messages when a channel is opened (`FetchBacklog`), only when it is opened; "load more" is 50 and is answered from the cached messages first, REST only when the cache runs out. No catch-up of channels that are not on screen, no prefetch. Check: nothing fetches history for channels nobody opened. - [x] **B4. Member lists by gateway subscription, once per guild.** Dissent (ningen `MemberState.Subscribe`): one guild-subscribe command (typing + threads + activities) the first time a guild is opened, deduplicated and retried only on failure. Check our op 14 usage (per channel, `[[0,99]]`) and that it is not repeated. - [x] **B5. Member search by gateway, throttled and deduplicated.** Dissent: `RequestGuildMembers` (op 8), at most one per 600 ms per guild, limit 50, empty queries skipped; `RequestMember` skips a member already in the state or already requested. Check: no REST member lookups where the gateway can answer, and a minimum gap between searches. - [x] **B6. Images, avatars and emoji from the public CDN, never through authenticated API calls.** Check: nothing fetches a picture with the account's token. ## C. Read state (acks) - [x] **C1. Ack only what the person has actually read.** Dissent (`internal/messages/view.go`): `MarkRead` runs when the window gains focus or the list is scrolled to its bottom, and only if the view is active (window focused *and* the view mapped). Check: we do not ack in the background or for a channel that is not on screen. - [x] **C2. Never ack twice, never ack your own.** Dissent (ningen `markRead`): returns if the channel is already read to that message with no mentions; no ack for a message the account authored; no ack for a message not in the cache; a `MESSAGE_ACK` from another client updates state without sending another ack. Check each (we de-duplicate per channel and message since 1.3.0; verify the rest). - [x] **C3. Unread and mention counts kept locally from events.** Dissent updates read state from `MESSAGE_CREATE` itself rather than refetching. Check: no polling of read state. ## D. Sending - [x] **D1. No outgoing typing indicator.** Dissent never calls the typing endpoint; it only displays other people's typing. Check: we send one (throttled to one per 8 s per channel since 1.3.0): decide whether to keep it, and whether to send it at all. - [x] **D2. A `nonce` on every message**, random per message, also used to match the gateway's echo to the pending message (`messages/key.go`, `view.go`). Check: every message-creating request carries one (added to text, files, voice, stickers and forwards in 1.3.1). - [x] **D3. `allowed_mentions` sent explicitly** (parse users/roles/everyone, `replied_user` set from the reply toggle). Check: present on every send (added in 1.3.1). - [x] **D4. Length limit enforced before sending**: 2000 characters, 4000 with Nitro (`composer.go`); an over-limit message is refused with a notice and never sent. Check: we refuse over-limit text before the request. - [x] **D5. Empty messages are not sent**; a bare "+emoji" is a reaction to a valid target, not a message. Check: nothing posts an empty body. - [x] **D6. Hard refusal to speak first in a DM.** Dissent (commit `ffdff32`, 2024-09-19): if a DM has no messages it refuses to open it: "please send a message via the official client first", because messaging a newly added person immediately triggers a warning. Check: we lock empty DMs (1.3.1) and also servers where the account has not spoken since joining. - [x] **D7. Uploads refused before they start when over the limit.** Dissent (commit `5b447f4`, 2025-07-03, fixing [#350](https://github.com/diamondburned/dissent/issues/350): "we suspect this was causing account warnings"): limit from arikawa `DetermineUploadSize` = 10 MiB, 50 MiB (Nitro Basic/Classic), 500 MiB (Nitro), or the guild's boost tier (50 MiB tier 2, 100 MiB tier 3), whichever is higher; a file over it is not added to the tray and an alert says so. Check: our limits match, and the check happens before any bytes go out (added in 1.3.1 with the same figures). - [x] **D8. Never cut a request off halfway.** The maintainer noted that arikawa used to cut the connection when an upload reached a size, and judged that "might cause Discord to trip off alerts". Dissent runs important writes on a background context so switching channel cannot abort them (`DeleteMessage`: "ensure it goes through even if the user switches away"; sends and uploads run to completion). Check: a navigation, a window close or a timeout does not abandon a send or an upload in flight. - [x] **D9. Pace between a text message and a file.** Not in Dissent. From the thread: [a user](https://github.com/diamondburned/dissent/issues/343) was flagged right after pasting an image straight after a message, and suggested a cooldown. Check: we wait before a file following a message (5 s) and before a message following a file (3 s) since 1.3.1, plus a 400 ms gap between any two writes. ## E. Connection management - [x] **E1. Backoff on reconnect.** Dissent (arikawa `DefaultGatewayOpts`): wait `4 s + 2 s * attempt` between attempts, with no cap on the attempt count. Check our delays and that a refused login does not retry quickly. - [x] **E2. Stop on authentication failure.** Close code 4004 (and 4010-4014) are fatal: no retry, the person is sent back to log in. Check: we stop on 4004, and treat 4003/4007/4009 as a lost session (new login, not a retry loop). - [x] **E3. Resume instead of re-identifying.** Dissent resumes while the last heartbeat echo is under 15 minutes old (`deadbeatDuration`), otherwise identifies; op 9 and close 4007 invalidate the session and reconnect. Check: ours resumes (we use Discord's own 3-minute rule, 150 s, and persist the session across restarts since 1.3.0). - [x] **E4. A reconnect must not look like a fresh login each time**, and a flapping connection must not turn into a burst of IDENTIFYs. Check: the login limit (3 an hour, 8 a day) and "Connect anyway" still hold, and nothing counts a resume. - [x] **E5. Closes the gateway on exit.** Dissent calls `state.Close()` on shutdown (`AlwaysCloseGracefully: true`: a proper close frame, which invalidates the session). Check what ours does on quit and whether we want the session kept for a quick restart instead. ## F. REST rate limiting - [x] **F1. Per-bucket and global limiter honouring Discord's headers**, plus 250 ms of extra delay on every bucket wait (arikawa `api/rate`), and retries on 429 that wait as long as Discord asked. Check: we obey `Retry-After`/`X-RateLimit-*`, wait rather than fail, and cap how long we will wait. ## G. People and documentation - [x] **G1. Be plain with the user about the risk.** Dissent's README: using an unofficial client at all is against Discord's terms and may get the account banned; username/password login is "strongly discouraged"; token login is recommended. Check what we tell people when they add a Discord account. ## Considered and not adopted by Dissent - Spoofing a browser/app identity (headers, `X-Super-Properties`, IDENTIFY properties). Suggested in the #343 thread (pointing at [discordo's `headers.go` and `props.go`](https://github.com/ayn2op/discordo/tree/main/internal/http)); Dissent does not do it, and neither do we. Recorded so the decision stays a decision. ## Where we are (context, not a checklist) - Both flags on our account followed the first message ever sent from moho in chasm server (2026-10-20 23:04 and 2026-10-21 20:03, each ~12-27 s after sending, then the session was revoked). Plain text, single messages, none in DMs, none in the older server we have spoken in for a year. - Already in 1.3.0/1.3.1: login limit and "Connect anyway", session resume across restarts, trimmed startup REST, typing and ack throttles, install id/locale/timezone headers, nonce and `allowed_mentions`, upload limit check, send pacing, first-word lock for servers and empty DMs.
Salastil added the feature label 2026-10-21 22:52:00 -04:00
Author
Owner

Built and deployed on development (moho 74f2c47, nobilis 7943767), not released yet. Done, item by item, where ours was weaker than Dissent's:

  • F1 rate limits. Reads were sent plain, so a 429 was a silent "nothing found" and the next request followed at once. Every read now goes through send_paced: it waits out any hold Discord has put on the client, notes X-RateLimit-Remaining: 0 and waits for the bucket to reset (+250 ms, as arikawa does), and on a 429 waits as long as it was told and asks again (twice at most). Writes share the same hold. Tested against a local listener.
  • A3 capabilities. IDENTIFY now declares five of Dissent's seven (lazy notes, versioned read states, versioned guild settings, guild-experiment populations, non-channel read states). Their only effect on READY is the shape of fields moho ignores or already reads in both shapes (user_guild_settings).
  • B3 history. Nothing is fetched at login any more: no first-page backfill of every new channel and DM (it was one request a channel). A conversation is read the first time it is opened, 30 messages, and its last activity (for ordering the DM list) comes from the id of its last message. Later opens ask only for what came after.
  • B4 member lists. One subscription a channel a connection. Joins and leaves no longer re-subscribe (on a busy server that was a request at every join), and a resync is at most one in ten seconds.
  • B2 own membership. READY's merged_members is used for our own member of each guild, so no guild is asked about (one request a guild at every fresh login).
  • C1/C2 acks. Acks are sent only while the window is visible and focused (a conversation opened at startup behind other windows is told when the window is next looked at); never for a conversation whose last word is our own; not repeated after a MESSAGE_ACK from another device.
  • D1 typing. Off by default for Discord, as Dissent has none; a switch in Settings > Discord turns it on (still at most one every eight seconds).
  • D4/D5. Text is checked before it is sent: over 2,000 characters (4,000 with Nitro) or empty is refused with a sentence, with no request.
  • E1. First reconnect wait is 4 s (arikawa's minimum), doubling to 60 s.
  • G1. Adding a Discord account now says plainly that third-party clients are against Discord's terms and what moho does and does not do about it.
  • E5, D8, E3 (checked, decided). We do not close the gateway with a close frame on quit, so the session stays resumable for a restart (the opposite of Dissent's choice, on purpose). Writes run as their own tasks in the daemon, so closing or switching a window never aborts one in flight; only quitting the daemon itself does.

Not done, and why

  • A3, the other two capabilities (DEDUPE_USER_OBJECTS, PRIORITIZED_READY_PAYLOAD): they move users out of READY into a table by id and split the rest into a second event, and our friends list, conversation names and presences are built from the objects that would no longer be there. They need a real READY to write against; nothing was invented.
  • A4 compress. Not adopted: the payload gains nothing we need.
  • B2 GET /users/@me/channels at a fresh login stays: READY's private channels were found incomplete in live testing, before the capabilities above. Worth re-checking against a live READY once they are in.
  • C1 scrolled-to-bottom. We do not ack as messages arrive in the open conversation (Dissent does, at the bottom); we ack only on opening it and on regaining focus.

Ticked: everything above except A3, A4, B2.

Built and deployed on `development` (moho `74f2c47`, nobilis `7943767`), not released yet. Done, item by item, where ours was weaker than Dissent's: - **F1 rate limits.** Reads were sent plain, so a 429 was a silent "nothing found" and the next request followed at once. Every read now goes through `send_paced`: it waits out any hold Discord has put on the client, notes `X-RateLimit-Remaining: 0` and waits for the bucket to reset (+250 ms, as arikawa does), and on a 429 waits as long as it was told and asks again (twice at most). Writes share the same hold. Tested against a local listener. - **A3 capabilities.** IDENTIFY now declares five of Dissent's seven (lazy notes, versioned read states, versioned guild settings, guild-experiment populations, non-channel read states). Their only effect on READY is the shape of fields moho ignores or already reads in both shapes (`user_guild_settings`). - **B3 history.** Nothing is fetched at login any more: no first-page backfill of every new channel and DM (it was one request a channel). A conversation is read the first time it is opened, 30 messages, and its last activity (for ordering the DM list) comes from the id of its last message. Later opens ask only for what came after. - **B4 member lists.** One subscription a channel a connection. Joins and leaves no longer re-subscribe (on a busy server that was a request at every join), and a resync is at most one in ten seconds. - **B2 own membership.** READY's `merged_members` is used for our own member of each guild, so no guild is asked about (one request a guild at every fresh login). - **C1/C2 acks.** Acks are sent only while the window is visible and focused (a conversation opened at startup behind other windows is told when the window is next looked at); never for a conversation whose last word is our own; not repeated after a `MESSAGE_ACK` from another device. - **D1 typing.** Off by default for Discord, as Dissent has none; a switch in Settings > Discord turns it on (still at most one every eight seconds). - **D4/D5.** Text is checked before it is sent: over 2,000 characters (4,000 with Nitro) or empty is refused with a sentence, with no request. - **E1.** First reconnect wait is 4 s (arikawa's minimum), doubling to 60 s. - **G1.** Adding a Discord account now says plainly that third-party clients are against Discord's terms and what moho does and does not do about it. - **E5, D8, E3 (checked, decided).** We do not close the gateway with a close frame on quit, so the session stays resumable for a restart (the opposite of Dissent's choice, on purpose). Writes run as their own tasks in the daemon, so closing or switching a window never aborts one in flight; only quitting the daemon itself does. **Not done, and why** - **A3, the other two capabilities** (DEDUPE_USER_OBJECTS, PRIORITIZED_READY_PAYLOAD): they move users out of READY into a table by id and split the rest into a second event, and our friends list, conversation names and presences are built from the objects that would no longer be there. They need a real READY to write against; nothing was invented. - **A4 `compress`.** Not adopted: the payload gains nothing we need. - **B2 `GET /users/@me/channels` at a fresh login** stays: READY's private channels were found incomplete in live testing, before the capabilities above. Worth re-checking against a live READY once they are in. - **C1 scrolled-to-bottom.** We do not ack as messages arrive in the open conversation (Dissent does, at the bottom); we ack only on opening it and on regaining focus. Ticked: everything above except A3, A4, B2.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Salastil/moho#338