diff options
author | 2023-01-22 22:41:22 +0200 | |
---|---|---|
committer | 2023-01-22 22:41:22 +0200 | |
commit | 0937e95e3e6e57d93b2935f91c73f27a5ddd3439 (patch) | |
tree | d39d728dea44ed0b502019c128cba71b2e69326d | |
parent | Merge pull request #2375 from python-discord/allow-passing-channel-objets-whe... (diff) | |
parent | Merge branch 'main' into 2332-permanent-role-view (diff) |
Merge pull request #2341 from shtlrs/2332-permanent-role-view
Allow members to self assign roles through a persistent view
-rw-r--r-- | bot/constants.py | 2 | ||||
-rw-r--r-- | bot/exts/info/subscribe.py | 120 | ||||
-rw-r--r-- | config-default.yml | 3 |
3 files changed, 107 insertions, 18 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..86a209214 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,11 +61,25 @@ 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 + __________ + interaction_owner: discord.Member + The member that initiated the interaction + """ + + interaction_owner: discord.Member - def __init__(self, member: discord.Member): - super().__init__() + def __init__(self, member: discord.Member, assignable_roles: list[AssignableRole]): + super().__init__(timeout=DELETE_MESSAGE_AFTER) self.interaction_owner = member + author_roles = [role.id for role in member.roles] + + for index, role in enumerate(assignable_roles): + row = index // ITEMS_PER_ROW + self.add_item(SingleRoleButton(role, role.role_id in author_roles, row)) async def interaction_check(self, interaction: Interaction) -> bool: """Ensure that the user clicking the button is the member who invoked the command.""" @@ -78,12 +93,12 @@ class RoleButtonView(discord.ui.View): 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 UNAVAILABLE_STYLE = discord.ButtonStyle.secondary - LABEL_FORMAT = "{action} role {role_name}." + LABEL_FORMAT = "{action} role {role_name}" CUSTOM_ID_FORMAT = "subscribe-{role_id}" def __init__(self, role: AssignableRole, assigned: bool, row: int): @@ -123,7 +138,7 @@ class SingleRoleButton(discord.ui.Button): self.assigned = not self.assigned await self.update_view(interaction) - await interaction.response.send_message( + await interaction.followup.send( self.LABEL_FORMAT.format(action="Added" if self.assigned else "Removed", role_name=self.role.name), ephemeral=True, ) @@ -133,15 +148,45 @@ 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 interaction.response.edit_message(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, assignable_roles: list[AssignableRole]): + super().__init__(timeout=None) + self.assignable_roles = assignable_roles + + @discord.ui.button( + style=discord.ButtonStyle.success, + label="Show all self assignable roles", + custom_id="toggle-available-roles-button", + row=1 + ) + async def show_all_self_assignable_roles(self, interaction: Interaction, button: discord.ui.Button) -> None: + """Sends the original subscription view containing the available self assignable roles.""" + view = RoleButtonView(interaction.user, self.assignable_roles) + await interaction.response.send_message( + view=view, + ephemeral=True + ) + + class Subscribe(commands.Cog): """Cog to allow user to self-assign & remove the roles present in ASSIGNABLE_ROLES.""" + GREETING_EMOJI = ":wave:" + + SELF_ASSIGNABLE_ROLES_MESSAGE = ( + f"Hi there {GREETING_EMOJI}," + "\nWe have self-assignable roles for server updates and events!" + "\nClick the button below to toggle them:" + ) + def __init__(self, bot: Bot): self.bot = bot self.assignable_roles: list[AssignableRole] = [] @@ -150,7 +195,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 +214,10 @@ class Subscribe(commands.Cog): self.assignable_roles.sort(key=operator.attrgetter("name")) self.assignable_roles.sort(key=operator.methodcaller("is_currently_available"), reverse=True) + placeholder_message_view_tuple = await self._fetch_or_create_self_assignable_roles_message() + self_assignable_roles_message, self_assignable_roles_view = placeholder_message_view_tuple + self._attach_persistent_roles_view(self_assignable_roles_message, self_assignable_roles_view) + @commands.cooldown(1, 10, commands.BucketType.member) @commands.command(name="subscribe", aliases=("unsubscribe",)) @redirect_output( @@ -178,22 +226,58 @@ 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 = RoleButtonView(ctx.author, self.assignable_roles) await ctx.send( "Click the buttons below to add or remove your roles!", - view=button_view, - delete_after=DELETE_MESSAGE_AFTER, + view=view, + delete_after=DELETE_MESSAGE_AFTER ) + async def _fetch_or_create_self_assignable_roles_message(self) -> tuple[discord.Message, discord.ui.View | None]: + """ + Fetches 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, None + + log.debug("Self assignable roles view message hasn't been found, creating a new one.") + view = AllSelfAssignableRolesView(self.assignable_roles) + placeholder_message = await roles_channel.send(self.SELF_ASSIGNABLE_ROLES_MESSAGE, view=view) + return placeholder_message, view + + def _attach_persistent_roles_view( + self, + placeholder_message: discord.Message, + persistent_roles_view: discord.ui.View | None = None + ) -> 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 + :param persistent_roles_view: The view attached to the placeholder_message + If none, a new view will be created + """ + if not persistent_roles_view: + persistent_roles_view = AllSelfAssignableRolesView(self.assignable_roles) + + self.bot.add_view(persistent_roles_view, message_id=placeholder_message.id) + 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..ac76a670a 100644 --- a/config-default.yml +++ b/config-default.yml @@ -235,6 +235,9 @@ guild: # Watch big_brother_logs: &BB_LOGS 468507907357409333 + # Information + roles: 851270062434156586 + moderation_categories: - *MODS_CATEGORY - *MODMAIL |