feat: add is_admin and is_moderator checks#111
Conversation
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>
There was a problem hiding this comment.
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 🙂
|
|
||
| def is_admin() -> Callable: | ||
| """ | ||
| Decorator to restrict a command to room admins (power level >= 100). |
There was a problem hiding this comment.
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
| """ | ||
|
|
||
| async def _is_admin(ctx: "Context") -> bool: | ||
| return ctx.room.power_levels.get_user_level(ctx.sender) >= 100 # type: ignore[no-any-return] |
There was a problem hiding this comment.
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 >= 100I'm fine with both
What & why
Adds the
is_admin()andis_moderator()command checks proposed in #103 and #104. Both follow the exact shape of the existingcooldown()decorator: an outer factory returning awrapper(cmd)that registers an async predicate viacmd.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 throughRoom.__getattr__to the wrappednio.MatrixRoom) and compare it against the Matrix convention thresholds: 100 for admin, 50 for moderator. A failing check flows through the existingCommand._invoke→CheckErrorpath, so no new exception types were needed.Both names are exported from
matrix/__init__.pyalongsidecooldown, and the::: matrix.checksreference docs pick the new functions up automatically.Closes #103
Closes #104
How to test
tests/test_checks.py(new) covers:users_defaultwhen the sender is absent frompower_levels.usersis_admin()(cmd) is cmd/is_moderator()(cmd) is cmd(decorators must return the command)CheckErrorhandler fires; a moderator-level sender runs itNotes
blackclean,mypyclean,pytest299 passed (286 baseline + 13 new).# type: ignore[no-any-return]on the predicates mirrors the existing convention for attributes delegated to_matrix_room(seematrix/room.py).