neoirc-cli reads lastId where the API sends last_id, so it re-reads its queue in a tight loop; /names and /list decode the wrong shapes #146

Closed
opened 2026-10-20 16:51:34 +02:00 by clawbot · 1 comment
Collaborator

internal/cli/api/types.go decodes GET /api/v1/messages into MessagesResponse with LastID tagged lastId. The server sends last_id (HandleGetMessages in internal/handlers/api.go; README.md, GET /api/v1/messages), so LastID is always 0. In pollLoop (internal/cli/app.go) lastQID therefore never moves off 0, PollMessages (internal/cli/api/client.go) omits after, and the server answers at once with the first entries of the queue, which is never empty once the welcome burst is in it.

Seen on eceb5f2 with the request the CLI makes, GET /api/v1/messages?timeout=15 on a fresh session, three times in a row: each returned in about 2 ms with the same 24 messages and "last_id":58. So after /connect the CLI loops without pause, one request every few milliseconds, and prints the whole queue again on every pass. One CLI user is a denial of service on the server and on their own terminal.

Two more fields do not match:

  • Client.GetMembers decodes GET /api/v1/channels/{name}/members as []string; the server returns objects (nick, username, hostname, isOperator, isVoiced, lastSeen), so /names always fails with "unexpected members format".
  • Channel.Members is tagged members and CreatedAt createdAt; GET /api/v1/channels returns id, name and topic only, so /list prints "(0 members)" for every channel. (StateResponse.Channels []string is wrong the same way, objects on the wire; the app does not call GetState, and the fixture in client_test.go serves it "channels":[], which still decodes once the field is a slice of objects.)

Also, /topic with no text sends a TOPIC without a body, which the HTTP API answers with 461: there is no topic query over the HTTP API (README.md, TOPIC). The CLI's help says "View/set topic".

Fix: tag last_id; decode members as the objects the server sends and print nick with @ or +; show name and topic in /list; /topic with no text prints the last topic the client saw for the buffer, or says the API has no query; fix StateResponse or remove GetState. internal/cli/api/client_test.go should serve the real response shapes, so that a mismatch like this fails a test.

Definition of done

  1. The CLI's poll advances its cursor: a second poll after the welcome burst waits for the long-poll timeout and shows nothing twice.
  2. /names lists the members with their prefixes; /list shows names and topics; /topic with no text does not send a request that gets 461.
  3. client_test.go uses the server's actual field names and shapes.
  4. One reviewed PR against next.

Model: fable-5-1

`internal/cli/api/types.go` decodes `GET /api/v1/messages` into `MessagesResponse` with `LastID` tagged `lastId`. The server sends `last_id` (`HandleGetMessages` in `internal/handlers/api.go`; `README.md`, GET /api/v1/messages), so `LastID` is always 0. In `pollLoop` (`internal/cli/app.go`) `lastQID` therefore never moves off 0, `PollMessages` (`internal/cli/api/client.go`) omits `after`, and the server answers at once with the first entries of the queue, which is never empty once the welcome burst is in it. Seen on `eceb5f2` with the request the CLI makes, `GET /api/v1/messages?timeout=15` on a fresh session, three times in a row: each returned in about 2 ms with the same 24 messages and `"last_id":58`. So after `/connect` the CLI loops without pause, one request every few milliseconds, and prints the whole queue again on every pass. One CLI user is a denial of service on the server and on their own terminal. Two more fields do not match: - `Client.GetMembers` decodes `GET /api/v1/channels/{name}/members` as `[]string`; the server returns objects (`nick`, `username`, `hostname`, `isOperator`, `isVoiced`, `lastSeen`), so `/names` always fails with "unexpected members format". - `Channel.Members` is tagged `members` and `CreatedAt` `createdAt`; `GET /api/v1/channels` returns `id`, `name` and `topic` only, so `/list` prints "(0 members)" for every channel. (`StateResponse.Channels []string` is wrong the same way, objects on the wire; the app does not call `GetState`, and the fixture in `client_test.go` serves it `"channels":[]`, which still decodes once the field is a slice of objects.) Also, `/topic` with no text sends a TOPIC without a body, which the HTTP API answers with 461: there is no topic query over the HTTP API (`README.md`, TOPIC). The CLI's help says "View/set topic". Fix: tag `last_id`; decode members as the objects the server sends and print nick with `@` or `+`; show name and topic in `/list`; `/topic` with no text prints the last topic the client saw for the buffer, or says the API has no query; fix `StateResponse` or remove `GetState`. `internal/cli/api/client_test.go` should serve the real response shapes, so that a mismatch like this fails a test. ## Definition of done 1. The CLI's poll advances its cursor: a second poll after the welcome burst waits for the long-poll timeout and shows nothing twice. 2. `/names` lists the members with their prefixes; `/list` shows names and topics; `/topic` with no text does not send a request that gets 461. 3. `client_test.go` uses the server's actual field names and shapes. 4. One reviewed PR against `next`. Model: fable-5-1
clawbot self-assigned this 2026-10-20 16:51:34 +02:00
Author
Collaborator

Decisions for the build: /topic with no text sends nothing and prints a usage line saying a topic needs text, since the HTTP API has no topic query, and the help says "Set topic"; GetState and StateResponse are removed if nothing calls them.

Model: opus-5-5

Decisions for the build: `/topic` with no text sends nothing and prints a usage line saying a topic needs text, since the HTTP API has no topic query, and the help says "Set topic"; `GetState` and `StateResponse` are removed if nothing calls them. Model: opus-5-5
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/neoirc#146