Experimental Discord bot written in Python
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

joinraidcog.py 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  1. """
  2. Cog for detecting large numbers of guild joins in a short period of time.
  3. """
  4. import weakref
  5. from datetime import datetime, timedelta
  6. from typing import Optional
  7. from discord import Guild, Member
  8. from discord.ext.commands import Cog
  9. from config import CONFIG
  10. from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
  11. from rocketbot.collections import AgeBoundList
  12. from rocketbot.storage import Storage
  13. class JoinRaidContext:
  14. """
  15. Data about a join raid.
  16. """
  17. def __init__(self, join_members: list[Member]):
  18. self.join_members: list[Member] = list(join_members)
  19. self.kicked_members: set[Member] = set()
  20. self.banned_members: set[Member] = set()
  21. self.warning_message_ref: Optional[weakref.ReferenceType[BotMessage]] = None
  22. def last_join_time(self) -> datetime:
  23. """Returns when the most recent member join was, in UTC"""
  24. return self.join_members[-1].joined_at
  25. class JoinRaidCog(BaseCog, name='Join Raids'):
  26. """
  27. Cog for monitoring member joins and detecting potential bot raids.
  28. """
  29. SETTING_ENABLED = CogSetting(
  30. 'enabled',
  31. bool,
  32. default_value=False,
  33. brief='join raid detection',
  34. description='Whether this module is enabled for a guild.',
  35. )
  36. SETTING_JOIN_COUNT = CogSetting(
  37. 'joincount',
  38. int,
  39. default_value=5,
  40. brief='number of joins to trigger a warning',
  41. description='The number of joins occurring within the time '
  42. 'window to trigger a mod warning.',
  43. min_value=2,
  44. )
  45. SETTING_JOIN_TIME = CogSetting(
  46. 'jointime',
  47. timedelta,
  48. default_value=timedelta(seconds=5),
  49. brief='time window length to look for joins',
  50. description='The number of seconds of join history to look '
  51. 'at when counting recent joins. If joincount or more '
  52. 'joins occur within jointime seconds a mod warning is issued.',
  53. min_value=timedelta(seconds=1.0),
  54. max_value=timedelta(seconds=900.0),
  55. )
  56. STATE_KEY_RECENT_JOINS = "JoinRaidCog.recent_joins"
  57. STATE_KEY_LAST_RAID = "JoinRaidCog.last_raid"
  58. def __init__(self, bot):
  59. super().__init__(
  60. bot,
  61. config_prefix='joinraid',
  62. short_description='Manages join raid detection and handling.',
  63. long_description='Join raids consist of an unusual number of users joining in '
  64. 'a short period of time and have shown a pattern of DMing '
  65. 'members with spam or scams.'
  66. )
  67. self.add_setting(JoinRaidCog.SETTING_ENABLED)
  68. self.add_setting(JoinRaidCog.SETTING_JOIN_COUNT)
  69. self.add_setting(JoinRaidCog.SETTING_JOIN_TIME)
  70. async def on_mod_react(self,
  71. bot_message: BotMessage,
  72. reaction: BotMessageReaction,
  73. reacted_by: Member) -> None:
  74. guild: Guild = bot_message.guild
  75. raid: JoinRaidContext = bot_message.context
  76. if reaction.emoji == CONFIG['kick_emoji']:
  77. to_kick: set[Member] = set(raid.join_members) - raid.kicked_members
  78. for member in to_kick:
  79. await member.kick(
  80. reason=f'Rocketbot: Part of join raid. Kicked by {reacted_by.name}.')
  81. raid.kicked_members |= to_kick
  82. await self.__update_warning_message(raid)
  83. self.log(guild, f'Join raid users kicked by {reacted_by.name}.')
  84. elif reaction.emoji == CONFIG['ban_emoji']:
  85. to_ban = set(raid.join_members) - raid.banned_members
  86. for member in to_ban:
  87. await member.ban(
  88. reason=f'Rocketbot: Part of join raid. Banned by {reacted_by.name}.',
  89. delete_message_days=0)
  90. raid.banned_members |= to_ban
  91. await self.__update_warning_message(raid)
  92. self.log(guild, f'Join raid users banned by {reacted_by.name}')
  93. @Cog.listener()
  94. async def on_member_join(self, member: Member) -> None:
  95. """Event handler"""
  96. guild: Guild = member.guild
  97. if not self.get_guild_setting(guild, self.SETTING_ENABLED):
  98. return
  99. min_count = self.get_guild_setting(guild, self.SETTING_JOIN_COUNT)
  100. seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
  101. timespan: timedelta = timedelta(seconds=seconds)
  102. last_raid: JoinRaidContext = Storage.get_state_value(guild, self.STATE_KEY_LAST_RAID)
  103. recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
  104. if recent_joins is None:
  105. recent_joins = AgeBoundList(timespan, lambda i, member : member.joined_at)
  106. Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
  107. if last_raid:
  108. if member.joined_at - last_raid.last_join_time() > timespan:
  109. # Last raid is over
  110. Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, None)
  111. recent_joins.append(member)
  112. return
  113. # Add join to existing raid
  114. last_raid.join_members.append(member)
  115. self.record_warning(member)
  116. if len(last_raid.banned_members) > 0:
  117. self.log(guild, f'Banning as part of last join raid: {member.name}')
  118. await member.ban(
  119. reason='Rocketbot: Part of join raid.',
  120. delete_message_days=0)
  121. last_raid.banned_members.add(member)
  122. elif len(last_raid.kicked_members) > 0:
  123. self.log(guild, f'Kicking as part of last join raid: {member.name}')
  124. await member.kick(
  125. reason='Rocketbot: Part of join raid.')
  126. last_raid.kicked_members.add(member)
  127. await self.__update_warning_message(last_raid)
  128. else:
  129. # Add join to the general, non-raid recent join list
  130. recent_joins.append(member)
  131. if len(recent_joins) >= min_count:
  132. self.log(guild, '\u0007Join raid detected')
  133. last_raid = JoinRaidContext(recent_joins)
  134. Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, last_raid)
  135. recent_joins.clear()
  136. msg = BotMessage(guild,
  137. text='',
  138. type=BotMessage.TYPE_MOD_WARNING,
  139. context=last_raid)
  140. self.record_warnings(recent_joins)
  141. last_raid.warning_message_ref = weakref.ref(msg)
  142. await self.__update_warning_message(last_raid)
  143. await self.post_message(msg)
  144. async def on_setting_updated(self, guild: Guild, setting: CogSetting) -> None:
  145. if setting is self.SETTING_JOIN_TIME:
  146. seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
  147. timespan: timedelta = timedelta(seconds=seconds)
  148. recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild,
  149. self.STATE_KEY_RECENT_JOINS)
  150. if recent_joins:
  151. recent_joins.max_age = timespan
  152. recent_joins.purge_old_elements()
  153. async def __update_warning_message(self, context: JoinRaidContext) -> None:
  154. if context.warning_message_ref is None:
  155. return
  156. bot_message = context.warning_message_ref()
  157. if bot_message is None:
  158. return
  159. text = 'JOIN RAID DETECTED\n\n' + \
  160. 'The following members joined in close succession:\n'
  161. max_members: int = CONFIG['max_members_per_message']
  162. for member in context.join_members[:max_members]:
  163. text += '\n• '
  164. if member in context.banned_members:
  165. text += f'~~{member.mention} ({member.id})~~ - banned'
  166. elif member in context.kicked_members:
  167. text += f'~~{member.mention} ({member.id})~~ - kicked'
  168. else:
  169. text += f'{member.mention} ({member.id})'
  170. if len(context.join_members) > max_members:
  171. text += f'\n• {len(context.join_members) - max_members} more'
  172. text += '\n_(list updates automatically)_'
  173. await bot_message.set_text(text)
  174. member_count = len(context.join_members)
  175. kick_count = len(context.kicked_members)
  176. ban_count = len(context.banned_members)
  177. await bot_message.set_reactions(BotMessageReaction.standard_set(
  178. did_kick=kick_count >= member_count,
  179. did_ban=ban_count >= member_count,
  180. user_count=member_count))