Skip to content

feat: add is_admin and is_moderator checks#111

Open
akagifreeez wants to merge 2 commits into
Code-Society-Lab:mainfrom
akagifreeez:feat/admin-moderator-checks
Open

feat: add is_admin and is_moderator checks#111
akagifreeez wants to merge 2 commits into
Code-Society-Lab:mainfrom
akagifreeez:feat/admin-moderator-checks

Conversation

@akagifreeez

Copy link
Copy Markdown

What & why

Adds the is_admin() and is_moderator() command checks proposed in #103 and #104. Both follow the exact shape of the existing cooldown() decorator: an outer factory returning a wrapper(cmd) that registers an async predicate via cmd.check(...) and returns the command.

The predicates read the sender's power level from the room (ctx.room.power_levels.get_user_level(ctx.sender), resolved through Room.__getattr__ to the wrapped nio.MatrixRoom) and compare it against the Matrix convention thresholds: 100 for admin, 50 for moderator. A failing check flows through the existing Command._invokeCheckError path, so no new exception types were needed.

Both names are exported from matrix/__init__.py alongside cooldown, and the ::: matrix.checks reference docs pick the new functions up automatically.

Closes #103
Closes #104

How to test

black --check matrix/ tests/ examples/
mypy matrix
pytest -v

tests/test_checks.py (new) covers:

  • power-level boundaries for both checks (100/99/50/49/0), parametrized
  • the fallback to users_default when the sender is absent from power_levels.users
  • is_admin()(cmd) is cmd / is_moderator()(cmd) is cmd (decorators must return the command)
  • end-to-end: a non-admin sender never runs the command body and the registered CheckError handler fires; a moderator-level sender runs it

Notes

  • Locally verified on this branch: black clean, mypy clean, pytest 299 passed (286 baseline + 13 new).
  • The # type: ignore[no-any-return] on the predicates mirrors the existing convention for attributes delegated to _matrix_room (see matrix/room.py).
  • Disclosure: this contribution was developed with LLM assistance (Claude); all changes were reviewed, and the test suite was run locally as above.

akagifreeez and others added 2 commits July 7, 2026 10:38
Adds two check decorators alongside the existing cooldown() in
matrix/checks.py, following the same pattern: an inner async
predicate that inspects ctx.room.power_levels (via nio's
MatrixRoom.power_levels / PowerLevels.get_user_level) and a wrapper
that registers it with Command.check() and returns the command.

- is_admin(): requires power level >= 100
- is_moderator(): requires power level >= 50

Both feed into the existing Command._invoke -> CheckError flow, no
new exception types needed.

Closes Code-Society-Lab#103, Code-Society-Lab#104

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@PenguinBoi12 PenguinBoi12 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.

Looks pretty good to me. I would have implemented #105 first, in its own PR, and leveraged it is_admin and is_moderator but it's fine for that PR and can be done in another one.

Also, another nit that I have is is_admin()/is_moderator() have hardcoded power-levels (100/50), I'd suggest to at least make 2 constant:

ADMIN_POWER_LEVEL: int = 100
MODERATOR_POWER_LEVEL: int = 50

Anyway, nothing is blocking, if you decide to do the nit, that's good otherwise it looks good to me 🦭

Don't forget to start ⭐ the project 🙂

Comment thread matrix/checks.py

def is_admin() -> Callable:
"""
Decorator to restrict a command to room admins (power level >= 100).

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.

nit: I think it would be great to add the reference (https://matrix.org/docs/communities/moderation/) so people reading this understand that it's not an arbitrary number.

Same thing for is_moderator

Comment thread matrix/checks.py
"""

async def _is_admin(ctx: "Context") -> bool:
return ctx.room.power_levels.get_user_level(ctx.sender) >= 100 # type: ignore[no-any-return]

@PenguinBoi12 PenguinBoi12 Jul 7, 2026

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.

nit: You shouldn't need the # type: ignore[no-any-return] but I see why you need it... If you want to you can fix the real issue which is that Command.check takes a Callback when it should probably have its own type alias.

It's a simple fix

# command.py
Callback = Callable[..., Coroutine[Any, Any, Any]]
+ CheckCallback = Callable[["Context"], Coroutine[Any, Any, bool]]
ErrorCallback = Callable[["Context", Exception], Coroutine[Any, Any, Any]]

 
-    def check(self, func: Callback) -> None:
+    def check(self, func: CheckCallback) -> None:

Then:

# checks.py
async def _is_admin(ctx: "Context") -> bool:
-     return ctx.room.power_levels.get_user_level(ctx.sender) >= 100  # type: ignore[no-any-return
+    return bool(ctx.room.power_levels.get_user_level(ctx.sender) >= 100)

async def _is_moderator(ctx: "Context") -> bool:
-     return ctx.room.power_levels.get_user_level(ctx.sender) >= 50  # type: ignore[no-any-return]
+    return bool(ctx.room.power_levels.get_user_level(ctx.sender) >= 50)

We still need to call bool but at least it's clearer than using # type: ignore[no-any-return]

Or we can:

level: int = ctx.room.power_levels.get_user_level(ctx.sender)
return level >= 100

I'm fine with both

@PenguinBoi12 PenguinBoi12 added the feature A new feature label Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature A new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Check: is_moderator Check: is_admin

2 participants