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:
user
2026-02-21 19:04:20 +01:00
parent e9528bd879
commit 073659607e
27 changed files with 987 additions and 735 deletions

View File

@@ -25,7 +25,7 @@ from plugins.urltitle import ( # noqa: E402, I001
_extract_urls,
_fetch_title,
_is_ignored_url,
_seen,
_ps,
on_privmsg,
)
@@ -40,6 +40,7 @@ class _FakeBot:
self.sent: list[tuple[str, str]] = []
self.nick = "derp"
self.prefix = "!"
self._pstate: dict = {}
self.config = {
"flaskpaste": {"url": "https://paste.mymx.me"},
"urltitle": {},
@@ -334,26 +335,28 @@ class TestFetchTitle:
class TestCooldown:
def setup_method(self):
_seen.clear()
self.bot = _FakeBot()
def test_first_access_not_cooled(self):
assert _check_cooldown("https://a.com", 300) is False
assert _check_cooldown(self.bot, "https://a.com", 300) is False
def test_second_access_within_window(self):
_check_cooldown("https://b.com", 300)
assert _check_cooldown("https://b.com", 300) is True
_check_cooldown(self.bot, "https://b.com", 300)
assert _check_cooldown(self.bot, "https://b.com", 300) is True
def test_after_cooldown_expires(self):
_seen["https://c.com"] = time.monotonic() - 400
assert _check_cooldown("https://c.com", 300) is False
seen = _ps(self.bot)["seen"]
seen["https://c.com"] = time.monotonic() - 400
assert _check_cooldown(self.bot, "https://c.com", 300) is False
def test_pruning(self):
"""Cache is pruned when it exceeds max size."""
seen = _ps(self.bot)["seen"]
old = time.monotonic() - 600
for i in range(600):
_seen[f"https://stale-{i}.com"] = old
_check_cooldown("https://new.com", 300)
assert len(_seen) < 600
seen[f"https://stale-{i}.com"] = old
_check_cooldown(self.bot, "https://new.com", 300)
assert len(seen) < 600
# ---------------------------------------------------------------------------
@@ -361,8 +364,6 @@ class TestCooldown:
# ---------------------------------------------------------------------------
class TestOnPrivmsg:
def setup_method(self):
_seen.clear()
def test_channel_url_previewed(self):
bot = _FakeBot()