Skip to content

Bouncers: ZNC's buffer replay, soju's push, search and networks - #11

Draft
rdrake wants to merge 3 commits into
developfrom
claude/project-thread-ydkzxo
Draft

rdrake wants to merge 3 commits into
developfrom
claude/project-thread-ydkzxo

Conversation

@rdrake

@rdrake rdrake commented Nov 8, 2026 •

Copy link
Copy Markdown
Owner

What

Bouncer compatibility for ZNC and soju. Every change keys on what the server offers, so nefarious2 and plain ircds see no change.

  • ZNC buffer replay (BATCH znc.in/playback) is delivered as catch-up: appended, counted unread, no notification or highlight. ZNC has no msgids, so a buffered line is known by its time, sender, target and text, counted per conversation; a buffer ZNC keeps across attaches is not shown twice.
  • The bouncer owns the channel list. Behind ZNC (znc.in/* caps) or soju (BOUNCER_NETID, soju.im/bouncer-networks) the saved channels are never JOINed; the bouncer's own JOINs rebuild the list, so a channel parted from another client stays parted. Same rule as gamja.
  • soju web push: soju.im/webpush is requested alongside draft/webpush. Same command, VAPID token and payload.
  • soju search: a server that answers FAIL SEARCH INVALID_PARAMS in is asked once per joined channel, one after another (soju 0.7 has no labeled-response), and the parts are merged into one page. A server with MSGREFTYPES=timestamp is asked for a hit's context by its time.
  • soju networks (soju.im/bouncer-networks): a login without a network is the control connection. After a SASL login, each network it lists is added as its own saved network, bound with BOUNCER BIND before CAP END. A network the user removes is not added back. Ids are validated, at most 20 per list. The service worker binds too, so replying from a notification reaches the right network.

Testing

  • New mocha tests in test/irc/bouncer.ts, plus historysearch.ts, webpush.ts and caps.ts updates. Full suite, lint, typecheck, build and the smoke scenario pass.
  • Checked live in Chromium against soju 0.7 (networks added, bound, channels joined) and ZNC 1.9 (buffer shown once, in order).

Notes

  • Conflicts lightly with Server password (PASS) for bouncers without client SASL #10 (server password): keep both options, and networks added from soju should also carry serverPassword. Whichever lands second takes that.
  • Not covered: TOFU certificate pinning for self-signed ZNC over TCP, ZNC's *playback module, CertFP.

ZNC and soju each left a gap Seance did not cover. Each fix goes by what
the server offers, not by which bouncer it is.

- ZNC's buffer replay (`BATCH +x znc.in/playback <target>`, on every
  attach) was unwrapped and handled as live lines. It now goes the
  catch-up's way: appended, counted as unread but silent. ZNC has no
  msgids, so a line not newer than the conversation's newest message is
  dropped, and a buffer ZNC keeps is not shown twice.
- Behind a bouncer (BOUNCER_NETID, soju.im/bouncer-networks, or a
  znc.in/* cap) the autojoin waits for the bouncer's own JOINs. If it gave
  any back it owns the list: a saved channel it did not give back was
  left from another client and is not JOINed again. A bouncer that gave
  none back gets the saved channels, as before.
- soju offers web push only as soju.im/webpush: the same command, VAPID
  token and payload as draft/webpush. Seance now asks for it too.
- Search: soju refuses a search over every channel without in=
  (`FAIL SEARCH INVALID_PARAMS in`). Seance then asks each joined channel
  (up to 30) and answers once, for the rest of the connection too. soju
  takes only timestamp references, so the window around a hit is asked
  for by the hit's time.
- soju.im/bouncer-networks: a login without a network is soju's control
  connection, which has no channels. It no longer JOINs there; it lists
  the networks and adds each new one as a network of its own, the same
  server and login bound with `BOUNCER BIND <id>` before CAP END. A
  network the user removes is not added back. With -notify, a network
  added on the bouncer later is added the same way.

Tests: test/irc/bouncer.ts (new), and the soju cases in historysearch,
webpush and caps. Docs: irc-layer.md § Bouncers, client-features.md
§ Searching history.
@rdrake rdrake self-assigned this Nov 8, 2026
- ZNC playback: know a buffered line by its time, sender, target and
  text, counted per conversation, instead of dropping everything not
  newer than the newest message. Lines seen live are skipped, two
  equal lines in one second both stay.
- Behind a bouncer the saved channels are never JOINed: the bouncer's
  own JOINs are the list.
- soju search without in=: ask the channels one after another (0.7 has
  no labeled-response), uncharged behind a bouncer, no cap, and a part
  that fails fails the search.
- soju networks: add only after a SASL login (soju binds no PASS
  login), only from a connection that is not itself bound, at most 20
  per list, ids checked before they are stored or sent, entries added
  by hand as account/name count as saved; with -notify the list is not
  asked for again.
- The service worker binds a soju network's own connection too, so a
  reply from a notification reaches that network.
The cap of 20 counted one list at a time, and each -notify line is a
list of one, so a bouncer announcing new ids one by one got all of them
added. Count what the entry offered before (bouncerSeen) against it,
which also bounds bouncerSeen.

This branch was previously deployed

1 inactive deployment
github-pages — 9949abd6 Deployed Nov 8, 2026 by rdrake via deploy #182
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants