Skip to content

Commit 7369051

Browse files
committed
fix: apply command group custom filters to sub-commands
A custom filter registered on a command group was only consulted by CommandGroupFilter.filter(), i.e. by the group handler, which the waking check excludes from the activated handlers. Sub-command handlers are evaluated independently and their CommandFilter never looked at the ancestor groups, so @group.custom_filter(...) silently did nothing for sub-commands. CommandFilter now keeps a reference to its parent CommandGroupFilter and checks the ancestor groups' custom filters once the command name matches. CommandGroupFilter also checks its ancestors so nested groups are gated consistently. Group filter results are cached on the event, keeping a filter call to once per message even though both the group handler and the matching sub-command consult it. Adds tests/unit/test_group_custom_filter.py covering the cascade, nested groups, the sub-command's own filter, single evaluation and the registration wiring.
1 parent fc6b326 commit 7369051

4 files changed

Lines changed: 193 additions & 2 deletions

File tree

‎astrbot/core/star/filter/command.py‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
from __future__ import annotations
2+
13
import inspect
24
import re
35
import types
46
import typing
5-
from typing import Any
7+
from typing import TYPE_CHECKING, Any
68

79
from astrbot.core.config import AstrBotConfig
810
from astrbot.core.platform.astr_message_event import AstrMessageEvent
@@ -11,6 +13,9 @@
1113
from . import HandlerFilter
1214
from .custom_filter import CustomFilter
1315

16+
if TYPE_CHECKING:
17+
from .command_group import CommandGroupFilter
18+
1419

1520
class GreedyStr(str):
1621
"""标记指令完成其他参数接收后的所有剩余文本。"""
@@ -37,13 +42,15 @@ def __init__(
3742
alias: set | None = None,
3843
handler_md: StarHandlerMetadata | None = None,
3944
parent_command_names: list[str] | None = None,
45+
parent_group: CommandGroupFilter | None = None,
4046
) -> None:
4147
self.command_name = command_name
4248
self.alias = alias if alias else set()
4349
self._original_command_name = command_name
4450
self.parent_command_names = (
4551
parent_command_names if parent_command_names is not None else [""]
4652
)
53+
self.parent_group = parent_group
4754
if handler_md:
4855
self.init_handler_md(handler_md)
4956
self.custom_filter_list: list[CustomFilter] = []
@@ -90,6 +97,23 @@ def custom_filter_ok(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
9097
return False
9198
return True
9299

100+
def parent_custom_filter_ok(
101+
self, event: AstrMessageEvent, cfg: AstrBotConfig
102+
) -> bool:
103+
"""Check the custom filters of every ancestor command group.
104+
105+
A filter registered on a command group is meant to constrain the whole
106+
group, including its sub-commands. Sub-command handlers are evaluated
107+
independently from the group handler, so the ancestor filters have to
108+
be checked here as well.
109+
"""
110+
parent_group = self.parent_group
111+
if parent_group is None:
112+
return True
113+
return parent_group.custom_filter_ok(
114+
event, cfg
115+
) and parent_group.ancestor_custom_filter_ok(event, cfg)
116+
93117
def validate_and_convert_params(
94118
self,
95119
params: list[Any],
@@ -205,6 +229,9 @@ def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
205229
if not ok:
206230
return False
207231

232+
if not self.parent_custom_filter_ok(event, cfg):
233+
return False
234+
208235
# 分割为列表
209236
ls = message_str.split(" ")
210237
# 去除空字符串

‎astrbot/core/star/filter/command_group.py‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,31 @@ def print_cmd_tree(
105105
return "".join(parts)
106106

107107
def custom_filter_ok(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
108+
# The same group filter may be evaluated several times for one message
109+
# (by the group handler and by each matching sub-command), so cache the
110+
# result on the event to keep side effects to a single call.
111+
cache_key = f"custom_filter_ok:{id(self)}"
112+
cached = event.get_extra(cache_key)
113+
if cached is not None:
114+
return cached
115+
116+
result = True
108117
for custom_filter in self.custom_filter_list:
109118
if not custom_filter.filter(event, cfg):
119+
result = False
120+
break
121+
event.set_extra(cache_key, result)
122+
return result
123+
124+
def ancestor_custom_filter_ok(
125+
self, event: AstrMessageEvent, cfg: AstrBotConfig
126+
) -> bool:
127+
"""Check the custom filters of every ancestor command group."""
128+
parent_group = self.parent_group
129+
while parent_group is not None:
130+
if not parent_group.custom_filter_ok(event, cfg):
110131
return False
132+
parent_group = parent_group.parent_group
111133
return True
112134

113135
def startswith(self, message_str: str) -> bool:
@@ -127,10 +149,13 @@ def filter(self, event: AstrMessageEvent, cfg: AstrBotConfig) -> bool:
127149
if not event.is_at_or_wake_command:
128150
return False
129151

130-
# 判断当前指令组的自定义过滤器
152+
# 判断当前指令组以及上层指令组的自定义过滤器
131153
if not self.custom_filter_ok(event, cfg):
132154
return False
133155

156+
if not self.ancestor_custom_filter_ok(event, cfg):
157+
return False
158+
134159
if self.equals(event.message_str.strip()):
135160
tree = (
136161
self.group_name

‎astrbot/core/star/register/star_handler.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ def register_command(
9292
alias,
9393
None,
9494
parent_command_names=parent_command_names,
95+
parent_group=command_name.parent_group,
9596
)
9697
command_name.parent_group.add_sub_command_filter(new_command)
9798
else:
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"""Tests for the cascade of command-group custom filters to sub-commands."""
2+
3+
from typing import Any
4+
5+
6+
class FakeEvent:
7+
"""Minimal AstrMessageEvent stand-in for filter unit tests."""
8+
9+
def __init__(self, message_str: str) -> None:
10+
self.message_str = message_str
11+
self.is_at_or_wake_command = True
12+
self._extras: dict[str, Any] = {}
13+
14+
def get_message_str(self) -> str:
15+
return self.message_str
16+
17+
def get_extra(self, key: str | None = None, default: Any = None) -> Any:
18+
if key is None:
19+
return default
20+
return self._extras.get(key, default)
21+
22+
def set_extra(self, key: str, value: Any) -> None:
23+
self._extras[key] = value
24+
25+
26+
def make_custom_filter(allowed: bool, calls: list[int] | None = None):
27+
"""Build a CustomFilter that returns ``allowed`` and counts invocations."""
28+
from astrbot.core.star.filter.custom_filter import CustomFilter
29+
30+
class _Filter(CustomFilter):
31+
def filter(self, event, cfg) -> bool:
32+
if calls is not None:
33+
calls.append(1)
34+
return allowed
35+
36+
return _Filter()
37+
38+
39+
def make_group(name: str, parent_group=None):
40+
from astrbot.core.star.filter.command_group import CommandGroupFilter
41+
42+
return CommandGroupFilter(name, parent_group=parent_group)
43+
44+
45+
def make_sub_command(name: str, group):
46+
from astrbot.core.star.filter.command import CommandFilter
47+
48+
sub_command = CommandFilter(
49+
name,
50+
parent_command_names=group.get_complete_command_names(),
51+
parent_group=group,
52+
)
53+
# Normally filled by init_handler_md() while decorating the handler.
54+
sub_command.handler_params = {}
55+
return sub_command
56+
57+
58+
def test_group_filter_blocks_sub_command():
59+
group = make_group("math")
60+
group.add_custom_filter(make_custom_filter(False))
61+
sub_command = make_sub_command("add", group)
62+
63+
assert sub_command.filter(FakeEvent("math add"), {}) is False
64+
65+
66+
def test_group_filter_allows_sub_command():
67+
group = make_group("math")
68+
group.add_custom_filter(make_custom_filter(True))
69+
sub_command = make_sub_command("add", group)
70+
71+
assert sub_command.filter(FakeEvent("math add"), {}) is True
72+
73+
74+
def test_nested_group_filter_blocks_sub_command():
75+
outer = make_group("math")
76+
outer.add_custom_filter(make_custom_filter(False))
77+
inner = make_group("calc", parent_group=outer)
78+
inner.add_custom_filter(make_custom_filter(True))
79+
sub_command = make_sub_command("add", inner)
80+
81+
assert sub_command.filter(FakeEvent("math calc add"), {}) is False
82+
83+
84+
def test_sub_command_filter_still_applies():
85+
group = make_group("math")
86+
group.add_custom_filter(make_custom_filter(True))
87+
sub_command = make_sub_command("add", group)
88+
sub_command.add_custom_filter(make_custom_filter(False))
89+
90+
assert sub_command.filter(FakeEvent("math add"), {}) is False
91+
92+
93+
def test_group_filter_is_evaluated_once_per_message():
94+
calls: list[int] = []
95+
group = make_group("math")
96+
group.add_custom_filter(make_custom_filter(True, calls))
97+
sub_command = make_sub_command("add", group)
98+
event = FakeEvent("math add")
99+
100+
assert group.filter(event, {}) is True
101+
assert sub_command.filter(event, {}) is True
102+
assert len(calls) == 1
103+
104+
105+
def test_group_filter_not_checked_for_unrelated_message():
106+
calls: list[int] = []
107+
group = make_group("math")
108+
group.add_custom_filter(make_custom_filter(False, calls))
109+
sub_command = make_sub_command("add", group)
110+
111+
assert sub_command.filter(FakeEvent("other add"), {}) is False
112+
assert calls == []
113+
114+
115+
def test_register_command_attaches_parent_group():
116+
from astrbot.core.star.filter.command_group import CommandGroupFilter
117+
from astrbot.core.star.register.star_handler import (
118+
RegisteringCommandable,
119+
register_command,
120+
)
121+
from astrbot.core.star.star_handler import star_handlers_registry
122+
123+
group = CommandGroupFilter("_scope_test_math")
124+
125+
async def handler(event):
126+
pass
127+
128+
md = None
129+
try:
130+
register_command(RegisteringCommandable(group), "_scope_test_add")(handler)
131+
md = star_handlers_registry.get_handler_by_full_name(
132+
f"{handler.__module__}_{handler.__name__}"
133+
)
134+
assert md is not None
135+
assert md.event_filters[0].parent_group is group
136+
finally:
137+
if md is not None:
138+
star_handlers_registry.remove(md)

0 commit comments

Comments
 (0)