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.

joinraid.py 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from discord import Guild, Intents, Member, Message, PartialEmoji, RawReactionActionEvent
  2. from discord.ext import commands
  3. from storage import Storage
  4. class JoinRecord:
  5. """
  6. Data object containing details about a guild join event.
  7. """
  8. def __init__(self, member: Member):
  9. self.member = member
  10. self.join_time = member.joined_at or datetime.now()
  11. self.is_kicked = False
  12. self.is_banned = False
  13. def age_seconds(self, now: datetime) -> float:
  14. """
  15. Returns the age of this join in seconds from the given "now" time.
  16. """
  17. a = now - self.join_time
  18. return float(a.total_seconds())
  19. class RaidPhase:
  20. """
  21. Enum of phases in a JoinRaid. Phases progress monotonically.
  22. """
  23. NONE = 0
  24. JUST_STARTED = 1
  25. CONTINUING = 2
  26. ENDED = 3
  27. class JoinRaid:
  28. """
  29. Tracks recent joins to a guild to detect join raids, where a large number of automated users
  30. all join at the same time.
  31. """
  32. def __init__(self):
  33. self.joins = []
  34. self.phase = RaidPhase.NONE
  35. # datetime when the raid started, or None.
  36. self.raid_start_time = None
  37. # Message posted to Discord to warn of the raid. Convenience property managed
  38. # by caller. Ignored by this class.
  39. self.warning_message = None
  40. def handle_join(self,
  41. member: Member,
  42. now: datetime,
  43. max_age_seconds: float,
  44. max_join_count: int) -> None:
  45. """
  46. Processes a new member join to a guild and detects join raids. Updates
  47. self.phase and self.raid_start_time properties.
  48. """
  49. # Check for existing record for this user
  50. print(f'handle_join({member.name}) start')
  51. join: JoinRecord = None
  52. i: int = 0
  53. while i < len(self.joins):
  54. elem = self.joins[i]
  55. if elem.member.id == member.id:
  56. print(f'Member {member.name} already in join list at index {i}. Removing.')
  57. join = self.joins.pop(i)
  58. join.join_time = now
  59. break
  60. i += 1
  61. # Add new record to end
  62. self.joins.append(join or JoinRecord(member))
  63. # Check raid status and do upkeep
  64. self.__process_joins(now, max_age_seconds, max_join_count)
  65. print(f'handle_join({member.name}) end')
  66. def __process_joins(self,
  67. now: datetime,
  68. max_age_seconds: float,
  69. max_join_count: int) -> None:
  70. """
  71. Processes self.joins after each addition, detects raids, updates self.phase,
  72. and throws out unneeded records.
  73. """
  74. print('__process_joins {')
  75. i: int = 0
  76. recent_count: int = 0
  77. should_cull: bool = self.phase == RaidPhase.NONE
  78. while i < len(self.joins):
  79. join: JoinRecord = self.joins[i]
  80. age: float = join.age_seconds(now)
  81. is_old: bool = age > max_age_seconds
  82. if not is_old:
  83. recent_count += 1
  84. print(f'- {i}. {join.member.name} is {age}s old - recent_count={recent_count}')
  85. if is_old and should_cull:
  86. self.joins.pop(i)
  87. print(f'- {i}. {join.member.name} is {age}s old - too old, removing')
  88. else:
  89. print(f'- {i}. {join.member.name} is {age}s old - moving on to next')
  90. i += 1
  91. is_raid = recent_count > max_join_count
  92. print(f'- is_raid {is_raid}')
  93. if is_raid:
  94. if self.phase == RaidPhase.NONE:
  95. self.phase = RaidPhase.JUST_STARTED
  96. self.raid_start_time = now
  97. print('- Phase moved to JUST_STARTED. Recording raid start time.')
  98. elif self.phase == RaidPhase.JUST_STARTED:
  99. self.phase = RaidPhase.CONTINUING
  100. print('- Phase moved to CONTINUING.')
  101. elif self.phase == self.phase in (RaidPhase.JUST_STARTED, RaidPhase.CONTINUING):
  102. self.phase = RaidPhase.ENDED
  103. print('- Phase moved to ENDED.')
  104. # Undo join add if the raid is over
  105. if self.phase == RaidPhase.ENDED and len(self.joins) > 0:
  106. last = self.joins.pop(-1)
  107. print(f'- Popping last join for {last.member.name}')
  108. print('} __process_joins')
  109. async def kick_all(self,
  110. reason: str = "Part of join raid") -> list[Member]:
  111. """
  112. Kicks all users in this join raid. Skips users who have already been
  113. flagged as having been kicked or banned. Returns a List of Members
  114. who were newly kicked.
  115. """
  116. kicks = []
  117. for join in self.joins:
  118. if join.is_kicked or join.is_banned:
  119. continue
  120. await join.member.kick(reason=reason)
  121. join.is_kicked = True
  122. kicks.append(join.member)
  123. self.phase = RaidPhase.ENDED
  124. return kicks
  125. async def ban_all(self,
  126. reason: str = "Part of join raid",
  127. delete_message_days: int = 0) -> list[Member]:
  128. """
  129. Bans all users in this join raid. Skips users who have already been
  130. flagged as having been banned. Users who were previously kicked can
  131. still be banned. Returns a List of Members who were newly banned.
  132. """
  133. bans = []
  134. for join in self.joins:
  135. if join.is_banned:
  136. continue
  137. await join.member.ban(reason=reason, delete_message_days=delete_message_days)
  138. join.is_banned = True
  139. bans.append(join.member)
  140. self.phase = RaidPhase.ENDED
  141. return bans
  142. class JoinRaidCog(commands.Cog):
  143. """
  144. Cog for monitoring member joins and detecting potential bot raids.
  145. """
  146. def __init__(self, bot):
  147. self.bot = bot
  148. @commands.group(
  149. brief='Manages join raid detection and handling',
  150. )
  151. @commands.has_permissions(ban_members=True)
  152. @commands.guild_only()
  153. async def joinraid(self, context: commands.Context):
  154. 'Command group'
  155. if context.invoked_subcommand is None:
  156. await context.send_help()
  157. @joinraid.command(
  158. name='enable',
  159. brief='Enables join raid detection',
  160. description='Join raid detection is off by default.',
  161. )
  162. async def joinraid_enable(self, context: commands.Context):
  163. 'Command handler'
  164. # TODO
  165. pass
  166. @joinraid.command(
  167. name='disable',
  168. brief='Disables join raid detection',
  169. description='Join raid detection is off by default.',
  170. )
  171. async def joinraid_disable(self, context: commands.Context):
  172. 'Command handler'
  173. # TODO
  174. pass
  175. @joinraid.command(
  176. name='setrate',
  177. brief='Sets the rate of joins which triggers a warning to mods',
  178. description='Each time a member joins, the join records from the ' +
  179. 'previous _x_ seconds are counted up, where _x_ is the number of ' +
  180. 'seconds configured by this command. If that count meets or ' +
  181. 'exceeds the maximum join count configured by this command then ' +
  182. 'a raid is detected and a warning is issued to the mods.',
  183. usage='<join_count> <seconds>',
  184. )
  185. async def joinraid_setrate(self, context: commands.Context,
  186. joinCount: int,
  187. seconds: int):
  188. 'Command handler'
  189. # TODO
  190. pass
  191. @joinraid.command(
  192. name='getrate',
  193. brief='Shows the rate of joins which triggers a warning to mods',
  194. )
  195. async def joinraid_getrate(self, context: commands.Context):
  196. 'Command handler'
  197. # TODO
  198. pass
  199. @commands.Cog.listener()
  200. async def on_raw_reaction_add(self, payload: RawReactionActionEvent):
  201. 'Event handler'
  202. # TODO
  203. pass
  204. @commands.Cog.listener()
  205. async def on_member_join(self, member: Member) -> None:
  206. 'Event handler'
  207. # TODO
  208. pass
  209. async def __send_warning(self) -> None:
  210. config = self.bot.get_cog('ConfigCog')
  211. if config is None:
  212. return
  213. config.