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

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