Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
"session.unpair.ok": "Removed both pair edges.",
"session.unpair.missing": "No pair in this session.",
"session.unpair.ambiguous": "This session has more than one pair. Specify the other UMO.",
"session.filter.usage": "Usage: /session filter <rule_id> [match|except] [subject|role|text] <value>, or /session filter <rule_id> clear [match|except|all]. Omit extra tokens to show the current filter. clear without a side equals all. text uses the rest of the line as a Python re.search pattern.",
"session.filter.missing": "No matching session-bridge rule.",
"session.filter.invalid_role": "Role must be guest, member, session_admin, session_owner, instance_operator, operator, or root.",
"session.filter.invalid_pattern": "The text pattern must be a valid Python re expression of at most 256 characters.",
"session.filter.limit": "Each filter dimension accepts at most 16 values.",
"session.filter.empty": "(all)",
"session.filter.body": "Filter for {rule_id}:\nmatch:\n{match}\nexcept:\n{except_}",
"session.filter.updated": "Updated filter for {rule_id}:\nmatch:\n{match}\nexcept:\n{except_}",
"session.filter.cleared": "Cleared filter for {rule_id}:\nmatch:\n{match}\nexcept:\n{except_}",
"session.disconnect.ok": "Session disconnected.",
"session.disconnect.missing": "No unbounded link in this session.",
"session.send.invalid": "Usage: /send <umo> <message or attachments>, or /send after /session connect. Keep the command header in the message text.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@
"session.unpair.ok": "已删除该 pair 的两条边。",
"session.unpair.missing": "当前会话没有 pair。",
"session.unpair.ambiguous": "当前会话有多对 pair,请补上对端 UMO。",
"session.filter.usage": "用法:/session filter <rule_id> [match|except] [subject|role|text] <值>,或 /session filter <rule_id> clear [match|except|all]。省略其余参数可查看当前过滤。clear 省略范围等于 all。text 把行内剩余内容当作 Python re.search 模式。",
"session.filter.missing": "没有找到对应的会话桥接规则。",
"session.filter.invalid_role": "角色必须是 guest、member、session_admin、session_owner、instance_operator、operator 或 root。",
"session.filter.invalid_pattern": "正文模式必须是不超过 256 个字符的合法 Python re 表达式。",
"session.filter.limit": "每个过滤维度最多 16 条。",
"session.filter.empty": "(全部)",
"session.filter.body": "{rule_id} 的过滤:\nmatch:\n{match}\nexcept:\n{except_}",
"session.filter.updated": "已更新 {rule_id} 的过滤:\nmatch:\n{match}\nexcept:\n{except_}",
"session.filter.cleared": "已清空 {rule_id} 的过滤:\nmatch:\n{match}\nexcept:\n{except_}",
"session.disconnect.ok": "已断开会话连接。",
"session.disconnect.missing": "当前会话没有无期限连接。",
"session.send.invalid": "用法:/send <umo> <文字或附件>,或先 /session connect 后再 /send。请保留消息文字中的指令头。",
Expand Down
132 changes: 128 additions & 4 deletions astrbot/builtin_stars/builtin_commands/commands/session.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from dataclasses import dataclass

from astrbot.api import Subject, star
from astrbot.api.event import AstrMessageEvent
from astrbot.api.platform import (
Expand All @@ -9,6 +11,20 @@
from .reply import reply_i18n
from .target import resolve_target_umo

_RULE_ID_CHARS = frozenset("0123456789abcdef")
_FILTER_SIDES = frozenset({"match", "except"})
_COMMAND_DIMENSIONS = {
"subject": "subjects",
"role": "roles",
"text": "text",
}


def _parse_rule_id(token: str) -> str:
if len(token) != 12 or any(char not in _RULE_ID_CHARS for char in token):
raise ValueError("Invalid filter arguments")
return token


def _resolve_listener(token: str, current_umo: str) -> str:
if token.lower() == "this":
Expand Down Expand Up @@ -55,10 +71,55 @@ def parse_unlink_spec(spec: str) -> str:
parts = spec.split()
if len(parts) != 1:
raise ValueError("Invalid unlink arguments")
rule_id = parts[0]
if len(rule_id) != 12 or any(char not in "0123456789abcdef" for char in rule_id):
raise ValueError("Invalid unlink arguments")
return rule_id
try:
return _parse_rule_id(parts[0])
except ValueError:
raise ValueError("Invalid unlink arguments") from None


@dataclass(frozen=True, slots=True)
class FilterSpec:
"""Parsed `/session filter` arguments."""

rule_id: str
action: str
side: str = ""
dimension: str = ""
value: str = ""


def parse_filter_spec(spec: str) -> FilterSpec:
"""Parse `/session filter <rule_id> [match|except|clear ...]`.

``text`` consumes the remainder as ``GreedyStr``. ``clear`` without a
side equals ``all``.
"""
stripped = spec.strip()
if not stripped:
raise ValueError("Invalid filter arguments")
parts = stripped.split()
rule_id = _parse_rule_id(parts[0])
if len(parts) == 1:
return FilterSpec(rule_id, "show")
if parts[1] == "clear":
if len(parts) == 2:
return FilterSpec(rule_id, "clear", "all")
if len(parts) == 3 and parts[2] in {"match", "except", "all"}:
return FilterSpec(rule_id, "clear", parts[2])
raise ValueError("Invalid filter arguments")
if parts[1] not in _FILTER_SIDES or len(parts) < 4:
raise ValueError("Invalid filter arguments")
command_dimension = parts[2]
dimension = _COMMAND_DIMENSIONS.get(command_dimension)
if dimension is None:
raise ValueError("Invalid filter arguments")
if command_dimension == "text":
value = stripped.split(None, 3)[3]
elif len(parts) == 4:
value = parts[3]
else:
raise ValueError("Invalid filter arguments")
return FilterSpec(rule_id, "append", parts[1], dimension, value)


def parse_pair_spec(spec: str) -> str:
Expand Down Expand Up @@ -476,6 +537,69 @@ async def unpair(self, event: AstrMessageEvent, spec: str = "") -> None:
"session.unpair.ok" if removed else "session.unpair.missing",
)

async def _format_filter_side(self, event: AstrMessageEvent, payload: dict) -> str:
lines = []
for key in ("subjects", "roles", "text"):
values = payload.get(key) or []
if values:
lines.append(f" {key}: {', '.join(values)}")
if lines:
return "\n".join(lines)
return await self.context.i18n.t(event, "session.filter.empty")

async def filter_rule(self, event: AstrMessageEvent, spec: str) -> None:
"""Show or change match/except filters on one directed edge."""
try:
parsed = parse_filter_spec(spec)
manager = self.context.bridges._manager
if parsed.action == "show":
result = await manager.get_filter(event, parsed.rule_id)
elif parsed.action == "clear":
result = await manager.clear_filter(event, parsed.rule_id, parsed.side)
else:
result = await manager.append_filter(
event,
parsed.rule_id,
parsed.side,
parsed.dimension,
parsed.value,
)
except PermissionError:
await reply_i18n(self.context, event, "session.bridge.denied")
return
except ValueError as exc:
message = str(exc)
if message == "Invalid filter role":
await reply_i18n(self.context, event, "session.filter.invalid_role")
return
if message == "Invalid filter pattern":
await reply_i18n(self.context, event, "session.filter.invalid_pattern")
return
if message == "Filter limit exceeded":
await reply_i18n(self.context, event, "session.filter.limit")
return
await reply_i18n(self.context, event, "session.filter.usage")
return
if result is None:
await reply_i18n(self.context, event, "session.filter.missing")
return
match, except_ = result
match_text = await self._format_filter_side(event, match)
except_text = await self._format_filter_side(event, except_)
key = "session.filter.body"
if parsed.action == "clear":
key = "session.filter.cleared"
elif parsed.action == "append":
key = "session.filter.updated"
await reply_i18n(
self.context,
event,
key,
rule_id=parsed.rule_id,
match=match_text,
except_=except_text,
)

async def disconnect(self, event: AstrMessageEvent) -> None:
"""Drop the unbounded link owned by the current actor."""
try:
Expand Down
8 changes: 8 additions & 0 deletions astrbot/builtin_stars/builtin_commands/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ async def session_unlink(
"""Remove a session-bridge rule by id."""
await self.session_c.unlink(event, spec)

@filter.permission("session.read")
@session.command("filter")
async def session_filter(
self, event: AstrMessageEvent, spec: GreedyStr = GreedyStr("")
) -> None:
"""Show or change match/except filters on one session-bridge edge."""
await self.session_c.filter_rule(event, spec)

@filter.permission("session.block")
@session.command("block")
async def session_block(
Expand Down
5 changes: 4 additions & 1 deletion astrbot/core/platform/message_projection.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,10 @@ def envelope_from_event(event: AstrMessageEvent) -> MessageEnvelope:
source_message_id=str(source_message_id) if source_message_id else None,
quote=quote,
created_at=event.created_at,
metadata={"platform_name": event.get_platform_name()},
metadata={
"platform_name": event.get_platform_name(),
"bot_account_id": str(event.get_self_id() or ""),
},
)


Expand Down
Loading
Loading