Experimental Discord bot written in Python
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

usernamecog.py 7.4KB

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