aboutsummaryrefslogtreecommitdiffstats
path: root/osterman/idle.py
blob: c80d0d077377c1f2c014d16add3515f9b8481319 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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)