aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGravatar Boris Muratov <[email protected]>2023-01-22 22:41:22 +0200
committerGravatar GitHub <[email protected]>2023-01-22 22:41:22 +0200
commit0937e95e3e6e57d93b2935f91c73f27a5ddd3439 (patch)
treed39d728dea44ed0b502019c128cba71b2e69326d
parentMerge pull request #2375 from python-discord/allow-passing-channel-objets-whe... (diff)
parentMerge 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.py2
-rw-r--r--bot/exts/info/subscribe.py120
-rw-r--r--config-default.yml3
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