fix: debounce WHO on JOIN to prevent flood on netsplit recovery

WHO doesn't support multiple targets (absent from TARGMAX on all
major IRCds). Replace per-nick WHO with a debounced per-channel WHO:
on JOIN, schedule WHO #channel after 2s delay. Subsequent JOINs
within the window reset the timer, so a netsplit producing dozens
of JOINs results in a single WHO.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user
2026-02-15 13:03:35 +01:00
parent fd8f72c3cc
commit 02ea81d059
2 changed files with 38 additions and 7 deletions

View File

@@ -87,6 +87,7 @@ class Bot:
self._admins: list[str] = config.get("bot", {}).get("admins", [])
self._opers: set[str] = set() # hostmasks of known IRC operators
self._caps: set[str] = set() # negotiated IRCv3 caps
self._who_pending: dict[str, asyncio.Task] = {} # debounced WHO per channel
self.state = StateStore()
# Rate limiter: default 2 msg/sec, burst of 5
rate_cfg = config.get("bot", {})
@@ -265,11 +266,16 @@ class Bot:
if "*" in flags:
self._opers.add(f"{nick}!{user}@{host}")
# JOIN — WHO the joining user to detect oper status
# JOIN — debounced WHO to detect oper status without flooding
if msg.command == "JOIN" and msg.nick and msg.nick != self.nick:
channel = msg.params[0] if msg.params else ""
if channel:
await self.conn.send(format_msg("WHO", msg.nick))
existing = self._who_pending.get(channel)
if existing and not existing.done():
existing.cancel()
self._who_pending[channel] = self._spawn(
self._delayed_who(channel), name=f"who:{channel}",
)
# QUIT — remove departed nicks from oper set
if msg.command == "QUIT" and msg.prefix:
@@ -298,6 +304,16 @@ class Bot:
if msg.command == "PRIVMSG" and msg.text:
self._dispatch_command(msg)
async def _delayed_who(self, channel: str, delay: float = 2.0) -> None:
"""Send WHO after a delay, debouncing rapid JOINs (e.g. netsplit)."""
try:
await asyncio.sleep(delay)
await self.conn.send(format_msg("WHO", channel))
except asyncio.CancelledError:
pass
finally:
self._who_pending.pop(channel, None)
async def _handle_ctcp(self, msg: Message) -> None:
"""Respond to CTCP VERSION, TIME, and PING queries."""
text = msg.text.strip("\x01")