aboutsummaryrefslogtreecommitdiffstats
path: root/osterman/idle.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 /osterman/idle.py
init: mostly vibed
Diffstat (limited to 'osterman/idle.py')
-rw-r--r--osterman/idle.py137
1 files changed, 137 insertions, 0 deletions
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 <nick> - 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)