|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+"""
|
|
|
2
|
+Cog for detecting large numbers of guild joins in a short period of time.
|
|
|
3
|
+"""
|
|
|
4
|
+import weakref
|
|
|
5
|
+from collections.abc import Sequence
|
|
|
6
|
+from datetime import datetime, timedelta
|
|
|
7
|
+from discord import Emoji, Guild, GuildChannel, GuildSticker, Invite, Member, Message, Role, Thread, User
|
|
|
8
|
+from discord.ext import commands
|
|
|
9
|
+from typing import List, Union
|
|
|
10
|
+
|
|
|
11
|
+from config import CONFIG
|
|
|
12
|
+from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
|
|
|
13
|
+from rocketbot.collections import AgeBoundList
|
|
|
14
|
+from rocketbot.storage import Storage
|
|
|
15
|
+
|
|
|
16
|
+class LogCog(BaseCog, name='Logging'):
|
|
|
17
|
+ """
|
|
|
18
|
+ Cog for logging notable events to a designated logging channel.
|
|
|
19
|
+ """
|
|
|
20
|
+ SETTING_ENABLED = CogSetting('enabled', bool,
|
|
|
21
|
+ brief='logging',
|
|
|
22
|
+ description='Whether this cog is enabled for a guild.')
|
|
|
23
|
+ SETTING_EDITS_ENABLED = CogSetting('edits_enabled', bool,
|
|
|
24
|
+ brief='post edits',
|
|
|
25
|
+ description='Whether to log when users edit their posts.')
|
|
|
26
|
+ SETTING_JOINS_ENABLED = CogSetting('joins_enabled', bool,
|
|
|
27
|
+ brief='joins',
|
|
|
28
|
+ description='Whether to log when new users join the server.')
|
|
|
29
|
+ SETTING_LEAVES_ENABLED = CogSetting('leaves_enabled', bool,
|
|
|
30
|
+ brief='leaves',
|
|
|
31
|
+ description='Whether to log when users leave the server.')
|
|
|
32
|
+
|
|
|
33
|
+ def __init__(self, bot):
|
|
|
34
|
+ super().__init__(bot)
|
|
|
35
|
+ self.add_setting(LogCog.SETTING_ENABLED)
|
|
|
36
|
+ self.add_setting(LogCog.SETTING_EDITS_ENABLED)
|
|
|
37
|
+ self.add_setting(LogCog.SETTING_JOINS_ENABLED)
|
|
|
38
|
+ self.add_setting(LogCog.SETTING_LEAVES_ENABLED)
|
|
|
39
|
+
|
|
|
40
|
+ @commands.group(
|
|
|
41
|
+ brief='Manages event logging',
|
|
|
42
|
+ )
|
|
|
43
|
+ @commands.has_permissions(ban_members=True)
|
|
|
44
|
+ @commands.guild_only()
|
|
|
45
|
+ async def log(self, context: commands.Context):
|
|
|
46
|
+ 'Logging command group'
|
|
|
47
|
+ if context.invoked_subcommand is None:
|
|
|
48
|
+ await context.send_help()
|
|
|
49
|
+
|
|
|
50
|
+ # Events - Channels
|
|
|
51
|
+
|
|
|
52
|
+ @commands.Cog.listener()
|
|
|
53
|
+ async def on_guild_channel_delete(self, channel: GuildChannel) -> None:
|
|
|
54
|
+ pass
|
|
|
55
|
+
|
|
|
56
|
+ @commands.Cog.listener()
|
|
|
57
|
+ async def on_guild_channel_create(self, channel: GuildChannel) -> None:
|
|
|
58
|
+ pass
|
|
|
59
|
+
|
|
|
60
|
+ @commands.Cog.listener()
|
|
|
61
|
+ async def on_guild_channel_update(self, before: GuildChannel, after: GuildChannel) -> None:
|
|
|
62
|
+ pass
|
|
|
63
|
+
|
|
|
64
|
+ # Events - Guilds
|
|
|
65
|
+
|
|
|
66
|
+ @commands.Cog.listener()
|
|
|
67
|
+ async def on_guild_available(self, guild: Guild) -> None:
|
|
|
68
|
+ pass
|
|
|
69
|
+
|
|
|
70
|
+ @commands.Cog.listener()
|
|
|
71
|
+ async def on_guild_unavailable(self, guild: Guild) -> None:
|
|
|
72
|
+ pass
|
|
|
73
|
+
|
|
|
74
|
+ @commands.Cog.listener()
|
|
|
75
|
+ async def on_guild_update(self, before: Guild, after: Guild) -> None:
|
|
|
76
|
+ pass
|
|
|
77
|
+
|
|
|
78
|
+ @commands.Cog.listener()
|
|
|
79
|
+ async def on_guild_emojis_update(self, guild: Guild, before: Sequence[Emoji], after: Sequence[Emoji]) -> None:
|
|
|
80
|
+ pass
|
|
|
81
|
+
|
|
|
82
|
+ @commands.Cog.listener()
|
|
|
83
|
+ async def on_guild_stickers_update(self, guild: Guild, before: Sequence[GuildSticker], after: Sequence[GuildSticker]) -> None:
|
|
|
84
|
+ pass
|
|
|
85
|
+
|
|
|
86
|
+ @commands.Cog.listener()
|
|
|
87
|
+ async def on_invite_create(self, invite: Invite) -> None:
|
|
|
88
|
+ pass
|
|
|
89
|
+
|
|
|
90
|
+ @commands.Cog.listener()
|
|
|
91
|
+ async def on_invite_delete(self, invite: Invite) -> None:
|
|
|
92
|
+ pass
|
|
|
93
|
+
|
|
|
94
|
+ # Events - Members
|
|
|
95
|
+
|
|
|
96
|
+ @commands.Cog.listener()
|
|
|
97
|
+ async def on_member_join(self, member: Member) -> None:
|
|
|
98
|
+ pass
|
|
|
99
|
+
|
|
|
100
|
+ @commands.Cog.listener()
|
|
|
101
|
+ async def on_member_remove(self, member: Member) -> None:
|
|
|
102
|
+ pass
|
|
|
103
|
+
|
|
|
104
|
+ @commands.Cog.listener()
|
|
|
105
|
+ async def on_member_update(self, before: Member, after: Member) -> None:
|
|
|
106
|
+ pass
|
|
|
107
|
+
|
|
|
108
|
+ @commands.Cog.listener()
|
|
|
109
|
+ async def on_user_update(self, before: User, after: User) -> None:
|
|
|
110
|
+ pass
|
|
|
111
|
+
|
|
|
112
|
+ @commands.Cog.listener()
|
|
|
113
|
+ async def on_member_ban(self, user: Union[User, Member]) -> None:
|
|
|
114
|
+ pass
|
|
|
115
|
+
|
|
|
116
|
+ @commands.Cog.listener()
|
|
|
117
|
+ async def on_member_unban(self, guild: Guild, user: Union[User, Member]) -> None:
|
|
|
118
|
+ pass
|
|
|
119
|
+
|
|
|
120
|
+ # Events - Messages
|
|
|
121
|
+
|
|
|
122
|
+ @commands.Cog.listener()
|
|
|
123
|
+ async def on_message_edit(self, before: Message, after: Message) -> None:
|
|
|
124
|
+ pass
|
|
|
125
|
+
|
|
|
126
|
+ @commands.Cog.listener()
|
|
|
127
|
+ async def on_message_delete(self, message: Message) -> None:
|
|
|
128
|
+ pass
|
|
|
129
|
+
|
|
|
130
|
+ @commands.Cog.listener()
|
|
|
131
|
+ async def on_bulk_message_delete(self, messages: List[Message]) -> None:
|
|
|
132
|
+ pass
|
|
|
133
|
+
|
|
|
134
|
+ # Events - Roles
|
|
|
135
|
+
|
|
|
136
|
+ @commands.Cog.listener()
|
|
|
137
|
+ async def on_guild_role_create(self, role: Role) -> None:
|
|
|
138
|
+ pass
|
|
|
139
|
+
|
|
|
140
|
+ @commands.Cog.listener()
|
|
|
141
|
+ async def on_guild_role_delete(self, role: Role) -> None:
|
|
|
142
|
+ pass
|
|
|
143
|
+
|
|
|
144
|
+ @commands.Cog.listener()
|
|
|
145
|
+ async def on_guild_role_update(self, before: Role, after: Role) -> None:
|
|
|
146
|
+ pass
|
|
|
147
|
+
|
|
|
148
|
+ # Events - Threads
|
|
|
149
|
+
|
|
|
150
|
+ @commands.Cog.listener()
|
|
|
151
|
+ async def on_thread_create(self, thread: Thread) -> None:
|
|
|
152
|
+ pass
|
|
|
153
|
+
|
|
|
154
|
+ @commands.Cog.listener()
|
|
|
155
|
+ async def on_thread_update(self, before: Thread, after: Thread) -> None:
|
|
|
156
|
+ pass
|
|
|
157
|
+
|
|
|
158
|
+ @commands.Cog.listener()
|
|
|
159
|
+ async def on_thread_delete(self, thread: Thread) -> None:
|
|
|
160
|
+ pass
|
|
|
161
|
+
|
|
|
162
|
+
|
|
|
163
|
+ def remove_me():
|
|
|
164
|
+ pass
|
|
|
165
|
+
|
|
|
166
|
+
|
|
|
167
|
+ @commands.Cog.listener()
|
|
|
168
|
+ async def on_member_join(self, member: Member) -> None:
|
|
|
169
|
+ 'Event handler'
|
|
|
170
|
+ guild: Guild = member.guild
|
|
|
171
|
+ if not self.get_guild_setting(guild, self.SETTING_ENABLED):
|
|
|
172
|
+ return
|
|
|
173
|
+ min_count = self.get_guild_setting(guild, self.SETTING_JOIN_COUNT)
|
|
|
174
|
+ seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
|
|
|
175
|
+ timespan: timedelta = timedelta(seconds=seconds)
|
|
|
176
|
+
|
|
|
177
|
+ last_raid: JoinRaidContext = Storage.get_state_value(guild, self.STATE_KEY_LAST_RAID)
|
|
|
178
|
+ recent_joins: AgeBoundList = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
|
|
|
179
|
+ if recent_joins is None:
|
|
|
180
|
+ recent_joins = AgeBoundList(timespan, lambda i, member : member.joined_at)
|
|
|
181
|
+ Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
|
|
|
182
|
+ if last_raid:
|
|
|
183
|
+ if member.joined_at - last_raid.last_join_time() > timespan:
|
|
|
184
|
+ # Last raid is over
|
|
|
185
|
+ Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, None)
|
|
|
186
|
+ recent_joins.append(member)
|
|
|
187
|
+ return
|
|
|
188
|
+ # Add join to existing raid
|
|
|
189
|
+ last_raid.join_members.append(member)
|
|
|
190
|
+ self.record_warning(member)
|
|
|
191
|
+ if len(last_raid.banned_members) > 0:
|
|
|
192
|
+ self.log(guild, f'Banning as part of last join raid: {member.name}')
|
|
|
193
|
+ await member.ban(
|
|
|
194
|
+ reason='Rocketbot: Part of join raid.',
|
|
|
195
|
+ delete_message_days=0)
|
|
|
196
|
+ last_raid.banned_members.add(member)
|
|
|
197
|
+ elif len(last_raid.kicked_members) > 0:
|
|
|
198
|
+ self.log(guild, f'Kicking as part of last join raid: {member.name}')
|
|
|
199
|
+ await member.kick(
|
|
|
200
|
+ reason='Rocketbot: Part of join raid.')
|
|
|
201
|
+ last_raid.kicked_members.add(member)
|
|
|
202
|
+ await self.__update_warning_message(last_raid)
|
|
|
203
|
+ else:
|
|
|
204
|
+ # Add join to the general, non-raid recent join list
|
|
|
205
|
+ recent_joins.append(member)
|
|
|
206
|
+ if len(recent_joins) >= min_count:
|
|
|
207
|
+ self.log(guild, '\u0007Join raid detected')
|
|
|
208
|
+ last_raid = JoinRaidContext(recent_joins)
|
|
|
209
|
+ Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, last_raid)
|
|
|
210
|
+ recent_joins.clear()
|
|
|
211
|
+ msg = BotMessage(guild,
|
|
|
212
|
+ text='',
|
|
|
213
|
+ type=BotMessage.TYPE_MOD_WARNING,
|
|
|
214
|
+ context=last_raid)
|
|
|
215
|
+ self.record_warnings(recent_joins)
|
|
|
216
|
+ last_raid.warning_message_ref = weakref.ref(msg)
|
|
|
217
|
+ await self.__update_warning_message(last_raid)
|
|
|
218
|
+ await self.post_message(msg)
|
|
|
219
|
+
|
|
|
220
|
+ async def on_setting_updated(self, guild: Guild, setting: CogSetting) -> None:
|
|
|
221
|
+ if setting is self.SETTING_JOIN_TIME:
|
|
|
222
|
+ seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
|
|
|
223
|
+ timespan: timedelta = timedelta(seconds=seconds)
|
|
|
224
|
+ recent_joins: AgeBoundList = Storage.get_state_value(guild,
|
|
|
225
|
+ self.STATE_KEY_RECENT_JOINS)
|
|
|
226
|
+ if recent_joins:
|
|
|
227
|
+ recent_joins.max_age = timespan
|
|
|
228
|
+ recent_joins.purge_old_elements()
|