Skip to content

fix(join): fire CHATHISTORY/WHO on server-initiated own JOIN#249

Merged
ValwareIRC merged 3 commits into
mainfrom
fix/server-initiated-join-chain
Jun 5, 2026
Merged

fix(join): fire CHATHISTORY/WHO on server-initiated own JOIN#249
ValwareIRC merged 3 commits into
mainfrom
fix/server-initiated-join-chain

Conversation

@ValwareIRC
Copy link
Copy Markdown
Contributor

@ValwareIRC ValwareIRC commented Jun 5, 2026

Summary

Empty-nicklist-on-Unreal/InspIRCd bug that #248 addressed for one entry point and missed for the other. Reproduced on UnrealIRCd Support; confirmed root cause via Raw IRC Log Viewer (no CHATHISTORY was sent, no WHO was sent) and recovery via manual /WHO #chan %cuhnfaro.

Diagnosis

The JOIN → CHATHISTORY → WHO chain lives inside IRCClient.joinChannel(). When the server auto-joins us — UnrealIRCd persistence module, sajoin, operblock perform-on-connect, InspIRCd auto-join — joinChannel() never runs. The JOIN handler at users.ts:115-147 created the channel entry with needsWhoRequest: true and stopped there. Nothing downstream cleared the flag; nicklist stayed empty forever.

Why Ergo never showed this even though it auto-joins too: Ergo's auto-join sends a draft/chathistory BATCH alongside the JOIN, so the batch close fires CHATHISTORY_LOADING(false)messages.ts:2028 sees needsWhoRequest=true → WHO goes out. UnrealIRCd and InspIRCd don't send a batch on auto-join, so nothing kicks the chain.

Fix

Mirror joinChannel's tail in the own-join + new-channel branch:

  • draft/chathistory cap present → send CHATHISTORY LATEST #chan * 50 + trigger CHATHISTORY_LOADING(true); existing batch-close → WHO path then runs.
  • Cap absent (InspIRCd networks without the module) → send WHO #chan %cuhnfaro immediately, clear needsWhoRequest.

ircClient I/O runs outside the setState callback — triggerEvent inside setState causes the nested-setState clobber that batches.ts:215-218 already documents.

Test plan

  • Unit: cap-present branch sends CHATHISTORY + LOADING(true), leaves needsWhoRequest=true
  • Unit: cap-absent branch sends WHO immediately, clears needsWhoRequest
  • Unit: joinChannel already ran (channel exists) → no double-send
  • Manual on the bugged UnrealIRCd Support session: server-initiated auto-join now populates the nicklist without /WHO
  • Manual on InspIRCd network: same
  • Manual on Ergo: still works (no regression)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Improved handling when the server signals you've joined a channel: chat history is now loaded automatically if supported, otherwise user info is requested so channels initialize correctly.
  • Tests
    • Added tests covering server-initiated self-joins to ensure history-loading and fallback behavior behave correctly and aren’t duplicated.

PR #248 stopped the CHATHISTORY-FAIL-into-permanently-leaked
needsWhoRequest leak when joinChannel() was the entry point. But
joinChannel is only one of the two entry points. The other one --
server-initiated auto-join (UnrealIRCd's persistence module, sajoin,
operblock perform-on-connect, InspIRCd auto-join) -- sends a JOIN that
the client never asked for. The handler at users.ts:115-147 created
the channel entry with needsWhoRequest=true and stopped there, never
firing CHATHISTORY or WHO. Nothing downstream cleared the flag, so
the nicklist stayed empty until something else triggered it (e.g. a
manual /who).

Why Ergo never showed this even though it auto-joins too: Ergo's
auto-join sends a draft/chathistory BATCH alongside the JOIN, so the
batch close fires CHATHISTORY_LOADING(false) -> messages.ts:2028 sees
needsWhoRequest=true -> WHO goes out. UnrealIRCd and InspIRCd don't
send a batch on auto-join, so nothing kicks the chain.

Mirror joinChannel's tail in the own-join + new-channel branch:

  - draft/chathistory cap present: send `CHATHISTORY LATEST #chan * 50`,
    trigger CHATHISTORY_LOADING(true), leave needsWhoRequest=true so the
    existing batch-close -> WHO path runs.
  - cap absent (InspIRCd networks that don't ship the module):
    send `WHO #chan %cuhnfaro` immediately and clear needsWhoRequest.

ircClient I/O runs OUTSIDE the setState callback -- triggerEvent inside
setState causes nested-setState clobber (same reason batches.ts:215-218
captures chathistoryChannelName before firing).

Tests cover all three branches: cap present, cap absent, and the
joinChannel-already-ran path (must not double-send).
@ValwareIRC ValwareIRC requested a review from matheusfillipe June 5, 2026 20:23
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Need an answer fast? Review this PR in Change Stack to ask focused questions about the PR or a changed range.

Review Change Stack

Warning

Review limit reached

@ValwareIRC, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 18 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1f4ffb25-a31d-436c-b43d-06182285b844

📥 Commits

Reviewing files that changed from the base of the PR and between 3bfb203 and 6ab84ab.

📒 Files selected for processing (1)
  • src/store/handlers/channels.ts
📝 Walkthrough

Walkthrough

This PR updates the server-initiated own-JOIN event handler to detect the draft/chathistory capability and conditionally trigger either chat history or user-list loading. Channel state is initialized with appropriate history flags, and conditional I/O (CHATHISTORY or WHO) is triggered after store state settles. Comprehensive test coverage validates both capability-present and capability-absent paths, plus the no-duplicate-request case.

Changes

Server-initiated own-JOIN with capability-driven history loading

Layer / File(s) Summary
Capability tracking and conditional I/O in JOIN handler
src/store/handlers/users.ts
The JOIN handler detects draft/chathistory capability when creating a channel for server-initiated own joins, initializes history-loading flags based on that detection, and conditionally sends either CHATHISTORY LATEST with a loading event or WHO after the store update completes.
Test suite for server-initiated own-JOIN with CHATHISTORY/WHO fanout
tests/store/join.test.ts
New test suite verifies the handler correctly branches on capability: sending CHATHISTORY and loading event with appropriate flags when supported, sending WHO when unsupported, and avoiding duplicate requests when the channel already exists.

Sequence Diagram

sequenceDiagram
  participant Server
  participant JoinHandler
  participant Store
  participant IrcClient
  
  Server->>JoinHandler: JOIN event for own nick (isOurJoin)
  JoinHandler->>JoinHandler: Check server capabilities for draft/chathistory
  JoinHandler->>Store: Create channel with history-loading flags based on capability
  Store->>JoinHandler: setState update completes
  
  alt draft/chathistory capability present
    JoinHandler->>IrcClient: sendRaw CHATHISTORY LATEST ... * 50
    JoinHandler->>JoinHandler: Emit CHATHISTORY_LOADING with isLoading: true
  else draft/chathistory not supported
    JoinHandler->>IrcClient: sendRaw WHO ... %cuhnfaro
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • obbyworld/obby#248: Directly overlaps with users.ts capability-driven CHATHISTORY/WHO fanout logic and history-loading flag initialization for server-initiated own joins.

Suggested reviewers

  • matheusfillipe

Poem

🐰 A channel joins by server's call,
We check if history's there at all,
CHATHISTORY or WHO we send,
The flags now guide us to the end,
Tests confirm both paths ring true! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing server-initiated own JOINs to trigger CHATHISTORY/WHO requests. It is concise, specific, and directly reflects the core bug fix and implementation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/server-initiated-join-chain

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Jun 5, 2026

Pages Preview
Preview URL: https://fix-server-initiated-join-ch.obsidianirc.pages.dev

Automated deployment preview for the PR in the Cloudflare Pages.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/store/handlers/users.ts (1)

151-151: ⚡ Quick win

Remove redundant boolean coercion.

hasChathistory is already a boolean (derived from !!server.capabilities?.includes(...) on line 130), so the !! prefix on line 151 is redundant.

♻️ Simplify to remove redundant coercion
-                    needsWhoRequest: !!hasChathistory,
+                    needsWhoRequest: hasChathistory,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/store/handlers/users.ts` at line 151, Remove the redundant boolean
coercion on the needsWhoRequest property: replace the double-bang expression
with the existing boolean variable hasChathistory (i.e., set needsWhoRequest to
hasChathistory). hasChathistory is already a boolean (derived from
!!server.capabilities?.includes(...) earlier), so update the assignment in the
object where needsWhoRequest is set to simply reference hasChathistory to avoid
unnecessary coercion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/store/handlers/users.ts`:
- Line 151: Remove the redundant boolean coercion on the needsWhoRequest
property: replace the double-bang expression with the existing boolean variable
hasChathistory (i.e., set needsWhoRequest to hasChathistory). hasChathistory is
already a boolean (derived from !!server.capabilities?.includes(...) earlier),
so update the assignment in the object where needsWhoRequest is set to simply
reference hasChathistory to avoid unnecessary coercion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2b7bcc3e-1e96-461d-a6fe-bff18422b8f6

📥 Commits

Reviewing files that changed from the base of the PR and between 53b6b91 and 80eb737.

📒 Files selected for processing (2)
  • src/store/handlers/users.ts
  • tests/store/join.test.ts

matheusfillipe
matheusfillipe previously approved these changes Jun 5, 2026
@ValwareIRC ValwareIRC merged commit f6d059f into main Jun 5, 2026
5 checks passed
ValwareIRC added a commit that referenced this pull request Jun 5, 2026
Six fixes for items mupuf raised against the bouncer-networks test
build (issue #120, 2026-05-21). Items 5, 6, 7, 10, 11, 12, 13 from
his list are general-client concerns that affect every connection,
not bouncer-specific -- tracked separately.

#1 deleteServer wipes every server sharing host:port
   ---------------------------------------------------
   The filter keyed on (host, port) to drop the saved row. Bouncer
   children share their parent's soju endpoint (only bouncerNetid
   distinguishes them), so deleting one bouncer-bound network silently
   removed the parent and every sibling from localStorage. On next
   reload nothing was there and the reconnect path surfaced as
   "Authentication required" spam.
   Fix: key the filter on the unique server id. New regression test
   pins it (deleteServerBouncer.test.ts, 3 cases).

#2 ADDNETWORK shows a spinner forever; user reloads to see the network
   ------------------------------------------------------------------
   The form's optimistic 500ms timer closed itself on no-FAIL, but the
   networks list never refreshed -- soju only pushes a BOUNCER NETWORK
   notification when the `-notify` cap is ACK'd, and some deployments
   don't advertise it. Fire BOUNCER LISTNETWORKS on the success path
   so the new row appears regardless of notify-cap state.

#3 Connect button text was illegible
   ---------------------------------
   The button used `bg-primary` -- a CSS-var-driven class that resolves
   to almost-white in the dark theme, against `text-white`. Switched
   every primary-action button in the bouncer panels to
   `bg-discord-primary` (#5865F2) + `text-white`, matching the
   Invitations panel's convention. Also fixed the never-actually-
   defined `bg-primary-hover` class.

#4 Networks not auto-joined
   ------------------------
   Already addressed by PR #249's server-initiated-JOIN fix, which is
   on this branch via the main-merge. PR #249's handler is now also
   gated on isBouncerControl (see #9 below) so the meta connection
   doesn't try to CHATHISTORY the empty channels soju auto-joins
   there.

#8 No way to set the WebSocket path (mupuf had to nginx-rewrite)
   ------------------------------------------------------------
   ircClient.connect already preserves a custom path when the host
   field contains a URL with one, but it was undiscoverable. Added
   helper text under the Server Host input explaining that a full
   URL like `wss://host:port/socket` is accepted, with a soju
   reverse-proxy callout. Translated across all 18 locales.

#9 "Cannot fetch chat history on bouncer connection" spam
   ------------------------------------------------------
   Soju FAILs every CHATHISTORY against the bouncer-networks meta
   session -- it has no real channels of its own. On resume-from-
   suspend, the reconnect-time loop in connection.ts fired one
   CHATHISTORY per cached channel, producing one error toast each.
   Gate every CHATHISTORY send-site on !isBouncerControl:
     - joinChannel (IRCClient.ts)
     - requestChathistoryBefore (IRCClient.ts)
     - reconnect-loop setTimeout (connection.ts)
     - server-initiated own-JOIN handler (users.ts)
   The bouncer control session is now treated like a cap-less
   server: WHO fires immediately, no CHATHISTORY round-trip.

Build + 882 tests green.
ValwareIRC added a commit that referenced this pull request Jun 5, 2026
Six fixes for items mupuf raised against the bouncer-networks test
build (issue #120, 2026-05-21). Items 5, 6, 7, 10, 11, 12, 13 from
his list are general-client concerns that affect every connection,
not bouncer-specific -- tracked separately.

#1 deleteServer wipes every server sharing host:port
   ---------------------------------------------------
   The filter keyed on (host, port) to drop the saved row. Bouncer
   children share their parent's soju endpoint (only bouncerNetid
   distinguishes them), so deleting one bouncer-bound network silently
   removed the parent and every sibling from localStorage. On next
   reload nothing was there and the reconnect path surfaced as
   "Authentication required" spam.
   Fix: key the filter on the unique server id. New regression test
   pins it (deleteServerBouncer.test.ts, 3 cases).

#2 ADDNETWORK shows a spinner forever; user reloads to see the network
   ------------------------------------------------------------------
   The form's optimistic 500ms timer closed itself on no-FAIL, but the
   networks list never refreshed -- soju only pushes a BOUNCER NETWORK
   notification when the `-notify` cap is ACK'd, and some deployments
   don't advertise it. Fire BOUNCER LISTNETWORKS on the success path
   so the new row appears regardless of notify-cap state.

#3 Connect button text was illegible
   ---------------------------------
   The button used `bg-primary` -- a CSS-var-driven class that resolves
   to almost-white in the dark theme, against `text-white`. Switched
   every primary-action button in the bouncer panels to
   `bg-discord-primary` (#5865F2) + `text-white`, matching the
   Invitations panel's convention. Also fixed the never-actually-
   defined `bg-primary-hover` class.

#4 Networks not auto-joined
   ------------------------
   Already addressed by PR #249's server-initiated-JOIN fix, which is
   on this branch via the main-merge. PR #249's handler is now also
   gated on isBouncerControl (see #9 below) so the meta connection
   doesn't try to CHATHISTORY the empty channels soju auto-joins
   there.

#8 No way to set the WebSocket path (mupuf had to nginx-rewrite)
   ------------------------------------------------------------
   ircClient.connect already preserves a custom path when the host
   field contains a URL with one, but it was undiscoverable. Added
   helper text under the Server Host input explaining that a full
   URL like `wss://host:port/socket` is accepted, with a soju
   reverse-proxy callout. Translated across all 18 locales.

#9 "Cannot fetch chat history on bouncer connection" spam
   ------------------------------------------------------
   Soju FAILs every CHATHISTORY against the bouncer-networks meta
   session -- it has no real channels of its own. On resume-from-
   suspend, the reconnect-time loop in connection.ts fired one
   CHATHISTORY per cached channel, producing one error toast each.
   Gate every CHATHISTORY send-site on !isBouncerControl:
     - joinChannel (IRCClient.ts)
     - requestChathistoryBefore (IRCClient.ts)
     - reconnect-loop setTimeout (connection.ts)
     - server-initiated own-JOIN handler (users.ts)
   The bouncer control session is now treated like a cap-less
   server: WHO fires immediately, no CHATHISTORY round-trip.

Build + 882 tests green.
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