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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  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, 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, Union
  12. from config import CONFIG
  13. from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
  14. from rocketbot.collections import AgeBoundList
  15. from rocketbot.storage import Storage
  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. def __init__(self, bot):
  33. super().__init__(bot)
  34. self.add_setting(LogCog.SETTING_ENABLED)
  35. self.add_setting(LogCog.SETTING_EDITS_ENABLED)
  36. self.add_setting(LogCog.SETTING_JOINS_ENABLED)
  37. self.add_setting(LogCog.SETTING_LEAVES_ENABLED)
  38. @commands.group(
  39. brief='Manages event logging',
  40. )
  41. @commands.has_permissions(ban_members=True)
  42. @commands.guild_only()
  43. async def log(self, context: commands.Context):
  44. 'Logging command group'
  45. if context.invoked_subcommand is None:
  46. await context.send_help()
  47. # Events - Channels
  48. @commands.Cog.listener()
  49. async def on_guild_channel_delete(self, channel: GuildChannel) -> None:
  50. pass
  51. @commands.Cog.listener()
  52. async def on_guild_channel_create(self, channel: GuildChannel) -> None:
  53. pass
  54. @commands.Cog.listener()
  55. async def on_guild_channel_update(self, before: GuildChannel, after: GuildChannel) -> None:
  56. pass
  57. # Events - Guilds
  58. @commands.Cog.listener()
  59. async def on_guild_available(self, guild: Guild) -> None:
  60. pass
  61. @commands.Cog.listener()
  62. async def on_guild_unavailable(self, guild: Guild) -> None:
  63. pass
  64. @commands.Cog.listener()
  65. async def on_guild_update(self, before: Guild, after: Guild) -> None:
  66. pass
  67. @commands.Cog.listener()
  68. async def on_guild_emojis_update(self, guild: Guild, before: Sequence[Emoji], after: Sequence[Emoji]) -> None:
  69. pass
  70. @commands.Cog.listener()
  71. async def on_guild_stickers_update(self, guild: Guild, before: Sequence[GuildSticker], after: Sequence[GuildSticker]) -> None:
  72. pass
  73. @commands.Cog.listener()
  74. async def on_invite_create(self, invite: Invite) -> None:
  75. pass
  76. @commands.Cog.listener()
  77. async def on_invite_delete(self, invite: Invite) -> None:
  78. pass
  79. # Events - Members
  80. @commands.Cog.listener()
  81. async def on_member_join(self, member: Member) -> None:
  82. pass
  83. @commands.Cog.listener()
  84. async def on_member_remove(self, member: Member) -> None:
  85. pass
  86. @commands.Cog.listener()
  87. async def on_member_update(self, before: Member, after: Member) -> None:
  88. pass
  89. @commands.Cog.listener()
  90. async def on_user_update(self, before: User, after: User) -> None:
  91. pass
  92. @commands.Cog.listener()
  93. async def on_member_ban(self, user: Union[User, Member]) -> None:
  94. pass
  95. @commands.Cog.listener()
  96. async def on_member_unban(self, guild: Guild, user: Union[User, Member]) -> None:
  97. pass
  98. # Events - Messages
  99. @commands.Cog.listener()
  100. async def on_message(self, message: Message):
  101. # print(f"Saw message {message.id} \"{message.content}\"")
  102. pass
  103. @commands.Cog.listener()
  104. async def on_message_edit(self, before: Message, after: Message) -> None:
  105. text = f'Message {after.jump_url} edited by **{after.author.name}** ({after.author.display_name} {after.author.id}).\n\n' + \
  106. f'Original markdown:\n> {escape_markdown(before.content)}\n\n' + \
  107. f'Updated markdown:\n> {escape_markdown(after.content)}'
  108. bot_message = BotMessage(after.guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
  109. await bot_message.update()
  110. @commands.Cog.listener()
  111. async def on_raw_message_edit(self, payload: RawMessageUpdateEvent) -> None:
  112. if payload.cached_message:
  113. return # already handled by on_message_edit
  114. guild = await self.bot.fetch_guild(payload.guild_id)
  115. if not guild:
  116. return
  117. channel = await guild.fetch_channel(payload.channel_id)
  118. if not channel:
  119. return
  120. message = await channel.fetch_message(payload.message_id)
  121. if not message:
  122. return
  123. text = f'Message {message.jump_url} edited by **{message.author.name}** ({message.author.display_name} {message.author.id}).\n\n' + \
  124. 'Original markdown unavailable in cache.\n\n' + \
  125. f'Updated markdown:\n> {escape_markdown(message.content)}'
  126. bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
  127. await bot_message.update()
  128. @commands.Cog.listener()
  129. async def on_raw_message_delete(self, payload: RawMessageDeleteEvent) -> None:
  130. print('Raw message deleted')
  131. if payload.cached_message:
  132. message = payload.cached_message
  133. text = f'Message by **{message.author.name}** ({message.author.display_name} {message.author.id}) deleted from {message.channel.mention}\n\n' + \
  134. f'Markdown:\n> {escape_markdown(message.content)}'
  135. bot_message = BotMessage(message.guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
  136. await bot_message.update()
  137. else:
  138. print(f'Looking up guild {payload.guild_id}')
  139. guild = await self.bot.fetch_guild(payload.guild_id)
  140. if not guild:
  141. return
  142. print(f'Looking up channel {payload.channel_id}')
  143. channel = await guild.fetch_channel(payload.channel_id)
  144. if not channel:
  145. return
  146. text = f'Message {payload.message_id} deleted in ' + channel.mention + ' but content and author not available in cache.'
  147. bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG, suppress_embeds=True)
  148. await bot_message.update()
  149. @commands.Cog.listener()
  150. async def on_raw_bulk_message_delete(self, payload: RawBulkMessageDeleteEvent) -> None:
  151. guild = await self.bot.fetch_guild(payload.guild_id)
  152. if not guild:
  153. return
  154. channel = await guild.fetch_channel(payload.channel_id)
  155. count = len(payload.message_ids)
  156. cached_count = len(payload.cached_messages)
  157. uncached_count = count - cached_count
  158. text = f'Bulk deletion of {count} message(s) from {channel.mention}.'
  159. if uncached_count == count:
  160. text += f' No cached content available for any of them.'
  161. elif uncached_count > 0:
  162. text += f' No cached content available for {uncached_count} of them.'
  163. bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
  164. await bot_message.update()
  165. for message in payload.cached_messages:
  166. text = f'Message by **{message.author.name}** ({message.author.display_name} {message.author.id}) bulk deleted from {message.channel.mention}\n\n' + \
  167. f'Markdown:\n> {escape_markdown(message.content)}'
  168. bot_message = BotMessage(guild, text, BotMessage.TYPE_LOG)
  169. await bot_message.update()
  170. # Events - Roles
  171. @commands.Cog.listener()
  172. async def on_guild_role_create(self, role: Role) -> None:
  173. pass
  174. @commands.Cog.listener()
  175. async def on_guild_role_delete(self, role: Role) -> None:
  176. pass
  177. @commands.Cog.listener()
  178. async def on_guild_role_update(self, before: Role, after: Role) -> None:
  179. pass
  180. # Events - Threads
  181. @commands.Cog.listener()
  182. async def on_thread_create(self, thread: Thread) -> None:
  183. pass
  184. @commands.Cog.listener()
  185. async def on_thread_update(self, before: Thread, after: Thread) -> None:
  186. pass
  187. @commands.Cog.listener()
  188. async def on_thread_delete(self, thread: Thread) -> None:
  189. pass
  190. # ------------------------------------------------------------------------
  191. def remove_me():
  192. pass
  193. # @commands.Cog.listener()
  194. # async def on_member_join(self, member: Member) -> None:
  195. # 'Event handler'
  196. # guild: Guild = member.guild
  197. # if not self.get_guild_setting(guild, self.SETTING_ENABLED):
  198. # return
  199. # min_count = self.get_guild_setting(guild, self.SETTING_JOIN_COUNT)
  200. # seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
  201. # timespan: timedelta = timedelta(seconds=seconds)
  202. # last_raid: JoinRaidContext = Storage.get_state_value(guild, self.STATE_KEY_LAST_RAID)
  203. # recent_joins: AgeBoundList = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
  204. # if recent_joins is None:
  205. # recent_joins = AgeBoundList(timespan, lambda i, member : member.joined_at)
  206. # Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
  207. # if last_raid:
  208. # if member.joined_at - last_raid.last_join_time() > timespan:
  209. # # Last raid is over
  210. # Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, None)
  211. # recent_joins.append(member)
  212. # return
  213. # # Add join to existing raid
  214. # last_raid.join_members.append(member)
  215. # self.record_warning(member)
  216. # if len(last_raid.banned_members) > 0:
  217. # self.log(guild, f'Banning as part of last join raid: {member.name}')
  218. # await member.ban(
  219. # reason='Rocketbot: Part of join raid.',
  220. # delete_message_days=0)
  221. # last_raid.banned_members.add(member)
  222. # elif len(last_raid.kicked_members) > 0:
  223. # self.log(guild, f'Kicking as part of last join raid: {member.name}')
  224. # await member.kick(
  225. # reason='Rocketbot: Part of join raid.')
  226. # last_raid.kicked_members.add(member)
  227. # await self.__update_warning_message(last_raid)
  228. # else:
  229. # # Add join to the general, non-raid recent join list
  230. # recent_joins.append(member)
  231. # if len(recent_joins) >= min_count:
  232. # self.log(guild, '\u0007Join raid detected')
  233. # last_raid = JoinRaidContext(recent_joins)
  234. # Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, last_raid)
  235. # recent_joins.clear()
  236. # msg = BotMessage(guild,
  237. # text='',
  238. # type=BotMessage.TYPE_MOD_WARNING,
  239. # context=last_raid)
  240. # self.record_warnings(recent_joins)
  241. # last_raid.warning_message_ref = weakref.ref(msg)
  242. # await self.__update_warning_message(last_raid)
  243. # await self.post_message(msg)