Compare commits
1 commit
master
...
custom-fie
Author | SHA1 | Date | |
---|---|---|---|
|
fb214d8f0b |
5 changed files with 82 additions and 31 deletions
|
@ -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
|
# Copyright (C) 2019 Tulir Asokan
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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)
|
allowed_msgtypes: Tuple[MessageType, ...] = (MessageType.TEXT, MessageType.EMOTE)
|
||||||
user_flood: Dict[UserID, FloodInfo]
|
user_flood: Dict[UserID, FloodInfo]
|
||||||
room_flood: Dict[RoomID, FloodInfo]
|
room_flood: Dict[RoomID, FloodInfo]
|
||||||
|
config: Config
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_config_class(cls) -> Type[BaseProxyConfig]:
|
def get_config_class(cls) -> Type[BaseProxyConfig]:
|
||||||
|
@ -100,6 +101,6 @@ class ReactBot(Plugin):
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
await rule.execute(evt, match)
|
await rule.execute(evt, match)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
self.log.exception(f"Failed to execute {name}")
|
self.log.warning(f"Failed to execute {name}: {e}")
|
||||||
return
|
return
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# 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
|
# Copyright (C) 2021 Tulir Asokan
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# 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.util.config import BaseProxyConfig, ConfigUpdateHelper
|
||||||
from mautrix.types import EventType
|
from mautrix.types import EventType
|
||||||
|
|
||||||
from .simplepattern import SimplePattern
|
from .simplepattern import SimplePattern, RegexPattern
|
||||||
from .template import Template
|
from .template import Template
|
||||||
from .rule import Rule, RPattern
|
from .rule import Rule, RPattern
|
||||||
|
|
||||||
|
@ -58,7 +58,8 @@ class Config(BaseProxyConfig):
|
||||||
not_matches=self._compile_all(rule.get("not_matches", [])),
|
not_matches=self._compile_all(rule.get("not_matches", [])),
|
||||||
type=EventType.find(rule["type"]) if "type" in rule else None,
|
type=EventType.find(rule["type"]) if "type" in rule else None,
|
||||||
template=self.templates[rule["template"]],
|
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:
|
except Exception as e:
|
||||||
raise ConfigError(f"Failed to load {name}") from e
|
raise ConfigError(f"Failed to load {name}") from e
|
||||||
|
|
||||||
|
@ -79,13 +80,16 @@ class Config(BaseProxyConfig):
|
||||||
def _compile(self, pattern: InputPattern) -> RPattern:
|
def _compile(self, pattern: InputPattern) -> RPattern:
|
||||||
flags = self.default_flags
|
flags = self.default_flags
|
||||||
raw = None
|
raw = None
|
||||||
|
field = None
|
||||||
if isinstance(pattern, dict):
|
if isinstance(pattern, dict):
|
||||||
flags = self._get_flags(pattern["flags"]) if "flags" in pattern else flags
|
flags = self._get_flags(pattern["flags"]) if "flags" in pattern else flags
|
||||||
raw = pattern.get("raw", False)
|
raw = pattern.get("raw", False)
|
||||||
|
field = pattern.get("field", None)
|
||||||
pattern = pattern["pattern"]
|
pattern = pattern["pattern"]
|
||||||
if raw is not False and (not flags & re.MULTILINE or raw is True):
|
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 (SimplePattern.compile(pattern, flags, raw, field=field)
|
||||||
return re.compile(pattern, flags=flags)
|
or RegexPattern(re.compile(pattern, flags=flags), field=field))
|
||||||
|
return RegexPattern(re.compile(pattern, flags=flags), field=field)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_variables(data: Dict[str, Any]) -> Dict[str, Any]:
|
def _parse_variables(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
# 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
|
# Copyright (C) 2021 Tulir Asokan
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# 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
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
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 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 maubot import MessageEvent
|
||||||
|
|
||||||
from .template import Template
|
from .template import Template
|
||||||
from .simplepattern import SimplePattern
|
from .simplepattern import SimplePattern, RegexPattern
|
||||||
|
|
||||||
RPattern = Union[Pattern, SimplePattern]
|
RPattern = Union[RegexPattern, SimplePattern]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Rule:
|
class Rule:
|
||||||
|
field: List[str]
|
||||||
rooms: Set[RoomID]
|
rooms: Set[RoomID]
|
||||||
not_rooms: Set[RoomID]
|
not_rooms: Set[RoomID]
|
||||||
matches: List[RPattern]
|
matches: List[RPattern]
|
||||||
not_matches: List[RPattern]
|
not_matches: List[RPattern]
|
||||||
|
|
||||||
template: Template
|
template: Template
|
||||||
type: Optional[EventType]
|
type: Optional[EventType]
|
||||||
|
room_id: Optional[RoomID]
|
||||||
|
state_event: bool
|
||||||
variables: Dict[str, Any]
|
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:
|
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 True
|
||||||
return False
|
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]:
|
def match(self, evt: MessageEvent) -> Optional[Match]:
|
||||||
if len(self.rooms) > 0 and evt.room_id not in self.rooms:
|
if len(self.rooms) > 0 and evt.room_id not in self.rooms:
|
||||||
return None
|
return None
|
||||||
elif evt.room_id in self.not_rooms:
|
elif evt.room_id in self.not_rooms:
|
||||||
return None
|
return None
|
||||||
|
data = self._get_value(evt, self.field)
|
||||||
for pattern in self.matches:
|
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 match:
|
||||||
if self._check_not_match(evt.content.body):
|
if self._check_not_match(evt, data):
|
||||||
return None
|
return None
|
||||||
return match
|
return match
|
||||||
return None
|
return None
|
||||||
|
@ -62,5 +86,10 @@ class Rule:
|
||||||
**{str(i): val for i, val in enumerate(match.groups())},
|
**{str(i): val for i, val in enumerate(match.groups())},
|
||||||
**match.groupdict(),
|
**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)
|
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)
|
||||||
|
|
|
@ -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
|
# Copyright (C) 2019 Tulir Asokan
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
from typing import Callable, List, Dict, Optional
|
from typing import Callable, List, Dict, Optional, Pattern, Match
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,15 +27,30 @@ class BlankMatch:
|
||||||
return {}
|
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:
|
class SimplePattern:
|
||||||
_ptm = BlankMatch()
|
_ptm = BlankMatch()
|
||||||
|
|
||||||
matcher: Callable[[str], bool]
|
matcher: Callable[[str], bool]
|
||||||
|
field: Optional[List[str]]
|
||||||
ignorecase: bool
|
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.matcher = matcher
|
||||||
self.ignorecase = ignorecase
|
self.ignorecase = ignorecase
|
||||||
|
self.field = field
|
||||||
|
|
||||||
def search(self, val: str) -> BlankMatch:
|
def search(self, val: str) -> BlankMatch:
|
||||||
if self.ignorecase:
|
if self.ignorecase:
|
||||||
|
@ -44,8 +59,8 @@ class SimplePattern:
|
||||||
return self._ptm
|
return self._ptm
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def compile(pattern: str, flags: re.RegexFlag = re.RegexFlag(0), force_raw: bool = False
|
def compile(pattern: str, flags: re.RegexFlag = re.RegexFlag(0), force_raw: bool = False,
|
||||||
) -> Optional['SimplePattern']:
|
field: Optional[List[str]] = None) -> Optional['SimplePattern']:
|
||||||
ignorecase = flags == re.IGNORECASE
|
ignorecase = flags == re.IGNORECASE
|
||||||
s_pattern = pattern.lower() if ignorecase else pattern
|
s_pattern = pattern.lower() if ignorecase else pattern
|
||||||
esc = ""
|
esc = ""
|
||||||
|
@ -54,13 +69,15 @@ class SimplePattern:
|
||||||
first, last = pattern[0], pattern[-1]
|
first, last = pattern[0], pattern[-1]
|
||||||
if first == '^' and last == '$' and (force_raw or esc == f"\\^{pattern[1:-1]}\\$"):
|
if first == '^' and last == '$' and (force_raw or esc == f"\\^{pattern[1:-1]}\\$"):
|
||||||
s_pattern = s_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:]}"):
|
elif first == '^' and (force_raw or esc == f"\\^{pattern[1:]}"):
|
||||||
s_pattern = s_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]}\\$"):
|
elif last == '$' and (force_raw or esc == f"{pattern[:-1]}\\$"):
|
||||||
s_pattern = s_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:
|
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
|
return None
|
||||||
|
|
|
@ -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
|
# Copyright (C) 2019 Tulir Asokan
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
|
Loading…
Reference in a new issue