Experimental Discord bot written in Python
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

joinagecog.py 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. import weakref
  2. from datetime import datetime, timedelta, timezone
  3. from typing import Optional
  4. from discord import Guild, Member, Interaction
  5. from discord.app_commands import Group, guild_only, default_permissions, Transform
  6. from discord.ext.commands import Cog
  7. from config import CONFIG
  8. from rocketbot.bot import Rocketbot
  9. from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
  10. from rocketbot.collections import AgeBoundList
  11. from rocketbot.storage import Storage
  12. from rocketbot.utils import TimeDeltaTransformer, MOD_PERMISSIONS
  13. class JoinAgeQueryContext:
  14. """
  15. Data about a join age query
  16. """
  17. def __init__(self, join_members: list[Member], timespan: timedelta):
  18. self.join_members = list(join_members)
  19. self.timespan: timedelta = timespan
  20. self.kicked_members: set[Member] = set()
  21. self.banned_members: set[Member] = set()
  22. self.results_message_ref: Optional[weakref.ReferenceType[BotMessage]] = None
  23. class JoinAgeCog(BaseCog, name='Join Age'):
  24. """
  25. Cog for finding users by when they joined.
  26. """
  27. SETTING_ENABLED = CogSetting('enabled', bool,
  28. brief='join age',
  29. description='Whether this cog is enabled for a guild.')
  30. SETTING_JOIN_TIME = CogSetting('jointime', timedelta,
  31. brief='maximum length of time to track new joins',
  32. description='The number of seconds of join history to maintain.',
  33. usage='<seconds:float>',
  34. min_value=1.0)
  35. STATE_KEY_RECENT_JOINS = "JoinAgeCog.recent_joins"
  36. def __init__(self, bot: Rocketbot):
  37. super().__init__(
  38. bot,
  39. config_prefix='joinage',
  40. name='join age',
  41. short_description='Tracks recently joined users with options to mass kick or ban.',
  42. )
  43. self.add_setting(JoinAgeCog.SETTING_ENABLED)
  44. self.add_setting(JoinAgeCog.SETTING_JOIN_TIME)
  45. joinage = Group(
  46. name='joinage',
  47. description='Queries for users who joined in the past span of time',
  48. guild_only=True,
  49. default_permissions=MOD_PERMISSIONS,
  50. extras={
  51. 'long_description': 'Searches for users who joined the server recently. ' + \
  52. 'Can use time spans like 30s, 5m, 1h, 7d, etc.',
  53. 'usage': '<time_period>',
  54. }
  55. )
  56. @joinage.command(
  57. name='search',
  58. description='Searches for users who joined recently',
  59. )
  60. @guild_only()
  61. @default_permissions(ban_members=True)
  62. async def search(self, interaction: Interaction, timespan: Transform[timedelta, TimeDeltaTransformer]) -> None:
  63. """Command handler"""
  64. guild: Guild = interaction.guild
  65. recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
  66. if recent_joins is None:
  67. max_age: timedelta = timedelta(seconds=self.get_guild_setting(guild, self.SETTING_JOIN_TIME))
  68. recent_joins = AgeBoundList(max_age, lambda i, member0 : member0.joined_at)
  69. Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
  70. results: list = []
  71. cutoff: datetime = datetime.now(timezone.utc) - timespan
  72. for member in recent_joins:
  73. if member.joined_at > cutoff:
  74. results.append(member)
  75. ctx = JoinAgeQueryContext(results, timespan)
  76. msg = BotMessage(guild,
  77. text='',
  78. type=BotMessage.TYPE_INFO,
  79. context=ctx)
  80. ctx.results_message_ref = weakref.ref(msg)
  81. await self.__update_results_message(ctx)
  82. await self.post_message(msg)
  83. await interaction.response.send_message(
  84. "Search started",
  85. ephemeral=True,
  86. )
  87. async def on_mod_react(self,
  88. bot_message: BotMessage,
  89. reaction: BotMessageReaction,
  90. reacted_by: Member) -> None:
  91. guild: Guild = bot_message.guild
  92. ctx: JoinAgeQueryContext = bot_message.context
  93. if reaction.emoji == CONFIG['kick_emoji']:
  94. to_kick = set(ctx.join_members) - ctx.kicked_members
  95. for member in to_kick:
  96. await member.kick(
  97. reason=f'Rocketbot: Mass kick based on join age, by {reacted_by.name}.')
  98. ctx.kicked_members |= to_kick
  99. await self.__update_results_message(ctx)
  100. self.log(guild, f'Users kicked by {reacted_by.name}.')
  101. elif reaction.emoji == CONFIG['ban_emoji']:
  102. to_ban = set(ctx.join_members) - ctx.banned_members
  103. for member in to_ban:
  104. await member.ban(
  105. reason=f'Rocketbot: Mass ban based on join age, by {reacted_by.name}.',
  106. delete_message_days=0)
  107. ctx.banned_members |= to_ban
  108. await self.__update_results_message(ctx)
  109. self.log(guild, f'Users banned by {reacted_by.name}')
  110. @Cog.listener()
  111. async def on_member_join(self, member: Member) -> None:
  112. """Event handler"""
  113. guild: Guild = member.guild
  114. if not self.get_guild_setting(guild, self.SETTING_ENABLED):
  115. return
  116. recent_joins: AgeBoundList[Member, datetime, timedelta] = Storage.get_state_value(guild, self.STATE_KEY_RECENT_JOINS)
  117. if recent_joins is None:
  118. max_age: timedelta = timedelta(seconds=self.get_guild_setting(guild, self.SETTING_JOIN_TIME))
  119. recent_joins = AgeBoundList(max_age, lambda i, member : member.joined_at)
  120. Storage.set_state_value(guild, self.STATE_KEY_RECENT_JOINS, recent_joins)
  121. recent_joins.append(member)
  122. async def __update_results_message(self, context: JoinAgeQueryContext) -> None:
  123. if context.results_message_ref is None:
  124. return
  125. bot_message = context.results_message_ref()
  126. if bot_message is None:
  127. return
  128. text = f'The following members joined in the last {context.timespan}\n\n'
  129. if len(context.join_members) > 0:
  130. max_members = CONFIG['max_members_per_message']
  131. for member in context.join_members[:max_members]:
  132. text += '\n• '
  133. if member in context.banned_members:
  134. text += f'~~{member.mention} ({member.id})~~ - banned'
  135. elif member in context.kicked_members:
  136. text += f'~~{member.mention} ({member.id})~~ - kicked'
  137. else:
  138. text += f'{member.mention} ({member.id})'
  139. if len(context.join_members) > max_members:
  140. text += f'\n• {len(context.join_members) - max_members} more'
  141. else:
  142. text += 'No members found. If the bot was recently restarted ' + \
  143. 'or JoinAgeCog just enabled, only new joins will be tracked.'
  144. await bot_message.set_text(text)
  145. if len(context.join_members) > 0:
  146. member_count = len(context.join_members)
  147. kick_count = len(context.kicked_members)
  148. ban_count = len(context.banned_members)
  149. await bot_message.set_reactions(BotMessageReaction.standard_set(
  150. did_kick=kick_count >= member_count,
  151. did_ban=ban_count >= member_count,
  152. user_count=member_count))