Compare commits

...

50 commits

Author SHA1 Message Date
Tulir Asokan
7fd6dd1a3c Fix linting 2023-10-05 22:31:27 +03:00
Tulir Asokan
3ca366fea9 Blacken and isort code, add pre-commit and CI linting 2023-10-05 22:22:10 +03:00
Tulir Asokan
3507b3b63a Update README.md 2023-10-05 22:13:26 +03:00
Tulir Asokan
e6949c3509 Simplify thread example using new native typed templates 2023-10-05 22:13:26 +03:00
Tulir Asokan
a3baf06ca0 Allow non-string types in variable templates and add magic omit value 2023-10-05 22:13:18 +03:00
Tulir Asokan
299cfdff46 Add example of replying in thread
Fixes #10
2023-10-05 21:53:41 +03:00
Tulir Asokan
feb1a37e11 Move CI script to main maubot repo 2022-06-19 14:27:31 +03:00
Tulir Asokan
a1e5d2c87d Set empty dicts to avoid additional errors when config is invalid 2022-02-24 22:58:34 +02:00
Tulir Asokan
e22fc9e3c1 Update CI artifact expiry 2021-11-28 15:35:43 +02:00
Tulir Asokan
d146f34f72 Bump version to 2.2.0 2021-08-18 00:03:06 +03:00
Tulir Asokan
3964aa6f12 Add support for capturing the match in simple matches 2021-07-28 20:21:44 +03:00
Tulir Asokan
16e4b8e6d8 Fix simple patterns throwing errors 2021-07-21 15:09:29 +03:00
Tulir Asokan
45e22185dc Add capture group example 2021-07-18 19:45:35 +03:00
Tulir Asokan
873cae5821 Fix indexes of capture groups
${{0}} now means the whole match and ${{1}} is the first capture group.
2021-07-18 19:41:35 +03:00
Tulir Asokan
28d6b05913 Log room ID in errors 2021-06-18 21:45:15 +03:00
Tulir Asokan
05e479bb88 Add example for random reactions 2021-06-16 22:24:52 +03:00
Tulir Asokan
e89a5773d8 Fix substituting multiple variables in templates. Fixes #6 2021-06-14 12:52:31 +03:00
Tulir Asokan
821e670fd5 Fix type hint 2020-12-11 20:14:23 +02:00
Tulir Asokan
b213481d7d Expose named capture groups and earlier variables in jinja variables (ref #5) 2020-12-11 19:59:19 +02:00
Tulir Asokan
0790b429b3 Bump version to 2.1.0 2020-09-30 22:21:41 +03:00
Tulir Asokan
8353e43e30 Fix using variables in the middle of template strings 2020-08-27 15:03:32 +03:00
Tulir Asokan
45ee715dc3 Fix or break config parsing 2019-10-17 19:10:16 +03:00
Tulir Asokan
217351d141 Add option to exclude specific rooms from a rule 2019-07-31 19:28:15 +03:00
Tulir Asokan
50141c8a92 Add .gitlab-ci.yml 2019-07-28 22:10:04 +03:00
Tulir Asokan
2ad417da70 Improve stallman regexes more 2019-06-23 15:33:59 +03:00
Tulir Asokan
5a332b13af Ignore archive and other words that start with arch 2019-06-23 15:26:25 +03:00
Tulir Asokan
4cb003f048 Simplify linux not_matches in stallman sample 2019-06-23 15:21:25 +03:00
Tulir Asokan
e7558dc00e Bump version to 2.0.0 2019-06-23 14:33:13 +03:00
Tulir Asokan
d5c0aa9e02 Update existing flood limits when editing config 2019-06-23 14:27:32 +03:00
Tulir Asokan
b26f9cf6eb Separate per-user and per-room flood limits 2019-06-23 14:20:25 +03:00
Tulir Asokan
85a7967888 Add initial flood prevention 2019-06-23 14:14:28 +03:00
Tulir Asokan
8e16575d3b Only allow m.text and m.emote as input 2019-06-23 13:57:57 +03:00
Tulir Asokan
54cee71497 Add missing dot 2019-06-23 13:45:23 +03:00
Tulir Asokan
b3ebb91598 Add attribution for stallman sample 2019-06-23 13:44:43 +03:00
Tulir Asokan
c5c2590a96 Split into many files 2019-06-23 13:43:10 +03:00
Tulir Asokan
44ae0529be Fix readme: the bot is not simple anymore 2019-06-23 13:13:43 +03:00
Tulir Asokan
f2ac1a9c8d Link to samples in README.md 2019-06-23 13:13:03 +03:00
Tulir Asokan
4d5a3a681c Add support for raw json template as content and add docs 2019-06-23 13:10:53 +03:00
Tulir Asokan
3900b04dba Allow non-string variables 2019-06-23 12:14:15 +03:00
Tulir Asokan
47e1d5d7ec Add sample configs for jesaribot and stallman bot 2019-06-23 12:05:10 +03:00
Tulir Asokan
9a0f6da774 Fix mistake in pattern raw flag 2019-06-23 12:03:56 +03:00
Tulir Asokan
98c3bfb252 Add support for regex flags 2019-06-23 03:45:00 +03:00
Tulir Asokan
3992db4464 Make non-regex matching faster 2019-06-23 03:15:22 +03:00
Tulir Asokan
2c54aa395a Fix mistake and handle errors 2019-06-23 02:56:44 +03:00
Tulir Asokan
0fcf7a8319 Make loading templates and rules cleaner 2019-06-23 02:12:09 +03:00
Tulir Asokan
d0e9aad1ff Remove unused imports 2019-06-23 02:08:30 +03:00
Tulir Asokan
d237d3bbc9 Ignore own events and fix initial pattern match 2019-06-23 02:02:02 +03:00
Tulir Asokan
1245e6540a Replace jesari with alot in example config 2019-06-23 01:57:15 +03:00
Tulir Asokan
4aa98b888b Update README.md 2019-06-23 01:53:44 +03:00
Tulir Asokan
f159305650 Over-engineer to support arbitrary response contents 2019-06-23 01:49:05 +03:00
19 changed files with 1238 additions and 81 deletions

24
.github/workflows/python-lint.yml vendored Normal file
View file

@ -0,0 +1,24 @@
name: Python lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- uses: isort/isort-action@master
with:
sortPaths: "./reactbot"
- uses: psf/black@stable
with:
src: "./reactbot"
- name: pre-commit
run: |
pip install pre-commit
pre-commit run -av trailing-whitespace
pre-commit run -av end-of-file-fixer
pre-commit run -av check-added-large-files

3
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,3 @@
include:
- project: 'maubot/maubot'
file: '/.gitlab-ci-plugin.yml'

19
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,19 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
- id: end-of-file-fixer
- id: check-added-large-files
- repo: https://github.com/psf/black
rev: 23.9.1
hooks:
- id: black
language_version: python3
files: ^reactbot/.*\.pyi?$
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
files: ^reactbot/.*\.pyi?$

View file

@ -1,2 +1,70 @@
# reactbot
A simple [maubot](https://github.com/maubot/maubot) that reacts to messages that match predefined rules.
A [maubot](https://github.com/maubot/maubot) that responds to messages that match predefined rules.
## Samples
* The [base config](base-config.yaml) contains a cookie reaction for TWIM submissions
in [#thisweekinmatrix:matrix.org](https://matrix.to/#/#thisweekinmatrix:matrix.org)
and an image response for "alot".
* [samples/jesari.yaml](samples/jesari.yaml) contains a replacement for [jesaribot](https://github.com/maubot/jesaribot).
* [samples/stallman.yaml](samples/stallman.yaml) contains a Stallman interject bot.
* [samples/random-reaction.yaml](samples/random-reaction.yaml) has an example of
a randomized reaction to matching messages.
* [samples/nitter.yaml](samples/nitter.yaml) has an example of matching tweet links
and responding with a corresponding nitter.net link.
* [samples/thread.yaml](samples/thread.yaml) has an example of replying in a thread.
## 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 that start with `{{` are parsed as jinja2 templates and get the
maubot event object in `event`. As of v3, variables are parsed using jinja2's
[native types mode](https://jinja.palletsprojects.com/en/3.1.x/nativetypes/),
which means the output can be a non-string type.
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

@ -1,5 +1,49 @@
templates:
reaction:
type: m.reaction
variables:
react_to_event: "{{event.content.get_reply_to() or event.event_id}}"
content:
m.relates_to:
rel_type: m.annotation
event_id: $${react_to_event}
key: $${reaction}
alot:
type: m.room.message
content:
msgtype: m.image
body: image.png
url: "mxc://maunium.net/eFnyRdgJOHlKXCxzoKPQbwLV"
info:
mimetype: image/png
w: 680
h: 510
size: 247492
thumbnail_url: "mxc://maunium.net/PMxffxMfcUZeWeeYMDCdghBG"
thumbnail_info:
w: 680
h: 510
mimetype: image/png
size: 233763
default_flags:
- ignorecase
antispam:
room:
max: 1
delay: 60
user:
max: 2
delay: 60
rules:
- rooms: ["!FPUfgzXYWTKgIrwKxW:matrix.org"]
matches: [^TWIM]
react_to_reply: true
reaction: 🍪
twim_cookies:
rooms: ["!FPUfgzXYWTKgIrwKxW:matrix.org"]
matches: [^TWIM]
template: reaction
variables:
reaction: 🍪
alot:
matches: [alot]
template: alot

View file

@ -1,6 +1,6 @@
maubot: 0.1.0
id: xyz.maubot.reactbot
version: 1.0.0
version: 2.2.0
license: AGPL-3.0-or-later
modules:
- reactbot

11
pyproject.toml Normal file
View file

@ -0,0 +1,11 @@
[tool.isort]
profile = "black"
force_to_top = "typing"
from_first = true
combine_as_imports = true
known_first_party = ["mautrix", "maubot"]
line_length = 99
[tool.black]
line-length = 99
target-version = ["py38"]

View file

@ -1,75 +0,0 @@
# reminder - 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
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from typing import Pattern, List, Set, Type
from attr import dataclass
import re
from mautrix.types import RoomID, EventType
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from maubot import Plugin, MessageEvent
from maubot.handlers import event
class Config(BaseProxyConfig):
def do_update(self, helper: ConfigUpdateHelper) -> None:
helper.copy("rules")
@dataclass
class Rule:
rooms: Set[RoomID]
matches: List[Pattern]
reaction: str
react_to_reply: bool
def is_match(self, evt: MessageEvent) -> bool:
if evt.room_id not in self.rooms:
return False
for match in self.matches:
if match.match(evt.content.body):
return True
return False
class ReactBot(Plugin):
rules: List[Rule]
@classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]:
return Config
async def start(self) -> None:
await super().start()
self.rules = []
self.on_external_config_update()
def on_external_config_update(self) -> None:
self.config.load_and_update()
self.rules = [Rule(rooms=set(rule.get("rooms", [])),
matches=[re.compile(match) for match in rule.get("matches")],
reaction=rule.get("reaction", "\U0001F44D"),
react_to_reply=rule.get("react_to_reply", False))
for rule in self.config["rules"]]
@event.on(EventType.ROOM_MESSAGE)
async def echo_handler(self, evt: MessageEvent) -> None:
for rule in self.rules:
if rule.is_match(evt):
if rule.react_to_reply and evt.content.get_reply_to():
await self.client.react(evt.room_id, evt.content.get_reply_to(), rule.reaction)
else:
await evt.react(rule.reaction)

1
reactbot/__init__.py Normal file
View file

@ -0,0 +1 @@
from .bot import ReactBot

109
reactbot/bot.py Normal file
View file

@ -0,0 +1,109 @@
# reminder - 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
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from typing import Dict, Tuple, Type
import time
from attr import dataclass
from maubot import MessageEvent, Plugin
from maubot.handlers import event
from mautrix.types import EventType, MessageType, RoomID, UserID
from mautrix.util.config import BaseProxyConfig
from .config import Config, ConfigError
@dataclass
class FloodInfo:
max: int
delay: int
count: int
last_message: int
def bump(self) -> bool:
now = int(time.time())
if self.last_message + self.delay < now:
self.count = 0
self.count += 1
if self.count > self.max:
return True
self.last_message = now
return False
class ReactBot(Plugin):
allowed_msgtypes: Tuple[MessageType, ...] = (MessageType.TEXT, MessageType.EMOTE)
user_flood: Dict[UserID, FloodInfo]
room_flood: Dict[RoomID, FloodInfo]
@classmethod
def get_config_class(cls) -> Type[BaseProxyConfig]:
return Config
async def start(self) -> None:
await super().start()
self.user_flood = {}
self.room_flood = {}
self.on_external_config_update()
def on_external_config_update(self) -> None:
self.config.load_and_update()
try:
self.config.parse_data()
except ConfigError:
self.log.exception("Failed to load config")
for fi in self.user_flood.values():
fi.max = self.config["antispam.user.max"]
fi.delay = self.config["antispam.user.delay"]
for fi in self.room_flood.values():
fi.max = self.config["antispam.room.max"]
fi.delay = self.config["antispam.room.delay"]
def _make_flood_info(self, for_type: str) -> "FloodInfo":
return FloodInfo(
max=self.config[f"antispam.{for_type}.max"],
delay=self.config[f"antispam.{for_type}.delay"],
count=0,
last_message=0,
)
def _get_flood_info(self, flood_map: dict, key: str, for_type: str) -> "FloodInfo":
try:
return flood_map[key]
except KeyError:
fi = flood_map[key] = self._make_flood_info(for_type)
return fi
def is_flood(self, evt: MessageEvent) -> bool:
return (
self._get_flood_info(self.user_flood, evt.sender, "user").bump()
or self._get_flood_info(self.room_flood, evt.room_id, "room").bump()
)
@event.on(EventType.ROOM_MESSAGE)
async def event_handler(self, evt: MessageEvent) -> None:
if evt.sender == self.client.mxid or evt.content.msgtype not in self.allowed_msgtypes:
return
for name, rule in self.config.rules.items():
match = rule.match(evt)
if match is not None:
if self.is_flood(evt):
return
try:
await rule.execute(evt, match)
except Exception:
self.log.exception(f"Failed to execute {name} in {evt.room_id}")
return

142
reactbot/config.py Normal file
View file

@ -0,0 +1,142 @@
# reminder - 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
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from typing import Any, Dict, List, Union
import re
from jinja2 import Template as JinjaStringTemplate
from jinja2.nativetypes import NativeTemplate as JinjaNativeTemplate
from mautrix.types import EventType
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
from .rule import RPattern, Rule
from .simplepattern import SimplePattern
from .template import Template
InputPattern = Union[str, Dict[str, str]]
class Config(BaseProxyConfig):
rules: Dict[str, Rule]
templates: Dict[str, Template]
default_flags: re.RegexFlag
def do_update(self, helper: ConfigUpdateHelper) -> None:
helper.copy("rules")
helper.copy("templates")
helper.copy("default_flags")
helper.copy("antispam.user.max")
helper.copy("antispam.user.delay")
helper.copy("antispam.room.max")
helper.copy("antispam.room.delay")
def parse_data(self) -> None:
self.default_flags = re.RegexFlag(0)
self.templates = {}
self.rules = {}
self.default_flags = self._get_flags(self["default_flags"])
self.templates = {
name: self._make_template(name, tpl) for name, tpl in self["templates"].items()
}
self.rules = {name: self._make_rule(name, rule) for name, rule in self["rules"].items()}
def _make_rule(self, name: str, rule: Dict[str, Any]) -> Rule:
try:
return Rule(
rooms=set(rule.get("rooms", [])),
not_rooms=set(rule.get("not_rooms", [])),
matches=self._compile_all(rule["matches"]),
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),
)
except Exception as e:
raise ConfigError(f"Failed to load {name}") from e
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=self._parse_content(tpl.get("content", None)),
).init()
except Exception as e:
raise ConfigError(f"Failed to load {name}") from e
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 _compile(self, pattern: InputPattern) -> RPattern:
flags = self.default_flags
raw = None
if isinstance(pattern, dict):
flags = self._get_flags(pattern["flags"]) if "flags" in pattern else flags
raw = pattern.get("raw", False)
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)
@staticmethod
def _parse_variables(data: Dict[str, Any]) -> Dict[str, Any]:
return {
name: (
JinjaNativeTemplate(var_tpl)
if isinstance(var_tpl, str) and var_tpl.startswith("{{")
else var_tpl
)
for name, var_tpl in data.get("variables", {}).items()
}
@staticmethod
def _parse_content(
content: Union[Dict[str, Any], str]
) -> Union[Dict[str, Any], JinjaStringTemplate]:
if not content:
return {}
elif isinstance(content, str):
return JinjaStringTemplate(content)
return content
@staticmethod
def _get_flags(flags: Union[str, List[str]]) -> re.RegexFlag:
output = re.RegexFlag(0)
for flag in flags:
flag = flag.lower()
if flag == "i" or flag == "ignorecase":
output |= re.IGNORECASE
elif flag == "s" or flag == "dotall":
output |= re.DOTALL
elif flag == "x" or flag == "verbose":
output |= re.VERBOSE
elif flag == "m" or flag == "multiline":
output |= re.MULTILINE
elif flag == "l" or flag == "locale":
output |= re.LOCALE
elif flag == "u" or flag == "unicode":
output |= re.UNICODE
elif flag == "a" or flag == "ascii":
output |= re.ASCII
return output
class ConfigError(Exception):
pass

65
reactbot/rule.py Normal file
View file

@ -0,0 +1,65 @@
# reminder - 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
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from typing import Any, Dict, List, Match, Optional, Pattern, Set, Union
from attr import dataclass
from maubot import MessageEvent
from mautrix.types import EventType, RoomID
from .simplepattern import SimplePattern
from .template import OmitValue, Template
RPattern = Union[Pattern, SimplePattern]
@dataclass
class Rule:
rooms: Set[RoomID]
not_rooms: Set[RoomID]
matches: List[RPattern]
not_matches: List[RPattern]
template: Template
type: Optional[EventType]
variables: Dict[str, Any]
def _check_not_match(self, body: str) -> bool:
for pattern in self.not_matches:
if pattern.search(body):
return True
return False
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
for pattern in self.matches:
match = pattern.search(evt.content.body)
if match:
if self._check_not_match(evt.content.body):
return None
return match
return None
async def execute(self, evt: MessageEvent, match: Match) -> None:
extra_vars = {
"0": match.group(0),
**{str(i + 1): val for i, val in enumerate(match.groups())},
**match.groupdict(),
}
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)

94
reactbot/simplepattern.py Normal file
View file

@ -0,0 +1,94 @@
# reminder - 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from typing import Callable, Dict, List, NamedTuple, Optional
import re
class SimpleMatch(NamedTuple):
value: str
def groups(self) -> List[str]:
return [self.value]
def group(self, group: int) -> Optional[str]:
if group == 0:
return self.value
return None
def groupdict(self) -> Dict[str, str]:
return {}
def matcher_equals(val: str, pattern: str) -> bool:
return val == pattern
def matcher_startswith(val: str, pattern: str) -> bool:
return val.startswith(pattern)
def matcher_endswith(val: str, pattern: str) -> bool:
return val.endswith(pattern)
def matcher_contains(val: str, pattern: str) -> bool:
return pattern in val
SimpleMatcherFunc = Callable[[str, str], bool]
class SimplePattern:
matcher: SimpleMatcherFunc
pattern: str
ignorecase: bool
def __init__(self, matcher: SimpleMatcherFunc, pattern: str, ignorecase: bool) -> None:
self.matcher = matcher
self.pattern = pattern
self.ignorecase = ignorecase
def search(self, val: str) -> SimpleMatch:
if self.ignorecase:
val = val.lower()
if self.matcher(val, self.pattern):
return SimpleMatch(self.pattern)
@staticmethod
def compile(
pattern: str, flags: re.RegexFlag = re.RegexFlag(0), force_raw: bool = False
) -> Optional["SimplePattern"]:
ignorecase = flags == re.IGNORECASE
s_pattern = pattern.lower() if ignorecase else pattern
esc = ""
if not force_raw:
esc = re.escape(pattern)
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]
func = matcher_equals
elif first == "^" and (force_raw or esc == f"\\^{pattern[1:]}"):
s_pattern = s_pattern[1:]
func = matcher_startswith
elif last == "$" and (force_raw or esc == f"{pattern[:-1]}\\$"):
s_pattern = s_pattern[:-1]
func = matcher_endswith
elif force_raw or esc == pattern:
func = matcher_contains
else:
# Not a simple pattern
return None
return SimplePattern(matcher=func, pattern=s_pattern, ignorecase=ignorecase)

115
reactbot/template.py Normal file
View file

@ -0,0 +1,115 @@
# reminder - 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
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# 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/>.
from typing import Any, Dict, List, Tuple, Union
from itertools import chain
import copy
import json
import re
from attr import dataclass
from jinja2 import Template as JinjaStringTemplate
from jinja2.nativetypes import Template as JinjaNativeTemplate
from mautrix.types import Event, EventType
class Key(str):
pass
variable_regex = re.compile(r"\$\${([0-9A-Za-z-_]+)}")
OmitValue = object()
global_vars = {
"omit": OmitValue,
}
Index = Union[str, int, Key]
@dataclass
class Template:
type: EventType
variables: Dict[str, Any]
content: Union[Dict[str, Any], JinjaStringTemplate]
_variable_locations: List[Tuple[Index, ...]] = None
def init(self) -> "Template":
self._variable_locations = []
self._map_variable_locations((), self.content)
return self
def _map_variable_locations(self, path: Tuple[Index, ...], data: Any) -> None:
if isinstance(data, list):
for i, v in enumerate(data):
self._map_variable_locations((*path, i), v)
elif isinstance(data, dict):
for k, v in data.items():
if variable_regex.search(k):
self._variable_locations.append((*path, Key(k)))
self._map_variable_locations((*path, k), v)
elif isinstance(data, str):
if variable_regex.search(data):
self._variable_locations.append(path)
@classmethod
def _recurse(cls, content: Any, path: Tuple[Index, ...]) -> Any:
if len(path) == 0:
return content
return cls._recurse(content[path[0]], path[1:])
@staticmethod
def _replace_variables(tpl: str, variables: Dict[str, Any]) -> str:
full_var_match = variable_regex.fullmatch(tpl)
if full_var_match:
# Whole field is a single variable, just return the value to allow non-string types.
return variables[full_var_match.group(1)]
return variable_regex.sub(lambda match: str(variables[match.group(1)]), tpl)
def execute(
self, evt: Event, rule_vars: Dict[str, Any], extra_vars: Dict[str, str]
) -> Dict[str, Any]:
variables = extra_vars
for name, template in chain(rule_vars.items(), self.variables.items()):
if isinstance(template, JinjaNativeTemplate):
rendered_var = template.render(event=evt, variables=variables, **global_vars)
if (
not isinstance(rendered_var, (str, int, list, tuple, dict, bool))
and rendered_var is not None
and rendered_var is not OmitValue
):
rendered_var = str(rendered_var)
variables[name] = rendered_var
else:
variables[name] = template
if isinstance(self.content, JinjaStringTemplate):
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])
key = path[-1]
if isinstance(key, Key):
key = str(key)
data[self._replace_variables(key, variables)] = data.pop(key)
else:
replaced_data = self._replace_variables(data[key], variables)
if replaced_data is OmitValue:
del data[key]
else:
data[key] = replaced_data
return content

26
samples/jesari.yaml Normal file
View file

@ -0,0 +1,26 @@
templates:
jesari:
type: m.room.message
content:
msgtype: m.image
body: putkiteippi.gif
url: mxc://maunium.net/LNjeTZvDEaUdQAROvWGHLLDi
info:
mimetype: image/gif
w: 1280
h: 535
size: 7500893
thumbnail_url: mxc://maunium.net/xdhlegZQgGwlMRzBfhNxyEfb
thumbnail_info:
mimetype: image/png
w: 800
h: 334
size: 417896
default_flags:
- ignorecase
rules:
jesari:
matches: [jesaro?i]
template: jesari

15
samples/nitter.yaml Normal file
View file

@ -0,0 +1,15 @@
templates:
nitter:
type: m.room.message
content:
msgtype: m.text
body: https://nitter.net/$${1}/status/$${2}
default_flags:
- ignorecase
rules:
twitter:
matches:
- https://twitter.com/(.+?)/status/(\d+)
template: nitter

View file

@ -0,0 +1,25 @@
templates:
random_reaction:
type: m.reaction
variables:
react_to_event: '{{event.event_id}}'
reaction: '{{ variables.reaction_choices | random }}'
content:
m.relates_to:
rel_type: m.annotation
event_id: $${react_to_event}
key: $${reaction}
default_flags:
- ignorecase
rules:
random:
matches:
- hmm
template: random_reaction
variables:
reaction_choices:
- 🤔
- 🧐
- 🤨

421
samples/stallman.yaml Normal file
View file

@ -0,0 +1,421 @@
# Messages from https://github.com/interwho/stallman-bot (MIT license)
templates:
plaintext_notice:
type: m.room.message
content:
msgtype: m.notice
body: $${message}
default_flags:
- ignorecase
antispam:
room:
max: 1
delay: 60
user:
max: 2
delay: 60
rules:
linux:
matches:
- linux
not_matches:
- gnu
- kernel
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. What you're referring to as Linux, is in fact, GNU/Linux, or as I've recently taken to calling it, GNU plus Linux. Linux is not an operating system unto itself, but rather another free component of a fully functioning GNU system made useful by the GNU corelibs, shell utilities and vital system components comprising a full OS as defined by POSIX.
Many computer users run a modified version of the GNU system every day, without realizing it. Through a peculiar turn of events, the version of GNU which is widely used today is often called "Linux", and many of its users are not aware that it is basically the GNU system, developed by the GNU Project.
There really is a Linux, and these people are using it, but it is just a part of the system they use. Linux is the kernel: the program in the system that allocates the machine's resources to the other programs that you run. The kernel is an essential part of an operating system, but useless by itself; it can only function in the context of a complete operating system. Linux is normally used in combination with the GNU operating system: the whole system is basically GNU with Linux added, or GNU/Linux. All the so-called "Linux" distributions are really distributions of GNU/Linux.
bsdstyle:
matches: [bsd( |-)style]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The expression "BSD-style license" leads to confusion because it lumps together licenses that have important differences. For instance, the original BSD license with the advertising clause is incompatible with the GNU General Public License, but the revised BSD license is compatible with the GPL.
To avoid confusion, it is best to name the specific license in question and avoid the vague term "BSD-style."
cloudcomp:
matches: [cloud computing, the cloud]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term "cloud computing" is a marketing buzzword with no clear meaning. It is used for a range of different activities whose only common characteristic is that they use the Internet for something beyond transmitting files. Thus, the term is a nexus of confusion. If you base your thinking on it, your thinking will be vague.
When thinking about or responding to a statement someone else has made using this term, the first step is to clarify the topic. Which kind of activity is the statement really about, and what is a good, clear term for that activity? Once the topic is clear, the discussion can head for a useful conclusion.
Curiously, Larry Ellison, a proprietary software developer, also noted the vacuity of the term "cloud computing." He decided to use the term anyway because, as a proprietary software developer, he isn't motivated by the same ideals as we are.
One of the many meanings of "cloud computing" is storing your data in online services. That exposes you to surveillance.
Another meaning (which overlaps that but is not the same thing) is Software as a Service, which denies you control over your computing.
Another meaning is renting a remote physical server, or virtual server. These can be ok under certain circumstances.
closed:
matches: [closed source]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Describing nonfree software as "closed" clearly refers to the term "open source". In the free software movement, we do not want to be confused with the open source camp, so we are careful to avoid saying things that would encourage people to lump us in with them. For instance, we avoid describing nonfree software as "closed". We call it "nonfree" or "proprietary".
commercial:
matches: [commercial]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please don't use "commercial" as a synonym for "nonfree." That confuses two entirely different issues.
A program is commercial if it is developed as a business activity. A commercial program can be free or nonfree, depending on its manner of distribution. Likewise, a program developed by a school or an individual can be free or nonfree, depending on its manner of distribution. The two questions--what sort of entity developed the program and what freedom its users have--are independent.
In the first decade of the free software movement, free software packages were almost always noncommercial; the components of the GNU/Linux operating system were developed by individuals or by nonprofit organizations such as the FSF and universities. Later, in the 1990s, free commercial software started to appear.
Free commercial software is a contribution to our community, so we should encourage it. But people who think that "commercial" means "nonfree" will tend to think that the "free commercial" combination is self-contradictory, and dismiss the possibility. Let's be careful not to use the word "commercial" in that way.
consumer:
matches: [consumer]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term "consumer," when used to refer to computer users, is loaded with assumptions we should reject. Playing a digital recording, or running a program, does not consume it.
The terms "producer" and "consumer" come from economic theory, and bring with them its narrow perspective and misguided assumptions. These tend to warp your thinking.
In addition, describing the users of software as "consumers" presumes a narrow role for them: it regards them as sheep that passively graze on what others make available to them.
This kind of thinking leads to travesties like the CBDTPA "Consumer Broadband and Digital Television Promotion Act" which would require copying restriction facilities in every digital device. If all the users do is "consume," then why should they mind?
The shallow economic conception of users as "consumers" tends to go hand in hand with the idea that published works are mere "content."
To describe people who are not limited to passive use of works, we suggest terms such as "individuals" and "citizens".
content:
matches: [content]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. If you want to describe a feeling of comfort and satisfaction, by all means say you are "content," but using the word as a noun to describe written and other works of authorship adopts an attitude you might rather avoid. It regards these works as a commodity whose purpose is to fill a box and make money. In effect, it disparages the works themselves.
Those who use this term are often the publishers that push for increased copyright power in the name of the authors ("creators," as they say) of the works. The term "content" reveals their real attitude towards these works and their authors. (See Courtney Love's open letter to Steve Case and search for "content provider" in that page. Alas, Ms. Love is unaware that the term "intellectual property" is also biased and confusing.)
However, as long as other people use the term "content provider", political dissidents can well call themselves "malcontent providers".
The term "content management" takes the prize for vacuity. "Content" means "some sort of information," and "management" in this context means "doing something with it." So a "content management system" is a system for doing something to some sort of information. Nearly all programs fit that description.
In most cases, that term really refers to a system for updating pages on a web site. For that, we recommend the term "web site revision system" (WRS).
digital_goods:
matches: [digital goods]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term "digital goods," as applied to copies of works of authorship, erroneously identifies them with physical goods--which cannot be copied, and which therefore have to be manufactured and sold.
digital_locks:
matches: [digital locks]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. "Digital locks" is used to refer to Digital Restrictions Management by some who criticize it. The problem with this term is that it fails to show what's wrong with the practice.
Locks are not necessarily an injustice. You probably own several locks, and their keys or codes as well; you may find them useful or troublesome, but either way they don't oppress you, because you can open and close them.
DRM is like a lock placed on you by someone else, who refuses to give you the key -- in other words, like handcuffs. Therefore, we call them "digital handcuffs", not "digital locks".
A number of campaigns have chosen the unwise term "digital locks"; therefore, to correct the mistake, we must work firmly against it. We may support a campaign that criticizes "digital locks", because we might agree with the substance; but when we do, we always state our rejection of that term and conspicuously say "digital handcuffs" so as to set a better example.
drm:
matches: [drm, digital rights management]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. "Digital Rights Management" refers to technical schemes designed to impose restrictions on computer users. The use of the word "rights" in this term is propaganda, designed to lead you unawares into seeing the issue from the viewpoint of the few that impose the restrictions, and ignoring that of the general public on whom these restrictions are imposed.
Good alternatives include "Digital Restrictions Management," and "digital handcuffs."
eco:
matches: [ecosystem]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. It is a mistake to describe the free software community, or any human community, as an "ecosystem," because that word implies the absence of ethical judgment.
The term "ecosystem" implicitly suggests an attitude of nonjudgmental observation: don't ask how what should happen, just study and explain what does happen. In an ecosystem, some organisms consume other organisms. We do not ask whether it is fair for an owl to eat a mouse or for a mouse to eat a plant, we only observe that they do so. Species' populations grow or shrink according to the conditions; this is neither right nor wrong, merely an ecological phenomenon.
By contrast, beings that adopt an ethical stance towards their surroundings can decide to preserve things that, on their own, might vanish--such as civil society, democracy, human rights, peace, public health, clean air and water, endangered species, traditional arts…and computer users' freedom.
freeware:
matches: [freeware]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please don't use the term "freeware" as a synonym for "free software." The term "freeware" was used often in the 1980s for programs released only as executables, with source code not available. Today it has no particular agreed-on definition.
When using languages other than English, please avoid borrowing English terms such as "free software" or "freeware." It is better to translate the term "free software" into your language.
By using a word in your own language, you show that you are really referring to freedom and not just parroting some mysterious foreign marketing concept. The reference to freedom may at first seem strange or disturbing to your compatriots, but once they see that it means exactly what it says, they will really understand what the issue is.
give:
matches: [give away software]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. It's misleading to use the term "give away" to mean "distribute a program as free software." This locution has the same problem as "for free": it implies the issue is price, not freedom. One way to avoid the confusion is to say "release as free software."
hacker:
matches: [hacker]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. A hacker is someone who enjoys playful cleverness--not necessarily with computers. The programmers in the old MIT free software community of the 60s and 70s referred to themselves as hackers. Around 1980, journalists who discovered the hacker community mistakenly took the term to mean "security breaker."
Please don't spread this mistake. People who break security are "crackers."
ip:
matches: [intellectual property]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Publishers and lawyers like to describe copyright as "intellectual property"--a term also applied to patents, trademarks, and other more obscure areas of law. These laws have so little in common, and differ so much, that it is ill-advised to generalize about them. It is best to talk specifically about "copyright," or about "patents," or about "trademarks."
The term "intellectual property" carries a hidden assumption--that the way to think about all these disparate issues is based on an analogy with physical objects, and our conception of them as physical property.
When it comes to copying, this analogy disregards the crucial difference between material objects and information: information can be copied and shared almost effortlessly, while material objects can't be.
To avoid spreading unnecessary bias and confusion, it is best to adopt a firm policy not to speak or even think in terms of "intellectual property".
The hypocrisy of calling these powers "rights" is starting to make the World "Intellectual Property" Organization embarrassed.
lamp:
matches: [(\s|^)lamp(\s|$)]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. "LAMP" stands for "Linux, Apache, MySQL and PHP"--a common combination of software to use on a web server, except that "Linux" in this context really refers to the GNU/Linux system. So instead of "LAMP" it should be "GLAMP": "GNU, Linux, Apache, MySQL and PHP."
market:
matches: [software market]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. It is misleading to describe the users of free software, or the software users in general, as a "market."
This is not to say there is no room for markets in the free software community. If you have a free software support business, then you have clients, and you trade with them in a market. As long as you respect their freedom, we wish you success in your market.
But the free software movement is a social movement, not a business, and the success it aims for is not a market success. We are trying to serve the public by giving it freedom--not competing to draw business away from a rival. To equate this campaign for freedom to a business' efforts for mere success is to deny the importance of freedom and legitimize proprietary software.
monetize:
matches: [monetize]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The natural meaning of "monetize" is "convert into money". If you make something and then convert it into money, that means there is nothing left except money, so nobody but you has gained anything, and you contribute nothing to the world.
By contrast, a productive and ethical business does not convert all of its product into money. Part of it is a contribution to the rest of the world.
mp3:
matches: [mp3 player]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. In the late 1990s it became feasible to make portable, solid-state digital audio players. Most support the patented MP3 codec, but not all. Some support the patent-free audio codecs Ogg Vorbis and FLAC, and may not even support MP3-encoded files at all, precisely to avoid these patents. To call such players "MP3 players" is not only confusing, it also puts MP3 in an undeserved position of privilege which encourages people to continue using that vulnerable format. We suggest the terms "digital audio player," or simply "audio player" if context permits.
open:
matches: [open source]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please avoid using the term "open" or "open source" as a substitute for "free software". Those terms refer to a different position based on different values. Free software is a political movement; open source is a development model. When referring to the open source position, using its name is appropriate; but please do not use it to label us or our work--that leads people to think we share those views.
pc:
matches: [(\s|^)pcs?(\s|$)]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. It's OK to use the abbreviation "PC" to refer to a certain kind of computer hardware, but please don't use it with the implication that the computer is running Microsoft Windows. If you install GNU/Linux on the same computer, it is still a PC.
The term "WC" has been suggested for a computer running Windows.
ps:
matches: [photoshop]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please avoid using the term "photoshop" as a verb, meaning any kind of photo manipulation or image editing in general. Photoshop is just the name of one particular image editing program, which should be avoided since it is proprietary. There are plenty of free programs for editing images, such as the GIMP.
piracy:
matches: [piracy, pirate]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Publishers often refer to copying they don't approve of as "piracy." In this way, they imply that it is ethically equivalent to attacking ships on the high seas, kidnapping and murdering the people on them. Based on such propaganda, they have procured laws in most of the world to forbid copying in most (or sometimes all) circumstances. (They are still pressuring to make these prohibitions more complete.)
If you don't believe that copying not approved by the publisher is just like kidnapping and murder, you might prefer not to use the word "piracy" to describe it. Neutral terms such as "unauthorized copying" (or "prohibited copying" for the situation where it is illegal) are available for use instead. Some of us might even prefer to use a positive term such as "sharing information with your neighbor."
powerpoint:
matches: [powerpoint, \sppt(\s|$)]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please avoid using the term "PowerPoint" to mean any kind of slide presentation. "PowerPoint" is just the name of one particular proprietary program to make presentations, and there are plenty of free program for presentations, such as TeX's beamer class and OpenOffice.org's Impress.
protection:
matches: [protection]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Publishers' lawyers love to use the term "protection" to describe copyright. This word carries the implication of preventing destruction or suffering; therefore, it encourages people to identify with the owner and publisher who benefit from copyright, rather than with the users who are restricted by it.
It is easy to avoid "protection" and use neutral terms instead. For example, instead of saying, "Copyright protection lasts a very long time," you can say, "Copyright lasts a very long time."
If you want to criticize copyright instead of supporting it, you can use the term "copyright restrictions." Thus, you can say, "Copyright restrictions last a very long time."
The term "protection" is also used to describe malicious features. For instance, "copy protection" is a feature that interferes with copying. From the user's point of view, this is obstruction. So we could call that malicious feature "copy obstruction." More often it is called Digital Restrictions Management (DRM)--see the Defective by Design campaign.
sellsoft:
matches: [sell software, selling software]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term "sell software" is ambiguous. Strictly speaking, exchanging a copy of a free program for a sum of money is selling; but people usually associate the term "sell" with proprietary restrictions on the subsequent use of the software. You can be more precise, and prevent confusion, by saying either "distributing copies of a program for a fee" or "imposing proprietary restrictions on the use of a program," depending on what you mean.
softwareindustry:
matches: [software industry]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term "software industry" encourages people to imagine that software is always developed by a sort of factory and then delivered to "consumers." The free software community shows this is not the case. Software businesses exist, and various businesses develop free and/or nonfree software, but those that develop free software are not run like factories.
The term "industry" is being used as propaganda by advocates of software patents. They call software development "industry" and then try to argue that this means it should be subject to patent monopolies. The European Parliament, rejecting software patents in 2003, voted to define "industry" as "automated production of material goods."
trustedcomp:
matches: [trusted computing]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. "Trusted computing" is the proponents' name for a scheme to redesign computers so that application developers can trust your computer to obey them instead of you. From their point of view, it is "trusted"; from your point of view, it is "treacherous."
vendor:
matches: [vendor]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please don't use the term "vendor" to refer generally to anyone that develops or packages software. Many programs are developed in order to sell copies, and their developers are therefore their vendors; this even includes some free software packages. However, many programs are developed by volunteers or organizations which do not intend to sell copies. These developers are not vendors. Likewise, only some of the packagers of GNU/Linux distributions are vendors. We recommend the general term "supplier" instead.
arch:
matches: (^|\s)arch($|\s)
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Arch has the two usual problems: there's no clear policy about what software can be included, and nonfree blobs are shipped with their kernel. Arch also has no policy about not distributing nonfree software through their normal channels.
centos:
matches: [centos]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. We're not aware of problems in CentOS aside from the two usual ones: there's no clear policy about what software can be included, and nonfree blobs are shipped with the kernel. Of course, with no firm policy in place, there might be other nonfree software included that we missed.
debian:
matches: [debian]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Debian's Social Contract states the goal of making Debian entirely free software, and Debian conscientiously keeps nonfree software out of the official Debian system. However, Debian also provides a repository of nonfree software. According to the project, this software is "not part of the Debian system," but the repository is hosted on many of the project's main servers, and people can readily learn about these nonfree packages by browsing Debian's online package database.
There is also a "contrib" repository; its packages are free, but some of them exist to load separately distributed proprietary programs. This too is not thoroughly separated from the main Debian distribution.
Previous releases of Debian included nonfree blobs with the kernel. With the release of Debian 6.0 ("squeeze") in February 2011, these blobs have been moved out of the main distribution to separate packages in the nonfree repository. However, the problem partly remains: the installer in some cases recommends these nonfree firmware files for the peripherals on the machine.
fedora:
matches: [fedora]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Fedora does have a clear policy about what can be included in the distribution, and it seems to be followed carefully. The policy requires that most software and all fonts be available under a free license, but makes an exception for certain kinds of nonfree firmware. Unfortunately, the decision to allow that firmware in the policy keeps Fedora from meeting the free system distribution guidelines.
seal:
matches: [(fuck|screw|go away|die)\s?(you)?\s?(linux|stallman|gpl|rms|richard|linus),
'((linux|stallman|gpl|rms|richard|linus) pls go|Shut your filthy hippy
mouth,? (stallman|rms|richard|linus))', (linux|stallman|gpl|rms|richard|linus)\s+is\s+]
template: plaintext_notice
variables:
message: |
What the fuck did you just fucking say about me, you little proprietary bitch? I'll have you know I graduated top of my class in the FSF, and I've been involved in numerous secret raids on Apple patents, and I have over 300 confirmed bug fixes. I am trained in Free Software Evangelizing and I'm the top code contributer for the entire GNU HURD. You are nothing to me but just another compile time error. I will wipe you the fuck out with precision the likes of which has never been seen before on this Earth, mark my fucking words. You think you can get away with saying that shit to me over the Internet? Think again, fucker. As we speak I am building a GUI using GTK+ and your IP is being traced right now so you better prepare for the storm, maggot. The storm that wipes out the pathetic little thing you call your life. You're fucking dead, kid. I can be anywhere, anytime, and I can decompile you in over seven hundred ways, and that's just with my Model M. Not only am I extensively trained in EMACS, but I have access to the entire arsenal of LISP functions and I will use it to its full extent to wipe your miserable ass off the face of the continent, you little shit. If only you could have known what unholy retribution your little "clever" comment was about to bring down upon you, maybe you would have held your fucking tongue. But you couldn't, you didn't, and now you're paying the price, you goddamn idiot. I will shit Freedom all over you and you will drown in it.
gentoo:
matches: [gentoo]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. I have a low opinion of Gentoo GNU/Linux.
Gentoo is a GNU/Linux distribution, but its developers don't recognize this; they call it "Gentoo Linux". That means they are treating me and the GNU Project disresepectfully.
More importantly, Gentoo steers the user towards nonfree programs, which is why it is not one of our recognized free distros.
mandriva:
matches: [mandriva]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Mandriva does have a stated policy about what can be included in the main system. It's based on Fedora's, which means that it also allows certain kinds of nonfree firmware to be included. On top of that, it permits software released under the original Artistic License to be included, even though that's a nonfree license.
Mandriva also provides nonfree software through dedicated repositories.
opensuse:
matches: [opensuse]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. OpenSUSE offers its users access to a repository of nonfree software. This is an instance of how "open" is weaker than "free".
redhat:
matches: [redhat, red hat, rhel]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Red Hat's enterprise distribution primarily follows the same licensing policies as Fedora, with one exception. Thus, we don't endorse it for the same reasons. In addition to those, Red Hat has no policy against making nonfree software available for the system through supplementary distribution channels.
slackware:
matches: [slackware]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Slackware has the two usual problems: there's no clear policy about what software can be included, and nonfree blobs are included in the kernel. It also ships with the nonfree image-viewing program xv. Of course, with no firm policy in place, there might be other nonfree software included that we missed.
ubuntu:
matches: [ubuntu]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Ubuntu provides specific repositories of nonfree software, and Canonical expressly promotes and recommends nonfree software under the Ubuntu name in some of their distribution channels. Ubuntu offers the option to install only free packages, which means it also offers the option to install nonfree packages too. In addition, the version of the kernel, included in Ubuntu contains firmware blobs.
Ubuntu's trademark policy prohibits commercial redistribution of exact copies of Ubuntu, denying an important freedom.
bsd:
matches: [freebsd, openbsd, netbsd, bsd]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. FreeBSD, NetBSD, and OpenBSD all include instructions for obtaining nonfree programs in their ports system. In addition, their kernels include nonfree firmware blobs.
Nonfree firmware programs used with the kernel, are called "blobs", and that's how we use the term. In BSD parlance, the term "blob" means something else: a nonfree driver. OpenBSD and perhaps other BSD distributions (called "projects" by BSD developers) have the policy of not including those. That is the right policy, as regards drivers; but when the developers say these distributions "contain no blobs", it causes a misunderstanding. They are not talking about firmware blobs.
No BSD distribution has policies against proprietary binary-only firmware that might be loaded even by free drivers.
compensation:
matches: [compensation]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. To speak of “compensation for authors” in connection with copyright carries the assumptions that (1) copyright exists for the sake of authors and (2) whenever we read something, we take on a debt to the author which we must then repay. The first assumption is simply false, and the second is outrageous.
“Compensating the rights-holders” adds a further swindle: you're supposed to imagine that means paying the authors, and occasionally it does, but most of the time it means a subsidy for the same publishing companies that are pushing unjust laws on us.
consume:
matches: [(^|\s)consume(\s|$)]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. “Consume” refers to what we do with food: we ingest it, after which the food as such no longer exists. By analogy, we employ the same word for other products whose use uses them up. Applying it to durable goods, such as clothing or appliances, is a stretch. Applying it to published works (programs, recordings on a disk or in a file, books on paper or in a file), whose nature is to last indefinitely and which can be run, played or read any number of times, is simply an error. Playing a recording, or running a program, does not consume it.
The term “consume” is associated with the economics of uncopiable material products, and leads people to transfer its conclusions unconsciously to copiable digital works — an error that proprietary software developers (and other publishers) dearly wish to encourage. Their twisted viewpoint comes through clearly in this article, which also refers to publications as “content.”
The narrow thinking associated with the idea that we “consume content” paves the way for laws such as the DMCA that forbid users to break the Digital Restrictions Management (DRM) facilities in digital devices. If users think what they do with these devices is “consume,” they may see such restrictions as natural.
It also encourages the acceptation of “streaming” services, which use DRM to limit use of digital recordings to a form that fits the word “consume.”
Why is this perverse usage spreading? Some may feel that the term sounds sophisticated; if that attracts you, rejecting it with cogent reasons can appear even more sophisticated. Others may be acting from business interests (their own, or their employers'). Their use of the term in prestigious forums gives the impression that it's the “correct” term.
To speak of “consuming” music, fiction, or any other artistic works is to treat them as products rather than as art. If you don't want to spread that attitude, you would do well to reject using the term “consume” for them. We recommend saying that someone “experiences” an artistic work or a work stating a point of view, and that someone “uses” a practical work.
creativecommons:
matches: [creative commons]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The most important licensing characteristic of a work is whether it is free. Creative Commons publishes seven licenses; three are free (CC BY, CC BY-SA and CC0) and the rest are nonfree. Thus, to describe a work as “Creative Commons licensed” fails to say whether it is free, and suggests that the question is not important. The statement may be accurate, but the omission is harmful.
To encourage people to pay attention to the most important distinction, always specify which Creative Commons license is used, as in “licensed under CC BY-SA.” If you don't know which license a certain work uses, find out and then make your statement.
creator:
matches: [creator]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term “creator” as applied to authors implicitly compares them to a deity (“the creator”). The term is used by publishers to elevate authors' moral standing above that of ordinary people in order to justify giving them increased copyright power, which the publishers can then exercise in their name. We recommend saying “author” instead. However, in many cases “copyright holder” is what you really mean. These two terms are not equivalent: often the copyright holder is not the author.
floss:
matches: [floss]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term “FLOSS,” meaning “Free/Libre and Open Source Software,” was coined as a way to be neutral between free software and open source. If neutrality is your goal, “FLOSS” is the best way to be neutral. But if you want to show you stand for freedom, don't use a neutral term.
forfree:
matches: [for free]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. If you want to say that a program is free software, please don't say that it is available “for free.” That term specifically means “for zero price.” Free software is a matter of freedom, not price.
Free software copies are often available for free—for example, by downloading via FTP. But free software copies are also available for a price on CD-ROMs; meanwhile, proprietary software copies are occasionally available for free in promotions, and some proprietary packages are normally available at no charge to certain users.
To avoid confusion, you can say that the program is available “as free software.”
foss:
matches: [foss]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term “FOSS,” meaning “Free and Open Source Software,” was coined as a way to be neutral between free software and open source, but it doesn't really do that. If neutrality is your goal, “FLOSS” is better. But if you want to show you stand for freedom, don't use a neutral term.
freelyavailable:
matches: [freely available]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Don't use “freely available software” as a synonym for “free software.” The terms are not equivalent. Software is “freely available” if anyone can easily get a copy. “Free software” is defined in terms of the freedom of users that have a copy of it. These are answers to different questions.
google:
matches: [(^|\s)google(\s|$)]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please avoid using the term “google” as a verb, meaning to search for something on the internet. “Google” is just the name of one particular search engine among others. We suggest to use the term “web search” instead. Try to use a search engine that respects your privacy; DuckDuckGo claims not to track its users, although we cannot confirm this.
saas:
matches: [saas, software as a service]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. We used to say that SaaS (short for “Software as a Service”) is an injustice, but then we found that there was a lot of variation in people's understanding of which activities count as SaaS. So we switched to a new term, “Service as a Software Substitute” or “SaaSS.” This term has two advantages: it wasn't used before, so our definition is the only one, and it explains what the injustice consists of.
In Spanish we continue to use the term “software como servicio” because the joke of “software como ser vicio” is too good to give up.
sharingeconomy:
matches: [sharing economy]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The term “sharing economy” is not a good way to refer to services such as Uber and Airbnb that arrange business transactions between people. We use the term “sharing” to refer to noncommercial cooperation, including noncommercial redistribution of exact copies of published works. Stretching the word “sharing” to include these transactions undermines its meaning, so we don't use it in this context.
A more suitable term for businesses like Uber is the “piecework service economy.”
skype:
matches: [skype]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Please avoid using the term “Skype” as a verb, meaning any kind of video communication or telephony over the Internet in general. “Skype” is just the name of one particular proprietary program, one that spies on its users. If you want to make video and voice calls over the Internet in a way that respects both your freedom and your privacy, try one of the numerous free Skype replacements.
sourcemodel:
matches: [source model]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. Wikipedia uses the term “source model” in a confused and ambiguous way. Ostensibly it refers to how a program's source is distributed, but the text confuses this with the development methodology. It distinguishes “open source” and ”shared source” as answers, but they overlap — Microsoft uses the latter as a marketing term to cover a range of practices, some of which are “open source”. Thus, this term really conveys no coherent information, but it provides an opportunity to say “open source” in pages describing free software programs.
theft:
matches: [theft]
template: plaintext_notice
variables:
message: |
I'd just like to interject for one moment. The supporters of a too-strict, repressive form of copyright often use words like “stolen” and “theft” to refer to copyright infringement. This is spin, but they would like you to take it for objective truth.
Under the US legal system, copyright infringement is not theft. Laws about theft are not applicable to copyright infringement. The supporters of repressive copyright are making an appeal to authority—and misrepresenting what authority says.
Unauthorized copying is forbidden by copyright law in many circumstances (not all!), but being forbidden doesn't make it wrong. In general, laws don't define right and wrong. Laws, at their best, attempt to implement justice. If the laws (the implementation) don't fit our ideas of right and wrong (the spec), the laws are what should change.
In addition, a US judge, presiding over a trial for copyright infringement, recognized that “piracy” and “theft” are smear-words.

50
samples/thread.yaml Normal file
View file

@ -0,0 +1,50 @@
templates:
always_in_thread:
type: m.room.message
variables:
thread_parent: '{{event.content.get_thread_parent() or event.event_id}}'
event_id: '{{event.event_id}}'
content:
msgtype: m.text
body: $${text}
m.relates_to:
rel_type: m.thread
event_id: $${thread_parent}
is_falling_back: true
m.in_reply_to:
event_id: $${event_id}
# Reply in thread if the message is already in a thread, otherwise use a normal reply.
# This currently requires using a jinja template as the content instead of a normal yaml map.
thread_or_reply:
type: m.room.message
variables:
relates_to: |
{{
{"rel_type": "m.thread", "event_id": event.content.get_thread_parent(), "is_falling_back": True, "m.in_reply_to": {"event_id": event.event_id}}
if event.content.get_thread_parent()
else {"m.in_reply_to": {"event_id": event.event_id}}
}}
content:
msgtype: m.text
body: $${text}
m.relates_to: $${relates_to}
antispam:
room:
max: 60
delay: 60
user:
max: 60
delay: 60
rules:
thread:
matches: [^!thread$]
template: always_in_thread
variables:
text: meow 3:<
maybe_thread:
matches: [^!thread --maybe$]
template: thread_or_reply
variables:
text: meow >:3