diff options
| author | 2021-10-12 20:38:54 +0100 | |
|---|---|---|
| committer | 2021-11-30 11:40:16 +0000 | |
| commit | 4f7010912ccc75ea1415bc5e1e10fbce17c43b69 (patch) | |
| tree | 02c8f4445f5c2fc61f50074e484e4db421b73eb6 | |
| parent | Add self assignable roles to config (diff) | |
Add an interactive subscribe command
This command gives the users a set of buttons to click to add or remove pre-determined announcement roles.
Adding or removing a role updates the button state to reflect the change and what would happen if the user clicks the button again.
Diffstat (limited to '')
| -rw-r--r-- | bot/exts/info/subscribe.py | 139 | 
1 files changed, 139 insertions, 0 deletions
| diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py new file mode 100644 index 000000000..edf8e8f9e --- /dev/null +++ b/bot/exts/info/subscribe.py @@ -0,0 +1,139 @@ +import logging + +import arrow +import discord +from discord.ext import commands +from discord.interactions import Interaction + +from bot import constants +from bot.bot import Bot +from bot.decorators import in_whitelist +from bot.utils import checks, members, scheduling + +# Tuple of tuples, where each inner tuple is a role id and a month number. +# The month number signifies what month the role should be assignable, +# use None for the month number if it should always be active. +ASSIGNABLE_ROLES = ( +    (constants.Roles.announcements, None), +    (constants.Roles.pyweek_announcements, None), +    (constants.Roles.lovefest, 2), +    (constants.Roles.advent_of_code, 12), +) +ITEMS_PER_ROW = 3 + +log = logging.getLogger(__name__) + + +class RoleButtonView(discord.ui.View): +    """A list of SingleRoleButtons to show to the member.""" + +    def __init__(self, member: discord.Member): +        super().__init__() +        self.interaction_owner = member + +    async def interaction_check(self, interaction: Interaction) -> bool: +        """Ensure that the user clicking the button is the member who invoked the command.""" +        if interaction.user != self.interaction_owner: +            await interaction.response.send_message( +                ":x: This is not your command to react to!", +                ephemeral=True +            ) +            return False +        return True + + +class SingleRoleButton(discord.ui.Button): +    """A button that adds or removes a role from the member depending on it's current state.""" + +    ADD_STYLE = discord.ButtonStyle.success +    REMOVE_STYLE = discord.ButtonStyle.secondary +    LABEL_FORMAT = "{action} role {role_name}" +    CUSTOM_ID_FORMAT = "subscribe-{role_id}" + +    def __init__(self, role: discord.Role, assigned: bool, row: int): +        super().__init__( +            style=self.REMOVE_STYLE if assigned else self.ADD_STYLE, +            label=self.LABEL_FORMAT.format(action="Remove" if assigned else "Add", role_name=role.name), +            custom_id=self.CUSTOM_ID_FORMAT.format(role_id=role.id), +            row=row, +        ) +        self.role = role +        self.assigned = assigned + +    async def callback(self, interaction: Interaction) -> None: +        """Update the member's role and change button text to reflect current text.""" +        await members.handle_role_change( +            interaction.user, +            interaction.user.remove_roles if self.assigned else interaction.user.add_roles, +            self.role, +        ) + +        self.assigned = not self.assigned +        await self.update_view(interaction) +        await interaction.response.send_message( +            self.LABEL_FORMAT.format(action="Added" if self.assigned else "Removed", role_name=self.role.name), +            ephemeral=True, +        ) + +    async def update_view(self, interaction: Interaction) -> None: +        """Updates the original interaction message with a new view object with the updated buttons.""" +        self.style = self.REMOVE_STYLE if self.assigned else self.ADD_STYLE +        self.label = self.LABEL_FORMAT.format(action="Remove" if self.assigned else "Add", role_name=self.role.name) +        try: +            await interaction.message.edit(view=self.view) +        except discord.NotFound: +            log.debug("Subscribe message for %s removed before buttons could be updated", interaction.user) +            self.view.stop() + + +class Subscribe(commands.Cog): +    """Cog to allow user to self-assign & remove the roles present in ASSIGNABLE_ROLES.""" + +    def __init__(self, bot: Bot): +        self.bot = bot +        self.init_task = scheduling.create_task(self.init_cog(), event_loop=self.bot.loop) +        self.assignable_roles: list[discord.Role] = [] +        self.guild: discord.Guild = None + +    async def init_cog(self) -> None: +        """Initialise the cog by resolving the role IDs in ASSIGNABLE_ROLES to role names.""" +        await self.bot.wait_until_guild_available() + +        current_month = arrow.utcnow().month +        self.guild = self.bot.get_guild(constants.Guild.id) + +        for role_id, month_available in ASSIGNABLE_ROLES: +            if month_available is not None and month_available != current_month: +                continue +            role = self.guild.get_role(role_id) +            if role is None: +                log.warning("Could not resolve %d to a role in the guild, skipping.", role_id) +                continue +            self.assignable_roles.append(role) + +    @commands.command(name="subscribe") +    @in_whitelist(channels=(constants.Channels.bot_commands,)) +    async def subscribe_command(self, ctx: commands.Context, *_) -> None:  # We don't actually care about the args +        """Display the member's current state for each role, and allow them to add/remove the roles.""" +        await self.init_task + +        button_view = RoleButtonView(ctx.author) +        for index, role in enumerate(self.assignable_roles): +            row = index // ITEMS_PER_ROW +            button_view.add_item(SingleRoleButton(role, role in ctx.author.roles, row)) + +        await ctx.send("Click the buttons below to add or remove your roles!", view=button_view) + +    # This cannot be static (must have a __func__ attribute). +    async def cog_command_error(self, ctx: commands.Context, error: Exception) -> None: +        """Check for & ignore any InWhitelistCheckFailure.""" +        if isinstance(error, checks.InWhitelistCheckFailure): +            error.handled = True + + +def setup(bot: Bot) -> None: +    """Load the Subscribe cog.""" +    if len(ASSIGNABLE_ROLES) > ITEMS_PER_ROW*5:  # Discord limits views to 5 rows of buttons. +        log.error("Too many roles for 5 rows, not loading the Subscribe cog.") +    else: +        bot.add_cog(Subscribe(bot)) | 
