diff options
-rw-r--r-- | bot/constants.py | 2 | ||||
-rw-r--r-- | bot/exts/info/subscribe.py | 136 | ||||
-rw-r--r-- | config-default.yml | 3 |
3 files changed, 126 insertions, 15 deletions
diff --git a/bot/constants.py b/bot/constants.py index 9851aea97..3c29ce887 100644 --- a/bot/constants.py +++ b/bot/constants.py @@ -465,6 +465,8 @@ class Channels(metaclass=YAMLGetter): big_brother_logs: int + roles: int + class Webhooks(metaclass=YAMLGetter): section = "guild" diff --git a/bot/exts/info/subscribe.py b/bot/exts/info/subscribe.py index 36304539f..63a23346f 100644 --- a/bot/exts/info/subscribe.py +++ b/bot/exts/info/subscribe.py @@ -13,6 +13,7 @@ from bot import constants from bot.bot import Bot from bot.decorators import redirect_output from bot.log import get_logger +from bot.utils.channel import get_or_fetch_channel @dataclass(frozen=True) @@ -60,10 +61,23 @@ log = get_logger(__name__) class RoleButtonView(discord.ui.View): - """A list of SingleRoleButtons to show to the member.""" + """ + A view that holds the list of SingleRoleButtons to show to the member. + + Attributes + __________ + anchor_message : discord.Message + The message to which this view will be attached + interaction_owner: discord.Member + The member that initiated the interaction + """ + + anchor_message: discord.Message def __init__(self, member: discord.Member): - super().__init__() + super().__init__(timeout=DELETE_MESSAGE_AFTER) + # We can't obtain the reference to the message before the view is sent + self.anchor_message = None self.interaction_owner = member async def interaction_check(self, interaction: Interaction) -> bool: @@ -76,9 +90,13 @@ class RoleButtonView(discord.ui.View): return False return True + async def on_timeout(self) -> None: + """Delete the original message that the view was sent along with.""" + await self.anchor_message.delete() + class SingleRoleButton(discord.ui.Button): - """A button that adds or removes a role from the member depending on it's current state.""" + """A button that adds or removes a role from the member depending on its current state.""" ADD_STYLE = discord.ButtonStyle.success REMOVE_STYLE = discord.ButtonStyle.red @@ -133,15 +151,50 @@ class SingleRoleButton(discord.ui.Button): 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) + await self.view.anchor_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 AllSelfAssignableRolesView(discord.ui.View): + """A persistent view that'll hold one button allowing interactors to toggle all available self-assignable roles.""" + + def __init__(self): + super(AllSelfAssignableRolesView, self).__init__(timeout=None) + + +class ShowAllSelfAssignableRolesButton(discord.ui.Button): + """A button that, when clicked, sends a view a user can interact with to assign one or all the available roles.""" + + CUSTOM_ID = "gotta-claim-them-all" + + def __init__(self, assignable_roles: list[AssignableRole]): + super().__init__( + style=discord.ButtonStyle.success, + label="Show all self assignable roles", + custom_id=self.CUSTOM_ID, + row=1 + ) + self.assignable_roles = assignable_roles + + async def callback(self, interaction: Interaction) -> t.Any: + """Sends the original subscription view containing the available self assignable roles.""" + await interaction.response.defer() + view = prepare_self_assignable_roles_view(interaction, self.assignable_roles) + message = await interaction.followup.send( + view=view, + ephemeral=True + ) + # Keep reference of the message that contains the view to be deleted + view.anchor_message = message + + class Subscribe(commands.Cog): """Cog to allow user to self-assign & remove the roles present in ASSIGNABLE_ROLES.""" + SELF_ASSIGNABLE_ROLES_MESSAGE = "Click on this button to show all self assignable roles" + def __init__(self, bot: Bot): self.bot = bot self.assignable_roles: list[AssignableRole] = [] @@ -150,7 +203,6 @@ class Subscribe(commands.Cog): async def cog_load(self) -> None: """Initialise the cog by resolving the role IDs in ASSIGNABLE_ROLES to role names.""" await self.bot.wait_until_guild_available() - self.guild = self.bot.get_guild(constants.Guild.id) for role in ASSIGNABLE_ROLES: @@ -170,6 +222,9 @@ class Subscribe(commands.Cog): self.assignable_roles.sort(key=operator.attrgetter("name")) self.assignable_roles.sort(key=operator.methodcaller("is_currently_available"), reverse=True) + initial_self_assignable_roles_message = await self.__search_for_self_assignable_roles_message() + self.__attach_persistent_roles_view(initial_self_assignable_roles_message) + @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe", aliases=("unsubscribe",)) @redirect_output( @@ -178,22 +233,73 @@ class Subscribe(commands.Cog): ) 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.""" - button_view = RoleButtonView(ctx.author) - author_roles = [role.id for role in ctx.author.roles] - for index, role in enumerate(self.assignable_roles): - row = index // ITEMS_PER_ROW - button_view.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) + view = prepare_self_assignable_roles_view(ctx, self.assignable_roles) - await ctx.send( + message = await ctx.send( "Click the buttons below to add or remove your roles!", - view=button_view, - delete_after=DELETE_MESSAGE_AFTER, + view=view, ) + # Keep reference of the message that contains the view to be deleted + view.anchor_message = message + + async def __search_for_self_assignable_roles_message(self) -> discord.Message: + """ + Searches for the message that holds the self assignable roles view. + + If the initial message isn't found, a new one will be created. + This message will always be needed to attach the persistent view to it + """ + roles_channel: discord.TextChannel = await get_or_fetch_channel(constants.Channels.roles) + + async for message in roles_channel.history(limit=30): + if message.content == self.SELF_ASSIGNABLE_ROLES_MESSAGE: + log.debug(f"Found self assignable roles view message: {message.id}") + return message + + log.debug("Self assignable roles view message hasn't been found, creating a new one.") + view = AllSelfAssignableRolesView() + view.add_item(ShowAllSelfAssignableRolesButton(self.assignable_roles)) + return await roles_channel.send(self.SELF_ASSIGNABLE_ROLES_MESSAGE, view=view) + + def __attach_persistent_roles_view( + self, + placeholder_message: discord.Message + ) -> None: + """ + Attaches the persistent view that toggles self assignable roles to its placeholder message. + + The message is searched for/created upon loading the Cog. + + Parameters + __________ + :param placeholder_message: The message that will hold the persistent view allowing + users to toggle the RoleButtonView + """ + view = AllSelfAssignableRolesView() + view.add_item(ShowAllSelfAssignableRolesButton(self.assignable_roles)) + self.bot.add_view(view, message_id=placeholder_message.id) + + +def prepare_self_assignable_roles_view( + trigger_action: commands.Context | Interaction, + assignable_roles: list[AssignableRole] +) -> discord.ui.View: + """Prepares the view containing the self assignable roles before its sent.""" + author = trigger_action.author if isinstance(trigger_action, commands.Context) else trigger_action.user + author_roles = [role.id for role in author.roles] + button_view = RoleButtonView(member=author) + button_view.anchor_message = trigger_action.message + + for index, role in enumerate(assignable_roles): + row = index // ITEMS_PER_ROW + button_view.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) + + return button_view async 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. + """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: await bot.add_cog(Subscribe(bot)) diff --git a/config-default.yml b/config-default.yml index 1d7a2ff78..86474c204 100644 --- a/config-default.yml +++ b/config-default.yml @@ -235,6 +235,9 @@ guild: # Watch big_brother_logs: &BB_LOGS 468507907357409333 + # Roles + roles: 851270062434156586 + moderation_categories: - *MODS_CATEGORY - *MODMAIL |