| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174 |
- from discord import Guild, Message
- from discord.ext import commands
- from datetime import timedelta
-
- from cogs.basecog import BaseCog, BotMessage
- from storage import Storage
-
- class Criterion:
- def __init__(self, type, **kwargs):
- self.type = type
- if type == 'contains':
- text = kwargs['text']
- self.text = text
- self.test = lambda m : text.lower() in m.content.lower()
- elif type == 'joinage':
- min = kwargs['min']
- self.min = min
- self.test = lambda m : m.created_at - m.author.joined_at < min
- else:
- raise RuntimeError(f'Unknown criterion type "{type}"')
-
- def matches(self, message: Message) -> bool:
- return self.test(message)
-
- @classmethod
- def decode(cls, val: dict):
- type = val['type']
- if type == 'contains':
- return Criterion(type, text=val['text'])
- elif type == 'joinage':
- return Criterion(type, min=timedelta(seconds=val['min']))
-
- class Pattern:
- def __init__(self, criteria: list, action: str, must_match_all: bool = True):
- self.criteria = criteria
- self.action = action
- self.must_match_all = must_match_all
-
- def matches(self, message: Message) -> bool:
- for criterion in self.criteria:
- crit_matches = criterion.matches(message)
- if crit_matches and not self.must_match_all:
- return True
- if not crit_matches and self.must_match_all:
- return False
- return self.must_match_all
-
- @classmethod
- def decode(cls, val: dict):
- match_all = val.get('must_match_all')
- action = val.get('action')
- encoded_criteria = val.get('criteria')
- criteria = []
- for ec in encoded_criteria:
- criteria.append(Criterion.decode(ec))
- return Pattern(criteria, action, match_all if isinstance(match_all, bool) else True)
-
- class PatternCog(BaseCog):
- def __init__(self, bot):
- super().__init__(bot)
-
- def __patterns(self, guild: Guild) -> list:
- patterns = Storage.get_state_value(guild, 'pattern_patterns')
- if patterns is None:
- patterns_encoded = Storage.get_config_value(guild, 'pattern_patterns')
- if patterns_encoded:
- patterns = []
- for pe in patterns_encoded:
- patterns.append(Pattern.decode(pe))
- Storage.set_state_value(guild, 'pattern_patterns', patterns)
- return patterns
-
- @commands.Cog.listener()
- async def on_message(self, message: Message) -> None:
- if message.author is None or \
- message.author.bot or \
- message.channel is None or \
- message.guild is None or \
- message.content is None or \
- message.content == '':
- return
- if message.author.permissions_in(message.channel).ban_members:
- # Ignore mods
- return
- patterns = self.__patterns(message.guild)
- for pattern in patterns:
- if pattern.matches(message):
- text = None
- if pattern.action == 'delete':
- await message.delete()
- text = f'Message from {message.author.mention} matched ' + \
- 'banned pattern. Deleted.'
- self.log(message.guild, 'Message matched pattern. Deleted.')
- elif pattern.action == 'kick':
- await message.delete()
- await message.author.kick(reason='Rocketbot: Message matched banned pattern')
- text = f'Message from {message.author.mention} matched ' + \
- 'banned pattern. Message deleted and user kicked.'
- self.log(message.guild,
- '\u0007Message matched pattern. Kicked ' + \
- f'{message.author.name} ({message.author.id}).')
- elif pattern.action == 'ban':
- await message.delete()
- await message.author.ban(reason='Rocketbot: Message matched banned pattern')
- text = f'Message from {message.author.mention} matched ' + \
- 'banned pattern. Message deleted and user banned.'
- self.log(message.guild,
- '\u0007Message matched pattern. Banned ' + \
- f'{message.author_name} ({message.author.id}).')
- if text:
- m = BotMessage(message.guild,
- text = msg,
- type = BotMessage.TYPE_MOD_WARNING)
- m.quote = message.content
- await self.post_message(m)
- break
-
- """
- Expression language samples:
-
- content contains "poop"
- content contains "poop" and content contains "tinkle"
- joinage < 600s
- (content contains "this" and content contains "that") or content contains "whatever"
-
- <field> <op> <value>
-
- Fields:
- content
- author.id
- author.name
- author.joinage
-
- Ops:
- ==
- !=
- <
- >
- <=
- >=
- contains, !contains -- plain strings
- matches, !matches -- regexes
-
- Value types:
- timedelta (600, 600s, 10m, 5m30s)
- number
- string
- regex
- mention
-
- Evaluation
- and
- or
- ( )
- !( )
- """
-
- @commands.group(
- brief='Manages message pattern matching',
- )
- @commands.has_permissions(ban_members=True)
- @commands.guild_only()
- async def patterns(self, context: commands.Context):
- 'Message pattern matching'
- if context.invoked_subcommand is None:
- await context.send_help()
-
- @patterns.command()
- async def addpattern(self, context: commands.Context, name: str, expression: str, *args):
- print(f'Pattern name: {name}')
- tokens = []
- tokens.append(expression)
- tokens += args
- print('Expression: ' + (' '.join(tokens)))
|