Experimental Discord bot written in Python
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

joinraidcog.py 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. from datetime import datetime, timedelta
  2. from discord import Guild, Member
  3. from discord.ext import commands
  4. import weakref
  5. from cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
  6. from config import CONFIG
  7. from rscollections import AgeBoundList
  8. from storage import Storage
  9. class JoinRaidContext:
  10. def __init__(self, join_members: list):
  11. self.join_members = list(join_members)
  12. self.kicked_members = set()
  13. self.banned_members = set()
  14. self.warning_message_ref = None
  15. def last_join_time(self) -> datetime:
  16. return self.join_members[-1].joined_at
  17. class JoinRaidCog(BaseCog):
  18. """
  19. Cog for monitoring member joins and detecting potential bot raids.
  20. """
  21. SETTING_ENABLED = CogSetting('enabled', bool,
  22. brief='join raid detection',
  23. description='Whether this cog is enabled for a guild.')
  24. SETTING_JOIN_COUNT = CogSetting('joincount', int,
  25. brief='number of joins to trigger a warning',
  26. description='The number of joins occuring within the time ' + \
  27. 'window to trigger a mod warning.',
  28. usage='<count:int>',
  29. min_value=2)
  30. SETTING_JOIN_TIME = CogSetting('jointime', float,
  31. brief='time window length to look for joins',
  32. description='The number of seconds of join history to look ' + \
  33. 'at when counting recent joins. If joincount or more ' + \
  34. 'joins occur within jointime seconds a mod warning is issued.',
  35. usage='<seconds:float>',
  36. min_value=1.0,
  37. max_value=900.0)
  38. STATE_KEY_RECENT_JOINS = "JoinRaidCog.recent_joins"
  39. STATE_KEY_LAST_RAID = "JoinRaidCog.last_raid"
  40. def __init__(self, bot):
  41. super().__init__(bot)
  42. self.add_setting(JoinRaidCog.SETTING_ENABLED)
  43. self.add_setting(JoinRaidCog.SETTING_JOIN_COUNT)
  44. self.add_setting(JoinRaidCog.SETTING_JOIN_TIME)
  45. @commands.group(
  46. brief='Manages join raid detection and handling',
  47. )
  48. @commands.has_permissions(ban_members=True)
  49. @commands.guild_only()
  50. async def joinraid(self, context: commands.Context):
  51. 'Command group'
  52. if context.invoked_subcommand is None:
  53. await context.send_help()
  54. async def on_mod_react(self,
  55. bot_message: BotMessage,
  56. reaction: BotMessageReaction,
  57. reacted_by: Member) -> None:
  58. guild: Guild = bot_message.guild
  59. raid: JoinRaidRecord = bot_message.context
  60. if reaction.emoji == CONFIG['kick_emoji']:
  61. to_kick = set(raid.join_members) - raid.kicked_members
  62. for member in to_kick:
  63. await member.kick(
  64. reason=f'Rocketbot: Part of join raid. Kicked by {reacted_by.name}.')
  65. raid.kicked_members |= to_kick
  66. await self.__update_warning_message(raid)
  67. self.log(guild, f'Join raid users kicked by {reacted_by.name}.')
  68. elif reaction.emoji == CONFIG['ban_emoji']:
  69. to_ban = set(raid.join_members) - raid.banned_members
  70. for member in to_ban:
  71. await member.ban(
  72. reason=f'Rocketbot: Part of join raid. Banned by {reacted_by.name}.',
  73. delete_message_days=0)
  74. raid.banned_members |= to_ban
  75. await self.__update_warning_message(raid)
  76. self.log(guild, f'Join raid users banned by {reacted_by.name}')
  77. @commands.Cog.listener()
  78. async def on_member_join(self, member: Member) -> None:
  79. 'Event handler'
  80. guild: Guild = member.guild
  81. if not self.get_guild_setting(guild, self.SETTING_ENABLED):
  82. return
  83. min_count = self.get_guild_setting(guild, self.SETTING_JOIN_COUNT)
  84. seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
  85. timespan: timedelta = timedelta(seconds=seconds)
  86. last_raid: JoinRaidContext = Storage.get_state_value(guild, self.STATE_KEY_LAST_RAID)
  87. recent_joins: AgeBoundList = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
  88. if recent_joins is None:
  89. recent_joins = AgeBoundList(timespan, lambda i, member : member.joined_at)
  90. Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
  91. if last_raid:
  92. if member.joined_at - last_raid.last_join_time() > timespan:
  93. # Last raid is over
  94. Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, None)
  95. recent_joins.append(member)
  96. return
  97. # Add join to existing raid
  98. last_raid.join_members.append(member)
  99. await self.__update_warning_message(last_raid)
  100. else:
  101. # Add join to the general, non-raid recent join list
  102. recent_joins.append(member)
  103. if len(recent_joins) >= min_count:
  104. self.log(guild, '\u0007Join raid detected')
  105. last_raid = JoinRaidContext(recent_joins)
  106. Storage.set_state_value(guild, self.STATE_KEY_LAST_RAID, last_raid)
  107. recent_joins.clear()
  108. msg = BotMessage(guild,
  109. text='',
  110. type=BotMessage.TYPE_MOD_WARNING,
  111. context=last_raid)
  112. last_raid.warning_message_ref = weakref.ref(msg)
  113. await self.__update_warning_message(last_raid)
  114. await self.post_message(msg)
  115. async def on_setting_updated(self, guild: Guild, setting: CogSetting) -> None:
  116. if setting is self.SETTING_JOIN_TIME:
  117. seconds = self.get_guild_setting(guild, self.SETTING_JOIN_TIME)
  118. timespan: timedelta = timedelta(seconds=seconds)
  119. recent_joins: AgeBoundList = Storage.get_state_value(guild,
  120. self.STATE_KEY_RECENT_JOINS)
  121. if recent_joins:
  122. recent_joins.max_age = timespan
  123. recent_joins.purge_old_elements()
  124. async def __update_warning_message(self, context: JoinRaidContext) -> None:
  125. if context.warning_message_ref is None:
  126. return
  127. bot_message = context.warning_message_ref()
  128. if bot_message is None:
  129. return
  130. text = 'JOIN RAID DETECTED\n\n' + \
  131. 'The following members joined in close succession:\n'
  132. for member in context.join_members:
  133. text += '\n• '
  134. if member in context.banned_members:
  135. text += f'~~{member.mention} ({member.id})~~ - banned'
  136. elif member in context.kicked_members:
  137. text += f'~~{member.mention} ({member.id})~~ - kicked'
  138. else:
  139. text += f'{member.mention} ({member.id})'
  140. text += '\n_(list updates automatically)_'
  141. await bot_message.set_text(text)
  142. member_count = len(context.join_members)
  143. kick_count = len(context.kicked_members)
  144. ban_count = len(context.banned_members)
  145. await bot_message.set_reactions(BotMessageReaction.standard_set(
  146. did_kick=kick_count >= member_count,
  147. did_ban=ban_count >= member_count,
  148. user_count=member_count))