From 5a8d568931d9b23ce0df1265d05259a7012081c9 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sun, 14 Jun 2026 01:46:29 +0300 Subject: init: mostly vibed --- osterman/idle.py | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 osterman/idle.py (limited to 'osterman/idle.py') diff --git a/osterman/idle.py b/osterman/idle.py new file mode 100644 index 0000000..c80d0d0 --- /dev/null +++ b/osterman/idle.py @@ -0,0 +1,137 @@ +""" +Sopel plugin: osterman - anti-idle + +Warns idle users via NOTICE, kicks if they stay idle past the kick threshold. +Idle state is tracked per channel. Thresholds are in minutes; set to 0 +to disable warn or kick. + +Degrades gracefully by privilege level: + halfop or op - warn and kick + none - warn only (cannot kick) + + !afk - reset your own idle timer + !idle on|off - enable/disable idle checking (ACL/owner; per channel if in channel, global if PM) + !idle - show how long a nick has been idle in this channel +""" + +import json +import os +import time + +from sopel import plugin + + +def _bot_has_halfop(bot, channel): + chan = bot.channels.get(channel) + if not chan: + return False + return chan.privileges.get(bot.nick, 0) >= plugin.HALFOP + + +def _chan_cfg(bot, channel): + global_cfg = bot.memory.get("os_db", {}).get("config", {}) + chan_cfg = (bot.memory.get("os_db", {}) + .get("channels", {}) + .get(channel.lower(), {}) + .get("config", {})) + return {**global_cfg, **chan_cfg} + + +@plugin.interval(60) +def _idle_tick(bot): + now = time.time() + last = bot.memory.get("os_last", {}) + + for channel_name, channel in list(bot.channels.items()): + cfg = _chan_cfg(bot, channel_name) + warn_min = cfg.get("idle_warn_min", 45) + kick_min = cfg.get("idle_kick_min", 60) + + if warn_min <= 0 and kick_min <= 0: + continue + + can_kick = _bot_has_halfop(bot, channel_name) + + for nick in list(channel.users): + if nick == bot.nick: + continue + idle_sec = now - last.get((channel_name, nick), now) + idle_min = idle_sec / 60 + + if kick_min > 0 and idle_min >= kick_min: + if can_kick: + bot.kick(channel_name, nick, f"idle for {int(idle_min)}m") + last.pop((channel_name, nick), None) + else: + bot.notice(f"you have been idle for {int(idle_min)}m in {channel_name} (cannot kick without halfop)", nick) + elif warn_min > 0 and idle_min >= warn_min: + bot.notice(f"you have been idle for {int(idle_min)}m in {channel_name} - say something or get kicked", nick) + + +@plugin.command("afk") +@plugin.require_chanmsg +def cmd_afk(bot, trigger): + channel = str(trigger.sender) + bot.memory.setdefault("os_last", {})[(channel, trigger.nick)] = time.time() + bot.notice("idle timer reset", trigger.nick) + + +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", []) + + +@plugin.command("idle", "id") +def cmd_idle(bot, trigger): + arg = (trigger.group(2) or "").strip().lower() + in_chan = str(trigger.sender).startswith("#") + + if arg in ("on", "off"): + if not _is_owner(bot, trigger) and not _is_acl(bot, trigger): + return + db = bot.memory.get("os_db", {}) + if in_chan: + channel = str(trigger.sender) + cfg = (db.setdefault("channels", {}) + .setdefault(channel.lower(), {}) + .setdefault("config", {})) + else: + cfg = db.setdefault("config", {}) + + if arg == "off": + cfg["idle_warn_min"] = 0 + cfg["idle_kick_min"] = 0 + bot.notice("idle checking disabled", trigger.nick) + else: + cfg["idle_warn_min"] = 45 + cfg["idle_kick_min"] = 60 + bot.notice("idle checking enabled (warn 45m, kick 60m)", trigger.nick) + + path = bot.memory.get("os_db_path", "/var/osterman/db.json") + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as fp: + json.dump(db, fp, indent=2) + return + + target = arg or trigger.nick + if not in_chan: + bot.notice("use !idle in a channel to check idle time", trigger.nick) + return + channel = str(trigger.sender) + last = bot.memory.get("os_last", {}).get((channel, target)) + if last is None: + bot.say(f"{target}: no idle data", trigger.sender) + return + diff = int(time.time() - last) + if diff < 60: + idle_str = f"{diff}s" + elif diff < 3600: + idle_str = f"{diff // 60}m" + elif diff < 86400: + idle_str = f"{diff // 3600}h {(diff % 3600) // 60}m" + else: + idle_str = f"{diff // 86400}d {(diff % 86400) // 3600}h" + bot.say(f"{target} idle: {idle_str}", trigger.sender) -- cgit v1.2.3