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.

usernamecog.py 6.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. """
  2. Cog for detecting username patterns.
  3. """
  4. from typing import Optional
  5. from discord import Guild, Member, Permissions, Interaction
  6. from discord.app_commands import Group
  7. from discord.ext.commands import Cog
  8. from config import CONFIG
  9. from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
  10. from rocketbot.storage import Storage
  11. class UsernamePatternContext:
  12. """
  13. BotMessage context for a flagged username
  14. """
  15. def __init__(self, member: Member) -> None:
  16. self.member: Member = member
  17. self.kicked_by: Optional[Member] = None
  18. self.banned_by: Optional[Member] = None
  19. self.ignored_by: Optional[Member] = None
  20. def reactions(self) -> list[BotMessageReaction]:
  21. """
  22. Generates updated BotMessageReactions based on context state.
  23. """
  24. r: list[BotMessageReaction] = []
  25. if self.ignored_by:
  26. r.append(BotMessageReaction(CONFIG['ignore_emoji'], False, f'Ignored by {self.ignored_by.name}'))
  27. elif self.banned_by:
  28. r.append(BotMessageReaction(CONFIG['ban_emoji'], False, f'Banned by {self.banned_by.name}'))
  29. elif self.kicked_by:
  30. r.append(BotMessageReaction(CONFIG['kick_emoji'], False, f'Kicked by {self.kicked_by.name}'))
  31. r.append(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user'))
  32. else:
  33. r.append(BotMessageReaction(CONFIG['kick_emoji'], True, 'Kick user'))
  34. r.append(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user'))
  35. r.append(BotMessageReaction(CONFIG['ignore_emoji'], True, 'Ignore warning'))
  36. return r
  37. class UsernamePatternCog(BaseCog, name='Username Pattern'):
  38. """
  39. Detects usernames that match certain flagged patterns. Posts a mod warning
  40. message on a match.
  41. """
  42. SETTING_ENABLED = CogSetting('enabled', bool,
  43. brief='username pattern detection',
  44. description='Whether new users are checked for common patterns.')
  45. SETTING_PATTERNS = CogSetting('patterns', None)
  46. def __init__(self, bot):
  47. super().__init__(
  48. bot,
  49. config_prefix='username',
  50. name='username patterns',
  51. short_description='Manages username pattern detection.',
  52. )
  53. self.add_setting(UsernamePatternCog.SETTING_ENABLED)
  54. def __get_patterns(self, guild: Guild) -> list[str]:
  55. """
  56. Returns an array of username patterns.
  57. """
  58. patterns: list[str] = self.get_guild_setting(guild, self.SETTING_PATTERNS)
  59. if patterns is None:
  60. patterns = []
  61. Storage.set_config_value(guild, 'UsernamePatternCog.patterns', patterns)
  62. return patterns
  63. @classmethod
  64. def __save_patterns(cls,
  65. guild: Guild,
  66. patterns: list[str]) -> None:
  67. """
  68. Saves username pattern array.
  69. """
  70. cls.set_guild_setting(guild, cls.SETTING_PATTERNS, patterns)
  71. username = Group(
  72. name='username',
  73. description='Manages username pattern detection.',
  74. guild_only=True,
  75. default_permissions=Permissions(Permissions.manage_messages.flag),
  76. )
  77. @username.command(
  78. description='Adds a username pattern',
  79. extras={
  80. 'long_description': 'Adds a username pattern.',
  81. 'usage': '<pattern>',
  82. },
  83. )
  84. async def add(self, interaction: Interaction, pattern: str) -> None:
  85. """Command handler"""
  86. norm_pattern = pattern.lower()
  87. patterns: list[str] = self.__get_patterns(interaction.guild)
  88. if norm_pattern in patterns:
  89. await interaction.response.send_message(
  90. f'Pattern `{norm_pattern}` already added.',
  91. ephemeral=True
  92. )
  93. return
  94. patterns.append(norm_pattern)
  95. self.__save_patterns(interaction.guild, patterns)
  96. await interaction.response.send_message(
  97. f'Pattern `{norm_pattern}` added.',
  98. ephemeral=True
  99. )
  100. @username.command(
  101. description='Removes a username pattern',
  102. extras={
  103. 'long_description': 'Removes an existing username pattern',
  104. 'usage': '<pattern>',
  105. },
  106. )
  107. async def remove(self, interaction: Interaction, pattern: str) -> None:
  108. """Command handler"""
  109. norm_pattern = pattern.lower()
  110. guild: Guild = interaction.guild
  111. patterns: list[str] = self.__get_patterns(guild)
  112. len_before = len(patterns)
  113. patterns = list(filter(lambda p: p != norm_pattern, patterns))
  114. if len(patterns) == len_before:
  115. await interaction.response.send_message(
  116. f'Pattern `{norm_pattern}` not found.',
  117. ephemeral=True,
  118. )
  119. return
  120. self.__save_patterns(guild, patterns)
  121. await interaction.response.send_message(
  122. f'Pattern `{norm_pattern}` removed.',
  123. ephemeral=True,
  124. )
  125. @username.command(
  126. description='Lists username patterns'
  127. )
  128. async def list(self, interaction: Interaction) -> None:
  129. """Command handler"""
  130. guild: Guild = interaction.guild
  131. patterns: list[str] = self.__get_patterns(guild)
  132. if len(patterns) == 0:
  133. await interaction.response.send_message(
  134. 'No patterns defined',
  135. ephemeral=True,
  136. )
  137. else:
  138. msg = 'Patterns:\n\n> `' + '`\n> `'.join(patterns) + '`'
  139. await interaction.response.send_message(
  140. msg,
  141. ephemeral=True,
  142. )
  143. @Cog.listener()
  144. async def on_member_join(self, member: Member) -> None:
  145. """Event handler"""
  146. for pattern in self.__get_patterns(member.guild):
  147. if self.matches(pattern, member.name):
  148. await self.handle_match(member, pattern)
  149. elif self.matches(pattern, member.display_name):
  150. await self.handle_match(member, pattern)
  151. def matches(self, pattern: str, subject: str) -> bool:
  152. """Checks if a username matches a given pattern"""
  153. if pattern is None:
  154. return False
  155. if subject is None:
  156. return False
  157. return pattern.lower() in subject.lower()
  158. async def handle_match(self, member: Member, pattern: str) -> None:
  159. """
  160. Handles a username match.
  161. """
  162. # TODO: Prevent double handling?
  163. self.log(member.guild, f'User {member.id} {member.display_name} matches pattern "{pattern}"')
  164. context = UsernamePatternContext(member)
  165. bm = BotMessage(
  166. member.guild,
  167. f'User {member.mention} ({str(member.id)}, {member.display_name}) has ' +
  168. f'username matching pattern `{pattern}`.',
  169. BotMessage.TYPE_INFO if self.was_warned_recently(member) else BotMessage.TYPE_MOD_WARNING,
  170. context)
  171. self.record_warning(member)
  172. await bm.set_reactions(context.reactions())
  173. await self.post_message(bm)
  174. async def on_mod_react(self,
  175. bot_message: BotMessage,
  176. reaction: BotMessageReaction,
  177. reacted_by: Member) -> None:
  178. context: UsernamePatternContext = bot_message.context
  179. if reaction.emoji == CONFIG['kick_emoji']:
  180. await context.member.kick(
  181. reason=f'Rocketbot: Flagged username pattern. Kicked by {reacted_by.name}.')
  182. context.kicked_by = reacted_by
  183. self.log(context.member.guild, f'User {context.member.name} kicked by {reacted_by.name}')
  184. await bot_message.set_reactions(context.reactions())
  185. elif reaction.emoji == CONFIG['ban_emoji']:
  186. await context.member.ban(
  187. reason=f'Rocketbot: Flagged username pattern. Banned by {reacted_by.name}.',
  188. delete_message_days=0)
  189. context.banned_by = reacted_by
  190. self.log(context.member.guild, f'User {context.member.name} banned by {reacted_by.name}')
  191. await bot_message.set_reactions(context.reactions())
  192. elif reaction.emoji == CONFIG['ignore_emoji']:
  193. context.ignored_by = reacted_by
  194. self.log(context.member.guild, f'Warning ignored by {reacted_by.name}')
  195. await bot_message.set_reactions(context.reactions())