aboutsummaryrefslogtreecommitdiffstats
path: root/herald/commands.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 /herald/commands.py
init: mostly vibed
Diffstat (limited to 'herald/commands.py')
-rw-r--r--herald/commands.py220
1 files changed, 220 insertions, 0 deletions
diff --git a/herald/commands.py b/herald/commands.py
new file mode 100644
index 0000000..9930902
--- /dev/null
+++ b/herald/commands.py
@@ -0,0 +1,220 @@
+"""
+Sopel plugin: herald/bikabot admin commands
+
+Owner-only commands for managing the bot at runtime.
+
+ !pause - pause keyword responses
+ !resume - resume keyword responses
+ !join #channel - join a channel
+ !part [#channel] - leave a channel (defaults to current)
+ !follow <on|off> - periodically join/leave channels to match owner's presence
+ !unfollow - leave all follow-joined channels immediately
+ !chans - list channels the bot is in
+ !help - show usage
+
+Automatic behaviour (always active, no commands needed):
+ - When the owner PARTs a channel, the bot leaves too (unless it's a preconfigured channel).
+ - When the owner QUITs, the bot leaves all channels except preconfigured ones.
+"""
+
+from sopel import plugin
+from sopel.tools import events
+
+
+def _is_owner(bot, trigger):
+ return trigger.nick == bot.settings.core.owner
+
+
+def _configured_channels(bot):
+ """Return a set of lowercased channel names from the bot's static config."""
+ chans = bot.settings.core.channels or []
+ return {c.split()[0].lower() for c in chans}
+
+
+def setup(bot):
+ bot.memory.setdefault("follow_enabled", False)
+ bot.memory.setdefault("follow_joined", set())
+ bot.memory.setdefault("whois_got_channels", False)
+ bot.memory.setdefault("herald_paused", False)
+
+
+# --- Owner presence tracking ---
+
+@plugin.event("PART")
+def on_owner_part(bot, trigger):
+ """Leave any channel the owner leaves, unless it's a preconfigured channel."""
+ if not _is_owner(bot, trigger):
+ return
+ channel = str(trigger.sender)
+ if channel.lower() in _configured_channels(bot):
+ return
+ if channel in {str(c) for c in bot.channels}:
+ bot.part(channel)
+ bot.memory.get("follow_joined", set()).discard(channel.lower())
+
+
+@plugin.event("QUIT")
+def on_owner_quit(bot, trigger):
+ """Leave all non-preconfigured channels when the owner disconnects."""
+ if not _is_owner(bot, trigger):
+ return
+ configured = _configured_channels(bot)
+ for chan in list(bot.channels):
+ if str(chan).lower() not in configured:
+ bot.part(chan)
+ bot.memory["follow_joined"] = set()
+
+
+# --- Manual commands ---
+
+@plugin.command("pause")
+def cmd_pause(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ bot.memory["herald_paused"] = True
+ bot.say("herald paused - keyword responses inactive")
+
+
+@plugin.command("resume")
+def cmd_resume(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ bot.memory["herald_paused"] = False
+ bot.say("herald resumed - keyword responses active")
+
+
+@plugin.command("join")
+def cmd_join(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ channel = (trigger.group(2) or "").strip()
+ if not channel.startswith("#"):
+ bot.say("usage: !join #channel")
+ return
+ bot.join(channel)
+
+
+@plugin.command("part")
+def cmd_part(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ channel = (trigger.group(2) or "").strip()
+ if not channel:
+ channel = trigger.sender
+ bot.part(channel)
+
+
+@plugin.command("follow")
+def cmd_follow(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ arg = (trigger.group(2) or "").strip().lower()
+ if arg == "on":
+ bot.memory["follow_enabled"] = True
+ bot.say("follow on - tracking your channels every 30s")
+ elif arg == "off":
+ bot.memory["follow_enabled"] = False
+ bot.say("follow off")
+ else:
+ state = "on" if bot.memory.get("follow_enabled") else "off"
+ bot.say(f"follow is {state} - usage: !follow <on|off>")
+
+
+@plugin.command("unfollow")
+def cmd_unfollow(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ joined = set(bot.memory.get("follow_joined", set()))
+ if not joined:
+ bot.say("not in any follow-joined channels")
+ return
+ for chan in joined:
+ bot.part(chan)
+ bot.memory["follow_joined"] = set()
+ bot.say(f"left {len(joined)} channel(s)")
+
+
+@plugin.command("chans")
+def cmd_chans(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ chans = sorted(str(c) for c in bot.channels)
+ if chans:
+ bot.say("in: " + " ".join(chans))
+ else:
+ bot.say("not in any channels")
+
+
+@plugin.command("help")
+def cmd_help(bot, trigger):
+ if not _is_owner(bot, trigger):
+ return
+ for line in [
+ "!pause - pause keyword responses",
+ "!resume - resume keyword responses",
+ "!join #channel - join a channel",
+ "!part [#channel] - leave a channel (defaults to current)",
+ "!follow <on|off> - follow owner: join channels to match owner's presence (checks every 30s)",
+ "!unfollow - immediately leave all follow-joined channels",
+ "!chans - list channels the bot is currently in",
+ "!help - this message",
+ ]:
+ bot.say(line, trigger.nick)
+
+
+# --- Follow tick (joining only) ---
+
+@plugin.interval(30)
+def _follow_tick(bot):
+ if not bot.memory.get("follow_enabled"):
+ return
+ bot.memory["whois_got_channels"] = False
+ owner = bot.settings.core.owner
+ bot.write(("WHOIS", owner))
+
+
+@plugin.rule(r"(.*)")
+@plugin.event(events.RPL_WHOISCHANNELS) # 319
+@plugin.priority("high")
+def _whois_channels(bot, trigger):
+ if not bot.memory.get("follow_enabled"):
+ return
+ owner = bot.settings.core.owner
+ if len(trigger.args) < 2 or trigger.args[1] != owner:
+ return
+
+ bot.memory["whois_got_channels"] = True
+ raw = (trigger.group(1) or "").strip()
+ owner_chans = set()
+ for part in raw.split():
+ chan = part.lstrip("@+~&%")
+ if chan.startswith("#"):
+ owner_chans.add(chan.lower())
+
+ current_chans = {str(c).lower() for c in bot.channels}
+ follow_joined = bot.memory.get("follow_joined", set())
+
+ for chan in owner_chans:
+ if chan not in current_chans:
+ bot.join(chan)
+ follow_joined.add(chan)
+
+ bot.memory["follow_joined"] = follow_joined
+
+
+@plugin.rule(r"(.*)")
+@plugin.event(events.RPL_ENDOFWHOIS) # 318
+@plugin.priority("high")
+def _whois_end(bot, trigger):
+ if not bot.memory.get("follow_enabled"):
+ return
+ owner = bot.settings.core.owner
+ if len(trigger.args) < 2 or trigger.args[1] != owner:
+ return
+ if bot.memory.get("whois_got_channels"):
+ return
+ # owner is offline, no 319 received; leave all follow-joined channels
+ follow_joined = set(bot.memory.get("follow_joined", set()))
+ for chan in follow_joined:
+ bot.part(chan)
+ bot.memory["follow_joined"] = set()