|
|
@@ -1,12 +1,15 @@
|
|
1
|
1
|
from datetime import datetime, timedelta
|
|
|
2
|
+from typing import cast
|
|
2
|
3
|
|
|
3
|
|
-from discord import Guild, Member
|
|
4
|
|
-from discord.ext import commands
|
|
|
4
|
+from discord import Guild, Member, Status
|
|
|
5
|
+from discord.ext import commands, tasks
|
|
|
6
|
+from discord.ext.tasks import Loop
|
|
5
|
7
|
|
|
6
|
8
|
from config import CONFIG
|
|
7
|
9
|
from rocketbot.cogs.basecog import BaseCog, BotMessage, CogSetting
|
|
8
|
10
|
from rocketbot.collections import AgeBoundDict
|
|
9
|
11
|
from rocketbot.storage import Storage
|
|
|
12
|
+from rocketbot.utils import bot_log
|
|
10
|
13
|
|
|
11
|
14
|
class AutoKickContext:
|
|
12
|
15
|
"""
|
|
|
@@ -22,6 +25,11 @@ class AutoKickContext:
|
|
22
|
25
|
self.last_kick = time
|
|
23
|
26
|
self.kick_count += 1
|
|
24
|
27
|
|
|
|
28
|
+class StatusCheckContext:
|
|
|
29
|
+ def __init__(self, member: Member):
|
|
|
30
|
+ self.member = member
|
|
|
31
|
+ self.joined_at = datetime.now()
|
|
|
32
|
+
|
|
25
|
33
|
class AutoKickCog(BaseCog, name='Auto Kick'):
|
|
26
|
34
|
"""
|
|
27
|
35
|
Cog for automatically kicking ALL new joins. For temporary use during join raids.
|
|
|
@@ -36,6 +44,13 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
|
|
36
|
44
|
'disables this feature (only kick, never ban).',
|
|
37
|
45
|
usage='<count:int>',
|
|
38
|
46
|
min_value=0)
|
|
|
47
|
+ SETTING_OFFLINE_ONLY = CogSetting('offlineonly', bool,
|
|
|
48
|
+ brief='whether to only kick users whose status is offline',
|
|
|
49
|
+ description='Compromised accounts may have a status of offline. ' + \
|
|
|
50
|
+ 'If this setting is enabled, the user\'s status will be ' + \
|
|
|
51
|
+ 'checked a few seconds after joining. If it is offline ' + \
|
|
|
52
|
+ 'they will be kicked.',
|
|
|
53
|
+ usage='<true|false>')
|
|
39
|
54
|
|
|
40
|
55
|
STATE_KEY_RECENT_KICKS = "AutoKickCog.recent_joins"
|
|
41
|
56
|
|
|
|
@@ -43,6 +58,10 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
|
|
43
|
58
|
super().__init__(bot)
|
|
44
|
59
|
self.add_setting(AutoKickCog.SETTING_ENABLED)
|
|
45
|
60
|
self.add_setting(AutoKickCog.SETTING_BAN_COUNT)
|
|
|
61
|
+ self.add_setting(AutoKickCog.SETTING_OFFLINE_ONLY)
|
|
|
62
|
+ self.status_check_members = []
|
|
|
63
|
+ timer: Loop = cast(Loop, self.status_check_timer)
|
|
|
64
|
+ timer.start()
|
|
46
|
65
|
|
|
47
|
66
|
@commands.group(
|
|
48
|
67
|
brief='Automatically kicks all new users as soon as they join',
|
|
|
@@ -60,6 +79,36 @@ class AutoKickCog(BaseCog, name='Auto Kick'):
|
|
60
|
79
|
guild: Guild = member.guild
|
|
61
|
80
|
if not self.get_guild_setting(guild, self.SETTING_ENABLED):
|
|
62
|
81
|
return
|
|
|
82
|
+ if self.get_guild_setting(guild, self.SETTING_OFFLINE_ONLY):
|
|
|
83
|
+ self.log(guild, f'New member {member.name} status is {member.status}')
|
|
|
84
|
+ self.status_check_members.append(StatusCheckContext(member))
|
|
|
85
|
+ return
|
|
|
86
|
+ self.__kick_or_ban_if_needed(member)
|
|
|
87
|
+
|
|
|
88
|
+ @tasks.loop(seconds=5.0)
|
|
|
89
|
+ async def status_check_timer(self):
|
|
|
90
|
+ 'Checks status of new members shortly after joining to see if they go offline'
|
|
|
91
|
+ contexts = self.status_check_members.copy()
|
|
|
92
|
+ self.status_check_members = []
|
|
|
93
|
+ now = datetime.now()
|
|
|
94
|
+ # bot_log(guild=None, cog_class=None, message=f'Found {len(contexts)} members to check')
|
|
|
95
|
+ for c in contexts:
|
|
|
96
|
+ context: StatusCheckContext = c
|
|
|
97
|
+ member: Member = context.member
|
|
|
98
|
+ guild: Guild = member.guild
|
|
|
99
|
+ if now - context.joined_at < timedelta(seconds=5.0):
|
|
|
100
|
+ # Too soon, check again later
|
|
|
101
|
+ self.status_check_members.append(context)
|
|
|
102
|
+ continue
|
|
|
103
|
+ if member.status != Status.offline:
|
|
|
104
|
+ # Online, ignore
|
|
|
105
|
+ self.log(guild, f'{member.name} status is {member.status}. Not kicking.')
|
|
|
106
|
+ continue
|
|
|
107
|
+ self.log(guild, f'{member.name} went offline 5s later')
|
|
|
108
|
+ await self.__kick_or_ban_if_needed(member)
|
|
|
109
|
+
|
|
|
110
|
+ async def __kick_or_ban_if_needed(self, member: Member):
|
|
|
111
|
+ guild: Guild = member.guild
|
|
63
|
112
|
recent_kicks: AgeBoundDict = Storage.get_state_value(guild, AutoKickCog.STATE_KEY_RECENT_KICKS)
|
|
64
|
113
|
if recent_kicks is None:
|
|
65
|
114
|
recent_kicks = AgeBoundDict(timedelta(seconds=3600), lambda i, context : context.last_kick)
|