|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+"""
|
|
|
2
|
+Cog for matching messages against guild-configurable criteria and taking
|
|
|
3
|
+automated actions on them.
|
|
|
4
|
+"""
|
|
1
|
5
|
import re
|
|
2
|
|
-from abc import ABC, abstractmethod
|
|
|
6
|
+from abc import ABCMeta, abstractmethod
|
|
3
|
7
|
from discord import Guild, Member, Message
|
|
4
|
8
|
from discord.ext import commands
|
|
5
|
9
|
|
|
6
|
|
-from cogs.basecog import BaseCog, BotMessage, BotMessageReaction
|
|
7
|
10
|
from config import CONFIG
|
|
8
|
|
-from rbutils import parse_timedelta
|
|
9
|
|
-from storage import Storage
|
|
|
11
|
+from rocketbot.cogs.basecog import BaseCog, BotMessage, BotMessageReaction
|
|
|
12
|
+from rocketbot.storage import Storage
|
|
|
13
|
+from rocketbot.utils import parse_timedelta
|
|
10
|
14
|
|
|
11
|
15
|
class PatternAction:
|
|
12
|
16
|
"""
|
|
|
@@ -20,7 +24,7 @@ class PatternAction:
|
|
20
|
24
|
arg_str = ', '.join(self.arguments)
|
|
21
|
25
|
return f'{self.action}({arg_str})'
|
|
22
|
26
|
|
|
23
|
|
-class PatternExpression(ABC):
|
|
|
27
|
+class PatternExpression(metaclass=ABCMeta):
|
|
24
|
28
|
"""
|
|
25
|
29
|
Abstract message matching expression.
|
|
26
|
30
|
"""
|
|
|
@@ -165,8 +169,9 @@ class PatternCog(BaseCog, name='Pattern Matching'):
|
|
165
|
169
|
try:
|
|
166
|
170
|
ps = PatternCompiler.parse_statement(name, statement)
|
|
167
|
171
|
patterns[name] = ps
|
|
168
|
|
- except Exception as e:
|
|
169
|
|
- self.log(guild, f'Error parsing saved statement "{name}". Skipping: {statement}. Error: {e}')
|
|
|
172
|
+ except PatternError as e:
|
|
|
173
|
+ self.log(guild, 'Error parsing saved statement ' + \
|
|
|
174
|
+ f'"{name}": "{e}" Statement: {statement}')
|
|
170
|
175
|
Storage.set_state_value(guild, 'PatternCog.patterns', patterns)
|
|
171
|
176
|
return patterns
|
|
172
|
177
|
|
|
|
@@ -302,7 +307,7 @@ class PatternCog(BaseCog, name='Pattern Matching'):
|
|
302
|
307
|
await context.message.reply(
|
|
303
|
308
|
f'{CONFIG["success_emoji"]} Pattern `{name}` added.',
|
|
304
|
309
|
mention_author=False)
|
|
305
|
|
- except Exception as e:
|
|
|
310
|
+ except PatternError as e:
|
|
306
|
311
|
await context.message.reply(
|
|
307
|
312
|
f'{CONFIG["failure_emoji"]} Error parsing statement. {e}',
|
|
308
|
313
|
mention_author=False)
|
|
|
@@ -339,6 +344,11 @@ class PatternCog(BaseCog, name='Pattern Matching'):
|
|
339
|
344
|
msg += f'Pattern `{name}`:\n```\n{statement.original}\n```\n'
|
|
340
|
345
|
await context.message.reply(msg, mention_author=False)
|
|
341
|
346
|
|
|
|
347
|
+class PatternError(RuntimeError):
|
|
|
348
|
+ """
|
|
|
349
|
+ Error thrown when parsing a pattern statement.
|
|
|
350
|
+ """
|
|
|
351
|
+
|
|
342
|
352
|
class PatternCompiler:
|
|
343
|
353
|
"""
|
|
344
|
354
|
Parses a user-provided message filter statement into a PatternStatement.
|
|
|
@@ -457,7 +467,7 @@ class PatternCompiler:
|
|
457
|
467
|
in_quote = ch
|
|
458
|
468
|
current_token = ch
|
|
459
|
469
|
elif ch == '\\':
|
|
460
|
|
- raise RuntimeError("Unexpected \\")
|
|
|
470
|
+ raise PatternError("Unexpected \\ outside quoted string")
|
|
461
|
471
|
elif ch in cls.WHITESPACE_CHARS:
|
|
462
|
472
|
if len(current_token) > 0:
|
|
463
|
473
|
tokens.append(current_token)
|
|
|
@@ -528,7 +538,7 @@ class PatternCompiler:
|
|
528
|
538
|
return (actions, token_index)
|
|
529
|
539
|
elif token == ',':
|
|
530
|
540
|
if len(current_action_tokens) < 1:
|
|
531
|
|
- raise RuntimeError('Unexpected ,')
|
|
|
541
|
+ raise PatternError('Unexpected ,')
|
|
532
|
542
|
a = PatternAction(current_action_tokens[0], current_action_tokens[1:])
|
|
533
|
543
|
cls.__validate_action(a)
|
|
534
|
544
|
actions.append(a)
|
|
|
@@ -536,19 +546,19 @@ class PatternCompiler:
|
|
536
|
546
|
else:
|
|
537
|
547
|
current_action_tokens.append(token)
|
|
538
|
548
|
token_index += 1
|
|
539
|
|
- raise RuntimeError('Unexpected end of line')
|
|
|
549
|
+ raise PatternError('Unexpected end of line in action list')
|
|
540
|
550
|
|
|
541
|
551
|
@classmethod
|
|
542
|
552
|
def __validate_action(cls, action: PatternAction) -> None:
|
|
543
|
553
|
args = cls.ACTION_TO_ARGS.get(action.action)
|
|
544
|
554
|
if args is None:
|
|
545
|
|
- raise RuntimeError(f'Unknown action "{action.action}"')
|
|
|
555
|
+ raise PatternError(f'Unknown action "{action.action}"')
|
|
546
|
556
|
if len(action.arguments) != len(args):
|
|
547
|
557
|
if len(args) == 0:
|
|
548
|
|
- raise RuntimeError(f'Action "{action.action}" expects no arguments, ' + \
|
|
|
558
|
+ raise PatternError(f'Action "{action.action}" expects no arguments, ' + \
|
|
549
|
559
|
f'got {len(action.arguments)}.')
|
|
550
|
560
|
else:
|
|
551
|
|
- raise RuntimeError(f'Action "{action.action}" expects {len(args)} ' + \
|
|
|
561
|
+ raise PatternError(f'Action "{action.action}" expects {len(args)} ' + \
|
|
552
|
562
|
f'arguments, got {len(action.arguments)}.')
|
|
553
|
563
|
for i, datatype in enumerate(args):
|
|
554
|
564
|
action.arguments[i] = cls.parse_value(action.arguments[i], datatype)
|
|
|
@@ -573,11 +583,11 @@ class PatternCompiler:
|
|
573
|
583
|
if len(subexpressions) == 1:
|
|
574
|
584
|
return (subexpressions[0], token_index)
|
|
575
|
585
|
if len(subexpressions) > 1:
|
|
576
|
|
- raise RuntimeError('Too many subexpressions')
|
|
|
586
|
+ raise PatternError('Too many subexpressions')
|
|
577
|
587
|
compound_operator = None
|
|
578
|
588
|
if tokens[token_index] == ')':
|
|
579
|
589
|
if len(subexpressions) == 0:
|
|
580
|
|
- raise RuntimeError('No subexpressions')
|
|
|
590
|
+ raise PatternError('No subexpressions')
|
|
581
|
591
|
if len(subexpressions) == 1:
|
|
582
|
592
|
return (subexpressions[0], token_index)
|
|
583
|
593
|
return (PatternCompoundExpression(last_compound_operator, subexpressions), token_index)
|
|
|
@@ -597,7 +607,7 @@ class PatternCompiler:
|
|
597
|
607
|
elif tokens[token_index] == '(':
|
|
598
|
608
|
(exp, next_index) = cls.read_expression(tokens, token_index + 1, depth + 1)
|
|
599
|
609
|
if tokens[next_index] != ')':
|
|
600
|
|
- raise RuntimeError('Expected )')
|
|
|
610
|
+ raise PatternError('Expected )')
|
|
601
|
611
|
subexpressions.append(exp)
|
|
602
|
612
|
token_index = next_index + 1
|
|
603
|
613
|
else:
|
|
|
@@ -605,7 +615,7 @@ class PatternCompiler:
|
|
605
|
615
|
subexpressions.append(simple)
|
|
606
|
616
|
token_index = next_index
|
|
607
|
617
|
if len(subexpressions) == 0:
|
|
608
|
|
- raise RuntimeError('No subexpressions')
|
|
|
618
|
+ raise PatternError('No subexpressions')
|
|
609
|
619
|
elif len(subexpressions) == 1:
|
|
610
|
620
|
return (subexpressions[0], token_index)
|
|
611
|
621
|
else:
|
|
|
@@ -619,39 +629,41 @@ class PatternCompiler:
|
|
619
|
629
|
the token index it left off at.
|
|
620
|
630
|
"""
|
|
621
|
631
|
if depth > 8:
|
|
622
|
|
- raise RuntimeError('Expression nests too deeply')
|
|
|
632
|
+ raise PatternError('Expression nests too deeply')
|
|
623
|
633
|
if token_index >= len(tokens):
|
|
624
|
|
- raise RuntimeError('Expected field name, found EOL')
|
|
|
634
|
+ raise PatternError('Expected field name, found EOL')
|
|
625
|
635
|
field = tokens[token_index]
|
|
626
|
636
|
token_index += 1
|
|
627
|
637
|
|
|
628
|
638
|
datatype = cls.FIELD_TO_TYPE.get(field)
|
|
629
|
639
|
if datatype is None:
|
|
630
|
|
- raise RuntimeError(f'No such field "{field}"')
|
|
|
640
|
+ raise PatternError(f'No such field "{field}"')
|
|
631
|
641
|
|
|
632
|
642
|
if token_index >= len(tokens):
|
|
633
|
|
- raise RuntimeError('Expected operator, found EOL')
|
|
|
643
|
+ raise PatternError('Expected operator, found EOL')
|
|
634
|
644
|
op = tokens[token_index]
|
|
635
|
645
|
token_index += 1
|
|
636
|
646
|
|
|
637
|
647
|
if op == '!':
|
|
638
|
648
|
if token_index >= len(tokens):
|
|
639
|
|
- raise RuntimeError('Expected operator, found EOL')
|
|
|
649
|
+ raise PatternError('Expected operator, found EOL')
|
|
640
|
650
|
op = '!' + tokens[token_index]
|
|
641
|
651
|
token_index += 1
|
|
642
|
652
|
|
|
643
|
653
|
allowed_ops = cls.TYPE_TO_OPERATORS[datatype]
|
|
644
|
654
|
if op not in allowed_ops:
|
|
645
|
655
|
if op in cls.OPERATORS_ALL:
|
|
646
|
|
- raise RuntimeError(f'Operator {op} cannot be used with field "{field}"')
|
|
647
|
|
- else:
|
|
648
|
|
- raise RuntimeError(f'Unrecognized operator "{op}" - allowed: {list(allowed_ops)}')
|
|
|
656
|
+ raise PatternError(f'Operator {op} cannot be used with field "{field}"')
|
|
|
657
|
+ raise PatternError(f'Unrecognized operator "{op}" - allowed: {list(allowed_ops)}')
|
|
649
|
658
|
|
|
650
|
659
|
if token_index >= len(tokens):
|
|
651
|
|
- raise RuntimeError('Expected value, found EOL')
|
|
|
660
|
+ raise PatternError('Expected value, found EOL')
|
|
652
|
661
|
value = tokens[token_index]
|
|
653
|
662
|
|
|
654
|
|
- value = cls.parse_value(value, datatype)
|
|
|
663
|
+ try:
|
|
|
664
|
+ value = cls.parse_value(value, datatype)
|
|
|
665
|
+ except ValueError as cause:
|
|
|
666
|
+ raise PatternError(f'Bad value {value}') from cause
|
|
655
|
667
|
|
|
656
|
668
|
token_index += 1
|
|
657
|
669
|
exp = PatternSimpleExpression(field, op, value)
|
|
|
@@ -660,7 +672,7 @@ class PatternCompiler:
|
|
660
|
672
|
@classmethod
|
|
661
|
673
|
def parse_value(cls, value: str, datatype: str):
|
|
662
|
674
|
"""
|
|
663
|
|
- Converts a value token to its Python value.
|
|
|
675
|
+ Converts a value token to its Python value. Raises ValueError on failure.
|
|
664
|
676
|
"""
|
|
665
|
677
|
if datatype == cls.TYPE_ID:
|
|
666
|
678
|
p = re.compile('^[0-9]+$')
|