""" 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 - 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 ") @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 - 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()