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

usernamecog.py 6.5KB

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