Experimental Discord bot written in Python
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

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