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:
@@ -118,32 +118,35 @@ def _delete_saved(bot, rid: str) -> None:
|
||||
bot.state.delete("remind", rid)
|
||||
|
||||
|
||||
# ---- In-memory tracking -----------------------------------------------------
|
||||
# ---- Per-bot runtime state --------------------------------------------------
|
||||
|
||||
# {rid: (task, target, nick, label, created, repeating)}
|
||||
_reminders: dict[str, tuple[asyncio.Task, str, str, str, str, bool]] = {}
|
||||
# Reverse lookup: (target, nick) -> [rid, ...]
|
||||
_by_user: dict[tuple[str, str], list[str]] = {}
|
||||
# Calendar-based rids (persisted)
|
||||
_calendar: set[str] = set()
|
||||
def _ps(bot):
|
||||
"""Per-bot plugin runtime state."""
|
||||
return bot._pstate.setdefault("remind", {
|
||||
"reminders": {},
|
||||
"by_user": {},
|
||||
"calendar": set(),
|
||||
})
|
||||
|
||||
|
||||
def _cleanup(rid: str, target: str, nick: str) -> None:
|
||||
def _cleanup(bot, rid: str, target: str, nick: str) -> None:
|
||||
"""Remove a reminder from tracking structures."""
|
||||
_reminders.pop(rid, None)
|
||||
_calendar.discard(rid)
|
||||
ps = _ps(bot)
|
||||
ps["reminders"].pop(rid, None)
|
||||
ps["calendar"].discard(rid)
|
||||
ukey = (target, nick)
|
||||
if ukey in _by_user:
|
||||
_by_user[ukey] = [r for r in _by_user[ukey] if r != rid]
|
||||
if not _by_user[ukey]:
|
||||
del _by_user[ukey]
|
||||
if ukey in ps["by_user"]:
|
||||
ps["by_user"][ukey] = [r for r in ps["by_user"][ukey] if r != rid]
|
||||
if not ps["by_user"][ukey]:
|
||||
del ps["by_user"][ukey]
|
||||
|
||||
|
||||
def _track(rid: str, task: asyncio.Task, target: str, nick: str,
|
||||
def _track(bot, rid: str, task: asyncio.Task, target: str, nick: str,
|
||||
label: str, created: str, repeating: bool) -> None:
|
||||
"""Add a reminder to in-memory tracking."""
|
||||
_reminders[rid] = (task, target, nick, label, created, repeating)
|
||||
_by_user.setdefault((target, nick), []).append(rid)
|
||||
ps = _ps(bot)
|
||||
ps["reminders"][rid] = (task, target, nick, label, created, repeating)
|
||||
ps["by_user"].setdefault((target, nick), []).append(rid)
|
||||
|
||||
|
||||
# ---- Coroutines -------------------------------------------------------------
|
||||
@@ -159,7 +162,7 @@ async def _remind_once(bot, rid: str, target: str, nick: str, label: str,
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
_cleanup(rid, target, nick)
|
||||
_cleanup(bot, rid, target, nick)
|
||||
|
||||
|
||||
async def _remind_repeat(bot, rid: str, target: str, nick: str, label: str,
|
||||
@@ -174,7 +177,7 @@ async def _remind_repeat(bot, rid: str, target: str, nick: str, label: str,
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
_cleanup(rid, target, nick)
|
||||
_cleanup(bot, rid, target, nick)
|
||||
|
||||
|
||||
async def _schedule_at(bot, rid: str, target: str, nick: str, label: str,
|
||||
@@ -191,7 +194,7 @@ async def _schedule_at(bot, rid: str, target: str, nick: str, label: str,
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
_cleanup(rid, target, nick)
|
||||
_cleanup(bot, rid, target, nick)
|
||||
|
||||
|
||||
async def _schedule_yearly(bot, rid: str, target: str, nick: str,
|
||||
@@ -219,16 +222,17 @@ async def _schedule_yearly(bot, rid: str, target: str, nick: str,
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
finally:
|
||||
_cleanup(rid, target, nick)
|
||||
_cleanup(bot, rid, target, nick)
|
||||
|
||||
|
||||
# ---- Restore on connect -----------------------------------------------------
|
||||
|
||||
def _restore(bot) -> None:
|
||||
"""Restore persisted calendar reminders from bot.state."""
|
||||
ps = _ps(bot)
|
||||
for rid in bot.state.keys("remind"):
|
||||
# Skip if already active
|
||||
entry = _reminders.get(rid)
|
||||
entry = ps["reminders"].get(rid)
|
||||
if entry and not entry[0].done():
|
||||
continue
|
||||
raw = bot.state.get("remind", rid)
|
||||
@@ -272,8 +276,8 @@ def _restore(bot) -> None:
|
||||
else:
|
||||
continue
|
||||
|
||||
_calendar.add(rid)
|
||||
_track(rid, task, target, nick, label, created, rtype == "yearly")
|
||||
ps["calendar"].add(rid)
|
||||
_track(bot, rid, task, target, nick, label, created, rtype == "yearly")
|
||||
|
||||
|
||||
@event("001")
|
||||
@@ -311,12 +315,13 @@ async def cmd_remind(bot, message):
|
||||
|
||||
# ---- List ----------------------------------------------------------------
|
||||
if sub == "list":
|
||||
rids = _by_user.get(ukey, [])
|
||||
ps = _ps(bot)
|
||||
rids = ps["by_user"].get(ukey, [])
|
||||
active = []
|
||||
for rid in rids:
|
||||
entry = _reminders.get(rid)
|
||||
entry = ps["reminders"].get(rid)
|
||||
if entry and not entry[0].done():
|
||||
if rid in _calendar:
|
||||
if rid in ps["calendar"]:
|
||||
# Show next fire time
|
||||
raw = bot.state.get("remind", rid)
|
||||
if raw:
|
||||
@@ -347,7 +352,7 @@ async def cmd_remind(bot, message):
|
||||
if not rid:
|
||||
await bot.reply(message, "Usage: !remind cancel <id>")
|
||||
return
|
||||
entry = _reminders.get(rid)
|
||||
entry = _ps(bot)["reminders"].get(rid)
|
||||
if entry and not entry[0].done() and entry[2] == nick:
|
||||
entry[0].cancel()
|
||||
_delete_saved(bot, rid)
|
||||
@@ -397,11 +402,11 @@ async def cmd_remind(bot, message):
|
||||
"created": created,
|
||||
}
|
||||
_save(bot, rid, data)
|
||||
_calendar.add(rid)
|
||||
_ps(bot)["calendar"].add(rid)
|
||||
task = asyncio.create_task(
|
||||
_schedule_at(bot, rid, target, nick, label, fire_utc, created),
|
||||
)
|
||||
_track(rid, task, target, nick, label, created, False)
|
||||
_track(bot, rid, task, target, nick, label, created, False)
|
||||
local_str = fire_dt.strftime("%Y-%m-%d %H:%M")
|
||||
await bot.reply(message, f"Reminder #{rid} set (at {local_str})")
|
||||
return
|
||||
@@ -459,12 +464,12 @@ async def cmd_remind(bot, message):
|
||||
"created": created,
|
||||
}
|
||||
_save(bot, rid, data)
|
||||
_calendar.add(rid)
|
||||
_ps(bot)["calendar"].add(rid)
|
||||
task = asyncio.create_task(
|
||||
_schedule_yearly(bot, rid, target, nick, label, fire_utc,
|
||||
month, day_raw, hour, minute, tz, created),
|
||||
)
|
||||
_track(rid, task, target, nick, label, created, True)
|
||||
_track(bot, rid, task, target, nick, label, created, True)
|
||||
local_str = fire_dt.strftime("%Y-%m-%d %H:%M")
|
||||
await bot.reply(message, f"Reminder #{rid} set (yearly {month_day}, next {local_str})")
|
||||
return
|
||||
@@ -501,7 +506,7 @@ async def cmd_remind(bot, message):
|
||||
_remind_once(bot, rid, target, nick, label, duration, created),
|
||||
)
|
||||
|
||||
_track(rid, task, target, nick, label, created, repeating)
|
||||
_track(bot, rid, task, target, nick, label, created, repeating)
|
||||
|
||||
kind = f"every {_format_duration(duration)}" if repeating else _format_duration(duration)
|
||||
await bot.reply(message, f"Reminder #{rid} set ({kind})")
|
||||
|
||||
Reference in New Issue
Block a user