Compare commits

...

27 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
15 changed files with 341 additions and 108 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

@ -7,6 +7,11 @@ A [maubot](https://github.com/maubot/maubot) that responds to messages that matc
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
@ -15,7 +20,10 @@ Templates contain the actual event type and content to be sent.
* `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`.
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

View file

@ -1,6 +1,6 @@
maubot: 0.1.0
id: xyz.maubot.reactbot
version: 2.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

@ -13,17 +13,15 @@
#
# 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 Type, Tuple, Dict
from itertools import chain
from typing import Dict, Tuple, Type
import time
from attr import dataclass
from mautrix.types import EventType, MessageType, UserID, RoomID
from mautrix.util.config import BaseProxyConfig
from maubot import Plugin, MessageEvent
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
@ -67,19 +65,22 @@ class ReactBot(Plugin):
self.config.parse_data()
except ConfigError:
self.log.exception("Failed to load config")
for fi in self.user_flood.items():
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.items():
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 _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':
def _get_flood_info(self, flood_map: dict, key: str, for_type: str) -> "FloodInfo":
try:
return flood_map[key]
except KeyError:
@ -87,8 +88,10 @@ class ReactBot(Plugin):
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())
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:
@ -102,5 +105,5 @@ class ReactBot(Plugin):
try:
await rule.execute(evt, match)
except Exception:
self.log.exception(f"Failed to execute {name}")
self.log.exception(f"Failed to execute {name} in {evt.room_id}")
return

View file

@ -13,17 +13,18 @@
#
# 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 List, Union, Dict, Any
from typing import Any, Dict, List, Union
import re
from jinja2 import Template as JinjaTemplate
from jinja2 import Template as JinjaStringTemplate
from jinja2.nativetypes import NativeTemplate as JinjaNativeTemplate
from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper
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
from .rule import Rule, RPattern
InputPattern = Union[str, Dict[str, str]]
@ -44,28 +45,36 @@ class Config(BaseProxyConfig):
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()}
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", [])),
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))
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()
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
@ -87,16 +96,24 @@ class Config(BaseProxyConfig):
return re.compile(pattern, flags=flags)
@staticmethod
def _parse_variables(data: Dict[str, Any]) -> Dict[str, JinjaTemplate]:
return {name: JinjaTemplate(var_tpl) for name, var_tpl
in data.get("variables", {}).items()}
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], JinjaTemplate]:
def _parse_content(
content: Union[Dict[str, Any], str]
) -> Union[Dict[str, Any], JinjaStringTemplate]:
if not content:
return {}
elif isinstance(content, str):
return JinjaTemplate(content)
return JinjaStringTemplate(content)
return content
@staticmethod

View file

@ -13,17 +13,15 @@
#
# 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 Optional, Match, Dict, List, Set, Union, Pattern
from typing import Any, Dict, List, Match, Optional, Pattern, Set, Union
from attr import dataclass
from jinja2 import Template as JinjaTemplate
from mautrix.types import RoomID, EventType
from maubot import MessageEvent
from mautrix.types import EventType, RoomID
from .template import Template
from .simplepattern import SimplePattern
from .template import OmitValue, Template
RPattern = Union[Pattern, SimplePattern]
@ -31,11 +29,12 @@ 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, JinjaTemplate]
variables: Dict[str, Any]
def _check_not_match(self, body: str) -> bool:
for pattern in self.not_matches:
@ -46,6 +45,8 @@ class Rule:
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:
@ -55,7 +56,10 @@ class Rule:
return None
async def execute(self, evt: MessageEvent, match: Match) -> None:
content = self.template.execute(evt=evt, rule_vars=self.variables,
extra_vars={str(i): val for i, val in
enumerate(match.groups())})
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)

View file

@ -1,5 +1,5 @@
# reminder - 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
# it under the terms of the GNU Affero General Public License as published by
@ -13,50 +13,82 @@
#
# 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, List, Optional
from typing import Callable, Dict, List, NamedTuple, Optional
import re
class BlankMatch:
@staticmethod
def groups() -> List[str]:
return []
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:
_ptm = BlankMatch()
matcher: Callable[[str], bool]
matcher: SimpleMatcherFunc
pattern: str
ignorecase: bool
def __init__(self, matcher: Callable[[str], bool], ignorecase: bool) -> None:
def __init__(self, matcher: SimpleMatcherFunc, pattern: str, ignorecase: bool) -> None:
self.matcher = matcher
self.pattern = pattern
self.ignorecase = ignorecase
def search(self, val: str) -> BlankMatch:
def search(self, val: str) -> SimpleMatch:
if self.ignorecase:
val = val.lower()
if self.matcher(val):
return self._ptm
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']:
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]}\\$"):
if first == "^" and last == "$" and (force_raw or esc == f"\\^{pattern[1:-1]}\\$"):
s_pattern = s_pattern[1:-1]
return SimplePattern(lambda val: val == s_pattern, ignorecase=ignorecase)
elif first == '^' and (force_raw or esc == f"\\^{pattern[1:]}"):
func = matcher_equals
elif first == "^" and (force_raw or esc == f"\\^{pattern[1:]}"):
s_pattern = s_pattern[1:]
return SimplePattern(lambda val: val.startswith(s_pattern), ignorecase=ignorecase)
elif last == '$' and (force_raw or esc == f"{pattern[:-1]}\\$"):
func = matcher_startswith
elif last == "$" and (force_raw or esc == f"{pattern[:-1]}\\$"):
s_pattern = s_pattern[:-1]
return SimplePattern(lambda val: val.endswith(s_pattern), ignorecase=ignorecase)
func = matcher_endswith
elif force_raw or esc == pattern:
return SimplePattern(lambda val: s_pattern in val, ignorecase=ignorecase)
return None
func = matcher_contains
else:
# Not a simple pattern
return None
return SimplePattern(matcher=func, pattern=s_pattern, ignorecase=ignorecase)

View file

@ -1,5 +1,5 @@
# reminder - 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
# it under the terms of the GNU Affero General Public License as published by
@ -13,16 +13,17 @@
#
# 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 Union, Dict, List, Tuple, Any
from typing import Any, Dict, List, Tuple, Union
from itertools import chain
import json
import copy
import json
import re
from attr import dataclass
from jinja2 import Template as JinjaTemplate
from jinja2 import Template as JinjaStringTemplate
from jinja2.nativetypes import Template as JinjaNativeTemplate
from mautrix.types import EventType, Event
from mautrix.types import Event, EventType
class Key(str):
@ -30,6 +31,11 @@ class Key(str):
variable_regex = re.compile(r"\$\${([0-9A-Za-z-_]+)}")
OmitValue = object()
global_vars = {
"omit": OmitValue,
}
Index = Union[str, int, Key]
@ -37,12 +43,12 @@ Index = Union[str, int, Key]
@dataclass
class Template:
type: EventType
variables: Dict[str, JinjaTemplate]
content: Union[Dict[str, Any], JinjaTemplate]
variables: Dict[str, Any]
content: Union[Dict[str, Any], JinjaStringTemplate]
_variable_locations: List[Tuple[Index, ...]] = None
def init(self) -> 'Template':
def init(self) -> "Template":
self._variable_locations = []
self._map_variable_locations((), self.content)
return self
@ -53,11 +59,11 @@ class Template:
self._map_variable_locations((*path, i), v)
elif isinstance(data, dict):
for k, v in data.items():
if variable_regex.match(k):
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.match(data):
if variable_regex.search(data):
self._variable_locations.append(path)
@classmethod
@ -68,20 +74,29 @@ class Template:
@staticmethod
def _replace_variables(tpl: str, variables: Dict[str, Any]) -> str:
for match in variable_regex.finditer(tpl):
val = variables[match.group(1)]
if match.start() == 0 and match.end() == len(tpl):
# Whole field is a single variable, just return the value to allow non-string types.
return val
tpl = tpl[:match.start()] + val + tpl[match.end():]
return tpl
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, JinjaTemplate], extra_vars: Dict[str, str]
) -> Dict[str, Any]:
variables = {**{name: template.render(event=evt)
for name, template in chain(self.variables.items(), rule_vars.items())},
**extra_vars}
if isinstance(self.content, JinjaTemplate):
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)
@ -92,5 +107,9 @@ class Template:
key = str(key)
data[self._replace_variables(key, variables)] = data.pop(key)
else:
data[key] = self._replace_variables(data[key], variables)
replaced_data = self._replace_variables(data[key], variables)
if replaced_data is OmitValue:
del data[key]
else:
data[key] = replaced_data
return content

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:
- 🤔
- 🧐
- 🤨

View file

@ -10,16 +10,20 @@ templates:
default_flags:
- ignorecase
antispam:
room:
max: 1
delay: 60
user:
max: 2
delay: 60
rules:
linux:
matches:
- linux
not_matches:
- pattern: gnu+linux
raw: true
- pattern: gnu/linux
raw: true
- gnu plus linux
- gnu
- kernel
template: plaintext_notice
variables:
@ -143,7 +147,7 @@ rules:
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]
matches: [(\s|^)lamp(\s|$)]
template: plaintext_notice
variables:
message: |
@ -176,7 +180,7 @@ rules:
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?]
matches: [(\s|^)pcs?(\s|$)]
template: plaintext_notice
variables:
message: |
@ -196,7 +200,7 @@ rules:
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]
matches: [powerpoint, \sppt(\s|$)]
template: plaintext_notice
variables:
message: |
@ -236,7 +240,7 @@ rules:
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]
matches: (^|\s)arch($|\s)
template: plaintext_notice
variables:
message: |
@ -325,7 +329,7 @@ rules:
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: [consume\s]
matches: [(^|\s)consume(\s|$)]
template: plaintext_notice
variables:
message: |
@ -375,7 +379,7 @@ rules:
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: [google\\s]
matches: [(^|\s)google(\s|$)]
template: plaintext_notice
variables:
message: |
@ -415,4 +419,3 @@ rules:
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