feat: add multi-server support
Connect to multiple IRC servers concurrently from a single config file. Plugins are loaded once and shared; per-server state is isolated via separate SQLite databases and per-bot runtime state (bot._pstate). - Add build_server_configs() for [servers.*] config layout - Bot.__init__ gains name parameter, _pstate dict for plugin isolation - cli.py runs multiple bots via asyncio.gather - 9 stateful plugins migrated from module-level dicts to _ps(bot) pattern - Backward compatible: legacy [server] config works unchanged Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -38,9 +38,14 @@ _SKIP_EXTS = frozenset({
|
||||
# Trailing punctuation to strip, but preserve balanced parens
|
||||
_TRAIL_CHARS = set(".,;:!?)>]")
|
||||
|
||||
# -- Module-level state ------------------------------------------------------
|
||||
# -- Per-bot state -----------------------------------------------------------
|
||||
|
||||
_seen: dict[str, float] = {}
|
||||
|
||||
def _ps(bot):
|
||||
"""Per-bot plugin runtime state."""
|
||||
return bot._pstate.setdefault("urltitle", {
|
||||
"seen": {},
|
||||
})
|
||||
|
||||
# -- HTML parser -------------------------------------------------------------
|
||||
|
||||
@@ -202,21 +207,22 @@ def _fetch_title(url: str) -> tuple[str, str]:
|
||||
# -- Cooldown ----------------------------------------------------------------
|
||||
|
||||
|
||||
def _check_cooldown(url: str, cooldown: int) -> bool:
|
||||
def _check_cooldown(bot, url: str, cooldown: int) -> bool:
|
||||
"""Return True if the URL is within the cooldown window."""
|
||||
seen = _ps(bot)["seen"]
|
||||
now = time.monotonic()
|
||||
last = _seen.get(url)
|
||||
last = seen.get(url)
|
||||
if last is not None and (now - last) < cooldown:
|
||||
return True
|
||||
|
||||
# Prune if cache is too large
|
||||
if len(_seen) >= _CACHE_MAX:
|
||||
if len(seen) >= _CACHE_MAX:
|
||||
cutoff = now - cooldown
|
||||
stale = [k for k, v in _seen.items() if v < cutoff]
|
||||
stale = [k for k, v in seen.items() if v < cutoff]
|
||||
for k in stale:
|
||||
del _seen[k]
|
||||
del seen[k]
|
||||
|
||||
_seen[url] = now
|
||||
seen[url] = now
|
||||
return False
|
||||
|
||||
|
||||
@@ -261,7 +267,7 @@ async def on_privmsg(bot, message):
|
||||
for url in urls:
|
||||
if _is_ignored_url(url, ignore_hosts):
|
||||
continue
|
||||
if _check_cooldown(url, cooldown):
|
||||
if _check_cooldown(bot, url, cooldown):
|
||||
continue
|
||||
|
||||
title, desc = await loop.run_in_executor(None, _fetch_title, url)
|
||||
|
||||
Reference in New Issue
Block a user