""" Sopel plugin: osterman admin & moderation commands Moderation (ACL / op / owner): !kick / !k [reason] - kick !ban / !b [reason] - ban + kick !ban list / !b list - view channel ban list (NOTICE) !bankick / !bk [reason] - ban + kick (explicit) !unban / !ub - unban !tempban / !tb [reason] - timed ban !mute / !mu - quiet (+q) !unmute / !umu - unquiet (-q) !voice / !v - give voice !devoice / !dv - remove voice !op / !o - give op !deop / !do - remove op !halfop / !hop - give halfop !dehalfop / !dhop - remove halfop !lock / !lk [m|i] - lock channel !unlock / !ulk [m|i] - unlock channel Auto-mode (ACL / op / owner - per channel): !autoop / !ao [nick|mask] - auto +o on join !autovoice / !av [nick|mask] - auto +v on join !autohalfop / !ahop [nick|mask] - auto +h on join Config (ACL / owner): !blacklist / !bl [nick|mask] - global ban list (all channels) !acl [nick] - trusted nicks (global) !whitelist / !wl [mask] - bypass all protection (global) !except / !ex [mask] - bypass protection (per channel) !filter / !f [pattern] - regex content filter (per channel) !badword / !bw [word] - word block list (per channel) !set / !s - set a config value !config / !cfg [#channel] - show channel config (NOTICE) !idle / !id on|off - toggle idle checking !idle / !id - show idle time Tracking: !seen - last seen time !info / !i - nick summary !log / !l [N] - event log (NOTICE, ACL/owner only) !afk - reset your idle timer Admin (owner): !join / !j #channel - join a channel !part / !p [#channel] - leave a channel !chans / !c - list channels !help / !h - this list (NOTICE) """ import fnmatch import json import os import threading import time from sopel import plugin _CONFIG_KEYS = { "flood_threshold": int, "flood_window_sec": int, "caps_filter": int, "caps_percent": int, "caps_min_len": int, "repeat_filter": int, "repeat_threshold": int, "repeat_window_sec": int, "clone_limit": int, "join_flood_filter": int, "join_flood_count": int, "join_flood_window": int, "nick_flood_filter": int, "nick_flood_count": int, "nick_flood_window": int, "badword_action": str, "tempban_max_min": int, "idle_warn_min": int, "idle_kick_min": int, "log_limit": int, "greet": int, } _CONFIG_DEFAULTS = { "flood_threshold": 5, "flood_window_sec": 10, "caps_filter": 1, "caps_percent": 70, "caps_min_len": 10, "repeat_filter": 1, "repeat_threshold": 3, "repeat_window_sec": 30, "clone_limit": 2, "join_flood_filter": 1, "join_flood_count": 5, "join_flood_window": 10, "nick_flood_filter": 1, "nick_flood_count": 3, "nick_flood_window": 60, "badword_action": "kick", "tempban_max_min": 60, "idle_warn_min": 45, "idle_kick_min": 60, "log_limit": 6, "greet": 1, } def _is_owner(bot, trigger): return trigger.nick == bot.settings.core.owner def _is_acl(bot, trigger): return trigger.nick in bot.memory.get("os_db", {}).get("acl", []) def _is_op(bot, trigger): chan = bot.channels.get(str(trigger.sender)) if not chan: return False return chan.privileges.get(trigger.nick, 0) >= plugin.OP def _is_authorized(bot, trigger): return _is_owner(bot, trigger) or _is_acl(bot, trigger) or _is_op(bot, trigger) def _global_db(bot): return bot.memory.get("os_db", {}) def _chan_db(bot, channel): db = bot.memory["os_db"] db.setdefault("channels", {}) return db["channels"].setdefault(channel.lower(), {}) def _save(bot): path = bot.memory["os_db_path"] os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w") as f: json.dump(bot.memory["os_db"], f, indent=2) def _bot_privs(bot, channel): chan = bot.channels.get(channel) if not chan: return 0 return chan.privileges.get(bot.nick, 0) def _bot_has_op(bot, channel): return _bot_privs(bot, channel) >= plugin.OP def _bot_has_halfop(bot, channel): return _bot_privs(bot, channel) >= plugin.HALFOP def _resolve_mask(bot, channel, target): if "!" not in target and "@" not in target: chan = bot.channels.get(channel) if chan: u = chan.users.get(target) if u: return f"*!{u.user}@{u.host}" return target def _notice(bot, nick, text): bot.notice(text, nick) # --- Moderation --- @plugin.command("kick", "k") @plugin.require_chanmsg def cmd_kick(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to kick") return args = (trigger.group(2) or "").split(None, 1) if not args or not args[0]: _notice(bot, trigger.nick, "usage: !kick [reason]") return bot.write(["KICK", channel, args[0], args[1] if len(args) > 1 else "kicked"]) @plugin.command("ban", "b") @plugin.require_chanmsg def cmd_ban(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) args = (trigger.group(2) or "").split(None, 1) if args and args[0].lower() == "list": cdb = _chan_db(bot, channel) bans = cdb.get("bans", []) if not bans: _notice(bot, trigger.nick, f"ban list for {channel}: (empty)") return _notice(bot, trigger.nick, f"-- ban list for {channel} ({len(bans)}) --") for b in bans: ts = time.strftime("%m-%d %H:%M", time.localtime(b.get("timestamp", 0))) exp = f" expires {time.strftime('%m-%d %H:%M', time.localtime(b['expires']))}" if b.get("expires") else "" _notice(bot, trigger.nick, f"{b['mask']} set by {b.get('added_by','?')} on {ts}{exp}") return if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to ban") return if not args or not args[0]: _notice(bot, trigger.nick, "usage: !ban [reason] or !ban list") return target = args[0] reason = args[1] if len(args) > 1 else "banned" mask = _resolve_mask(bot, channel, target) has_op = _bot_has_op(bot, channel) if has_op: bot.write(["MODE", channel, "+b", mask]) else: _notice(bot, trigger.nick, f"(halfop only: kicking {target}, cannot set +b)") bot.write(["KICK", channel, target.split("!")[0] if "!" in target else target, reason]) if has_op: with bot.memory["os_lock"]: _chan_db(bot, channel).setdefault("bans", []).append({ "mask": mask, "added_by": trigger.nick, "timestamp": int(time.time()), }) _save(bot) @plugin.command("bankick", "bk") @plugin.require_chanmsg def cmd_bankick(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to bankick") return args = (trigger.group(2) or "").split(None, 1) if not args or not args[0]: _notice(bot, trigger.nick, "usage: !bankick [reason]") return target = args[0] reason = args[1] if len(args) > 1 else "banned" mask = _resolve_mask(bot, channel, target) has_op = _bot_has_op(bot, channel) if has_op: bot.write(["MODE", channel, "+b", mask]) else: _notice(bot, trigger.nick, f"(halfop only: kicking {target}, cannot set +b)") bot.write(["KICK", channel, target.split("!")[0] if "!" in target else target, reason]) if has_op: with bot.memory["os_lock"]: _chan_db(bot, channel).setdefault("bans", []).append({ "mask": mask, "added_by": trigger.nick, "timestamp": int(time.time()), }) _save(bot) @plugin.command("unban", "ub") @plugin.require_chanmsg def cmd_unban(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_op(bot, channel): _notice(bot, trigger.nick, "need op to unban") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !unban ") return mask = _resolve_mask(bot, channel, args[0]) bot.write(["MODE", channel, "-b", mask]) with bot.memory["os_lock"]: cdb = _chan_db(bot, channel) before = len(cdb.get("bans", [])) cdb["bans"] = [ b for b in cdb.get("bans", []) if not fnmatch.fnmatch(b["mask"], mask) and b["mask"] != mask ] removed = before - len(cdb["bans"]) _save(bot) msg = f"unbanned {mask}" if removed: msg += f" ({removed} removed from db)" _notice(bot, trigger.nick, msg) @plugin.command("tempban", "tb") @plugin.require_chanmsg def cmd_tempban(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to tempban") return args = (trigger.group(2) or "").split(None, 2) if len(args) < 2: _notice(bot, trigger.nick, "usage: !tempban [reason]") return target = args[0] global_cfg = bot.memory.get("os_db", {}).get("config", {}) chan_cfg = bot.memory.get("os_db", {}).get("channels", {}).get(channel.lower(), {}).get("config", {}) max_min = {**global_cfg, **chan_cfg}.get("tempban_max_min", 60) try: minutes = max(1, min(int(args[1]), max_min)) except ValueError: _notice(bot, trigger.nick, "minutes must be a number") return reason = args[2] if len(args) > 2 else f"tempban ({minutes}m)" mask = _resolve_mask(bot, channel, target) has_op = _bot_has_op(bot, channel) if has_op: bot.write(["MODE", channel, "+b", mask]) bot.write(["KICK", channel, target.split("!")[0] if "!" in target else target, reason]) if has_op: expires = int(time.time()) + minutes * 60 def unban(): bot.write(["MODE", channel, "-b", mask]) with bot.memory["os_lock"]: cdb = _chan_db(bot, channel) cdb["bans"] = [b for b in cdb.get("bans", []) if b.get("mask") != mask] _save(bot) t = threading.Timer(minutes * 60, unban) t.daemon = True t.start() with bot.memory["os_lock"]: _chan_db(bot, channel).setdefault("bans", []).append({ "mask": mask, "added_by": trigger.nick, "timestamp": int(time.time()), "expires": expires, "channel": channel, }) _save(bot) _notice(bot, trigger.nick, f"tempbanned {mask} for {minutes}m") else: _notice(bot, trigger.nick, f"(halfop only: kicked {target}, cannot set timed ban)") @plugin.command("mute", "mu") @plugin.require_chanmsg def cmd_mute(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to mute") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !mute ") return mask = _resolve_mask(bot, channel, args[0]) bot.write(["MODE", channel, "+q", mask]) @plugin.command("unmute", "umu") @plugin.require_chanmsg def cmd_unmute(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to unmute") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !unmute ") return mask = _resolve_mask(bot, channel, args[0]) bot.write(["MODE", channel, "-q", mask]) @plugin.command("voice", "v") @plugin.require_chanmsg def cmd_voice(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to set voice") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !voice ") return bot.write(["MODE", channel, "+v", args[0]]) @plugin.command("devoice", "dv") @plugin.require_chanmsg def cmd_devoice(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_halfop(bot, channel): _notice(bot, trigger.nick, "need at least halfop to remove voice") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !devoice ") return bot.write(["MODE", channel, "-v", args[0]]) @plugin.command("op", "o") @plugin.require_chanmsg def cmd_op(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_op(bot, channel): _notice(bot, trigger.nick, "need op to give op") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !op ") return bot.write(["MODE", channel, "+o", args[0]]) @plugin.command("deop", "do") @plugin.require_chanmsg def cmd_deop(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_op(bot, channel): _notice(bot, trigger.nick, "need op to remove op") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !deop ") return bot.write(["MODE", channel, "-o", args[0]]) @plugin.command("halfop", "hop") @plugin.require_chanmsg def cmd_halfop(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_op(bot, channel): _notice(bot, trigger.nick, "need op to give halfop") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !halfop ") return bot.write(["MODE", channel, "+h", args[0]]) @plugin.command("dehalfop", "dhop") @plugin.require_chanmsg def cmd_dehalfop(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_op(bot, channel): _notice(bot, trigger.nick, "need op to remove halfop") return args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, "usage: !dehalfop ") return bot.write(["MODE", channel, "-h", args[0]]) @plugin.command("lock", "lk") @plugin.require_chanmsg def cmd_lock(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_op(bot, channel): _notice(bot, trigger.nick, "need op to lock channel") return arg = (trigger.group(2) or "").strip().lower() if arg == "m": bot.write(["MODE", channel, "+m"]) _notice(bot, trigger.nick, "channel locked (+m moderated)") elif arg == "i": bot.write(["MODE", channel, "+i"]) _notice(bot, trigger.nick, "channel locked (+i invite-only)") else: bot.write(["MODE", channel, "+mi"]) _notice(bot, trigger.nick, "channel locked (+mi moderated + invite-only)") @plugin.command("unlock", "ulk") @plugin.require_chanmsg def cmd_unlock(bot, trigger): if not _is_authorized(bot, trigger): return channel = str(trigger.sender) if not _bot_has_op(bot, channel): _notice(bot, trigger.nick, "need op to unlock channel") return arg = (trigger.group(2) or "").strip().lower() if arg == "m": bot.write(["MODE", channel, "-m"]) _notice(bot, trigger.nick, "channel unlocked (-m moderated)") elif arg == "i": bot.write(["MODE", channel, "-i"]) _notice(bot, trigger.nick, "channel unlocked (-i invite-only)") else: bot.write(["MODE", channel, "-mi"]) _notice(bot, trigger.nick, "channel unlocked (-mi moderated + invite-only)") # --- Auto-mode --- def _automode_cmd(bot, trigger, key, mode_char, label): """Generic handler for !autoop, !autovoice, !autohalfop.""" if not _is_authorized(bot, trigger): return channel = str(trigger.sender) args = (trigger.group(2) or "").split() if not args: _notice(bot, trigger.nick, f"usage: !{label} [nick|mask]") return sub = args[0].lower() cdb = _chan_db(bot, channel) if sub == "list": entries = cdb.get(key, []) _notice(bot, trigger.nick, f"{label} for {channel}: " + (", ".join(entries) if entries else "(empty)")) elif sub == "add" and len(args) > 1: mask = args[1] if mask not in cdb.setdefault(key, []): cdb[key].append(mask) _save(bot) _notice(bot, trigger.nick, f"added {mask} to {label} for {channel}") elif sub == "del" and len(args) > 1: mask = args[1] cdb[key] = [m for m in cdb.get(key, []) if m != mask] _save(bot) _notice(bot, trigger.nick, f"removed {mask} from {label} for {channel}") else: _notice(bot, trigger.nick, f"usage: !{label} [nick|mask]") @plugin.command("autoop", "ao") @plugin.require_chanmsg def cmd_autoop(bot, trigger): _automode_cmd(bot, trigger, "autoop", "o", "autoop") @plugin.command("autovoice", "av") @plugin.require_chanmsg def cmd_autovoice(bot, trigger): _automode_cmd(bot, trigger, "autovoice", "v", "autovoice") @plugin.command("autohalfop", "ahop") @plugin.require_chanmsg def cmd_autohalfop(bot, trigger): _automode_cmd(bot, trigger, "autohalfop", "h", "autohalfop") # --- Config --- @plugin.command("blacklist", "bl") def cmd_blacklist(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return args = (trigger.group(2) or "").split() dest = str(trigger.sender) if str(trigger.sender).startswith("#") else trigger.nick if not args: _notice(bot, trigger.nick, "usage: !blacklist [nick|mask]") return sub = args[0].lower() db = _global_db(bot) if sub == "list": bl = db.get("blacklist", []) _notice(bot, trigger.nick, "blacklist: " + (", ".join(bl) if bl else "(empty)")) elif sub == "add" and len(args) > 1: mask = args[1] if mask not in db.setdefault("blacklist", []): db["blacklist"].append(mask) _save(bot) _notice(bot, trigger.nick, f"added {mask} to blacklist") elif sub == "del" and len(args) > 1: mask = args[1] db["blacklist"] = [m for m in db.get("blacklist", []) if m != mask] _save(bot) _notice(bot, trigger.nick, f"removed {mask} from blacklist") else: _notice(bot, trigger.nick, "usage: !blacklist [nick|mask]") @plugin.command("acl") @plugin.require_chanmsg def cmd_acl(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return args = (trigger.group(2) or "").split() channel = str(trigger.sender) if not args: _notice(bot, trigger.nick, "usage: !acl [nick]") return sub = args[0].lower() db = _global_db(bot) if sub == "list": acl = db.get("acl", []) _notice(bot, trigger.nick, "acl: " + (", ".join(acl) if acl else "(empty)")) elif sub == "add" and len(args) > 1: nick = args[1] if nick not in db.setdefault("acl", []): db["acl"].append(nick) _save(bot) _notice(bot, trigger.nick, f"added {nick} to acl") elif sub == "del" and len(args) > 1: nick = args[1] db["acl"] = [n for n in db.get("acl", []) if n != nick] _save(bot) _notice(bot, trigger.nick, f"removed {nick} from acl") else: _notice(bot, trigger.nick, "usage: !acl [nick]") @plugin.command("whitelist", "wl") @plugin.require_chanmsg def cmd_wl(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return args = (trigger.group(2) or "").split() channel = str(trigger.sender) if not args: _notice(bot, trigger.nick, "usage: !wl [mask]") return sub = args[0].lower() db = _global_db(bot) if sub == "list": wl = db.get("whitelist", []) _notice(bot, trigger.nick, "whitelist: " + (", ".join(wl) if wl else "(empty)")) elif sub == "add" and len(args) > 1: mask = args[1] if mask not in db.setdefault("whitelist", []): db["whitelist"].append(mask) _save(bot) _notice(bot, trigger.nick, f"added {mask} to whitelist") elif sub == "del" and len(args) > 1: mask = args[1] db["whitelist"] = [m for m in db.get("whitelist", []) if m != mask] _save(bot) _notice(bot, trigger.nick, f"removed {mask} from whitelist") else: _notice(bot, trigger.nick, "usage: !wl [mask]") @plugin.command("except", "ex") @plugin.require_chanmsg def cmd_except(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return args = (trigger.group(2) or "").split() channel = str(trigger.sender) if not args: _notice(bot, trigger.nick, "usage: !except [mask]") return sub = args[0].lower() cdb = _chan_db(bot, channel) if sub == "list": exc = cdb.get("exceptions", []) _notice(bot, trigger.nick, f"exceptions for {channel}: " + (", ".join(exc) if exc else "(empty)")) elif sub == "add" and len(args) > 1: mask = args[1] if mask not in cdb.setdefault("exceptions", []): cdb["exceptions"].append(mask) _save(bot) _notice(bot, trigger.nick, f"added {mask} to exceptions for {channel}") elif sub == "del" and len(args) > 1: mask = args[1] cdb["exceptions"] = [m for m in cdb.get("exceptions", []) if m != mask] _save(bot) _notice(bot, trigger.nick, f"removed {mask} from exceptions for {channel}") else: _notice(bot, trigger.nick, "usage: !except [mask]") @plugin.command("filter", "f") @plugin.require_chanmsg def cmd_filter(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return args = (trigger.group(2) or "").split(None, 1) channel = str(trigger.sender) if not args: _notice(bot, trigger.nick, "usage: !filter [pattern]") return sub = args[0].lower() cdb = _chan_db(bot, channel) if sub == "list": filters = cdb.get("filters", []) if not filters: _notice(bot, trigger.nick, f"filters for {channel}: (none)") else: for i, f in enumerate(filters): _notice(bot, trigger.nick, f"[{i}] {f}") elif sub == "add" and len(args) > 1: cdb.setdefault("filters", []).append(args[1]) _save(bot) _notice(bot, trigger.nick, f"filter added: {args[1]}") elif sub == "del" and len(args) > 1: try: removed = cdb["filters"].pop(int(args[1])) _save(bot) _notice(bot, trigger.nick, f"filter removed: {removed}") except (ValueError, IndexError): _notice(bot, trigger.nick, "usage: !filter del (use !filter list for indices)") else: _notice(bot, trigger.nick, "usage: !filter [pattern]") @plugin.command("badword", "bw") @plugin.require_chanmsg def cmd_badword(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return args = (trigger.group(2) or "").split(None, 1) channel = str(trigger.sender) if not args: _notice(bot, trigger.nick, "usage: !badword [word]") return sub = args[0].lower() cdb = _chan_db(bot, channel) if sub == "list": words = cdb.get("badwords", []) if not words: _notice(bot, trigger.nick, f"badwords for {channel}: (none)") else: for i, w in enumerate(words): _notice(bot, trigger.nick, f"[{i}] {w}") elif sub == "add" and len(args) > 1: word = args[1].lower() cdb.setdefault("badwords", []).append(word) _save(bot) _notice(bot, trigger.nick, f"badword added: {word}") elif sub == "del" and len(args) > 1: try: removed = cdb["badwords"].pop(int(args[1])) _save(bot) _notice(bot, trigger.nick, f"badword removed: {removed}") except (ValueError, IndexError): _notice(bot, trigger.nick, "usage: !badword del (use !badword list for indices)") else: _notice(bot, trigger.nick, "usage: !badword [word]") @plugin.command("set", "s") def cmd_set(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return args = (trigger.group(2) or "").split() in_chan = str(trigger.sender).startswith("#") channel = str(trigger.sender) if in_chan else None dest = trigger.nick if len(args) < 2: keys = ", ".join(_CONFIG_KEYS) _notice(bot, dest, f"usage: !set - keys: {keys}") return key, raw = args[0], args[1] if key not in _CONFIG_KEYS: _notice(bot, dest, f"unknown key '{key}' - valid keys: {', '.join(_CONFIG_KEYS)}") return try: value = _CONFIG_KEYS[key](raw) except ValueError: _notice(bot, dest, f"invalid value for {key}: expected {_CONFIG_KEYS[key].__name__}") return if in_chan: _chan_db(bot, channel).setdefault("config", {})[key] = value _notice(bot, dest, f"set {key} = {value} for {channel}") else: if not _is_owner(bot, trigger): return _global_db(bot).setdefault("config", {})[key] = value _notice(bot, dest, f"set global default {key} = {value}") _save(bot) @plugin.command("config", "cfg") def cmd_config(bot, trigger): if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): return in_chan = str(trigger.sender).startswith("#") arg = (trigger.group(2) or "").strip() if arg.startswith("#"): channel = arg elif in_chan: channel = str(trigger.sender) else: _notice(bot, trigger.nick, "usage: !config [#channel]") return global_cfg = _global_db(bot).get("config", {}) channel_cfg = _chan_db(bot, channel).get("config", {}) _notice(bot, trigger.nick, f"-- config for {channel} --") for key in sorted(_CONFIG_KEYS): hardcoded = _CONFIG_DEFAULTS.get(key, "?") global_val = global_cfg.get(key) channel_val = channel_cfg.get(key) if channel_val is not None: _notice(bot, trigger.nick, f"{key} = {channel_val} (channel override; global: {global_val if global_val is not None else hardcoded})") elif global_val is not None: _notice(bot, trigger.nick, f"{key} = {global_val} (global; default: {hardcoded})") else: _notice(bot, trigger.nick, f"{key} = {hardcoded} (default)") # --- Admin --- @plugin.command("join", "j") def cmd_join(bot, trigger): if not _is_owner(bot, trigger): return channel = (trigger.group(2) or "").strip() if not channel.startswith("#"): _notice(bot, trigger.nick, "usage: !join #channel") return bot.join(channel) @plugin.command("part", "p") def cmd_part(bot, trigger): if not _is_owner(bot, trigger): return channel = (trigger.group(2) or "").strip() or str(trigger.sender) bot.part(channel) @plugin.command("chans", "c") def cmd_chans(bot, trigger): if not _is_owner(bot, trigger): return chans = sorted(str(c) for c in bot.channels) _notice(bot, trigger.nick, ("in: " + " ".join(chans)) if chans else "not in any channels")