diff --git a/reactbot/bot.py b/reactbot/bot.py
index b893110..cd06466 100644
--- a/reactbot/bot.py
+++ b/reactbot/bot.py
@@ -1,4 +1,4 @@
-# reminder - A maubot plugin that reacts to messages that match predefined rules.
+# reactbot - A maubot plugin that reacts to messages that match predefined rules.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
@@ -49,6 +49,7 @@ class ReactBot(Plugin):
allowed_msgtypes: Tuple[MessageType, ...] = (MessageType.TEXT, MessageType.EMOTE)
user_flood: Dict[UserID, FloodInfo]
room_flood: Dict[RoomID, FloodInfo]
+ config: Config
@classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]:
@@ -100,6 +101,6 @@ class ReactBot(Plugin):
return
try:
await rule.execute(evt, match)
- except Exception:
- self.log.exception(f"Failed to execute {name}")
+ except Exception as e:
+ self.log.warning(f"Failed to execute {name}: {e}")
return
diff --git a/reactbot/config.py b/reactbot/config.py
index 8cb9ba5..1ed6f38 100644
--- a/reactbot/config.py
+++ b/reactbot/config.py
@@ -1,5 +1,5 @@
-# reminder - A maubot plugin that reacts to messages that match predefined rules.
-# Copyright (C) 2019 Tulir Asokan
+# reactbot - A maubot plugin that reacts to messages that match predefined rules.
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -21,7 +21,7 @@ from jinja2 import Template as JinjaTemplate
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from mautrix.types import EventType
-from .simplepattern import SimplePattern
+from .simplepattern import SimplePattern, RegexPattern
from .template import Template
from .rule import Rule, RPattern
@@ -58,7 +58,8 @@ class Config(BaseProxyConfig):
not_matches=self._compile_all(rule.get("not_matches", [])),
type=EventType.find(rule["type"]) if "type" in rule else None,
template=self.templates[rule["template"]],
- variables=self._parse_variables(rule))
+ variables=self._parse_variables(rule),
+ field=rule.get("field", ["content", "body"]))
except Exception as e:
raise ConfigError(f"Failed to load {name}") from e
@@ -79,13 +80,16 @@ class Config(BaseProxyConfig):
def _compile(self, pattern: InputPattern) -> RPattern:
flags = self.default_flags
raw = None
+ field = None
if isinstance(pattern, dict):
flags = self._get_flags(pattern["flags"]) if "flags" in pattern else flags
raw = pattern.get("raw", False)
+ field = pattern.get("field", None)
pattern = pattern["pattern"]
if raw is not False and (not flags & re.MULTILINE or raw is True):
- return SimplePattern.compile(pattern, flags, raw) or re.compile(pattern, flags=flags)
- return re.compile(pattern, flags=flags)
+ return (SimplePattern.compile(pattern, flags, raw, field=field)
+ or RegexPattern(re.compile(pattern, flags=flags), field=field))
+ return RegexPattern(re.compile(pattern, flags=flags), field=field)
@staticmethod
def _parse_variables(data: Dict[str, Any]) -> Dict[str, Any]:
diff --git a/reactbot/rule.py b/reactbot/rule.py
index f7703d2..59cc21f 100644
--- a/reactbot/rule.py
+++ b/reactbot/rule.py
@@ -1,5 +1,5 @@
-# reminder - A maubot plugin that reacts to messages that match predefined rules.
-# Copyright (C) 2019 Tulir Asokan
+# reactbot - A maubot plugin that reacts to messages that match predefined rules.
+# Copyright (C) 2021 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
@@ -13,46 +13,70 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Optional, Match, Dict, List, Set, Union, Pattern, Any
+from typing import Optional, Match, Dict, List, Set, Union, Any
+import json
from attr import dataclass
-from jinja2 import Template as JinjaTemplate
-from mautrix.types import RoomID, EventType
+from mautrix.types import RoomID, EventType, Serializable
from maubot import MessageEvent
from .template import Template
-from .simplepattern import SimplePattern
+from .simplepattern import SimplePattern, RegexPattern
-RPattern = Union[Pattern, SimplePattern]
+RPattern = Union[RegexPattern, SimplePattern]
@dataclass
class Rule:
+ field: List[str]
rooms: Set[RoomID]
not_rooms: Set[RoomID]
matches: List[RPattern]
not_matches: List[RPattern]
+
template: Template
type: Optional[EventType]
+ room_id: Optional[RoomID]
+ state_event: bool
variables: Dict[str, Any]
- def _check_not_match(self, body: str) -> bool:
+ def _check_not_match(self, evt: MessageEvent, data: str) -> bool:
for pattern in self.not_matches:
- if pattern.search(body):
+ pattern_data = self._get_value(evt, pattern.field) if pattern.field else data
+ if pattern.search(pattern_data):
return True
return False
+ @staticmethod
+ def _get_value(evt: MessageEvent, field: List[str]) -> str:
+ data = evt
+ for part in field:
+ try:
+ data = evt[part]
+ except KeyError:
+ return ""
+ if isinstance(data, (str, int)):
+ return str(data)
+ elif isinstance(data, Serializable):
+ return json.dumps(data.serialize())
+ elif isinstance(data, (dict, list)):
+ return json.dumps(data)
+ else:
+ return str(data)
+
def match(self, evt: MessageEvent) -> Optional[Match]:
if len(self.rooms) > 0 and evt.room_id not in self.rooms:
return None
elif evt.room_id in self.not_rooms:
return None
+ data = self._get_value(evt, self.field)
for pattern in self.matches:
- match = pattern.search(evt.content.body)
+ pattern_data = self._get_value(evt, pattern.field) if pattern.field else data
+ match = pattern.search(pattern_data)
if match:
- if self._check_not_match(evt.content.body):
+ if self._check_not_match(evt, data):
return None
return match
return None
@@ -62,5 +86,10 @@ class Rule:
**{str(i): val for i, val in enumerate(match.groups())},
**match.groupdict(),
}
+ room_id = self.room_id or evt.room_id
+ event_type = self.type or self.template.type
content = self.template.execute(evt=evt, rule_vars=self.variables, extra_vars=extra_vars)
- await evt.client.send_message_event(evt.room_id, self.type or self.template.type, content)
+ if self.state_event:
+ await evt.client.send_state_event(room_id, event_type, content)
+ else:
+ await evt.client.send_message_event(room_id, event_type, content)
diff --git a/reactbot/simplepattern.py b/reactbot/simplepattern.py
index d9e74e1..c64b09b 100644
--- a/reactbot/simplepattern.py
+++ b/reactbot/simplepattern.py
@@ -1,4 +1,4 @@
-# reminder - A maubot plugin that reacts to messages that match predefined rules.
+# reactbot - A maubot plugin that reacts to messages that match predefined rules.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify
@@ -13,7 +13,7 @@
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see .
-from typing import Callable, List, Dict, Optional
+from typing import Callable, List, Dict, Optional, Pattern, Match
import re
@@ -27,15 +27,30 @@ class BlankMatch:
return {}
+class RegexPattern:
+ pattern: Pattern
+ field: Optional[List[str]]
+
+ def __init__(self, pattern: Pattern, field: Optional[List[str]] = None) -> None:
+ self.pattern = pattern
+ self.field = field
+
+ def search(self, val: str) -> Match:
+ return self.pattern.search(val)
+
+
class SimplePattern:
_ptm = BlankMatch()
matcher: Callable[[str], bool]
+ field: Optional[List[str]]
ignorecase: bool
- def __init__(self, matcher: Callable[[str], bool], ignorecase: bool) -> None:
+ def __init__(self, matcher: Callable[[str], bool], ignorecase: bool,
+ field: Optional[List[str]] = None) -> None:
self.matcher = matcher
self.ignorecase = ignorecase
+ self.field = field
def search(self, val: str) -> BlankMatch:
if self.ignorecase:
@@ -44,8 +59,8 @@ class SimplePattern:
return self._ptm
@staticmethod
- def compile(pattern: str, flags: re.RegexFlag = re.RegexFlag(0), force_raw: bool = False
- ) -> Optional['SimplePattern']:
+ def compile(pattern: str, flags: re.RegexFlag = re.RegexFlag(0), force_raw: bool = False,
+ field: Optional[List[str]] = None) -> Optional['SimplePattern']:
ignorecase = flags == re.IGNORECASE
s_pattern = pattern.lower() if ignorecase else pattern
esc = ""
@@ -54,13 +69,15 @@ class SimplePattern:
first, last = pattern[0], pattern[-1]
if first == '^' and last == '$' and (force_raw or esc == f"\\^{pattern[1:-1]}\\$"):
s_pattern = s_pattern[1:-1]
- return SimplePattern(lambda val: val == s_pattern, ignorecase=ignorecase)
+ return SimplePattern(lambda val: val == s_pattern, ignorecase=ignorecase, field=field)
elif first == '^' and (force_raw or esc == f"\\^{pattern[1:]}"):
s_pattern = s_pattern[1:]
- return SimplePattern(lambda val: val.startswith(s_pattern), ignorecase=ignorecase)
+ return SimplePattern(lambda val: val.startswith(s_pattern), ignorecase=ignorecase,
+ field=field)
elif last == '$' and (force_raw or esc == f"{pattern[:-1]}\\$"):
s_pattern = s_pattern[:-1]
- return SimplePattern(lambda val: val.endswith(s_pattern), ignorecase=ignorecase)
+ return SimplePattern(lambda val: val.endswith(s_pattern), ignorecase=ignorecase,
+ field=field)
elif force_raw or esc == pattern:
- return SimplePattern(lambda val: s_pattern in val, ignorecase=ignorecase)
+ return SimplePattern(lambda val: s_pattern in val, ignorecase=ignorecase, field=field)
return None
diff --git a/reactbot/template.py b/reactbot/template.py
index 13ee593..3dab95d 100644
--- a/reactbot/template.py
+++ b/reactbot/template.py
@@ -1,4 +1,4 @@
-# reminder - A maubot plugin that reacts to messages that match predefined rules.
+# reactbot - A maubot plugin that reacts to messages that match predefined rules.
# Copyright (C) 2019 Tulir Asokan
#
# This program is free software: you can redistribute it and/or modify