浏览代码

Lots more logging. Many discord.py upgrade fixes.

master
Rocketsoup 1年前
父节点
当前提交
d243d596a4
共有 7 个文件被更改,包括 536 次插入113 次删除
  1. 17
    5
      bot.py
  2. 1
    1
      rocketbot/cogs/basecog.py
  3. 1
    1
      rocketbot/cogs/crosspostcog.py
  4. 10
    0
      rocketbot/cogs/generalcog.py
  5. 491
    103
      rocketbot/cogs/logcog.py
  6. 2
    2
      rocketbot/cogs/patterncog.py
  7. 14
    1
      rocketbot/cogsetting.py

+ 17
- 5
bot.py 查看文件

@@ -18,7 +18,7 @@ from rocketbot.cogs.crosspostcog import CrossPostCog
18 18
 from rocketbot.cogs.generalcog import GeneralCog
19 19
 from rocketbot.cogs.joinagecog import JoinAgeCog
20 20
 from rocketbot.cogs.joinraidcog import JoinRaidCog
21
-from rocketbot.cogs.logcog import LogCog
21
+from rocketbot.cogs.logcog import LoggingCog
22 22
 from rocketbot.cogs.patterncog import PatternCog
23 23
 from rocketbot.cogs.urlspamcog import URLSpamCog
24 24
 from rocketbot.cogs.usernamecog import UsernamePatternCog
@@ -41,11 +41,13 @@ class Rocketbot(commands.Bot):
41 41
 		super().__init__(command_prefix, **kwargs)
42 42
 
43 43
 	async def on_command_error(self, context: commands.Context, exception):
44
+		for line in traceback.format_stack():
45
+			print(line.strip())
44 46
 		if context.guild is None or \
45 47
 				context.message.channel is None or \
46 48
 				context.message.author.bot:
47 49
 			return
48
-		if not context.message.author.permissions_in(context.message.channel).ban_members:
50
+		if not context.message.channel.permissions_for(context.message.author).ban_members:
49 51
 			# Don't tell non-mods about errors
50 52
 			return
51 53
 		if isinstance(exception, (commands.errors.CommandError, )):
@@ -55,7 +57,14 @@ class Rocketbot(commands.Bot):
55 57
 				mention_author=False)
56 58
 			return
57 59
 		# Stack trace everything else
58
-		traceback.print_exception(type(exception), exception, exception.__traceback__)
60
+		# traceback.print_exception(type(exception), exception, exception.__traceback__)
61
+
62
+	async def on_error(self, event_method, args=None, kwargs=None):
63
+		print('Event caused error')
64
+		print(f'	event method: {event_method}')
65
+		print(f'	event args: {args}')
66
+		print(f'	event kwargs: {kwargs}')
67
+		print(traceback.format_exc())
59 68
 
60 69
 async def start_bot():
61 70
 	intents = Intents.default()
@@ -76,7 +85,7 @@ async def start_bot():
76 85
 	await bot.add_cog(CrossPostCog(bot))
77 86
 	await bot.add_cog(JoinAgeCog(bot))
78 87
 	await bot.add_cog(JoinRaidCog(bot))
79
-	await bot.add_cog(LogCog(bot))
88
+	await bot.add_cog(LoggingCog(bot))
80 89
 	await bot.add_cog(PatternCog(bot))
81 90
 	await bot.add_cog(URLSpamCog(bot))
82 91
 	await bot.add_cog(UsernamePatternCog(bot))
@@ -84,4 +93,7 @@ async def start_bot():
84 93
 	await bot.start(CONFIG['client_token'], reconnect=True)
85 94
 	print('\nBot aborted')
86 95
 
87
-asyncio.run(start_bot())
96
+try:
97
+	asyncio.run(start_bot())
98
+except KeyboardInterrupt:
99
+	pass

+ 1
- 1
rocketbot/cogs/basecog.py 查看文件

@@ -213,7 +213,7 @@ class BaseCog(commands.Cog):
213 213
 		if reaction is None or not reaction.is_enabled:
214 214
 			# Can't use this reaction with this message
215 215
 			return
216
-		if not member.permissions_in(channel).ban_members:
216
+		if not channel.permissions_for(member).ban_members:
217 217
 			# Not a mod (could make permissions configurable per BotMessageReaction some day)
218 218
 			return
219 219
 		await self.on_mod_react(bot_message, reaction, member)

+ 1
- 1
rocketbot/cogs/crosspostcog.py 查看文件

@@ -79,7 +79,7 @@ class CrossPostCog(BaseCog, name='Crosspost Detection'):
79 79
 		self.max_spam_contexts = 12
80 80
 
81 81
 	async def __record_message(self, message: Message) -> None:
82
-		if message.author.permissions_in(message.channel).ban_members:
82
+		if message.channel.permissions_for(message.author).ban_members:
83 83
 			# User exempt from spam detection
84 84
 			return
85 85
 		if len(message.content) < self.get_guild_setting(message.guild, self.SETTING_MIN_LENGTH):

+ 10
- 0
rocketbot/cogs/generalcog.py 查看文件

@@ -29,11 +29,21 @@ class GeneralCog(BaseCog, name='General'):
29 29
 		self.is_connected = True
30 30
 
31 31
 	@commands.Cog.listener()
32
+	async def on_disconnect(self):
33
+		'Event handler'
34
+		print('on_disconnect')
35
+
36
+	@commands.Cog.listener()
32 37
 	async def on_ready(self):
33 38
 		'Event handler'
34 39
 		print('on_ready')
35 40
 		self.is_ready = True
36 41
 
42
+	@commands.Cog.listener()
43
+	async def on_resumed(self):
44
+		'Event handler'
45
+		print('on_resumed')
46
+
37 47
 	@commands.command(
38 48
 		brief='Posts a test warning',
39 49
 		description='Tests whether a warning channel is configured for this ' + \

+ 491
- 103
rocketbot/cogs/logcog.py 查看文件

@@ -4,47 +4,36 @@ Cog for detecting large numbers of guild joins in a short period of time.
4 4
 import weakref
5 5
 from collections.abc import Sequence
6 6
 from datetime import datetime, timedelta
7
-from discord import Emoji, Guild, GuildSticker, Invite, Member, Message, RawBulkMessageDeleteEvent, RawMessageDeleteEvent, RawMessageUpdateEvent, Role, Thread, User
7
+from discord import AuditLogAction, AuditLogEntry, Emoji, Guild, GuildSticker, Invite, Member, Message, RawBulkMessageDeleteEvent, RawMessageDeleteEvent, RawMessageUpdateEvent, Role, Thread, User
8 8
 from discord.abc import GuildChannel
9 9
 from discord.ext import commands
10 10
 from discord.utils import escape_markdown
11
-from typing import List, Union
11
+from typing import List, Optional, Union
12
+import traceback
12 13
 
13 14
 from config import CONFIG
14 15
 from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
15 16
 from rocketbot.collections import AgeBoundList
16 17
 from rocketbot.storage import Storage
17 18
 
18
-class LogCog(BaseCog, name='Logging'):
19
+class LoggingCog(BaseCog, name='Logging'):
19 20
 	"""
20 21
 	Cog for logging notable events to a designated logging channel.
21 22
 	"""
22 23
 	SETTING_ENABLED = CogSetting('enabled', bool,
23 24
 			brief='logging',
24 25
 			description='Whether this cog is enabled for a guild.')
25
-	SETTING_EDITS_ENABLED = CogSetting('edits_enabled', bool,
26
-			brief='post edits',
27
-			description='Whether to log when users edit their posts.')
28
-	SETTING_JOINS_ENABLED = CogSetting('joins_enabled', bool,
29
-			brief='joins',
30
-			description='Whether to log when new users join the server.')
31
-	SETTING_LEAVES_ENABLED = CogSetting('leaves_enabled', bool,
32
-			brief='leaves',
33
-			description='Whether to log when users leave the server.')
34 26
 
35 27
 	def __init__(self, bot):
36 28
 		super().__init__(bot)
37
-		self.add_setting(LogCog.SETTING_ENABLED)
38
-		self.add_setting(LogCog.SETTING_EDITS_ENABLED)
39
-		self.add_setting(LogCog.SETTING_JOINS_ENABLED)
40
-		self.add_setting(LogCog.SETTING_LEAVES_ENABLED)
29
+		self.add_setting(LoggingCog.SETTING_ENABLED)
41 30
 
42 31
 	@commands.group(
43 32
 		brief='Manages event logging',
44 33
 	)
45 34
 	@commands.has_permissions(ban_members=True)
46 35
 	@commands.guild_only()
47
-	async def log(self, context: commands.Context):
36
+	async def logging(self, context: commands.Context):
48 37
 		'Logging command group'
49 38
 		if context.invoked_subcommand is None:
50 39
 			await context.send_help()
@@ -53,15 +42,66 @@ class LogCog(BaseCog, name='Logging'):
53 42
 
54 43
 	@commands.Cog.listener()
55 44
 	async def on_guild_channel_delete(self, channel: GuildChannel) -> None:
56
-		pass
45
+		"""
46
+		Called whenever a guild channel is deleted or created.
47
+
48
+		Note that you can get the guild from guild.
49
+
50
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_guild_channel_delete
51
+		"""
52
+		guild = channel.guild
53
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
54
+			return
55
+		text = f'Channel **{channel.name}** deleted.'
56
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
57
+		await bot_message.update()
57 58
 
58 59
 	@commands.Cog.listener()
59 60
 	async def on_guild_channel_create(self, channel: GuildChannel) -> None:
60
-		pass
61
+		"""
62
+		Called whenever a guild channel is deleted or created.
63
+
64
+		Note that you can get the guild from guild.
65
+
66
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_guild_channel_create
67
+		"""
68
+		guild = channel.guild
69
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
70
+			return
71
+		text = f'Channel **{channel.name}** created. {channel.mention}'
72
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
73
+		await bot_message.update()
61 74
 
62 75
 	@commands.Cog.listener()
63 76
 	async def on_guild_channel_update(self, before: GuildChannel, after: GuildChannel) -> None:
64
-		pass
77
+		"""
78
+		Called whenever a guild channel is updated. e.g. changed name, topic,
79
+		permissions.
80
+
81
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_guild_channel_update
82
+		"""
83
+		guild = after.guild
84
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
85
+			return
86
+		changes = []
87
+		if after.name != before.name:
88
+			changes.append(f'Name: `{before.name}` -> `{after.name}`')
89
+		if after.category != before.category:
90
+			changes.append(f'Category: {before.category.name if before.category else None}' + \
91
+		  		f' -> {after.category.name if after.category else None}')
92
+		if after.changed_roles != before.changed_roles:
93
+			changes.append('Roles changed')
94
+		if after.overwrites != before.overwrites:
95
+			changes.append('Permission overwrites changed')
96
+		if after.position != before.position:
97
+			changes.append(f'Position: {before.position} -> {after.position}')
98
+
99
+		if len(changes) == 0:
100
+			return
101
+		text = f'Channel **{before.name}** updated. Changes:\n'
102
+		text += '* ' + '\n* '.join(changes)
103
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
104
+		await bot_message.update()
65 105
 
66 106
 	# Events - Guilds
67 107
 
@@ -87,87 +127,410 @@ class LogCog(BaseCog, name='Logging'):
87 127
 
88 128
 	@commands.Cog.listener()
89 129
 	async def on_invite_create(self, invite: Invite) -> None:
90
-		pass
130
+		"""
131
+		Called when an Invite is created. You must have manage_channels to receive this.
132
+
133
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_invite_create
134
+		"""
135
+		guild = invite.guild
136
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
137
+			return
138
+		text = f'Invite code `{invite.code}` created by {self.__describe_user(invite.inviter)}. '
139
+		if invite.max_age == 0:
140
+			text += "Doesn't expire."
141
+		else:
142
+			text += f'Expires in {invite.max_age} seconds.'
143
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
144
+		await bot_message.update()
91 145
 
92 146
 	@commands.Cog.listener()
93 147
 	async def on_invite_delete(self, invite: Invite) -> None:
94
-		pass
148
+		"""
149
+		Called when an Invite is deleted. You must have manage_channels to receive this.
150
+
151
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_invite_delete
152
+		"""
153
+		guild = invite.guild
154
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
155
+			return
156
+		if invite.inviter:
157
+			text = f'Invite code `{invite.code}` deleted. Originally created by {self.__describe_user(invite.inviter)}.'
158
+		else:
159
+			text = f'Invite code `{invite.code}` deleted.'
160
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
161
+		await bot_message.update()
95 162
 
96 163
 	# Events - Members
97 164
 
98 165
 	@commands.Cog.listener()
99 166
 	async def on_member_join(self, member: Member) -> None:
100
-		pass
167
+		"""
168
+		Called when a Member joins a Guild.
169
+
170
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_member_join
171
+		"""
172
+		guild = member.guild
173
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
174
+			return
175
+		text = f'Member joined server: {self.__describe_user(member)}.'
176
+		flags = []
177
+		noteworthy = False
178
+
179
+		if member.flags.did_rejoin:
180
+			flags.append('Rejoined this server')
181
+
182
+		if member.public_flags.active_developer:
183
+			flags.append('Is an active developer')
184
+		if member.public_flags.hypesquad:
185
+			flags.append('Is a HypeSquad Events member')
186
+		if member.public_flags.hypesquad_bravery:
187
+			flags.append('Is a HypeSquad Bravery member')
188
+		if member.public_flags.hypesquad_brilliance:
189
+			flags.append('Is a HypeSquad Brilliance member')
190
+		if member.public_flags.hypesquad_balance:
191
+			flags.append('Is a HypeSquad Balance member')
192
+		if member.public_flags.early_supporter:
193
+			flags.append('Is an early supporter')
194
+
195
+		if member.public_flags.spammer:
196
+			flags.append('**Is flagged as a spammer**')
197
+			noteworthy = True
198
+		if member.public_flags.discord_certified_moderator:
199
+			flags.append('**Is a Discord Certified Moderator**')
200
+			noteworthy = True
201
+		if member.public_flags.early_verified_bot_developer:
202
+			flags.append('**Is a verified bot developer**')
203
+			noteworthy = True
204
+		if member.public_flags.verified_bot:
205
+			flags.append('**Is a verified bot**')
206
+			noteworthy = True
207
+		if member.public_flags.bug_hunter or member.public_flags.bug_hunter_level_2:
208
+			flags.append('**Is a bug hunter**')
209
+			noteworthy = True
210
+		if member.public_flags.system:
211
+			flags.append('**Is a Discord system user**')
212
+			noteworthy = True
213
+		if member.public_flags.staff:
214
+			flags.append('**Is Discord staff**')
215
+			noteworthy = True
216
+		if member.public_flags.partner:
217
+			flags.append('**Is a Discord partner**')
218
+			noteworthy = True
219
+
220
+		if len(flags) > 0:
221
+			text += '\n* ' + '\n* '.join(flags)
222
+		if noteworthy:
223
+			text += f'\n\nLink: {member.mention}'
224
+			bot_message = BotMessage(guild, text, BotMessage.TYPE_MOD_WARNING)
225
+		else:
226
+			bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
227
+		await bot_message.update()
101 228
 
102 229
 	@commands.Cog.listener()
103 230
 	async def on_member_remove(self, member: Member) -> None:
104
-		pass
231
+		"""
232
+		Called when a Member leaves a Guild.
233
+
234
+		If the guild or member could not be found in the internal cache this event
235
+		will not be called, you may use on_raw_member_remove() instead.
236
+
237
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_member_remove
238
+		"""
239
+		guild = member.guild
240
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
241
+			return
242
+		is_kick = False
243
+		kicker = None
244
+		kick_reason = None
245
+		entry = await self.__find_audit_entry(member, AuditLogAction.kick)
246
+		if entry:
247
+			is_kick = True
248
+			kicker = entry.user
249
+			kick_reason = entry.reason
250
+
251
+		if is_kick:
252
+			if kicker and kicker != member:
253
+				text = f'Member kicked from the server: {self.__describe_user(member)} by **{kicker.name}**'
254
+			else:
255
+				text = f'Member kicked from the server: {self.__describe_user(member)}'
256
+		else:
257
+			text = f'Member left server: {self.__describe_user(member)}'
258
+		if kick_reason:
259
+			text += f'\nReason: "{kick_reason}"'
260
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
261
+		await bot_message.update()
105 262
 
106 263
 	@commands.Cog.listener()
107 264
 	async def on_member_update(self, before: Member, after: Member) -> None:
108
-		pass
265
+		"""
266
+		Called when a Member updates their profile.
267
+
268
+		This is called when one or more of the following things change:
269
+		* nickname
270
+		* roles
271
+		* pending
272
+		* timeout
273
+		* guild avatar
274
+		* flags
275
+
276
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_member_update
277
+		"""
278
+		guild = after.guild
279
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
280
+			return
281
+		changes = []
282
+		if after.nick != before.nick:
283
+			changes.append(f'Nick: `{before.nick}` -> `{after.nick}`')
284
+		if after.roles != before.roles:
285
+			added_role_names = []
286
+			removed_role_names = []
287
+			for role in before.roles:
288
+				if role not in after.roles:
289
+					removed_role_names.append(role.name)
290
+			for role in after.roles:
291
+				if role not in before.roles:
292
+					added_role_names.append(role.name)
293
+			if len(removed_role_names) > 0:
294
+				changes.append(f'Removed roles: ~~**{"**~~, ~~**".joined(removed_role_names)}**~~')
295
+			if len(added_role_names) > 0:
296
+				changes.append(f'Added roles: **{"**, **".joined(added_role_names)}**')
297
+		if after.pending != before.pending:
298
+			pass  # not that interesting and probably noisy
299
+		if after.timed_out_until != before.timed_out_until:
300
+			if after.timed_out_until:
301
+				delta = after.timed_out_until - datetime.now()
302
+				changes.append(f'Timed out for `{delta}`')
303
+			elif before.timed_out_until:
304
+				changes.append('Timeout cleared')
305
+		before_guild_avatar = before.guild_avatar.url if before.guild_avatar else None
306
+		after_guild_avatar = after.guild_avatar.url if after.guild_avatar else None
307
+		if after_guild_avatar != before_guild_avatar:
308
+			changes.append(f'Guild avatar: <{before_guild_avatar}> -> <{after_guild_avatar}>')
309
+		if after.flags != before.flags:
310
+			flag_changes = []
311
+			for (name, after_value) in after.iter():
312
+				before_value = getattr(before, name)
313
+				if after_value != before_value:
314
+					flag_changes.append(f'`{name}` = `{before_value}` -> `{after_value}`')
315
+			if len(flag_changes) > 0:
316
+				changes.append(f'Flag changes: {", ".join(flag_changes)}')
317
+
318
+		if len(changes) == 0:
319
+			return
320
+		text = f'Details for member {self.__describe_user(before)} changed:\n'
321
+		text += '* ' + '\n* '.join(changes)
322
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
323
+		await bot_message.update()
109 324
 
110 325
 	@commands.Cog.listener()
111 326
 	async def on_user_update(self, before: User, after: User) -> None:
112
-		pass
327
+		"""
328
+		Called when a User updates their profile.
329
+
330
+		This is called when one or more of the following things change:
331
+		* avatar
332
+		* username
333
+		* discriminator
334
+
335
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_user_update
336
+		"""
337
+		if hasattr(after, 'guild'):
338
+			guild = after.guild
339
+		else:
340
+			return
341
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
342
+			return
343
+		changes = []
344
+		before_avatar_url = before.avatar.url if before.avatar else None
345
+		after_avatar_url = after.avatar.url if after.avatar else None
346
+		if after_avatar_url != before_avatar_url:
347
+			changes.append(f'Avatar URL: <{before_avatar_url}> -> <{after_avatar_url}>')
348
+		if after.name != before.name:
349
+			changes.append(f'Username: `{before.name}` -> `{after.name}`')
350
+		if len(changes) == 0:
351
+			return
352
+		text = f'Details for user {self.__describe_user(before)} changed:\n'
353
+		text += '* ' + '\n* '.join(changes)
354
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
355
+		await bot_message.update()
113 356
 
114 357
 	@commands.Cog.listener()
115 358
 	async def on_member_ban(self, user: Union[User, Member]) -> None:
116
-		pass
359
+		"""
360
+		Called when user gets banned from a Guild.
361
+
362
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_member_ban
363
+		"""
364
+		if hasattr(user, 'guild'):
365
+			guild = user
366
+		else:
367
+			return
368
+		guild = user.guild
369
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
370
+			return
371
+		banner = None
372
+		ban_reason = None
373
+		entry = await self.__find_audit_entry(user, AuditLogAction.ban)
374
+		if entry:
375
+			banner = entry.user
376
+			ban_reason = entry.reason
377
+		if banner:
378
+			text = f'Member {self.__describe_user(user)} banned by **{banner.name}**.'
379
+			if ban_reason:
380
+				text += f'\nReason: "{ban_reason}"'
381
+		else:
382
+			text = f'Member {self.__describe_user(user)} banned.'
383
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
384
+		await bot_message.update()
385
+
386
+	async def __find_audit_entry(self, user: Union[User, Member], action: AuditLogAction, max_age: int = 10) -> Optional[AuditLogEntry]:
387
+		"""
388
+		Searches the audit log for the most recent entry of a given type for a
389
+		given user. Intended for finding the relevant entry for a ban/kick that
390
+		just occurred.
391
+		"""
392
+		if hasattr(user, 'guild') and user.guild:
393
+			guild = user.guild
394
+		else:
395
+			return None
396
+		now = datetime.now()
397
+		async for entry in guild.audit_logs():
398
+			age_seconds = now.timestamp() - entry.created_at.timestamp()
399
+			if entry.action == action and entry.target == user and age_seconds <= max_age:
400
+				return entry
401
+		return None
117 402
 
118 403
 	@commands.Cog.listener()
119 404
 	async def on_member_unban(self, guild: Guild, user: Union[User, Member]) -> None:
120
-		pass
405
+		"""
406
+		Called when a User gets unbanned from a Guild.
407
+
408
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_member_unban
409
+		"""
410
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
411
+			return
412
+		text = f'Member {self.__describe_user(user)} unbanned'
413
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
414
+		await bot_message.update()
121 415
 
122 416
 	# Events - Messages
123 417
 
124 418
 	@commands.Cog.listener()
125
-	async def on_message(self, message: Message):
419
+	async def on_message(self, message: Message) -> None:
420
+		"""
421
+		Called when a Message is created and sent.
422
+
423
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_message
424
+		"""
126 425
 		# print(f"Saw message {message.id} \"{message.content}\"")
127
-		pass
128 426
 
129 427
 	@commands.Cog.listener()
130 428
 	async def on_message_edit(self, before: Message, after: Message) -> None:
131
-		text = f'Message {after.jump_url} edited by **{after.author.name}** ({after.author.display_name} {after.author.id}).\n\n' + \
132
-			f'Original markdown:\n> {escape_markdown(before.content)}\n\n' + \
133
-			f'Updated markdown:\n> {escape_markdown(after.content)}'
134
-		bot_message = BotMessage(after.guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
429
+		"""
430
+		Called when a Message receives an update event. If the message is not
431
+		found in the internal message cache, then these events will not be
432
+		called. Messages might not be in cache if the message is too old or the
433
+		client is participating in high traffic guilds.
434
+
435
+		If this occurs increase the max_messages parameter or use the
436
+		on_raw_message_edit() event instead.
437
+
438
+		The following non-exhaustive cases trigger this event:
439
+		* A message has been pinned or unpinned.
440
+		* The message content has been changed.
441
+		* The message has received an embed.
442
+			* For performance reasons, the embed server does not do this in a
443
+			“consistent” manner.
444
+		* The message’s embeds were suppressed or unsuppressed.
445
+		* A call message has received an update to its participants or ending time.
446
+
447
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_message_edit
448
+		"""
449
+		guild = after.guild
450
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
451
+			return
452
+		if after.author.id == self.bot.user.id:
453
+			return
454
+		if after.content == before.content:
455
+			# Most likely an embed being updated
456
+			return
457
+		text = f'Message {after.jump_url} edited by {self.__describe_user(after.author)}.\n' + \
458
+			f'Original markdown:\n{self.__quote_markdown(before.content)}\n' + \
459
+			f'Updated markdown:\n{self.__quote_markdown(after.content)}'
460
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
135 461
 		await bot_message.update()
136 462
 
137 463
 	@commands.Cog.listener()
138 464
 	async def on_raw_message_edit(self, payload: RawMessageUpdateEvent) -> None:
465
+		"""
466
+		Called when a message is edited. Unlike on_message_edit(), this is called
467
+		regardless of the state of the internal message cache.
468
+
469
+		If the message is found in the message cache, it can be accessed via
470
+		RawMessageUpdateEvent.cached_message. The cached message represents the
471
+		message before it has been edited. For example, if the content of a
472
+		message is modified and triggers the on_raw_message_edit() coroutine,
473
+		the RawMessageUpdateEvent.cached_message will return a Message object
474
+		that represents the message before the content was modified.
475
+
476
+		Due to the inherently raw nature of this event, the data parameter
477
+		coincides with the raw data given by the gateway.
478
+
479
+		Since the data payload can be partial, care must be taken when accessing
480
+		stuff in the dictionary. One example of a common case of partial data is
481
+		when the 'content' key is inaccessible. This denotes an “embed” only
482
+		edit, which is an edit in which only the embeds are updated by the
483
+		Discord embed server.
484
+
485
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_raw_message_edit
486
+		"""
139 487
 		if payload.cached_message:
140 488
 			return  # already handled by on_message_edit
141 489
 		guild = await self.bot.fetch_guild(payload.guild_id)
142 490
 		if not guild:
143 491
 			return
492
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
493
+			return
144 494
 		channel = await guild.fetch_channel(payload.channel_id)
145 495
 		if not channel:
146 496
 			return
147 497
 		message = await channel.fetch_message(payload.message_id)
148 498
 		if not message:
149 499
 			return
150
-		text = f'Message {message.jump_url} edited by **{message.author.name}** ({message.author.display_name} {message.author.id}).\n\n' + \
151
-			'Original markdown unavailable in cache.\n\n' + \
152
-			f'Updated markdown:\n> {escape_markdown(message.content)}'
500
+		text = f'Message {message.jump_url} edited by {self.__describe_user(message.author)}.\n' + \
501
+			'Original markdown unavailable in cache.\n' + \
502
+			f'Updated markdown:\n{self.__quote_markdown(message.content)}'
153 503
 		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
154 504
 		await bot_message.update()
155 505
 
156 506
 	@commands.Cog.listener()
157 507
 	async def on_raw_message_delete(self, payload: RawMessageDeleteEvent) -> None:
158
-		print('Raw message deleted')
508
+		"""
509
+		Called when a message is deleted. Unlike on_message_delete(), this is
510
+		called regardless of the message being in the internal message cache or not.
511
+
512
+		If the message is found in the message cache, it can be accessed via
513
+		RawMessageDeleteEvent.cached_message
514
+
515
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_raw_message_delete
516
+		"""
159 517
 		if payload.cached_message:
160 518
 			message = payload.cached_message
161
-			text = f'Message by **{message.author.name}** ({message.author.display_name} {message.author.id}) deleted from {message.channel.mention}\n\n' + \
162
-				f'Markdown:\n> {escape_markdown(message.content)}'
519
+			guild = message.guild
520
+			if not self.get_guild_setting(guild, self.SETTING_ENABLED):
521
+				return
522
+			if message.author.id == self.bot.user.id:
523
+				return
524
+			text = f'Message by {self.__describe_user(message.author)} deleted from {message.channel.mention}. ' + \
525
+				f'Markdown:\n{self.__quote_markdown(message.content)}'
163 526
 			bot_message = BotMessage(message.guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
164 527
 			await bot_message.update()
165 528
 		else:
166
-			print(f'Looking up guild {payload.guild_id}')
167 529
 			guild = await self.bot.fetch_guild(payload.guild_id)
168 530
 			if not guild:
169 531
 				return
170
-			print(f'Looking up channel {payload.channel_id}')
532
+			if not self.get_guild_setting(guild, self.SETTING_ENABLED):
533
+				return
171 534
 			channel = await guild.fetch_channel(payload.channel_id)
172 535
 			if not channel:
173 536
 				return
@@ -177,9 +540,21 @@ class LogCog(BaseCog, name='Logging'):
177 540
 
178 541
 	@commands.Cog.listener()
179 542
 	async def on_raw_bulk_message_delete(self, payload: RawBulkMessageDeleteEvent) -> None:
543
+		"""
544
+		Called when a bulk delete is triggered. Unlike on_bulk_message_delete(),
545
+		this is called regardless of the messages being in the internal message
546
+		cache or not.
547
+
548
+		If the messages are found in the message cache, they can be accessed via
549
+		RawBulkMessageDeleteEvent.cached_messages
550
+
551
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_raw_bulk_message_delete
552
+		"""
180 553
 		guild = await self.bot.fetch_guild(payload.guild_id)
181 554
 		if not guild:
182 555
 			return
556
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
557
+			return
183 558
 		channel = await guild.fetch_channel(payload.channel_id)
184 559
 		count = len(payload.message_ids)
185 560
 		cached_count = len(payload.cached_messages)
@@ -193,8 +568,8 @@ class LogCog(BaseCog, name='Logging'):
193 568
 		await bot_message.update()
194 569
 
195 570
 		for message in payload.cached_messages:
196
-			text = f'Message by **{message.author.name}** ({message.author.display_name} {message.author.id}) bulk deleted from {message.channel.mention}\n\n' + \
197
-				f'Markdown:\n> {escape_markdown(message.content)}'
571
+			text = f'Message by {self.__describe_user(message.author)} bulk deleted from {message.channel.mention}. ' + \
572
+				f'Markdown:\n{self.__quote_markdown(message.content)}'
198 573
 			bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
199 574
 			await bot_message.update()
200 575
 
@@ -202,15 +577,76 @@ class LogCog(BaseCog, name='Logging'):
202 577
 
203 578
 	@commands.Cog.listener()
204 579
 	async def on_guild_role_create(self, role: Role) -> None:
205
-		pass
580
+		"""
581
+		Called when a Guild creates or deletes a new Role.
582
+
583
+		To get the guild it belongs to, use Role.guild.
584
+
585
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_guild_role_create
586
+		"""
587
+		guild = role.guild
588
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
589
+			return
590
+		text = f'Role created: **{role.name}**'
591
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
592
+		await bot_message.update()
206 593
 
207 594
 	@commands.Cog.listener()
208 595
 	async def on_guild_role_delete(self, role: Role) -> None:
209
-		pass
596
+		"""
597
+		Called when a Guild creates or deletes a new Role.
598
+
599
+		To get the guild it belongs to, use Role.guild.
600
+
601
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_guild_role_delete
602
+		"""
603
+		guild = role.guild
604
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
605
+			return
606
+		text = f'Role removed: **{role.name}**'
607
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
608
+		await bot_message.update()
210 609
 
211 610
 	@commands.Cog.listener()
212 611
 	async def on_guild_role_update(self, before: Role, after: Role) -> None:
213
-		pass
612
+		"""
613
+		Called when a Role is changed guild-wide.
614
+
615
+		https://discordpy.readthedocs.io/en/stable/api.html#discord.on_guild_role_update
616
+		"""
617
+		guild = after.guild
618
+		if not self.get_guild_setting(guild, self.SETTING_ENABLED):
619
+			return
620
+		changes = []
621
+		if after.name != before.name:
622
+			changes.append(f'Name: `{before.name}` -> `{after.name}`')
623
+		if after.hoist != before.hoist:
624
+			changes.append(f'Hoisted: `{before.hoist}` -> `{after.hoist}`')
625
+		if after.position != before.position:
626
+			changes.append(f'Position: `{before.position}` -> `{after.position}`')
627
+		if after.unicode_emoji != before.unicode_emoji:
628
+			changes.append(f'Emoji: {before.unicode_emoji} -> {after.unicode_emoji}')
629
+		if after.mentionable != before.mentionable:
630
+			changes.append(f'Mentionable: `{before.mentionable}` -> `{after.mentionable}`')
631
+		if after.permissions != before.permissions:
632
+			changes.append('Permissions edited')
633
+		if after.color != before.color:
634
+			changes.append('Color edited')
635
+		before_icon_url = before.icon.url if before.icon else None
636
+		after_icon_url = after.icon.url if after.icon else None
637
+		if after_icon_url != before_icon_url:
638
+			changes.append(f'Icon: <{before_icon_url}> -> <{after_icon_url}>')
639
+		before_icon_url = before.display_icon.url if before.display_icon else None
640
+		after_icon_url = after.display_icon.url if after.display_icon else None
641
+		if after_icon_url != before_icon_url:
642
+			changes.append(f'Display icon: <{before_icon_url}> -> <{after_icon_url}>')
643
+
644
+		if len(changes) == 0:
645
+			return
646
+		text = 'Role updated. Changes:\n'
647
+		text += '* ' + '\n* '.join(changes)
648
+		bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
649
+		await bot_message.update()
214 650
 
215 651
 	# Events - Threads
216 652
 
@@ -228,59 +664,11 @@ class LogCog(BaseCog, name='Logging'):
228 664
 
229 665
 
230 666
 	# ------------------------------------------------------------------------
231
-	def remove_me():
232
-		pass
233
-
234
-
235
-	# @commands.Cog.listener()
236
-	# async def on_member_join(self, member: Member) -> None:
237
-	# 	'Event handler'
238
-	# 	guild: Guild = member.guild
239
-	# 	if not self.get_guild_setting(guild, self.SETTING_ENABLED):
240
-	# 		return
241
-	# 	min_count = self.get_guild_setting(guild, self.SETTING_JOIN_COUNT)
242
-	# 	seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
243
-	# 	timespan: timedelta = timedelta(seconds=seconds)
244
-
245
-	# 	last_raid: JoinRaidContext = Storage.get_state_value(guild, self.STATE_KEY_LAST_RAID)
246
-	# 	recent_joins: AgeBoundList = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
247
-	# 	if recent_joins is None:
248
-	# 		recent_joins = AgeBoundList(timespan, lambda i, member : member.joined_at)
249
-	# 		Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
250
-	# 	if last_raid:
251
-	# 		if member.joined_at - last_raid.last_join_time() > timespan:
252
-	# 			# Last raid is over
253
-	# 			Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, None)
254
-	# 			recent_joins.append(member)
255
-	# 			return
256
-	# 		# Add join to existing raid
257
-	# 		last_raid.join_members.append(member)
258
-	# 		self.record_warning(member)
259
-	# 		if len(last_raid.banned_members) > 0:
260
-	# 			self.log(guild, f'Banning as part of last join raid: {member.name}')
261
-	# 			await member.ban(
262
-	# 				reason='Rocketbot: Part of join raid.',
263
-	# 				delete_message_days=0)
264
-	# 			last_raid.banned_members.add(member)
265
-	# 		elif len(last_raid.kicked_members) > 0:
266
-	# 			self.log(guild, f'Kicking as part of last join raid: {member.name}')
267
-	# 			await member.kick(
268
-	# 				reason='Rocketbot: Part of join raid.')
269
-	# 			last_raid.kicked_members.add(member)
270
-	# 		await self.__update_warning_message(last_raid)
271
-	# 	else:
272
-	# 		# Add join to the general, non-raid recent join list
273
-	# 		recent_joins.append(member)
274
-	# 		if len(recent_joins) >= min_count:
275
-	# 			self.log(guild, '\u0007Join raid detected')
276
-	# 			last_raid = JoinRaidContext(recent_joins)
277
-	# 			Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, last_raid)
278
-	# 			recent_joins.clear()
279
-	# 			msg = BotMessage(guild,
280
-	# 					text='',
281
-	# 					type=BotMessage.TYPE_MOD_WARNING,
282
-	# 					context=last_raid)
283
-	# 			self.record_warnings(recent_joins)
284
-	# 			last_raid.warning_message_ref = weakref.ref(msg)
285
-	# 			await self.__update_warning_message(last_raid)
286
-	# 			await self.post_message(msg)
667
+	def __quote_markdown(self, s: str) -> str:
668
+		return '> ' + escape_markdown(s).replace('\n', '\n> ')
669
+
670
+	def __describe_user(self, user: Union[User, Member]) -> str:
671
+		"""
672
+		Standardized markdown describing a user or member.
673
+		"""
674
+		return f'**{user.name}** ({user.display_name} {user.id})'

+ 2
- 2
rocketbot/cogs/patterncog.py 查看文件

@@ -40,7 +40,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
40 40
 		patterns: dict[str, PatternStatement] = Storage.get_state_value(guild,
41 41
 			'PatternCog.patterns')
42 42
 		if patterns is None:
43
-			jsons: list[dict] = self.get_guild_setting(guild, self.SETTING_PATTERNS)
43
+			jsons: list[dict] = self.get_guild_setting(guild, self.SETTING_PATTERNS) or []
44 44
 			pattern_list: list[PatternStatement] = []
45 45
 			for json in jsons:
46 46
 				try:
@@ -88,7 +88,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
88 88
 				message.content is None or \
89 89
 				message.content == '':
90 90
 			return
91
-		if message.author.permissions_in(message.channel).ban_members:
91
+		if message.channel.permissions_for(message.author).ban_members:
92 92
 			# Ignore mods
93 93
 			return
94 94
 

+ 14
- 1
rocketbot/cogsetting.py 查看文件

@@ -115,6 +115,7 @@ class CogSetting:
115 115
 				commands.guild_only(),
116 116
 			])
117 117
 		command.cog = cog
118
+		self.__fix_command(command)
118 119
 		return command
119 120
 
120 121
 	def __make_setter_command(self, cog: Cog) -> Command:
@@ -169,9 +170,10 @@ class CogSetting:
169 170
 				commands.has_permissions(ban_members=True),
170 171
 				commands.guild_only(),
171 172
 			])
172
-		# Passing `cog` in init gets ignored and set to `None` so set after.
173
+		# HACK: Passing `cog` in init gets ignored and set to `None` so set after.
173 174
 		# This ensures the callback is passed the cog as `self` argument.
174 175
 		command.cog = cog
176
+		self.__fix_command(command)
175 177
 		return command
176 178
 
177 179
 	def __make_enable_command(self, cog: Cog) -> Command:
@@ -195,6 +197,7 @@ class CogSetting:
195 197
 				commands.guild_only(),
196 198
 			])
197 199
 		command.cog = cog
200
+		self.__fix_command(command)
198 201
 		return command
199 202
 
200 203
 	def __make_disable_command(self, cog: Cog) -> Command:
@@ -218,8 +221,18 @@ class CogSetting:
218 221
 				commands.guild_only(),
219 222
 			])
220 223
 		command.cog = cog
224
+		self.__fix_command(command)
221 225
 		return command
222 226
 
227
+	def __fix_command(self, command: Command) -> None:
228
+		"""
229
+		HACK: Fixes bug in discord.py 2.3.2 where it's requiring the user to
230
+		supply the context argument. This removes that argument from the list.
231
+		"""
232
+		params = command.params
233
+		del params['context']
234
+		command.params = params
235
+
223 236
 	@classmethod
224 237
 	def set_up_all(cls, cog: Cog, bot: Bot, settings: list) -> None:
225 238
 		"""

正在加载...
取消
保存