| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234 |
- """
- Cog for detecting username patterns.
- """
- from typing import Optional
-
- from discord import Guild, Member, Permissions, Interaction
- from discord.app_commands import Group
- from discord.ext.commands import Cog
-
- from config import CONFIG
- from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction, CogSetting
- from rocketbot.storage import Storage
- from rocketbot.utils import MOD_PERMISSIONS
-
-
- class UsernamePatternContext:
- """
- BotMessage context for a flagged username
- """
- def __init__(self, member: Member) -> None:
- self.member: Member = member
- self.kicked_by: Optional[Member] = None
- self.banned_by: Optional[Member] = None
- self.ignored_by: Optional[Member] = None
-
- def reactions(self) -> list[BotMessageReaction]:
- """
- Generates updated BotMessageReactions based on context state.
- """
- r: list[BotMessageReaction] = []
- if self.ignored_by:
- r.append(BotMessageReaction(CONFIG['ignore_emoji'], False, f'Ignored by {self.ignored_by.name}'))
- elif self.banned_by:
- r.append(BotMessageReaction(CONFIG['ban_emoji'], False, f'Banned by {self.banned_by.name}'))
- elif self.kicked_by:
- r.append(BotMessageReaction(CONFIG['kick_emoji'], False, f'Kicked by {self.kicked_by.name}'))
- r.append(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user'))
- else:
- r.append(BotMessageReaction(CONFIG['kick_emoji'], True, 'Kick user'))
- r.append(BotMessageReaction(CONFIG['ban_emoji'], True, 'Ban user'))
- r.append(BotMessageReaction(CONFIG['ignore_emoji'], True, 'Ignore warning'))
- return r
-
- class UsernamePatternCog(BaseCog, name='Username Pattern'):
- """
- Detects usernames that match certain flagged patterns. Posts a mod warning
- message on a match.
- """
-
- SETTING_ENABLED = CogSetting(
- 'enabled',
- bool,
- default_value=False,
- brief='username pattern detection',
- description='Whether new users are checked for common patterns.',
- )
-
- SETTING_PATTERNS = CogSetting('patterns', None, default_value=None)
-
- def __init__(self, bot):
- super().__init__(
- bot,
- config_prefix='username',
- short_description='Manages username pattern detection.',
- )
- self.add_setting(UsernamePatternCog.SETTING_ENABLED)
-
- def __get_patterns(self, guild: Guild) -> list[str]:
- """
- Returns an array of username patterns.
- """
- patterns: list[str] = self.get_guild_setting(guild, self.SETTING_PATTERNS)
- if patterns is None:
- patterns = []
- Storage.set_config_value(guild, 'UsernamePatternCog.patterns', patterns)
- return patterns
-
- @classmethod
- def __save_patterns(cls,
- guild: Guild,
- patterns: list[str]) -> None:
- """
- Saves username pattern array.
- """
- cls.set_guild_setting(guild, cls.SETTING_PATTERNS, patterns)
-
- username = Group(
- name='username',
- description='Manages username pattern detection.',
- guild_only=True,
- default_permissions=MOD_PERMISSIONS,
- )
-
- @username.command(
- description='Adds a username pattern to match against new members.',
- extras={
- 'long_description': 'Adds a username pattern.',
- 'usage': '<pattern>',
- },
- )
- async def add(self, interaction: Interaction, pattern: str) -> None:
- """
- Adds a username pattern to match against new members.
-
- Parameters
- ----------
- interaction : Interaction
- pattern : str
- a substring to look for in usernames
- """
- norm_pattern = pattern.lower()
- patterns: list[str] = self.__get_patterns(interaction.guild)
- if norm_pattern in patterns:
- await interaction.response.send_message(
- f'{CONFIG["warning_emoji"]} Pattern `{norm_pattern}` already added.',
- ephemeral=True
- )
- return
- patterns.append(norm_pattern)
- self.__save_patterns(interaction.guild, patterns)
- await interaction.response.send_message(
- f'{CONFIG["success_emoji"]} Pattern `{norm_pattern}` added.',
- ephemeral=True
- )
-
- @username.command(
- description='Removes a username pattern.',
- extras={
- 'long_description': 'Removes an existing username pattern',
- 'usage': '<pattern>',
- },
- )
- async def remove(self, interaction: Interaction, pattern: str) -> None:
- """
- Removes a username pattern.
-
- Parameters
- ----------
- interaction : Interaction
- pattern : str
- the existing username pattern to remove
- """
- norm_pattern = pattern.lower()
- guild: Guild = interaction.guild
- patterns: list[str] = self.__get_patterns(guild)
- len_before = len(patterns)
- patterns = list(filter(lambda p: p != norm_pattern, patterns))
- if len(patterns) == len_before:
- await interaction.response.send_message(
- f'{CONFIG["warning_emoji"]} Pattern `{norm_pattern}` not found.',
- ephemeral=True,
- )
- return
- self.__save_patterns(guild, patterns)
- await interaction.response.send_message(
- f'{CONFIG["success_emoji"]} Pattern `{norm_pattern}` removed.',
- ephemeral=True,
- )
-
- @username.command(
- description='Lists existing username patterns.'
- )
- async def list(self, interaction: Interaction) -> None:
- """Command handler"""
- guild: Guild = interaction.guild
- patterns: list[str] = self.__get_patterns(guild)
- if len(patterns) == 0:
- await interaction.response.send_message(
- f'{CONFIG["success_emoji"]} No patterns defined.',
- ephemeral=True,
- )
- else:
- msg = f'{CONFIG["info_emoji"]} Patterns:\n\n> `' + '`\n> `'.join(patterns) + '`'
- await interaction.response.send_message(
- msg,
- ephemeral=True,
- )
-
- @Cog.listener()
- async def on_member_join(self, member: Member) -> None:
- """Event handler"""
- for pattern in self.__get_patterns(member.guild):
- if self.matches(pattern, member.name):
- await self.handle_match(member, pattern)
- elif self.matches(pattern, member.display_name):
- await self.handle_match(member, pattern)
-
- def matches(self, pattern: str, subject: str) -> bool:
- """Checks if a username matches a given pattern"""
- if pattern is None:
- return False
- if subject is None:
- return False
- return pattern.lower() in subject.lower()
-
- async def handle_match(self, member: Member, pattern: str) -> None:
- """
- Handles a username match.
- """
- # TODO: Prevent double handling?
- self.log(member.guild, f'User {member.id} {member.display_name} matches pattern "{pattern}"')
- context = UsernamePatternContext(member)
- bm = BotMessage(
- member.guild,
- f'User {member.mention} ({str(member.id)}, {member.display_name}) has ' +
- f'username matching pattern `{pattern}`.',
- BotMessage.TYPE_INFO if self.was_warned_recently(member) else BotMessage.TYPE_MOD_WARNING,
- context)
- self.record_warning(member)
- await bm.set_reactions(context.reactions())
- await self.post_message(bm)
-
- async def on_mod_react(self,
- bot_message: BotMessage,
- reaction: BotMessageReaction,
- reacted_by: Member) -> None:
- context: UsernamePatternContext = bot_message.context
- if reaction.emoji == CONFIG['kick_emoji']:
- await context.member.kick(
- reason=f'Rocketbot: Flagged username pattern. Kicked by {reacted_by.name}.')
- context.kicked_by = reacted_by
- self.log(context.member.guild, f'User {context.member.name} kicked by {reacted_by.name}')
- await bot_message.set_reactions(context.reactions())
- elif reaction.emoji == CONFIG['ban_emoji']:
- await context.member.ban(
- reason=f'Rocketbot: Flagged username pattern. Banned by {reacted_by.name}.',
- delete_message_days=0)
- context.banned_by = reacted_by
- self.log(context.member.guild, f'User {context.member.name} banned by {reacted_by.name}')
- await bot_message.set_reactions(context.reactions())
- elif reaction.emoji == CONFIG['ignore_emoji']:
- context.ignored_by = reacted_by
- self.log(context.member.guild, f'Warning ignored by {reacted_by.name}')
- await bot_message.set_reactions(context.reactions())
|