Skip to content

feat: add is_admin and is_moderator checks - #111

Merged
PenguinBoi12 merged 4 commits into
Code-Society-Lab:mainfrom
akagifreeez:feat/admin-moderator-checks
Jul 22, 2026
Merged

feat: add is_admin and is_moderator checks#111
PenguinBoi12 merged 4 commits into
Code-Society-Lab:mainfrom
akagifreeez:feat/admin-moderator-checks

Conversation

@akagifreeez

Copy link
Copy Markdown
Contributor

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 Outdated

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 Outdated
"""

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
Addresses the review nit on Code-Society-Lab#111: is_admin() and is_moderator() had
their thresholds hardcoded (100/50). They are now module-level
constants, ADMIN_POWER_LEVEL and MODERATOR_POWER_LEVEL, referenced by
the checks, their docstrings and the boundary tests. An extra test
pins the values to the m.room.power_levels defaults so they cannot
drift silently.

No behaviour change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@akagifreeez

Copy link
Copy Markdown
Contributor Author

Thanks for the review!

Pushed the nit in 406d945: ADMIN_POWER_LEVEL = 100 and MODERATOR_POWER_LEVEL = 50 are now module-level constants in matrix/checks.py, used by both checks, referenced from their docstrings, and used for the boundary cases in the tests. I also added a small test pinning them to the m.room.power_levels defaults so they can't drift silently. No behaviour change.

Agreed that #105 is the cleaner primitive — these two would sit nicely on top of has_power_level once it exists.

Local run on the new commit: black --check matrix/ tests/ examples/, mypy matrix, pytest → 300 passed. The workflows for this push are sitting at "action required" (fork PR), so they need your approval to run.

The push also dismissed the approval, so this needs another look when you get the chance. Thanks again!

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

LGTM 🦭

@PenguinBoi12
PenguinBoi12 merged commit 9598df1 into Code-Society-Lab:main Jul 22, 2026
4 checks passed
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