Skip to content

fix: apply command group custom filters to sub-commands - #10646

Open
AraragiEro wants to merge 1 commit into
AstrBotDevs:masterfrom
AraragiEro:fix/group-custom-filter-cascade
Open

AraragiEro wants to merge 1 commit into
AstrBotDevs:masterfrom
AraragiEro:fix/group-custom-filter-cascade

Conversation

@AraragiEro

@AraragiEro AraragiEro commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

A custom filter registered on a command group does not constrain its sub-commands.

@group.custom_filter(...) stores the filter in CommandGroupFilter.custom_filter_list, which is only consulted by CommandGroupFilter.filter() — i.e. by the group handler. The waking check excludes group handlers from the activated handlers, and sub-command handlers are evaluated independently: their CommandFilter.filter() only checks their own custom_filter_list and never looks at the ancestor groups.

So @filter.custom_filter(...) on a command group silently does nothing for sub-commands (its only remaining effect was suppressing the "insufficient parameters" tree when only the group name is sent). Verified with a minimal probe on master:

case sub-command handler passes
group filter denies, sub-command name matches True (still runs)
sub-command has its own denying filter False

For comparison, a group-level permission_type is enforced, because waking_check special-cases PermissionTypeFilter and stops the event propagation.

Fix

  • CommandFilter keeps a reference to its parent CommandGroupFilter (parent_group=..., wired in register_command) and checks the ancestor groups' custom filters once the command name matches.
  • CommandGroupFilter.filter() also checks its ancestors, so nested groups are gated consistently (e.g. a filter on math also applies to math calc add).
  • Group filter results are cached on the event (custom_filter_ok:<id>), so a filter is evaluated once per message even though both the group handler and the matching sub-command consult it. Without the cache a filter that sends a hint (as suggested in the docs) would fire twice.

Tests

tests/unit/test_group_custom_filter.py:

  • group filter blocks / allows a sub-command
  • nested groups: an outer deny blocks a deep sub-command
  • the sub-command's own filter still applies
  • a group filter is evaluated exactly once per message
  • the group filter is not evaluated for unrelated messages
  • register_command attaches the parent group to the created CommandFilter

Notes

Summary by Sourcery

Ensure command-group custom filters constrain all matching sub-commands and nested command groups.

Bug Fixes:

  • Apply command-group custom filters to matching sub-commands, including commands nested under multiple groups.
  • Prevent group custom filters from running for unrelated messages and ensure each filter is evaluated only once per message.

Enhancements:

  • Propagate parent group context to registered command filters and consistently enforce ancestor group filters.

Tests:

  • Add coverage for group-filter propagation, nested groups, filter precedence, evaluation caching, unrelated messages, help behavior, and parent-group registration.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/star/filter/command_group.py" line_range="116-121" />
<code_context>
+        if cached is not None:
+            return cached
+
+        result = True
         for custom_filter in self.custom_filter_list:
             if not custom_filter.filter(event, cfg):
+                result = False
+                break
+        event.set_extra(cache_key, result)
+        return result
+
</code_context>
<issue_to_address>
**Group filters run on unrelated messages**

When a message reaches `waking_check` while a registered command-group handler is enabled, but the message does not start with that group’s command name, `waking_check` calls each handler filter before checking whether its command matches, and `CommandGroupFilter.filter()` calls `custom_filter_ok()` before `startswith()`. A group custom filter that sends a hint therefore sends it for unrelated messages too, despite the unrelated-message test passing when only the sub-command filter is called directly.

Check whether the message matches the group before evaluating its custom filters, while retaining the filter check for exact group-name messages that produce the insufficient-parameters help.
</issue_to_address>

### Comment 2
<location path="astrbot/core/star/filter/command.py" line_range="45" />
<code_context>
         alias: set | None = None,
         handler_md: StarHandlerMetadata | None = None,
         parent_command_names: list[str] | None = None,
+        parent_group: CommandGroupFilter | None = None,
     ) -> None:
         self.command_name = command_name
</code_context>
<issue_to_address>
**Type hint inspection raises NameError**

When a plugin or tooling caller evaluates the constructor annotations with `typing.get_type_hints`, `typing.get_type_hints(CommandFilter.__init__)` tries to resolve the new `CommandGroupFilter` annotation, but that name is only imported under `TYPE_CHECKING`, so introspection raises `NameError` instead of returning the constructor hints.

Make the annotation resolvable at runtime or avoid exposing an unresolved forward reference in the constructor hints.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: astrbot/core/star/filter/command_group.py:121, astrbot/core/star/filter/command.py:45


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread astrbot/core/star/filter/command_group.py
Comment thread astrbot/core/star/filter/command.py Outdated
@AraragiEro
AraragiEro force-pushed the fix/group-custom-filter-cascade branch from 7369051 to ec8cc5f Compare October 20, 2026 06:08
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 command group 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.

CommandGroupFilter now matches the command name before evaluating the custom filters, so a filter with side effects (for example sending a hint) is not triggered by unrelated messages. The parent group annotation uses a small protocol so the constructor type hints stay resolvable at runtime without a circular import.

Adds tests/unit/test_group_custom_filter.py covering the cascade, nested groups, the sub-command's own filter, single evaluation, group-name help behavior and the registration wiring.
@AraragiEro
AraragiEro force-pushed the fix/group-custom-filter-cascade branch from ec8cc5f to bcebd70 Compare October 20, 2026 09:05
@AraragiEro

Copy link
Copy Markdown
Contributor Author

Both findings addressed in bcebd70.

1. Group filters run on unrelated messages

CommandGroupFilter.filter() now checks startswith() (whole-word group match) before evaluating any custom filters, so a filter with side effects is not triggered by unrelated messages. The filter is still checked before the insufficient-parameters help is raised, so a denying filter keeps suppressing the help tree.

Tests added:

  • test_group_handler_skips_filters_for_unrelated_message
  • test_group_handler_shows_help_for_exact_group_name
  • test_group_handler_hides_help_when_filter_denies

2. Type hint inspection raises NameError

parent_group is now annotated with a small CommandGroupLike protocol defined in command.py itself. It describes the structural type of CommandGroupFilter (parent_group, custom_filter_ok(), ancestor_custom_filter_ok()), so no forward reference to a TYPE_CHECKING-only name is exposed.

Verified locally with the stub-loaded modules:

get_type_hints ok: ['alias', 'command_name', 'handler_md', 'parent_command_names', 'parent_group', 'return']

ruff format --check and ruff check pass on all touched files, and the nine new/updated filter tests pass.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant