diff options
| -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 | 
