""" Sopel plugin: bot controller Owner-only commands to start, stop, and restart IRC bots directly as processes. Alfred manages them; no rc-service or sudo required. Bots are discovered automatically from ~/.config/sopel/*.cfg - add a new config file there and it will appear in !bot list on the next command. !bot list - show status of all managed bots !bot all - control all bots at once !bot - control a bot !bot status - show status of one bot """ import glob import os import subprocess import time from sopel import plugin import os as _os, sys as _sys _alfred_dir = _os.path.dirname(_os.path.abspath(__file__)) if _alfred_dir not in _sys.path: _sys.path.insert(0, _alfred_dir) import helpstrings as h SOPEL = "/usr/bin/sopel" CFG_DIR = "/home/ahmed/.config/sopel" EXCLUDE = {"alfred"} def _discover(): paths = glob.glob(os.path.join(CFG_DIR, "*.cfg")) return { os.path.splitext(os.path.basename(p))[0]: p for p in sorted(paths) if os.path.splitext(os.path.basename(p))[0] not in EXCLUDE } def _is_owner(bot, trigger): return trigger.nick == bot.settings.core.owner def _running(cfg_path): r = subprocess.run(["pgrep", "-f", f"sopel.*{cfg_path}"], capture_output=True) return r.returncode == 0 def _start(name, cfg_path): if _running(cfg_path): return f"{name}: already running" subprocess.Popen( [SOPEL, "--config", cfg_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, close_fds=True, ) return f"{name}: started" def _stop(name, cfg_path): r = subprocess.run(["pkill", "-f", f"sopel.*{cfg_path}"], capture_output=True) return f"{name}: stopped" if r.returncode == 0 else f"{name}: not running" def _restart(name, cfg_path): _stop(name, cfg_path) for _ in range(10): time.sleep(0.2) if not _running(cfg_path): break return _start(name, cfg_path) @plugin.command("bot") def cmd_bot(bot, trigger): if not _is_owner(bot, trigger): return bots = _discover() args = (trigger.group(2) or "").split() if not args: bot.say(h.BOT_TOPIC) return if args[0] == "list": if not bots: bot.say("no bots found") return bot.say(" | ".join( f"{n}:{'up' if _running(p) else 'down'}" for n, p in bots.items() )) return if args[0] == "all": if len(args) < 2 or args[1] not in ("start", "stop", "restart"): bot.say("usage: !bot all ") return action = args[1] if not bots: bot.say("no bots found") return fn = {"start": _start, "stop": _stop, "restart": _restart}[action] results = [fn(n, p) for n, p in bots.items()] bot.say(" | ".join(results)) return name = args[0] if name not in bots: bot.say(f"unknown bot - known: {', '.join(bots)}") return if len(args) < 2: bot.say(f"usage: !bot {name} ") return cfg_path = bots[name] action = args[1] if action == "start": bot.say(_start(name, cfg_path)) elif action == "stop": bot.say(_stop(name, cfg_path)) elif action == "restart": bot.say(_restart(name, cfg_path)) elif action == "status": bot.say(f"{name}: {'running' if _running(cfg_path) else 'stopped'}") else: bot.say(f"usage: !bot {name} ")