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:
@@ -18,10 +18,15 @@ _MIN_INTERVAL = 60
|
||||
_MAX_INTERVAL = 604800 # 7 days
|
||||
_MAX_JOBS = 20
|
||||
|
||||
# -- Module-level tracking ---------------------------------------------------
|
||||
# -- Per-bot plugin runtime state --------------------------------------------
|
||||
|
||||
_jobs: dict[str, dict] = {}
|
||||
_tasks: dict[str, asyncio.Task] = {}
|
||||
|
||||
def _ps(bot):
|
||||
"""Per-bot plugin runtime state."""
|
||||
return bot._pstate.setdefault("cron", {
|
||||
"jobs": {},
|
||||
"tasks": {},
|
||||
})
|
||||
|
||||
|
||||
# -- Pure helpers ------------------------------------------------------------
|
||||
@@ -101,7 +106,7 @@ async def _cron_loop(bot, key: str) -> None:
|
||||
"""Repeating loop: sleep, then dispatch the stored command."""
|
||||
try:
|
||||
while True:
|
||||
data = _jobs.get(key)
|
||||
data = _ps(bot)["jobs"].get(key)
|
||||
if not data:
|
||||
return
|
||||
await asyncio.sleep(data["interval"])
|
||||
@@ -118,33 +123,36 @@ async def _cron_loop(bot, key: str) -> None:
|
||||
|
||||
def _start_job(bot, key: str) -> None:
|
||||
"""Create and track a cron task."""
|
||||
existing = _tasks.get(key)
|
||||
ps = _ps(bot)
|
||||
existing = ps["tasks"].get(key)
|
||||
if existing and not existing.done():
|
||||
return
|
||||
task = asyncio.create_task(_cron_loop(bot, key))
|
||||
_tasks[key] = task
|
||||
ps["tasks"][key] = task
|
||||
|
||||
|
||||
def _stop_job(key: str) -> None:
|
||||
def _stop_job(bot, key: str) -> None:
|
||||
"""Cancel and remove a cron task."""
|
||||
task = _tasks.pop(key, None)
|
||||
ps = _ps(bot)
|
||||
task = ps["tasks"].pop(key, None)
|
||||
if task and not task.done():
|
||||
task.cancel()
|
||||
_jobs.pop(key, None)
|
||||
ps["jobs"].pop(key, None)
|
||||
|
||||
|
||||
# -- Restore on connect -----------------------------------------------------
|
||||
|
||||
def _restore(bot) -> None:
|
||||
"""Rebuild cron tasks from persisted state."""
|
||||
ps = _ps(bot)
|
||||
for key in bot.state.keys("cron"):
|
||||
existing = _tasks.get(key)
|
||||
existing = ps["tasks"].get(key)
|
||||
if existing and not existing.done():
|
||||
continue
|
||||
data = _load(bot, key)
|
||||
if data is None:
|
||||
continue
|
||||
_jobs[key] = data
|
||||
ps["jobs"][key] = data
|
||||
_start_job(bot, key)
|
||||
|
||||
|
||||
@@ -211,7 +219,7 @@ async def cmd_cron(bot, message):
|
||||
if not found_key:
|
||||
await bot.reply(message, f"No cron job #{cron_id}")
|
||||
return
|
||||
_stop_job(found_key)
|
||||
_stop_job(bot, found_key)
|
||||
_delete(bot, found_key)
|
||||
await bot.reply(message, f"Removed cron #{cron_id}")
|
||||
return
|
||||
@@ -275,7 +283,7 @@ async def cmd_cron(bot, message):
|
||||
"added_by": message.nick,
|
||||
}
|
||||
_save(bot, key, data)
|
||||
_jobs[key] = data
|
||||
_ps(bot)["jobs"][key] = data
|
||||
_start_job(bot, key)
|
||||
|
||||
fmt_interval = _format_duration(interval)
|
||||
|
||||
Reference in New Issue
Block a user