Add support for raw json template as content and add docs
This commit is contained in:
parent
3900b04dba
commit
4d5a3a681c
2 changed files with 80 additions and 9 deletions
53
README.md
53
README.md
|
@ -1,2 +1,55 @@
|
||||||
# reactbot
|
# reactbot
|
||||||
A simple [maubot](https://github.com/maubot/maubot) that responds to messages that match predefined rules.
|
A simple [maubot](https://github.com/maubot/maubot) that responds to messages that match predefined rules.
|
||||||
|
|
||||||
|
## Config format
|
||||||
|
### Templates
|
||||||
|
Templates contain the actual event type and content to be sent.
|
||||||
|
* `type` - The Matrix event type to send
|
||||||
|
* `content` - The event content. Either an object or jinja2 template that produces JSON.
|
||||||
|
* `variables` - A key-value map of variables.
|
||||||
|
|
||||||
|
Variables are parsed as jinja2 templates and get the maubot event object in `event`.
|
||||||
|
|
||||||
|
If the content is a string, it'll be parsed as a jinja2 template and the output
|
||||||
|
will be parsed as JSON. The content jinja2 template will get `event` just like
|
||||||
|
variable templates, but it will also get all of the variables.
|
||||||
|
|
||||||
|
If the content is an object, that object is what will be sent as the content.
|
||||||
|
The object can contain variables using a custom syntax: All instances of
|
||||||
|
`$${variablename}` will be replaced with the value matching `variablename`.
|
||||||
|
This works in object keys and values and list items. If a key/value/item only
|
||||||
|
consists of a variable insertion, the variable may be of any type. If there's
|
||||||
|
something else than the variable, the variable will be concatenated using `+`,
|
||||||
|
which means it should be a string.
|
||||||
|
|
||||||
|
### Default flags
|
||||||
|
Default regex flags. Most Python regex flags are available.
|
||||||
|
See [docs](https://docs.python.org/3/library/re.html#re.A).
|
||||||
|
|
||||||
|
Most relevant flags:
|
||||||
|
* `i` / `ignorecase` - Case-insensitive matching.
|
||||||
|
* `s` / `dotall` - Make `.` match any character at all, including newline.
|
||||||
|
* `x` / `verbose` - Ignore comments and whitespace in regex.
|
||||||
|
* `m` / `multiline` - When specified, `^` and `$` match the start and end of
|
||||||
|
line respectively instead of start and end of whole string.
|
||||||
|
|
||||||
|
### Rules
|
||||||
|
Rules have five fields. Only `matches` and `template` are required.
|
||||||
|
* `rooms` - The list of rooms where the rule should apply.
|
||||||
|
If empty, the rule will apply to all rooms the bot is in.
|
||||||
|
* `matches` - The regex or list of regexes to match.
|
||||||
|
* `template` - The name of the template to use.
|
||||||
|
* `variables` - A key-value map of variables to extend or override template variables.
|
||||||
|
Like with template variables, the values are parsed as Jinja2 templates.
|
||||||
|
|
||||||
|
The regex(es) in `matches` can either be simple strings containing the pattern,
|
||||||
|
or objects containing additional info:
|
||||||
|
* `pattern` - The regex to match.
|
||||||
|
* `flags` - Regex flags (replaces default flags).
|
||||||
|
* `raw` - Whether or not the regex should be forced to be raw.
|
||||||
|
|
||||||
|
If `raw` is `true` OR the pattern contains no special regex characters other
|
||||||
|
than `^` at the start and/or `$` at the end, the pattern will be considered
|
||||||
|
"raw". Raw patterns don't use regex, but instead use faster string operators
|
||||||
|
(equality, starts/endwith, contains). Patterns with the `multiline` flag will
|
||||||
|
never be converted into raw patterns implicitly.
|
||||||
|
|
36
reactbot.py
36
reactbot.py
|
@ -16,6 +16,7 @@
|
||||||
from typing import (NewType, Optional, Callable, Pattern, Match, Union, Dict, List, Tuple, Set,
|
from typing import (NewType, Optional, Callable, Pattern, Match, Union, Dict, List, Tuple, Set,
|
||||||
Type, Any)
|
Type, Any)
|
||||||
from itertools import chain
|
from itertools import chain
|
||||||
|
import json
|
||||||
import copy
|
import copy
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
@ -69,6 +70,7 @@ class SimplePattern:
|
||||||
|
|
||||||
RMatch = Union[Match, BlankMatch]
|
RMatch = Union[Match, BlankMatch]
|
||||||
RPattern = Union[Pattern, SimplePattern]
|
RPattern = Union[Pattern, SimplePattern]
|
||||||
|
InputPattern = Union[str, Dict[str, str]]
|
||||||
|
|
||||||
Index = NewType("Index", Union[str, int, Key])
|
Index = NewType("Index", Union[str, int, Key])
|
||||||
|
|
||||||
|
@ -79,7 +81,7 @@ variable_regex = re.compile(r"\$\${([0-9A-Za-z-_]+)}")
|
||||||
class Template:
|
class Template:
|
||||||
type: EventType
|
type: EventType
|
||||||
variables: Dict[str, JinjaTemplate]
|
variables: Dict[str, JinjaTemplate]
|
||||||
content: Dict[str, Any]
|
content: Union[Dict[str, Any], JinjaTemplate]
|
||||||
|
|
||||||
_variable_locations: List[Tuple[Index, ...]] = None
|
_variable_locations: List[Tuple[Index, ...]] = None
|
||||||
|
|
||||||
|
@ -122,6 +124,9 @@ class Template:
|
||||||
variables = {**{name: template.render(event=evt)
|
variables = {**{name: template.render(event=evt)
|
||||||
for name, template in chain(self.variables.items(), rule_vars.items())},
|
for name, template in chain(self.variables.items(), rule_vars.items())},
|
||||||
**extra_vars}
|
**extra_vars}
|
||||||
|
if isinstance(self.content, JinjaTemplate):
|
||||||
|
raw_json = self.content.render(event=evt, **variables)
|
||||||
|
return json.loads(raw_json)
|
||||||
content = copy.deepcopy(self.content)
|
content = copy.deepcopy(self.content)
|
||||||
for path in self._variable_locations:
|
for path in self._variable_locations:
|
||||||
data: Dict[str, Any] = self._recurse(content, path[:-1])
|
data: Dict[str, Any] = self._recurse(content, path[:-1])
|
||||||
|
@ -187,16 +192,26 @@ class ReactBot(Plugin):
|
||||||
return {name: JinjaTemplate(var_tpl) for name, var_tpl
|
return {name: JinjaTemplate(var_tpl) for name, var_tpl
|
||||||
in data.get("variables", {}).items()}
|
in data.get("variables", {}).items()}
|
||||||
|
|
||||||
|
def _parse_content(self, content: Union[Dict[str, Any], str]
|
||||||
|
) -> Union[Dict[str, Any], JinjaTemplate]:
|
||||||
|
if not content:
|
||||||
|
return {}
|
||||||
|
elif isinstance(content, str):
|
||||||
|
return JinjaTemplate(content)
|
||||||
|
return content
|
||||||
|
|
||||||
def _make_template(self, name: str, tpl: Dict[str, Any]) -> Template:
|
def _make_template(self, name: str, tpl: Dict[str, Any]) -> Template:
|
||||||
try:
|
try:
|
||||||
return Template(type=EventType.find(tpl.get("type", "m.room.message")),
|
return Template(type=EventType.find(tpl.get("type", "m.room.message")),
|
||||||
variables=self._parse_variables(tpl),
|
variables=self._parse_variables(tpl),
|
||||||
content=tpl.get("content", {})).init()
|
content=self._parse_content(tpl.get("content", None))).init()
|
||||||
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
|
||||||
|
|
||||||
def _get_flags(self, flags: str) -> re.RegexFlag:
|
def _get_flags(self, flags: Union[str, List[str]]) -> re.RegexFlag:
|
||||||
output = self.default_flags
|
if not flags:
|
||||||
|
return self.default_flags
|
||||||
|
output = re.RegexFlag(0)
|
||||||
for flag in flags:
|
for flag in flags:
|
||||||
flag = flag.lower()
|
flag = flag.lower()
|
||||||
if flag == "i" or flag == "ignorecase":
|
if flag == "i" or flag == "ignorecase":
|
||||||
|
@ -215,14 +230,14 @@ class ReactBot(Plugin):
|
||||||
output |= re.ASCII
|
output |= re.ASCII
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def _compile(self, pattern: str) -> RPattern:
|
def _compile(self, pattern: InputPattern) -> RPattern:
|
||||||
flags = self.default_flags
|
flags = self.default_flags
|
||||||
raw = False
|
raw = None
|
||||||
if isinstance(pattern, dict):
|
if isinstance(pattern, dict):
|
||||||
flags = self._get_flags(pattern.get("flags", ""))
|
flags = self._get_flags(pattern.get("flags", ""))
|
||||||
raw = pattern.get("raw", False)
|
raw = pattern.get("raw", False)
|
||||||
pattern = pattern["pattern"]
|
pattern = pattern["pattern"]
|
||||||
if not flags or flags == re.IGNORECASE:
|
if raw is not False and (not flags & re.MULTILINE or raw is True):
|
||||||
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 = ""
|
||||||
|
@ -242,8 +257,11 @@ class ReactBot(Plugin):
|
||||||
return SimplePattern(lambda val: s_pattern in val, ignorecase=ignorecase)
|
return SimplePattern(lambda val: s_pattern in val, ignorecase=ignorecase)
|
||||||
return re.compile(pattern, flags=flags)
|
return re.compile(pattern, flags=flags)
|
||||||
|
|
||||||
def _compile_all(self, patterns: List[str]) -> List[RPattern]:
|
def _compile_all(self, patterns: Union[InputPattern, List[InputPattern]]) -> List[RPattern]:
|
||||||
return [self._compile(pattern) for pattern in patterns]
|
if isinstance(patterns, list):
|
||||||
|
return [self._compile(pattern) for pattern in patterns]
|
||||||
|
else:
|
||||||
|
return [self._compile(patterns)]
|
||||||
|
|
||||||
def _make_rule(self, name: str, rule: Dict[str, Any]) -> Rule:
|
def _make_rule(self, name: str, rule: Dict[str, Any]) -> Rule:
|
||||||
try:
|
try:
|
||||||
|
|
Loading…
Reference in a new issue