44from collections .abc import Callable , Container
55
66from discord .ext import commands
7- from discord .ext .commands import Command , Context
7+ from discord .ext .commands import Command , Context , errors
88from pydis_core .utils import logging
99
1010from bot .constants import Channels
@@ -70,28 +70,66 @@ async def guarded_listener(*args, **kwargs) -> None:
7070 return decorator
7171
7272
73- def in_month_command (* allowed_months : Month ) -> Callable :
73+ def in_month_command (* allowed_months : Month , roles : tuple [ int , ...] = () ) -> Callable :
7474 """
75- Check whether the command was invoked in one of `enabled_months`.
75+ Check whether the command was invoked in one of `allowed_months`.
76+
77+ The check can be limited to certain roles.
78+ To enable the supplied months for everyone, don't set a value for `roles`.
79+
80+ If a command is decorated several times with this, it only needs to pass one of the checks.
7681
7782 Uses the current UTC month at the time of running the predicate.
7883 """
7984 async def predicate (ctx : Context ) -> bool :
80- current_month = resolve_current_month ()
81- can_run = current_month in allowed_months
82-
83- log .debug (
84- f"Command '{ ctx .command } ' is locked to months { human_months (allowed_months )} . "
85- f"Invoking it in month { current_month !s} is { 'allowed' if can_run else 'disallowed' } ."
86- )
87- if can_run :
85+ command = ctx .command
86+ if "month_checks" not in command .extras :
87+ log .debug (f"No month checks found for command { command } ." )
8888 return True
89- raise InMonthCheckFailure (f"Command can only be used in { human_months (allowed_months )} " )
9089
91- return commands .check (predicate )
90+ everyone_error = None
91+ privileged_user = False
92+ allowed_months_for_user = set ()
93+ current_month = resolve_current_month ()
94+ user_roles = set (r .id for r in ctx .author .roles ) if ctx .author .roles else set ()
95+
96+ for checked_roles , checked_months in command .extras ["month_checks" ].items ():
97+ if checked_roles :
98+ if not set (checked_roles ) & user_roles :
99+ log .debug (f"Month check for roles { checked_roles } doesn't apply to { ctx .author } ." )
100+ continue
101+
102+ if current_month in checked_months :
103+ log .debug (f"Month check for roles { checked_roles } passed for { ctx .author } ." )
104+ return True
105+
106+ log .debug (f"Month check for roles { checked_roles } didn't pass for { ctx .author } ." )
107+ if not checked_roles :
108+ everyone_error = InMonthCheckFailure (f"Command can only be used in { human_months (checked_months )} " )
109+ else :
110+ privileged_user = True
111+ allowed_months_for_user |= set (checked_months )
112+
113+ if privileged_user :
114+ allowed_months_for_user = sorted (allowed_months_for_user )
115+ raise InMonthCheckFailure (f"You can run this command only in { human_months (allowed_months_for_user )} " )
116+ if everyone_error :
117+ raise everyone_error
118+ # There's no general access to this command, and the user has no relevant roles.
119+ raise errors .MissingAnyRole (list ({r for rs in command .extras ["month_checks" ] for r in rs }))
120+
121+ def decorator (func : Command ) -> Command :
122+ if "month_checks" in func .extras :
123+ func .extras ["month_checks" ][roles ] = allowed_months
124+ return func
125+
126+ func .extras ["month_checks" ] = {roles : allowed_months }
127+ return commands .check (predicate )(func )
128+
129+ return decorator
92130
93131
94- def in_month (* allowed_months : Month ) -> Callable :
132+ def in_month (* allowed_months : Month , roles : tuple [ int , ...] = () ) -> Callable :
95133 """
96134 Universal decorator for season-locking commands and listeners alike.
97135
@@ -113,10 +151,12 @@ def decorator(callable_: Callable) -> Callable:
113151 # Functions decorated as commands are turned into instances of `Command`
114152 if isinstance (callable_ , Command ):
115153 log .debug (f"Command { callable_ .qualified_name } will be locked to { human_months (allowed_months )} " )
116- actual_deco = in_month_command (* allowed_months )
154+ actual_deco = in_month_command (* allowed_months , roles = roles )
117155
118156 # D.py will assign this attribute when `callable_` is registered as a listener
119157 elif hasattr (callable_ , "__cog_listener__" ):
158+ if roles :
159+ raise ValueError ("Role restrictions are not available for listeners." )
120160 log .debug (f"Listener { callable_ .__qualname__ } will be locked to { human_months (allowed_months )} " )
121161 actual_deco = in_month_listener (* allowed_months )
122162
0 commit comments