aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--bot/constants.py2
-rw-r--r--bot/exts/info/subscribe.py136
-rw-r--r--config-default.yml3
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