diff options
Diffstat (limited to 'alfred/bots.py')
| -rw-r--r-- | alfred/bots.py | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/alfred/bots.py b/alfred/bots.py new file mode 100644 index 0000000..de0a1d4 --- /dev/null +++ b/alfred/bots.py @@ -0,0 +1,133 @@ +""" +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 <start|stop|restart> - control all bots at once + !bot <name> <start|stop|restart> - control a bot + !bot <name> 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 <start|stop|restart>") + 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} <start|stop|restart|status>") + 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} <start|stop|restart|status>") |
