From 8b40f9a5eddc7cc56a955e92c6797cae525b9513 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Mon, 20 Nov 2023 10:46:12 +0200 Subject: [PATCH 01/10] Migrated reaction roles to use views instead --- bot/cogs/roles.py | 64 -------------- bot/config.py | 13 +-- bot/extensions/selectable_roles/__init__.py | 7 ++ bot/extensions/selectable_roles/commands.py | 93 +++++++++++++++++++++ bot/extensions/selectable_roles/views.py | 29 +++++++ cli.py | 2 +- example.env | 8 -- utils/transformers.py | 3 +- 8 files changed, 132 insertions(+), 87 deletions(-) delete mode 100644 bot/cogs/roles.py create mode 100644 bot/extensions/selectable_roles/__init__.py create mode 100644 bot/extensions/selectable_roles/commands.py create mode 100644 bot/extensions/selectable_roles/views.py diff --git a/bot/cogs/roles.py b/bot/cogs/roles.py deleted file mode 100644 index 8aad6028..00000000 --- a/bot/cogs/roles.py +++ /dev/null @@ -1,64 +0,0 @@ -import discord -from discord.ext import commands - -from bot.config import settings - - -class Roles(commands.Cog): - def __init__(self, bot): - self.bot = bot - - @property - def lvl_20_role(self): - return self.bot.guild.get_role(settings.reaction_roles.required_role_id) - - @property - def roles(self) -> dict: - # return { - # # emoji_id: role_id - # 736112775352287232: self.bot.guild.get_role(740689745532682290), # python - # 737030552770576395: self.bot.guild.get_role(740691624979333272), # csharp - # 740694148876599409: self.bot.guild.get_role(740690985926787093), # js - # 740694256053911675: self.bot.guild.get_role(740690527325782038), # java - # 740694321216356564: self.bot.guild.get_role(740691348977352754), # ruby - # } - return { - emoji_id: self.bot.guild.get_role(role_id) for emoji_id, role_id in settings.reaction_roles.roles.items() - } - - @commands.Cog.listener() - async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): - if payload.message_id != settings.reaction_roles.message_id: - return - - if ( - any(role in payload.member.roles for role in self.roles.values()) - or self.lvl_20_role not in payload.member.roles - ): - message = await self.bot.get_channel(payload.channel_id).fetch_message(settings.reaction_roles.message_id) - return await message.remove_reaction(payload.emoji, payload.member) - - await payload.member.add_roles(self.roles[payload.emoji.id]) - try: - await payload.member.send(f"Gave you the **{self.roles[payload.emoji.id].name}** role!") - except discord.HTTPException: - pass - - @commands.Cog.listener() - async def on_raw_reaction_remove(self, payload: discord.RawReactionActionEvent): - member = self.bot.guild.get_member(payload.user_id) - if payload.message_id != settings.reaction_roles.message_id: - return - - if self.roles[payload.emoji.id] not in member.roles or self.lvl_20_role not in member.roles: - return - - await member.remove_roles(self.roles[payload.emoji.id]) - try: - await member.send(f"Removed your **{self.roles[payload.emoji.id].name}** role!") - except discord.HTTPException: - pass - - -async def setup(bot: commands.Bot): - await bot.add_cog(Roles(bot=bot)) diff --git a/bot/config.py b/bot/config.py index 260dc16b..0319e95b 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,6 +1,6 @@ import json import logging -from typing import Dict, List +from typing import List from pydantic import BaseModel, BaseSettings, PostgresDsn, ValidationError, validator @@ -62,16 +62,6 @@ class Postgres(BaseModel): uri: PostgresDsn -class ReactionRoles(BaseModel): - required_role_id: int # [lvl 20] Developer - roles: Dict[int, int] # Dict[emoji_id, role_id] - message_id: int - - @validator("roles", pre=True) - def val_func(cls, val): - return {int(k): v for k, v in json.loads(val).items()} - - class Tags(BaseModel): log_channel_id: int required_role_id: int # [lvl 30] Engineer @@ -110,7 +100,6 @@ class Settings(BaseSettings): postgres: Postgres guild: Guild moderation: Moderation - reaction_roles: ReactionRoles tags: Tags timathon: Timathon hastebin: Hastebin diff --git a/bot/extensions/selectable_roles/__init__.py b/bot/extensions/selectable_roles/__init__.py new file mode 100644 index 00000000..adc2a3c4 --- /dev/null +++ b/bot/extensions/selectable_roles/__init__.py @@ -0,0 +1,7 @@ +from bot.core import DiscordBot + +from .commands import SelectableRoleCommands + + +async def setup(bot: DiscordBot) -> None: + await bot.add_cog(SelectableRoleCommands(bot=bot)) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py new file mode 100644 index 00000000..1a733459 --- /dev/null +++ b/bot/extensions/selectable_roles/commands.py @@ -0,0 +1,93 @@ +import discord +from discord import app_commands +from discord.ext import commands + +from bot import core +from bot.extensions.selectable_roles.views import CreateSelectableRoleView, SelectableRoleOptions +from utils.transformers import MessageTransformer + + +@app_commands.default_permissions(administrator=True) +class SelectableRoleCommands(commands.GroupCog, group_name="selectable-role"): + def __init__(self, bot: core.DiscordBot): + self.bot = bot + + # TODO: Make the view work even after a restart + self._create_selectable_role_view = CreateSelectableRoleView(timeout=None) + self.bot.add_view(self._create_selectable_role_view) + + @app_commands.command() + async def create(self, interaction: core.InteractionType, title: str, channel: discord.TextChannel = None): + """Create a new selectable role message""" + + if channel is None: + channel = interaction.channel + + await channel.send(title, view=self._create_selectable_role_view) + await interaction.response.send_message( + "Successfully created selectable role message! Please add role options to it by using /selectable-role add", + ephemeral=True, + ) + + @app_commands.command() + async def add( + self, + interaction: core.InteractionType, + message: app_commands.Transform[discord.Message, MessageTransformer], + text: str, + role: discord.Role, + emoji: str, + ): + """Add a selectable role to a message""" + + if message.author != self.bot.user: + return await interaction.response.send_message( + "This message is not a selectable role message.", ephemeral=True + ) + + emoji = discord.utils.get(interaction.guild.emojis, name=emoji) + view = discord.ui.View.from_message(message, timeout=None) + options = [] + if view.children: + options = view.children[0].options + + view = CreateSelectableRoleView(timeout=None) + view.add_item( + SelectableRoleOptions( + options + [discord.SelectOption(label=role.name, description=text, value=str(role.id), emoji=emoji)] + ) + ) + + await message.edit(view=view) + await interaction.response.send_message("Successfully added role to selectable role message!", ephemeral=True) + + @app_commands.command() + async def remove( + self, + interaction: core.InteractionType, + message: app_commands.Transform[discord.Message, MessageTransformer], + role: discord.Role, + ): + """Remove a selectable role from a message""" + + if message.author != self.bot.user: + return await interaction.response.send_message( + "This message is not a selectable role message.", ephemeral=True + ) + + view = discord.ui.View.from_message(message, timeout=None) + options = [] + if view.children: + options = view.children[0].options + options = [option for option in options if option.value != str(role.id)] + + if len(options) == 0: + await message.edit(view=None) + else: + view = CreateSelectableRoleView(timeout=None) + view.add_item(SelectableRoleOptions(options)) + await message.edit(view=view) + + await interaction.response.send_message( + "Successfully removed role from selectable role message!", ephemeral=True + ) diff --git a/bot/extensions/selectable_roles/views.py b/bot/extensions/selectable_roles/views.py new file mode 100644 index 00000000..81784bd6 --- /dev/null +++ b/bot/extensions/selectable_roles/views.py @@ -0,0 +1,29 @@ +import discord +from discord import ui + +from bot import core + + +class SelectableRoleOptions(discord.ui.Select): + def __init__(self, options: list[discord.SelectOption]): + super().__init__( + placeholder="Select a role", + custom_id=CreateSelectableRoleView.DROPDOWN_CUSTOM_ID, + options=options, + ) + + async def callback(self, interaction: core.InteractionType): + selected_role_id = int(self.values[0]) + role = interaction.guild.get_role(selected_role_id) + + other_roles = [ + interaction.guild.get_role(int(option.value)) for option in self.options if option != selected_role_id + ] + + await interaction.user.remove_roles(*other_roles, reason="Selectable role") + await interaction.user.add_roles(role, reason="Selectable role") + await interaction.response.send_message(f"Successfully added {role.mention} to you!", ephemeral=True) + + +class CreateSelectableRoleView(ui.View): + DROPDOWN_CUSTOM_ID = "extensions:selectable_roles:dropdown" diff --git a/cli.py b/cli.py index b151094b..ee93edc1 100644 --- a/cli.py +++ b/cli.py @@ -140,9 +140,9 @@ async def main(ctx): "bot.extensions.custom_roles", "bot.extensions.polls", "bot.extensions.youtube", + "bot.extensions.selectable_roles", "bot.cogs._help", "bot.cogs.clashofcode", - "bot.cogs.roles", ) intents = discord.Intents.all() diff --git a/example.env b/example.env index f0d5222f..4925ac89 100644 --- a/example.env +++ b/example.env @@ -47,14 +47,6 @@ MODERATION__STAFF_ROLE_ID= NOTIFICATION__CHANNEL_ID=0 NOTIFICATION__ROLE_ID=0 -# --- Reaction Roles -# Access to reaction roles -REACTION_ROLES__REQUIRED_ROLE_ID=0 -# Dict[emoji_id: int, role_id: int] -# Leave no space or use double quotes `"` e.g: "{\"0\": 0}" -REACTION_ROLES__ROLES={"0":0} -REACTION_ROLES__MESSAGE_ID=0 - # --- Tags TAGS__LOG_CHANNEL_ID=0 # Access to tag commands diff --git a/utils/transformers.py b/utils/transformers.py index 6558a717..aef3ab91 100644 --- a/utils/transformers.py +++ b/utils/transformers.py @@ -13,7 +13,6 @@ async def transform(self, interaction: core.InteractionType, value: str, /): try: parts: list[str] = value.split("/") - # check that there are 2 parts if len(parts) != 2: return await interaction.channel.fetch_message(int(value)) @@ -23,7 +22,7 @@ async def transform(self, interaction: core.InteractionType, value: str, /): channel = interaction.guild.get_channel(channel_id) return await channel.fetch_message(message_id) except (ValueError, TypeError, IndexError, AttributeError): - await interaction.response.send_message("Please provide a valid message URL.", ephemeral=True) + await interaction.response.send_message("Please provide a valid message ID.", ephemeral=True) except discord.HTTPException: await interaction.response.send_message("Sorry, I couldn't find that message...", ephemeral=True) From bb713c288379361d2f5c71688540875d20a0d7d8 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Mon, 20 Nov 2023 12:02:24 +0200 Subject: [PATCH 02/10] Fixed transformer, added embed to selectable_roles and made it possible to remove selectable roles. --- bot/config.py | 5 +-- bot/extensions/selectable_roles/commands.py | 44 +++++++++++++++++---- bot/extensions/selectable_roles/views.py | 6 +++ utils/transformers.py | 19 +++++++-- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/bot/config.py b/bot/config.py index 0319e95b..d31adeb7 100644 --- a/bot/config.py +++ b/bot/config.py @@ -1,6 +1,5 @@ import json import logging -from typing import List from pydantic import BaseModel, BaseSettings, PostgresDsn, ValidationError, validator @@ -14,7 +13,7 @@ class AoC(BaseModel): class Bot(BaseModel): - commands_channels_ids: List[int] + commands_channels_ids: list[int] games_channel_id: int # #bot-games token: str @@ -48,7 +47,7 @@ class Guild(BaseModel): class Moderation(BaseModel): - admin_roles_ids: List[int] + admin_roles_ids: list[int] staff_role_id: int @validator("admin_roles_ids", pre=True) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 1a733459..2400e199 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -17,13 +17,28 @@ def __init__(self, bot: core.DiscordBot): self.bot.add_view(self._create_selectable_role_view) @app_commands.command() - async def create(self, interaction: core.InteractionType, title: str, channel: discord.TextChannel = None): + async def create( + self, + interaction: core.InteractionType, + title: str, + channel: discord.TextChannel = None, + description: str = None, + footer: str = None, + ): """Create a new selectable role message""" if channel is None: channel = interaction.channel - await channel.send(title, view=self._create_selectable_role_view) + embed = discord.Embed( + title=title, + description=description, + color=discord.Color.gold(), + ) + if footer: + embed.set_footer(text=footer) + + await channel.send(embed=embed, view=self._create_selectable_role_view) await interaction.response.send_message( "Successfully created selectable role message! Please add role options to it by using /selectable-role add", ephemeral=True, @@ -34,7 +49,7 @@ async def add( self, interaction: core.InteractionType, message: app_commands.Transform[discord.Message, MessageTransformer], - text: str, + description: str, role: discord.Role, emoji: str, ): @@ -54,11 +69,15 @@ async def add( view = CreateSelectableRoleView(timeout=None) view.add_item( SelectableRoleOptions( - options + [discord.SelectOption(label=role.name, description=text, value=str(role.id), emoji=emoji)] + options + + [discord.SelectOption(label=role.name, description=description, value=str(role.id), emoji=emoji)] ) ) - await message.edit(view=view) + embed = message.embeds[0] + embed.add_field(name=f"{emoji} {role.name}", value=description, inline=False) + + await message.edit(embed=embed, view=view) await interaction.response.send_message("Successfully added role to selectable role message!", ephemeral=True) @app_commands.command() @@ -76,17 +95,26 @@ async def remove( ) view = discord.ui.View.from_message(message, timeout=None) + embed = message.embeds[0] options = [] if view.children: options = view.children[0].options - options = [option for option in options if option.value != str(role.id)] + + try: + role_index = [option.value for option in options].index(str(role.id)) + options.pop(role_index) + embed.remove_field(role_index) + except ValueError: + return await interaction.response.send_message( + "The role you provided is not a selectable role in this message.", ephemeral=True + ) if len(options) == 0: - await message.edit(view=None) + await message.edit(embed=embed, view=None) else: view = CreateSelectableRoleView(timeout=None) view.add_item(SelectableRoleOptions(options)) - await message.edit(view=view) + await message.edit(embed=embed, view=view) await interaction.response.send_message( "Successfully removed role from selectable role message!", ephemeral=True diff --git a/bot/extensions/selectable_roles/views.py b/bot/extensions/selectable_roles/views.py index 81784bd6..5024fc76 100644 --- a/bot/extensions/selectable_roles/views.py +++ b/bot/extensions/selectable_roles/views.py @@ -9,10 +9,16 @@ def __init__(self, options: list[discord.SelectOption]): super().__init__( placeholder="Select a role", custom_id=CreateSelectableRoleView.DROPDOWN_CUSTOM_ID, + min_values=0, options=options, ) async def callback(self, interaction: core.InteractionType): + if not self.values: + roles = [interaction.guild.get_role(int(option.value)) for option in self.options] + await interaction.user.remove_roles(*roles, reason="Selectable role") + return await interaction.response.send_message("Successfully removed the role from you!", ephemeral=True) + selected_role_id = int(self.values[0]) role = interaction.guild.get_role(selected_role_id) diff --git a/utils/transformers.py b/utils/transformers.py index aef3ab91..74d831ef 100644 --- a/utils/transformers.py +++ b/utils/transformers.py @@ -9,11 +9,18 @@ class MessageTransformer(app_commands.Transformer): + """ + Transform any of the given formats to a Message instance: + + - 1176092816762486784 # message_id + - 1024375283857506436/1176092816762486784 # channel_id/message_id + - https://discord.com/channels/1024375277679284315/1024375283857506436/1176092816762486784 # message_url + """ + async def transform(self, interaction: core.InteractionType, value: str, /): + parts: list[str] = value.split("/") try: - parts: list[str] = value.split("/") - - if len(parts) != 2: + if len(parts) == 1: return await interaction.channel.fetch_message(int(value)) message_id = int(parts[-1]) @@ -22,7 +29,11 @@ async def transform(self, interaction: core.InteractionType, value: str, /): channel = interaction.guild.get_channel(channel_id) return await channel.fetch_message(message_id) except (ValueError, TypeError, IndexError, AttributeError): - await interaction.response.send_message("Please provide a valid message ID.", ephemeral=True) + if len(parts) == 1: + message = "Please provide a valid message ID." + else: + message = "Please provide a valid message URL." + await interaction.response.send_message(message, ephemeral=True) except discord.HTTPException: await interaction.response.send_message("Sorry, I couldn't find that message...", ephemeral=True) From 5c13693eb97a4383411a80a55cd7b501ee49c6fc Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:12:08 +0200 Subject: [PATCH 03/10] Switched to using commands to get roles --- bot/extensions/selectable_roles/commands.py | 163 +++++++++--------- bot/models/__init__.py | 2 + .../migrations/006_down__selectable_roles.sql | 1 + .../migrations/006_up__selectable_roles.sql | 6 + bot/models/selectable_roles.py | 20 +++ 5 files changed, 106 insertions(+), 86 deletions(-) create mode 100644 bot/models/migrations/006_down__selectable_roles.sql create mode 100644 bot/models/migrations/006_up__selectable_roles.sql create mode 100644 bot/models/selectable_roles.py diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 2400e199..18c3dc55 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -1,121 +1,112 @@ +from typing import List + import discord from discord import app_commands from discord.ext import commands +from pydantic import BaseModel from bot import core -from bot.extensions.selectable_roles.views import CreateSelectableRoleView, SelectableRoleOptions -from utils.transformers import MessageTransformer +from bot.models import SelectableRole + + +class Role(BaseModel): + name: str + id: int @app_commands.default_permissions(administrator=True) -class SelectableRoleCommands(commands.GroupCog, group_name="selectable-role"): +@app_commands.guild_only() +class SelectableRoleCommands(commands.GroupCog, group_name="role"): def __init__(self, bot: core.DiscordBot): self.bot = bot + self.roles: dict[int, list[Role]] = {} + + def update_roles(self, guild_id: int, data: tuple[str, int]) -> None: + if self.roles.get(guild_id): + self.roles[guild_id].append(Role(name=data[0], id=data[1])) + else: + self.roles[guild_id] = [Role(name=data[0], id=data[1])] - # TODO: Make the view work even after a restart - self._create_selectable_role_view = CreateSelectableRoleView(timeout=None) - self.bot.add_view(self._create_selectable_role_view) + async def cog_load(self) -> None: + query = "SELECT * FROM selectable_roles" + records = await SelectableRole.fetch(query) + + for record in records: + self.update_roles(record.guild_id, (record.role_name, record.role_id)) + + async def role_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: + if interaction.data["options"][0]["name"] == "add": + roles = interaction.guild.roles + else: + if not self.roles.get(interaction.guild.id): + return [] + roles = self.roles[interaction.guild.id] + + return [ + app_commands.Choice(name=role.name, value=str(role.id)) + for role in roles + if current.lower() in role.name.lower() + ][:25] @app_commands.command() - async def create( + @app_commands.autocomplete(role=role_autocomplete) + async def get( self, interaction: core.InteractionType, - title: str, - channel: discord.TextChannel = None, - description: str = None, - footer: str = None, + role: str, ): - """Create a new selectable role message""" + """Get the selected role""" - if channel is None: - channel = interaction.channel + if not self.roles.get(interaction.guild.id): + return await interaction.response.send_message("There are no selectable roles!", ephemeral=True) - embed = discord.Embed( - title=title, - description=description, - color=discord.Color.gold(), - ) - if footer: - embed.set_footer(text=footer) + role = interaction.guild.get_role(int(role)) + if role is None: + return await interaction.response.send_message("That role doesn't exist!", ephemeral=True) + await interaction.user.add_roles(role, reason="Selectable role") - await channel.send(embed=embed, view=self._create_selectable_role_view) - await interaction.response.send_message( - "Successfully created selectable role message! Please add role options to it by using /selectable-role add", - ephemeral=True, - ) + to_remove = [] + for role_ in self.roles[interaction.guild.id]: + if role_.id != role.id: + to_remove.append(interaction.guild.get_role(role_.id)) + await interaction.user.remove_roles(*to_remove, reason="Selectable role") + + await interaction.response.send_message(f"Successfully added {role.mention} to you!", ephemeral=True) @app_commands.command() + @app_commands.autocomplete(role=role_autocomplete) async def add( self, interaction: core.InteractionType, - message: app_commands.Transform[discord.Message, MessageTransformer], - description: str, - role: discord.Role, - emoji: str, + role: str, ): - """Add a selectable role to a message""" - - if message.author != self.bot.user: - return await interaction.response.send_message( - "This message is not a selectable role message.", ephemeral=True - ) - - emoji = discord.utils.get(interaction.guild.emojis, name=emoji) - view = discord.ui.View.from_message(message, timeout=None) - options = [] - if view.children: - options = view.children[0].options - - view = CreateSelectableRoleView(timeout=None) - view.add_item( - SelectableRoleOptions( - options - + [discord.SelectOption(label=role.name, description=description, value=str(role.id), emoji=emoji)] - ) - ) - - embed = message.embeds[0] - embed.add_field(name=f"{emoji} {role.name}", value=description, inline=False) + """Add a selectable role to the database""" - await message.edit(embed=embed, view=view) - await interaction.response.send_message("Successfully added role to selectable role message!", ephemeral=True) + role = interaction.guild.get_role(int(role)) + await SelectableRole.ensure_exists(interaction.guild.id, role.id, role.name) + self.update_roles(interaction.guild.id, (role.name, role.id)) + await interaction.response.send_message(f"Successfully added {role.mention} to the database!", ephemeral=True) @app_commands.command() + @app_commands.autocomplete(role=role_autocomplete) async def remove( self, interaction: core.InteractionType, - message: app_commands.Transform[discord.Message, MessageTransformer], - role: discord.Role, + role: str, ): - """Remove a selectable role from a message""" - - if message.author != self.bot.user: - return await interaction.response.send_message( - "This message is not a selectable role message.", ephemeral=True - ) - - view = discord.ui.View.from_message(message, timeout=None) - embed = message.embeds[0] - options = [] - if view.children: - options = view.children[0].options - - try: - role_index = [option.value for option in options].index(str(role.id)) - options.pop(role_index) - embed.remove_field(role_index) - except ValueError: - return await interaction.response.send_message( - "The role you provided is not a selectable role in this message.", ephemeral=True - ) - - if len(options) == 0: - await message.edit(embed=embed, view=None) - else: - view = CreateSelectableRoleView(timeout=None) - view.add_item(SelectableRoleOptions(options)) - await message.edit(embed=embed, view=view) + """Remove a selectable role from the database""" + + if not self.roles.get(interaction.guild.id): + return await interaction.response.send_message("There are no selectable roles!", ephemeral=True) + + role = interaction.guild.get_role(int(role)) + query = "DELETE FROM selectable_roles WHERE guild_id = $1 AND role_id = $2" + await SelectableRole.execute(query, interaction.guild.id, role.id) + + for i, role_ in enumerate(self.roles[interaction.guild.id]): + if role_.id == role.id: + del self.roles[interaction.guild.id][i] await interaction.response.send_message( - "Successfully removed role from selectable role message!", ephemeral=True + f"Successfully removed {role.mention} from the database!", ephemeral=True ) diff --git a/bot/models/__init__.py b/bot/models/__init__.py index 2bb857fe..8e96bc24 100644 --- a/bot/models/__init__.py +++ b/bot/models/__init__.py @@ -7,6 +7,7 @@ from .model import Model from .persisted_role import PersistedRole from .rep import Rep +from .selectable_roles import SelectableRole from .tag import Tag from .user import User @@ -22,4 +23,5 @@ IgnoredChannel, LevellingRole, CustomRole, + SelectableRole, ) # Fixes F401 diff --git a/bot/models/migrations/006_down__selectable_roles.sql b/bot/models/migrations/006_down__selectable_roles.sql new file mode 100644 index 00000000..6401dc3a --- /dev/null +++ b/bot/models/migrations/006_down__selectable_roles.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS selectable_roles; diff --git a/bot/models/migrations/006_up__selectable_roles.sql b/bot/models/migrations/006_up__selectable_roles.sql new file mode 100644 index 00000000..35ec8f08 --- /dev/null +++ b/bot/models/migrations/006_up__selectable_roles.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS selectable_roles ( + guild_id BIGINT NOT NULL, + role_id BIGINT NOT NULL, + role_name VARCHAR NOT NULL, + PRIMARY KEY (guild_id, role_id) +); diff --git a/bot/models/selectable_roles.py b/bot/models/selectable_roles.py new file mode 100644 index 00000000..04cf8d44 --- /dev/null +++ b/bot/models/selectable_roles.py @@ -0,0 +1,20 @@ +from .model import Model + + +class SelectableRole(Model): + guild_id: int + role_id: int + role_name: str + + @classmethod + async def ensure_exists(cls, guild_id: int, role_id: int, role_name: str): + """Inserts or updates the selectable role.""" + query = """ + INSERT INTO selectable_roles (guild_id, role_id, role_name) + VALUES ($1, $2, $3) + ON CONFLICT (guild_id, role_id) + DO UPDATE SET + role_name = $3 + """ + + return await cls.fetchrow(query, guild_id, role_id, role_name) From 4835bb9e388086d57a50029e314560b6bf86a64d Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Tue, 21 Nov 2023 17:32:17 +0200 Subject: [PATCH 04/10] bug fixes --- bot/extensions/selectable_roles/commands.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 18c3dc55..18d58a5c 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -57,12 +57,13 @@ async def get( ): """Get the selected role""" - if not self.roles.get(interaction.guild.id): - return await interaction.response.send_message("There are no selectable roles!", ephemeral=True) + if not self.roles.get(interaction.guild.id) or not role.isdigit(): + return await interaction.response.send_message("That role isn't selectable!", ephemeral=True) role = interaction.guild.get_role(int(role)) - if role is None: - return await interaction.response.send_message("That role doesn't exist!", ephemeral=True) + if role is None or not any(role.id == role_.id for role_ in self.roles[interaction.guild.id]): + return await interaction.response.send_message("That role isn't selectable!", ephemeral=True) + await interaction.user.add_roles(role, reason="Selectable role") to_remove = [] From ab9c21ca4d2bfd1cddad9e8788fa9888ceab6926 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Sun, 26 Nov 2023 20:08:55 +0200 Subject: [PATCH 05/10] fixed permissions and added list command --- bot/extensions/selectable_roles/commands.py | 32 +++++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 18d58a5c..723ac232 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -14,9 +14,14 @@ class Role(BaseModel): id: int -@app_commands.default_permissions(administrator=True) -@app_commands.guild_only() -class SelectableRoleCommands(commands.GroupCog, group_name="role"): +class SelectableRoleCommands(commands.Cog): + admin_commands = app_commands.Group( + name="selectable-roles", + description="Commands for managing selectable roles", + default_permissions=discord.Permissions(administrator=True), + guild_only=True, + ) + def __init__(self, bot: core.DiscordBot): self.bot = bot self.roles: dict[int, list[Role]] = {} @@ -48,7 +53,8 @@ async def role_autocomplete(self, interaction: discord.Interaction, current: str if current.lower() in role.name.lower() ][:25] - @app_commands.command() + @app_commands.command(name="get-role") + @app_commands.guild_only() @app_commands.autocomplete(role=role_autocomplete) async def get( self, @@ -74,7 +80,7 @@ async def get( await interaction.response.send_message(f"Successfully added {role.mention} to you!", ephemeral=True) - @app_commands.command() + @admin_commands.command() @app_commands.autocomplete(role=role_autocomplete) async def add( self, @@ -88,7 +94,7 @@ async def add( self.update_roles(interaction.guild.id, (role.name, role.id)) await interaction.response.send_message(f"Successfully added {role.mention} to the database!", ephemeral=True) - @app_commands.command() + @admin_commands.command() @app_commands.autocomplete(role=role_autocomplete) async def remove( self, @@ -111,3 +117,17 @@ async def remove( await interaction.response.send_message( f"Successfully removed {role.mention} from the database!", ephemeral=True ) + + @admin_commands.command() + async def list( + self, + interaction: core.InteractionType, + ): + """List all selectable roles""" + + if not self.roles.get(interaction.guild.id): + return await interaction.response.send_message("There are no selectable roles!", ephemeral=True) + + roles = [f"<@&{role.id}>" for role in self.roles[interaction.guild.id]] + embed = discord.Embed(title="Selectable roles", description="\n".join(roles), color=discord.Color.gold()) + await interaction.response.send_message(embed=embed) From f2edb9186735450d5c4c70d6a9b536e9de57dd07 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Thu, 30 Nov 2023 14:58:41 +0200 Subject: [PATCH 06/10] removed useless code --- bot/extensions/selectable_roles/commands.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 723ac232..2dafb0a4 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -40,16 +40,12 @@ async def cog_load(self) -> None: self.update_roles(record.guild_id, (record.role_name, record.role_id)) async def role_autocomplete(self, interaction: discord.Interaction, current: str) -> List[app_commands.Choice[str]]: - if interaction.data["options"][0]["name"] == "add": - roles = interaction.guild.roles - else: - if not self.roles.get(interaction.guild.id): - return [] - roles = self.roles[interaction.guild.id] + if not self.roles.get(interaction.guild.id): + return [] return [ app_commands.Choice(name=role.name, value=str(role.id)) - for role in roles + for role in self.roles[interaction.guild.id] if current.lower() in role.name.lower() ][:25] @@ -81,15 +77,13 @@ async def get( await interaction.response.send_message(f"Successfully added {role.mention} to you!", ephemeral=True) @admin_commands.command() - @app_commands.autocomplete(role=role_autocomplete) async def add( self, interaction: core.InteractionType, - role: str, + role: discord.Role, ): """Add a selectable role to the database""" - role = interaction.guild.get_role(int(role)) await SelectableRole.ensure_exists(interaction.guild.id, role.id, role.name) self.update_roles(interaction.guild.id, (role.name, role.id)) await interaction.response.send_message(f"Successfully added {role.mention} to the database!", ephemeral=True) From 3d380fbd0cd812b906aa3791d15058e221995e7f Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Fri, 1 Dec 2023 20:03:39 +0200 Subject: [PATCH 07/10] remove view file --- bot/extensions/selectable_roles/views.py | 35 ------------------------ 1 file changed, 35 deletions(-) delete mode 100644 bot/extensions/selectable_roles/views.py diff --git a/bot/extensions/selectable_roles/views.py b/bot/extensions/selectable_roles/views.py deleted file mode 100644 index 5024fc76..00000000 --- a/bot/extensions/selectable_roles/views.py +++ /dev/null @@ -1,35 +0,0 @@ -import discord -from discord import ui - -from bot import core - - -class SelectableRoleOptions(discord.ui.Select): - def __init__(self, options: list[discord.SelectOption]): - super().__init__( - placeholder="Select a role", - custom_id=CreateSelectableRoleView.DROPDOWN_CUSTOM_ID, - min_values=0, - options=options, - ) - - async def callback(self, interaction: core.InteractionType): - if not self.values: - roles = [interaction.guild.get_role(int(option.value)) for option in self.options] - await interaction.user.remove_roles(*roles, reason="Selectable role") - return await interaction.response.send_message("Successfully removed the role from you!", ephemeral=True) - - selected_role_id = int(self.values[0]) - role = interaction.guild.get_role(selected_role_id) - - other_roles = [ - interaction.guild.get_role(int(option.value)) for option in self.options if option != selected_role_id - ] - - await interaction.user.remove_roles(*other_roles, reason="Selectable role") - await interaction.user.add_roles(role, reason="Selectable role") - await interaction.response.send_message(f"Successfully added {role.mention} to you!", ephemeral=True) - - -class CreateSelectableRoleView(ui.View): - DROPDOWN_CUSTOM_ID = "extensions:selectable_roles:dropdown" From 4d6e3a3555cd137822c82f79db342962601e2676 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Fri, 1 Dec 2023 21:32:21 +0200 Subject: [PATCH 08/10] fixed issue where roles were getting pushed to memory more than once --- bot/extensions/selectable_roles/commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 2dafb0a4..5c84eb74 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -28,6 +28,9 @@ def __init__(self, bot: core.DiscordBot): def update_roles(self, guild_id: int, data: tuple[str, int]) -> None: if self.roles.get(guild_id): + for role in self.roles[guild_id]: + if role.id == data[1]: + return self.roles[guild_id].append(Role(name=data[0], id=data[1])) else: self.roles[guild_id] = [Role(name=data[0], id=data[1])] From efcae3bdba6de2208c115d68bcc71b1945c09439 Mon Sep 17 00:00:00 2001 From: FirePlank <44502537+FirePlank@users.noreply.github.com> Date: Fri, 1 Dec 2023 21:34:13 +0200 Subject: [PATCH 09/10] added break --- bot/extensions/selectable_roles/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 5c84eb74..755e40db 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -110,6 +110,7 @@ async def remove( for i, role_ in enumerate(self.roles[interaction.guild.id]): if role_.id == role.id: del self.roles[interaction.guild.id][i] + break await interaction.response.send_message( f"Successfully removed {role.mention} from the database!", ephemeral=True From 02c7598681eca8797264f6b459af97d3a8bf3116 Mon Sep 17 00:00:00 2001 From: HETHAT Date: Fri, 1 Dec 2023 21:37:44 +0100 Subject: [PATCH 10/10] exclude non-assignable roles --- bot/extensions/selectable_roles/commands.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/bot/extensions/selectable_roles/commands.py b/bot/extensions/selectable_roles/commands.py index 755e40db..56475199 100644 --- a/bot/extensions/selectable_roles/commands.py +++ b/bot/extensions/selectable_roles/commands.py @@ -87,6 +87,12 @@ async def add( ): """Add a selectable role to the database""" + if not role.is_assignable(): + return await interaction.response.send_message( + "That role is non-assignable by the bot. Please ensure the bot has the necessary permissions.", + ephemeral=True, + ) + await SelectableRole.ensure_exists(interaction.guild.id, role.id, role.name) self.update_roles(interaction.guild.id, (role.name, role.id)) await interaction.response.send_message(f"Successfully added {role.mention} to the database!", ephemeral=True)