Skip to content

Commit 70cf617

Browse files
mbaruhlemonyte
andauthored
Allow certain roles to run AoC commands in November (#171)
* Allow certain roles to run AoC commands in November * Fix role condition for listeners Co-authored-by: lemonyte <49930425+lemonyte@users.noreply.github.com> * Use MissingAnyRole instead of generic CheckFailure --------- Co-authored-by: lemonyte <49930425+lemonyte@users.noreply.github.com>
1 parent 20b9a4b commit 70cf617

File tree

3 files changed

+61
-15
lines changed

3 files changed

+61
-15
lines changed

bot/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ class _Roles(EnvConfig, env_prefix="ROLE_"):
154154
admins: int = 267628507062992896
155155
advent_of_code: int = 518565788744024082
156156
code_jam_event_team: int = 787816728474288181
157+
core_devs: int = 587606783669829632
157158
events_lead: int = 778361735739998228
158159
event_runner: int = 940911658799333408
159160
summer_aoc: int = 988801794668908655

bot/exts/advent_of_code/_cog.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737

3838
AOC_REDIRECT = (Channels.advent_of_code_commands, Channels.sir_lancebot_playground, Channels.bot_commands)
3939

40+
EARLY_ACCESS_ROLES = (Roles.admins, Roles.events_lead, Roles.core_devs)
41+
4042

4143
class AdventOfCode(commands.Cog):
4244
"""Advent of Code festivities! Ho Ho Ho!"""
@@ -386,6 +388,7 @@ async def aoc_unlink_account(self, ctx: commands.Context) -> None:
386388
await ctx.reply("You don't have an Advent of Code account linked.")
387389

388390
@in_month(Month.DECEMBER, Month.JANUARY, Month.FEBRUARY)
391+
@in_month(Month.NOVEMBER, roles=EARLY_ACCESS_ROLES)
389392
@adventofcode_group.command(
390393
name="dayandstar",
391394
aliases=("daynstar", "daystar"),
@@ -424,6 +427,7 @@ async def aoc_day_and_star_leaderboard(
424427
await message.edit(view=None)
425428

426429
@in_month(Month.DECEMBER, Month.JANUARY, Month.FEBRUARY)
430+
@in_month(Month.NOVEMBER, roles=EARLY_ACCESS_ROLES)
427431
@adventofcode_group.command(
428432
name="leaderboard",
429433
aliases=("board", "lb"),
@@ -505,6 +509,7 @@ async def aoc_global_leaderboard(self, ctx: commands.Context) -> None:
505509
await ctx.send(embed=self.cached_no_global)
506510

507511
@in_month(Month.DECEMBER, Month.JANUARY, Month.FEBRUARY)
512+
@in_month(Month.NOVEMBER, roles=EARLY_ACCESS_ROLES)
508513
@adventofcode_group.command(
509514
name="stats",
510515
aliases=("dailystats", "ds"),

bot/utils/decorators.py

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from collections.abc import Callable, Container
55

66
from discord.ext import commands
7-
from discord.ext.commands import Command, Context
7+
from discord.ext.commands import Command, Context, errors
88
from pydis_core.utils import logging
99

1010
from 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

Comments
 (0)