Add support for raw json template as content and add docs

This commit is contained in:
Tulir Asokan 2019-06-23 13:10:53 +03:00
parent 3900b04dba
commit 4d5a3a681c
2 changed files with 80 additions and 9 deletions

View file

@ -1,2 +1,55 @@
# reactbot
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.

View file

@ -16,6 +16,7 @@
from typing import (NewType, Optional, Callable, Pattern, Match, Union, Dict, List, Tuple, Set,
Type, Any)
from itertools import chain
import json
import copy
import re
@ -69,6 +70,7 @@ class SimplePattern:
RMatch = Union[Match, BlankMatch]
RPattern = Union[Pattern, SimplePattern]
InputPattern = Union[str, Dict[str, str]]
Index = NewType("Index", Union[str, int, Key])
@ -79,7 +81,7 @@ variable_regex = re.compile(r"\$\${([0-9A-Za-z-_]+)}")
class Template:
type: EventType
variables: Dict[str, JinjaTemplate]
content: Dict[str, Any]
content: Union[Dict[str, Any], JinjaTemplate]
_variable_locations: List[Tuple[Index, ...]] = None
@ -122,6 +124,9 @@ class Template:
variables = {**{name: template.render(event=evt)
for name, template in chain(self.variables.items(), rule_vars.items())},
**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)
for path in self._variable_locations:
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
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:
try:
return Template(type=EventType.find(tpl.get("type", "m.room.message")),
variables=self._parse_variables(tpl),
content=tpl.get("content", {})).init()
content=self._parse_content(tpl.get("content", None))).init()
except Exception as e:
raise ConfigError(f"Failed to load {name}") from e
def _get_flags(self, flags: str) -> re.RegexFlag:
output = self.default_flags
def _get_flags(self, flags: Union[str, List[str]]) -> re.RegexFlag:
if not flags:
return self.default_flags
output = re.RegexFlag(0)
for flag in flags:
flag = flag.lower()
if flag == "i" or flag == "ignorecase":
@ -215,14 +230,14 @@ class ReactBot(Plugin):
output |= re.ASCII
return output
def _compile(self, pattern: str) -> RPattern:
def _compile(self, pattern: InputPattern) -> RPattern:
flags = self.default_flags
raw = False
raw = None
if isinstance(pattern, dict):
flags = self._get_flags(pattern.get("flags", ""))
raw = pattern.get("raw", False)
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
s_pattern = pattern.lower() if ignorecase else pattern
esc = ""
@ -242,8 +257,11 @@ class ReactBot(Plugin):
return SimplePattern(lambda val: s_pattern in val, ignorecase=ignorecase)
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]:
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:
try: