diff --git a/README.md b/README.md index 682b881..be50792 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,54 @@ +English | [中文](README_ZH.md) + # reactbot -A [maubot](https://github.com/maubot/maubot) that responds to messages that match predefined rules. +A [maubot](https://github.com/maubot/maubot) that responds to messages based on 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". +## Examples +* [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. +* [samples/stallman.yaml](samples/stallman.yaml) contains a Stallman interjection bot. +* [samples/random-reaction.yaml](samples/random-reaction.yaml) has an example of a random reaction to matching messages. +* [samples/nitter.yaml](samples/nitter.yaml) has an example of matching tweet links and responding with corresponding nitter.net links. +* [samples/thread.yaml](samples/thread.yaml) has an example of replying in threads. -## Config format +## Configuration 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. +* `type` - The Matrix event type to send. +* `content` - The event content. Can be an object or a jinja2 template that generates JSON. +* `variables` - Key-value mapping 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. +Variables starting with `{{` will be parsed as jinja2 templates and will get the maubot event object in `event`. From v3 onwards, variables are parsed using jinja2's [native types mode](https://jinja.palletsprojects.com/en/3.1.x/nativetypes/), meaning the output can be non-string types. -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 a string, it will be parsed as a jinja2 template, and the output will be parsed as JSON. Content jinja2 templates will get `event` like variable templates but will also get all 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. +If the content is an object, the object will be sent as the content. Objects can include variables using custom syntax: all instances of `$${variablename}` will be replaced with the value matching `variablename`. This applies to object keys and values as well as list items. If a key/value/item only contains a variable insertion, the variable can be of any type. If there is other content besides the variable, the variable will be concatenated using `+`, meaning 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). +### Default Flags +Default regular expression flags. Most Python regular expression flags are available. See [documentation](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. +* `s` / `dotall` - Makes `.` match any character, including newlines. +* `x` / `verbose` - Ignores whitespace and comments in the regex. +* `m` / `multiline` - When specified, `^` and `$` match the start and end of each line, not just the start and end of the 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. +Rules only require `matches` and `template`. +* `rooms` - List of rooms (internal room IDs) the rule applies to. If empty, the rule applies to all rooms the bot is in. +* `not_rooms` - Exclude certain rooms (internal room IDs). +* `users` - List of users the rule applies to. If empty, the rule applies to all users. +* `not_users` - Exclude certain users. +* `matches` - Regular expression or list of regular expressions to match. +* `template` - Name of the template to use. +* `variables` - Key-value mapping to extend or override template variables. Like template variables, values will be parsed as Jinja2 templates. +* `only_text` - Should only respond to text-type messages (including notice-type events)? Defaults to false. +* `not_thread` - Should not respond to messages in threads? Defaults to false. +* `is_reedit` - Should not respond to edits? Defaults to false. + +Regular expressions in `matches` can be simple strings containing the pattern or objects containing additional information: +* `pattern` - The regular expression to match. +* `flags` - Regular expression flags (override default flags). +* `raw` - Whether the regular expression should be forced to raw. + +If `raw` is `true` or the pattern does not contain special regex characters other than leading `^` and/or trailing `$`, the pattern will be treated as "raw". Raw patterns do not use regular expressions but faster string operators (equals, starts/endswith, contains). Patterns with the `multiline` flag will never be implicitly converted to raw patterns. \ No newline at end of file diff --git a/README_ZH.md b/README_ZH.md new file mode 100644 index 0000000..cdeb824 --- /dev/null +++ b/README_ZH.md @@ -0,0 +1,54 @@ +[English](README.md) | 中文 + +# reactbot +一个[maubot](https://github.com/maubot/maubot) ,根据预定义规则响应消息。 + +## 示例 +* [base config](base-config.yaml) 包含一个针对 [#thisweekinmatrix:matrix.org](https://matrix.to/#/#thisweekinmatrix:matrix.org) 中 TWIM 提交的 cookie 反应和一个针对 "alot" 的图片响应。 +* [samples/jesari.yaml](samples/jesari.yaml) 包含一个 [jesaribot](https://github.com/maubot/jesaribot) 的替代品。 +* [samples/stallman.yaml](samples/stallman.yaml) 包含一个 Stallman 插话机器人。 +* [samples/random-reaction.yaml](samples/random-reaction.yaml) 有一个对匹配消息进行随机反应的示例。 +* [samples/nitter.yaml](samples/nitter.yaml) 有一个匹配推文链接并回应相应 nitter.net 链接的示例。 +* [samples/thread.yaml](samples/thread.yaml) 有一个在线程中回复的示例。 + +## 配置格式 +### Templates +模板包含要发送的实际事件类型和内容。 +* `type` - 要发送的 Matrix 事件类型 +* `content` - 事件内容。可以是对象或生成 JSON 的 jinja2 模板。 +* `variables` - 变量的键值对映射。 + +以 `{{` 开头的变量将被解析为 jinja2 模板,并在 `event` 中获取 maubot 事件对象。从 v3 开始,变量使用 jinja2 的[原生类型模式](https://jinja.palletsprojects.com/en/3.1.x/nativetypes/)进行解析 ,这意味着输出可以是非字符串类型。 + +如果内容是字符串,它将被解析为 jinja2 模板,输出将被解析为 JSON。内容 jinja2 模板将像变量模板一样获取 `event`,但它也将获取所有变量。 + +如果内容是对象,该对象将作为内容发送。对象可以使用自定义语法包含变量:所有 `$${variablename}` 实例将被替换为与 `variablename` 匹配的值。这适用于对象键和值以及列表项。如果键/值/项仅包含变量插入,则变量可以是任何类型。如果除了变量还有其他内容,变量将使用 `+` 连接,这意味着它应该是字符串。 + +### Default flags +默认正则表达式标志。大多数 Python 正则表达式标志可用。参见[文档](https://docs.python.org/3/library/re.html#re.A) 。 + +最相关的标志: +* `i` / `ignorecase` - 不区分大小写匹配。 +* `s` / `dotall` - 使 `.` 匹配任何字符,包括换行符。 +* `x` / `verbose` - 忽略正则表达式中的注释和空白。 +* `m` / `multiline` - 指定时,`^` 和 `$` 分别匹配行的开始和结束,而不是整个字符串的开始和结束。 + +### Rules +规则只有 `matches` 和 `template` 是必需的。 +* `rooms` - 规则适用的房间列表(内部房间ID)。如果为空,规则将适用于机器人所在的所有房间。 +* `not_rooms` - 排除某些房间(内部房间ID)。 +* `users` - 规则适用的用户列表。如果为空,规则将适用于所有用户。 +* `not_users` - 排除某些用户。 +* `matches` - 要匹配的正则表达式或正则表达式列表。 +* `template` - 要使用的模板名称。 +* `variables` - 扩展或覆盖模板变量的键值对映射。与模板变量一样,值将被解析为 Jinja2 模板。 +* `only_text` - 是否只回应文本类消息(包括通知类型的事件)?默认为false +* `not_thread` - 是否不回应线程内的消息?默认为false +* `is_reedit` - 否不回应编辑?默认为false + +`matches` 中的正则表达式可以是包含模式的简单字符串,也可以是包含附加信息的对象: +* `pattern` - 要匹配的正则表达式。 +* `flags` - 正则表达式标志(替换默认标志)。 +* `raw` - 正则表达式是否应被强制为原始。 + +如果 `raw` 为 `true` 或模式不包含除开头的 `^` 和/或结尾的 `$` 之外的特殊正则表达式字符,则模式将被视为“原始”。原始模式不使用正则表达式,而是使用更快的字符串操作符(相等、starts/endwith、contains)。带有 `multiline` 标志的模式将永远不会被隐式转换为原始模式。 diff --git a/base-config.yaml b/base-config.yaml index f99e872..42e9189 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -1,49 +1,114 @@ 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 + text_reaction: + type: m.reaction + variables: + react_to_event: '{{event.event_id}}' + content: + m.relates_to: + rel_type: m.annotation + event_id: $${react_to_event} + key: $${reaction} default_flags: - ignorecase - antispam: - room: - max: 1 - delay: 60 - user: - max: 2 - delay: 60 + room: + max: 60 + delay: 60 +# max: 在指定的时间间隔(由 delay 定义)内,允许每个用户发送的最大消息数量。 +# delay: 时间间隔,单位为秒。在这个时间间隔内,用户发送的消息数量不能超过 max 值。 + user: + max: 30 + delay: 60 rules: - twim_cookies: - rooms: ["!FPUfgzXYWTKgIrwKxW:matrix.org"] - matches: [^TWIM] - template: reaction - variables: - reaction: 🍪 - alot: - matches: [alot] - template: alot + like_button: + users: ['@know:meiu.xyz', '@aibot1:meiu.xyz', '@aibot2:meiu.xyz', '@aibot3:meiu.xyz'] + not_rooms: ["!erVdKitClvcjVzMjUw:meiu.xyz"] + only_text: true + matches: [.*] + template: text_reaction + variables: + reaction: 👍点赞 + to_aishow: + users: ['@know:meiu.xyz', '@aibot1:meiu.xyz', '@aibot2:meiu.xyz', '@aibot3:meiu.xyz'] + not_rooms: ["!erVdKitClvcjVzMjUw:meiu.xyz"] + only_text: true + matches: [.*] + template: text_reaction + variables: + reaction: 🤖转发 + collect_button: + users: ['@know:meiu.xyz', '@aibot1:meiu.xyz', '@aibot2:meiu.xyz', '@aibot3:meiu.xyz'] + not_rooms: ["!erVdKitClvcjVzMjUw:meiu.xyz"] + only_text: true + matches: [.*] + template: text_reaction + variables: + reaction: ❤️收藏 + voice_button: + users: ['@know:meiu.xyz', '@aibot1:meiu.xyz', '@aibot2:meiu.xyz', '@aibot3:meiu.xyz'] + not_rooms: ["!erVdKitClvcjVzMjUw:meiu.xyz"] + only_text: true + matches: [.*] + template: text_reaction + variables: + reaction: 🎤语音 + rewrite_button: + users: ['@know:meiu.xyz', '@aibot1:meiu.xyz', '@aibot2:meiu.xyz', '@aibot3:meiu.xyz'] + not_rooms: ["!erVdKitClvcjVzMjUw:meiu.xyz"] + only_text: true + matches: [.*] + template: text_reaction + variables: + reaction: ♻️重制 + rag_button: + users: ['@know:meiu.xyz', '@aibot1:meiu.xyz', '@aibot2:meiu.xyz', '@aibot3:meiu.xyz'] + not_rooms: ["!erVdKitClvcjVzMjUw:meiu.xyz"] + only_text: true + matches: [.*] + template: text_reaction + variables: + reaction: 🌾采集 + + + todo_idea: + users: ['@ming:meiu.xyz'] + rooms: ['!WtMywSEpJiIxLlYxzZ:meiu.xyz'] + not_thread: true + matches: [.*] + template: text_reaction + variables: + reaction: 💡想法 + todo_plan: + users: ['@ming:meiu.xyz'] + rooms: ['!WtMywSEpJiIxLlYxzZ:meiu.xyz'] + not_thread: true + matches: [.*] + template: text_reaction + variables: + reaction: 📝计划 + todo_doing: + users: ['@ming:meiu.xyz'] + rooms: ['!WtMywSEpJiIxLlYxzZ:meiu.xyz'] + not_thread: true + matches: [.*] + template: text_reaction + variables: + reaction: ⏳进行 + todo_achieve: + users: ['@ming:meiu.xyz'] + rooms: ['!WtMywSEpJiIxLlYxzZ:meiu.xyz'] + not_thread: true + matches: [.*] + template: text_reaction + variables: + reaction: ✅完成 + todo_pause: + users: ['@ming:meiu.xyz'] + rooms: ['!WtMywSEpJiIxLlYxzZ:meiu.xyz'] + not_thread: true + matches: [.*] + template: text_reaction + variables: + reaction: 💤搁置 \ No newline at end of file diff --git a/maubot.yaml b/maubot.yaml index 13f6a9e..2dcab3a 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,9 +1,9 @@ maubot: 0.1.0 id: xyz.maubot.reactbot -version: 2.2.0 +version: 2.2.10 license: AGPL-3.0-or-later modules: - reactbot main_class: ReactBot extra_files: -- base-config.yaml +- base-config.yaml \ No newline at end of file diff --git a/reactbot/bot.py b/reactbot/bot.py index a31fa31..25eb259 100644 --- a/reactbot/bot.py +++ b/reactbot/bot.py @@ -45,7 +45,7 @@ def bump(self) -> bool: class ReactBot(Plugin): - allowed_msgtypes: Tuple[MessageType, ...] = (MessageType.TEXT, MessageType.EMOTE) + allowed_msgtypes: Tuple[MessageType, ...] = (MessageType.TEXT, MessageType.EMOTE, MessageType.NOTICE) user_flood: Dict[UserID, FloodInfo] room_flood: Dict[RoomID, FloodInfo] @@ -95,15 +95,17 @@ def is_flood(self, evt: MessageEvent) -> bool: @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: + if evt.sender == 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 并使用 continue 可以确保即使某个规则被认为是“洪水攻击”,其他规则依然有机会被执行。 return try: await rule.execute(evt, match) except Exception: self.log.exception(f"Failed to execute {name} in {evt.room_id}") - return + # 去掉最后的 return 会导致所有匹配的规则都被执行。这种设计适用于需要同时处理多个规则的情况。 + # return diff --git a/reactbot/config.py b/reactbot/config.py index b264a6b..a11d90d 100644 --- a/reactbot/config.py +++ b/reactbot/config.py @@ -59,6 +59,11 @@ def _make_rule(self, name: str, rule: Dict[str, Any]) -> Rule: return Rule( rooms=set(rule.get("rooms", [])), not_rooms=set(rule.get("not_rooms", [])), + users=set(rule.get("users", [])), + not_users=set(rule.get("not_users", [])), + only_text=rule.get("only_text", False), + not_thread=rule.get("not_thread", False), + is_reedit=rule.get("is_reedit", False), 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, diff --git a/reactbot/rule.py b/reactbot/rule.py index 07e714d..870d50a 100644 --- a/reactbot/rule.py +++ b/reactbot/rule.py @@ -18,7 +18,7 @@ from attr import dataclass from maubot import MessageEvent -from mautrix.types import EventType, RoomID +from mautrix.types import EventType, RoomID, UserID,RelationType,MessageType from .simplepattern import SimplePattern from .template import OmitValue, Template @@ -30,6 +30,11 @@ class Rule: rooms: Set[RoomID] not_rooms: Set[RoomID] + users: Set[UserID] + not_users: Set[UserID] + not_thread: bool + only_text: bool + is_reedit: bool matches: List[RPattern] not_matches: List[RPattern] template: Template @@ -47,6 +52,16 @@ def match(self, evt: MessageEvent) -> Optional[Match]: return None elif evt.room_id in self.not_rooms: return None + if len(self.users) > 0 and evt.sender not in self.users: + return None + elif evt.sender in self.not_users: + return None + if self.not_thread and evt.content.relates_to and evt.content.relates_to.rel_type == RelationType.THREAD: + return None + if self.only_text and (evt.content.msgtype != MessageType.TEXT and evt.content.msgtype != MessageType.NOTICE): + return None + if self.is_reedit == False and (evt.content.relates_to and evt.content.relates_to.rel_type == RelationType.REPLACE): + return None for pattern in self.matches: match = pattern.search(evt.content.body) if match: