Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3686f60
add the roles channel to the config
shtlrs Nov 26, 2022
5795140
add the AllSelfAssignableRolesView and its corresponding ClaimAllSelf…
shtlrs Nov 26, 2022
6ab9f9c
add the logic for attaching the persistent view
shtlrs Nov 26, 2022
b25ad80
add assignable_roles as a property to the ClaimAllSelfAssignableRoles…
shtlrs Nov 26, 2022
1d60403
add implementation of the button's callback that'll handle assigning …
shtlrs Nov 26, 2022
af58814
rely on original_message to delete view once it times out
shtlrs Nov 27, 2022
3cedc99
restore original value of DELETE_MESSAGE_AFTER
shtlrs Nov 27, 2022
6c61d34
rename ClaimAllSelfAssignableRolesButton to ShowAllSelfAssignableRole…
shtlrs Nov 27, 2022
bde96fd
update message content of the persistent view
shtlrs Nov 27, 2022
dd4903c
fix docstrings of the ShowAllSelfAssignableRolesButton's callback fun…
shtlrs Nov 27, 2022
bd4b0b4
rename prepare_available_role_subscription_view to prepare_self_assig…
shtlrs Nov 27, 2022
54dfcf9
update docs for the newly added view & button
shtlrs Nov 27, 2022
28c733c
rename the method that attaches the persistent view
shtlrs Nov 27, 2022
c628722
remove waring in setup docstrings
shtlrs Nov 27, 2022
f748ceb
misc style & doc improvements
shtlrs Nov 27, 2022
c14e75a
add docstrings to RoleButtonView
shtlrs Nov 27, 2022
e7d0987
make the roles view ephemeral when sent in roles channel
shtlrs Nov 27, 2022
d8850b4
Merge branch 'main' into 2332-permanent-role-view
shtlrs Nov 27, 2022
0a60b5d
do not use name mangling
shtlrs Dec 3, 2022
28dac62
call super without referencing the current class
shtlrs Dec 3, 2022
72b2f80
rename #Roles section to #Information
shtlrs Dec 3, 2022
24c36e5
use button decorator instead of subclassing discord.ui.Button
shtlrs Dec 4, 2022
a6fa4b1
update custom id of the AllSelfAssignableRolesView's button
shtlrs Dec 4, 2022
c630e1b
make SELF_ASSIGNABLE_ROLES_MESSAGE more inviting for people to intera…
shtlrs Dec 4, 2022
37b43a6
ditch prepare_self_assignable_roles_view by constructing the buttons …
shtlrs Dec 4, 2022
6c6b2bc
relay the newly created view to __attach_persistent_roles_view
shtlrs Dec 4, 2022
7118c51
edit view through interaction.response.edit_message
shtlrs Dec 4, 2022
7976bc2
use follow.send to send the role update message
shtlrs Dec 4, 2022
91a869e
do not use name mangling in _attach_persistent_roles_view
shtlrs Dec 4, 2022
c55b9ff
fix Zig's nit comments
shtlrs Dec 18, 2022
52d69de
fix typos in the SELF_ASSIGNABLE_ROLES_MESSAGE
shtlrs Jan 14, 2023
8af86ae
Merge branch 'main' into 2332-permanent-role-view
mbaruh Jan 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bot/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,8 @@ class Channels(metaclass=YAMLGetter):

big_brother_logs: int

roles: int


class Webhooks(metaclass=YAMLGetter):
section = "guild"
Expand Down
120 changes: 102 additions & 18 deletions bot/exts/info/subscribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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)
Expand Down Expand Up @@ -60,11 +61,25 @@ def get_readable_available_months(self) -> str:


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."""
Expand All @@ -78,12 +93,12 @@ async def interaction_check(self, interaction: Interaction) -> bool:


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):
Expand Down Expand Up @@ -123,7 +138,7 @@ async def callback(self, interaction: Interaction) -> None:

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,
)
Expand All @@ -133,15 +148,45 @@ async def update_view(self, interaction: Interaction) -> None:
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] = []
Expand All @@ -150,7 +195,6 @@ def __init__(self, bot: Bot):
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:
Expand All @@ -170,6 +214,10 @@ async def cog_load(self) -> None:
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(
Expand All @@ -178,22 +226,58 @@ async def cog_load(self) -> None:
)
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we are returning None for the discord.ui.View? Couldn't we just use discord.ui.View.from_message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't really recreate the view using from_message, as this persistent view needs to be instantiated with the self assignable roles, and from_message cannot pick that up.


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))
3 changes: 3 additions & 0 deletions config-default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ guild:
# Watch
big_brother_logs: &BB_LOGS 468507907357409333

# Information
roles: 851270062434156586

moderation_categories:
- *MODS_CATEGORY
- *MODMAIL
Expand Down