Experimental Discord bot written in Python
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

autokickcog.py 5.7KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. from datetime import datetime, timedelta
  2. from typing import cast
  3. from discord import Guild, Member, Status
  4. from discord.ext import commands, tasks
  5. from discord.ext.tasks import Loop
  6. from config import CONFIG
  7. from rocketbot.cogs.basecog import BaseCog, BotMessage, CogSetting
  8. from rocketbot.collections import AgeBoundDict
  9. from rocketbot.storage import Storage
  10. from rocketbot.utils import bot_log
  11. class AutoKickContext:
  12. """
  13. Data about a join raid.
  14. """
  15. def __init__(self, member: Member, first_kick: datetime):
  16. self.member = member
  17. self.first_kick = first_kick
  18. self.last_kick = first_kick
  19. self.kick_count = 1
  20. def record_kick(self, time: datetime):
  21. self.last_kick = time
  22. self.kick_count += 1
  23. class StatusCheckContext:
  24. def __init__(self, member: Member):
  25. self.member = member
  26. self.joined_at = datetime.now()
  27. class AutoKickCog(BaseCog, name='Auto Kick'):
  28. """
  29. Cog for automatically kicking ALL new joins. For temporary use during join raids.
  30. """
  31. SETTING_ENABLED = CogSetting('enabled', bool,
  32. brief='autokick',
  33. description='Whether this cog is enabled for a guild.')
  34. SETTING_BAN_COUNT = CogSetting('bancount', int,
  35. brief='number of repeat kicks before a ban',
  36. description='The number of times a user can join and be kicked ' + \
  37. 'before the next rejoin will result in a ban. A value of 0 ' + \
  38. 'disables this feature (only kick, never ban).',
  39. usage='<count:int>',
  40. min_value=0)
  41. SETTING_OFFLINE_ONLY = CogSetting('offlineonly', bool,
  42. brief='whether to only kick users whose status is offline',
  43. description='Compromised accounts may have a status of offline. ' + \
  44. 'If this setting is enabled, the user\'s status will be ' + \
  45. 'checked a few seconds after joining. If it is offline ' + \
  46. 'they will be kicked.',
  47. usage='<true|false>')
  48. STATE_KEY_RECENT_KICKS = "AutoKickCog.recent_joins"
  49. def __init__(self, bot):
  50. super().__init__(bot)
  51. self.add_setting(AutoKickCog.SETTING_ENABLED)
  52. self.add_setting(AutoKickCog.SETTING_BAN_COUNT)
  53. self.add_setting(AutoKickCog.SETTING_OFFLINE_ONLY)
  54. self.status_check_members = []
  55. timer: Loop = cast(Loop, self.status_check_timer)
  56. timer.start()
  57. @commands.group(
  58. brief='Automatically kicks all new users as soon as they join',
  59. )
  60. @commands.has_permissions(ban_members=True)
  61. @commands.guild_only()
  62. async def autokick(self, context: commands.Context):
  63. 'Auto-kick'
  64. if context.invoked_subcommand is None:
  65. await context.send_help()
  66. @commands.Cog.listener()
  67. async def on_member_join(self, member: Member) -> None:
  68. 'Event handler'
  69. guild: Guild = member.guild
  70. if not self.get_guild_setting(guild, self.SETTING_ENABLED):
  71. return
  72. if self.get_guild_setting(guild, self.SETTING_OFFLINE_ONLY):
  73. self.log(guild, f'New member {member.name} status is {member.status}')
  74. self.status_check_members.append(StatusCheckContext(member))
  75. return
  76. self.__kick_or_ban_if_needed(member)
  77. @tasks.loop(seconds=5.0)
  78. async def status_check_timer(self):
  79. 'Checks status of new members shortly after joining to see if they go offline'
  80. contexts = self.status_check_members.copy()
  81. self.status_check_members = []
  82. now = datetime.now()
  83. # bot_log(guild=None, cog_class=None, message=f'Found {len(contexts)} members to check')
  84. for c in contexts:
  85. context: StatusCheckContext = c
  86. member: Member = context.member
  87. guild: Guild = member.guild
  88. if now - context.joined_at < timedelta(seconds=5.0):
  89. # Too soon, check again later
  90. self.status_check_members.append(context)
  91. continue
  92. if member.status != Status.offline:
  93. # Online, ignore
  94. self.log(guild, f'{member.name} status is {member.status}. Not kicking.')
  95. continue
  96. self.log(guild, f'{member.name} went offline 5s later')
  97. await self.__kick_or_ban_if_needed(member)
  98. async def __kick_or_ban_if_needed(self, member: Member):
  99. guild: Guild = member.guild
  100. recent_kicks: AgeBoundDict = Storage.get_state_value(guild, AutoKickCog.STATE_KEY_RECENT_KICKS)
  101. if recent_kicks is None:
  102. recent_kicks = AgeBoundDict(timedelta(seconds=3600), lambda i, context : context.last_kick)
  103. Storage.set_state_value(guild, self.STATE_KEY_RECENT_KICKS, recent_kicks)
  104. context: AutoKickContext = recent_kicks.get(member.id)
  105. if context is None:
  106. context = AutoKickContext(member, datetime.now())
  107. recent_kicks[member.id] = context
  108. else:
  109. context.record_kick(datetime.now())
  110. max_kick_count: int = self.get_guild_setting(guild, self.SETTING_BAN_COUNT)
  111. disable_help = f'To disable this feature: `{CONFIG["command_prefix"]}autokick disable`.'
  112. ban_help = f'To configure ban threshold: `{CONFIG["command_prefix"]}autokick ' + \
  113. 'setbancount #` (0 to disable).'
  114. if max_kick_count > 0 and context.kick_count > max_kick_count:
  115. await member.ban(reason=f'Rocketbot: Ban after {context.kick_count} joins',
  116. delete_message_days=0)
  117. msg = BotMessage(guild,
  118. text=f'Banned {member.mention} ({member.id}) after {context.kick_count} joins. ' + \
  119. disable_help + ' ' + ban_help,
  120. type=BotMessage.TYPE_INFO,
  121. context=None)
  122. await self.post_message(msg)
  123. self.log(guild, f'Banned {member.name} after {context.kick_count} joins')
  124. else:
  125. await member.kick(reason='Rocketbot: Autokick enabled.')
  126. msg = BotMessage(guild,
  127. text=f'Autokicked {member.mention} ({member.id}) ' + \
  128. f'({AutoKickCog.ordinal(context.kick_count)} time). ' + \
  129. disable_help + ' ' + ban_help,
  130. type=BotMessage.TYPE_INFO,
  131. context=None)
  132. await self.post_message(msg)
  133. self.log(guild, f'Autokicked {member.name} ' + \
  134. f'({AutoKickCog.ordinal(context.kick_count)} time)')
  135. @staticmethod
  136. def ordinal(val: int):
  137. 'Formats an integer with an ordinal suffix (English only)'
  138. if val % 10 == 1:
  139. return f'{val}st'
  140. if val % 10 == 2:
  141. return f'{val}nd'
  142. if val % 10 == 3:
  143. return f'{val}rd'
  144. return f'{val}th'