aboutsummaryrefslogtreecommitdiffstats
path: root/alfred/bots.py
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /alfred/bots.py
init: mostly vibed
Diffstat (limited to 'alfred/bots.py')
-rw-r--r--alfred/bots.py133
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>")