import asyncio import json from typing import Optional from enum import Enum import discord from discord.ui.button import button, Button, ButtonStyle from discord.ui.select import select, Select, RoleSelect, ChannelSelect, SelectOption from meta import LionBot, conf from meta.errors import UserInputError, ResponseTimedOut, SafeCancellation from utils.lib import utc_now, MessageArgs, error_embed, tabulate from utils.ui import ( MessageUI, ConfigEditor, FastModal, error_handler_for, ModalRetryUI, MsgEditor, Confirm, HookedItem, AsComponents, ) from babel.translator import ctx_locale from wards import equippable_role from .. import babel from ..data import MenuType, RoleMenuData from ..rolemenu import RoleMenu, RoleMenuRole from ..menuoptions import RoleMenuOptions from ..templates import templates _p = babel._p class RoleEditorInput(FastModal): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @error_handler_for(UserInputError) async def rerequest(self, interaction, error): await ModalRetryUI(self, error.msg).respond_to(interaction) class AChannelSelect(HookedItem, ChannelSelect): ... class EditorMode(Enum): OPTIONS = 0 ROLES = 1 STYLE = 2 class MenuEditor(MessageUI): _listening = {} # (channelid, callerid) -> active MenuEditor def _init_children(self): # HACK to stop ViewWeights complaining that this UI has too many children # Children will be correctly initialised after parent init. return [] def __init__(self, bot: LionBot, menu: RoleMenu, **kwargs): super().__init__(**kwargs) self._children = super()._init_children() self.bot = bot self.menu = menu self.data: RoleMenuData = bot.get_cog('RoleMenuCog').data self.listen_key = None # UI State self.mode: EditorMode = EditorMode.ROLES self.page_count: int = 1 self.pagen: int = 0 self.page_block: list[RoleMenuRole] = [] self._preview: Optional[discord.Interaction] = None # ----- UI API ----- async def update_preview(self): """ Update the preview message if it exists. """ if self._preview is not None: args = await self.menu.make_args() view = await self.menu.make_view() try: await self._preview.edit_original_response(**args.edit_args, view=view) except discord.NotFound: self._preview = None except discord.HTTPException as e: # Due to emoji validation on creation and message edit validation, # This should be very rare. # Might happen if e.g. a custom emoji is deleted between opening the editor # and showing the preview. # Just show the error to the user and let them deal with it or rerun the editor. t = self.bot.translator.t title = t(_p( 'ui:menu_editor|preview|error:title', "Display Error!" )) desc = t(_p( 'ui:menu_editor|preview|error:desc', "Failed to display preview!\n" "**Error:** `{exception}`" )).format( exception=e.text ) embed = discord.Embed( colour=discord.Colour.brand_red(), title=title, description=desc ) try: await self._preview.edit_original_response(embed=embed) except discord.HTTPException: # If we can't even edit the preview message now, something is probably wrong with the connection # Just silently ignore pass async def cleanup(self): self._listening.pop(self.listen_key, None) await super().cleanup() async def run(self, interaction: discord.Interaction, **kwargs): self.listen_key = (interaction.channel.id, interaction.user.id, self.menu.data.menuid) existing = self._listening.get(self.listen_key, None) if existing: await existing.quit() self._listening[self.listen_key] = self await super().run(interaction, **kwargs) async def quit(self): if self._preview is not None and not self._preview.is_expired(): try: await self._preview.delete_original_response() except discord.HTTPException: pass await super().quit() # ----- Components ----- # -- Options Components -- # Menu Options Button @button(label="OPTIONS_BUTTON_PLACEHOLDER", style=ButtonStyle.grey) async def options_button(self, press: discord.Interaction, pressed: Button): """ Change mode to 'Options'. """ await press.response.defer() self.mode = EditorMode.OPTIONS await self.refresh() async def options_button_refresh(self): t = self.bot.translator.t button = self.options_button button.label = t(_p( 'ui:menu_editor|button:options|label', "Menu Options" )) if self.mode is EditorMode.OPTIONS: button.style = ButtonStyle.blurple else: button.style = ButtonStyle.grey # Bulk Edit Button @button(label="BULK_EDIT_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple) async def bulk_edit_button(self, press: discord.Interaction, pressed: Button): """ Open a Config-like modal to textually edit the Menu options. """ t = self.bot.translator.t instances = ( self.menu.config.name, self.menu.config.sticky, self.menu.config.refunds, self.menu.config.obtainable, self.menu.config.required_role, ) fields = [instance.input_field for instance in instances] fields = [field for field in fields if fields] originals = [field.value for field in fields] modal = ConfigEditor( *fields, title=t(_p( 'ui:menu_editor|button:bulk_edit|modal|title', "Menu Options" )) ) @modal.submit_callback() async def save_options(interaction: discord.Interaction): modified = [] for instance, field, original in zip(instances, fields, originals): if field.value != original: # Option was modified, attempt to parse userstr = field.value.strip() if not userstr: new_data = None else: new_data = await instance._parse_string( instance.parent_id, userstr, guildid=self.menu.data.guildid ) instance.data = new_data modified.append(instance) if modified: # All fields have been parsed, it is safe to respond await interaction.response.defer(thinking=True, ephemeral=True) # Write settings for instance in modified: await instance.write() # Refresh the UI await self.refresh(thinking=interaction) else: # Nothing was modified, quietly accept await interaction.response.defer(thinking=False) await press.response.send_modal(modal) async def bulk_edit_button_refresh(self): t = self.bot.translator.t button = self.bulk_edit_button button.label = t(_p( 'ui:menu_editor|button:bulk_edit|label', "Bulk Edit" )) # Toggle Sticky Button @button(label="STICKY_BUTTON_PLACEHOLDER", style=ButtonStyle.grey) async def sticky_button(self, press: discord.Interaction, pressed: Button): """ Toggle the menu.config.sticky flag. """ await press.response.defer(thinking=True, ephemeral=True) instance = self.menu.config.sticky instance.value = not instance.value await instance.write() await self.refresh(thinking=press) async def sticky_button_refresh(self): t = self.bot.translator.t button = self.sticky_button button.label = t(_p( 'ui:menu_editor|button:sticky|label', "Toggle Sticky" )) if self.menu.config.sticky.value: button.style = ButtonStyle.blurple else: button.style = ButtonStyle.grey # Toggle Refunds Button @button(label="REFUNDS_BUTTON_PLACEHOLDER", style=ButtonStyle.grey) async def refunds_button(self, press: discord.Interaction, pressed: Button): """ Toggle the menu.config.refunds flag. """ await press.response.defer(thinking=True, ephemeral=True) instance = self.menu.config.refunds instance.value = not instance.value await instance.write() await self.refresh(thinking=press) async def refunds_button_refresh(self): t = self.bot.translator.t button = self.refunds_button button.label = t(_p( 'ui:menu_editor|button:refunds|label', "Toggle Refunds" )) if self.menu.config.refunds.value: button.style = ButtonStyle.blurple else: button.style = ButtonStyle.grey # Required Roles Menu @select(cls=RoleSelect, placeholder="REQROLES_MENU_PLACEHOLDER", min_values=0, max_values=1) async def reqroles_menu(self, selection: discord.Interaction, selected: RoleSelect): """ Set or reset the required role option for this menu. """ await selection.response.defer(thinking=True, ephemeral=True) if selected.values: new_data = selected.values[0].id else: new_data = None instance = self.menu.config.required_role instance.data = new_data await instance.write() await self.refresh(thinking=selection) async def reqroles_menu_refresh(self): t = self.bot.translator.t menu = self.reqroles_menu menu.placeholder = t(_p( 'ui:menu_editor|menu:reqroles|placeholder', "Select Required Role" )) # -- Roles Components -- # Modify Roles Button @button(label="MODIFY_ROLES_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple) async def modify_roles_button(self, press: discord.Interaction, pressed: Button): """ Change mode to 'Roles'. """ await press.response.defer() self.mode = EditorMode.ROLES await self.refresh() async def modify_roles_button_refresh(self): t = self.bot.translator.t button = self.modify_roles_button button.label = t(_p( 'ui:menu_editor|button:modify_roles|label', "Modify Roles" )) if self.mode is EditorMode.ROLES: button.style = ButtonStyle.blurple else: button.style = ButtonStyle.grey async def _edit_menu_role(self, interaction: discord.Interaction, menurole: RoleMenuRole): """ Handle edit flow for the given RoleMenuRole. Opens the modal editor, and upon submit, also opens the RoleEditor. """ t = self.bot.translator.t config = menurole.config instances = ( config.label, config.emoji, config.description, config.price, config.duration, ) fields = [instance.input_field for instance in instances] fields = [field for field in fields if fields] originals = [field.value for field in fields] modal = ConfigEditor( *fields, title=t(_p( 'ui:menu_editor|role_editor|modal|title', "Edit Menu Role" )) ) @modal.submit_callback() async def save_options(interaction: discord.Interaction): await interaction.response.defer(thinking=True, ephemeral=True) modified = [] for instance, field, original in zip(instances, fields, originals): if field.value != original: # Option was modified, attempt to parse userstr = field.value.strip() if not userstr: new_data = None else: new_data = await instance._parse_string( instance.parent_id, userstr, interaction=interaction ) instance.data = new_data modified.append(instance) if modified: # Write settings for instance in modified: await instance.write() # Refresh the UI await self.refresh(thinking=interaction) await self.update_preview() await self.menu.update_message() if self.menu.data.menutype is MenuType.REACTION: try: await self.menu.update_reactons() except SafeCancellation as e: await interaction.followup.send( embed=discord.Embed( colour=discord.Colour.brand_red(), description=e.msg ), ephemeral=True ) else: await interaction.delete_original_response() await interaction.response.send_modal(modal) # Add Roles Menu @select(cls=RoleSelect, placeholder="ADD_ROLES_MENU_PLACEHOLDER", min_values=0, max_values=25) async def add_roles_menu(self, selection: discord.Interaction, selected: RoleSelect): """ Add one or multiple roles to the menu. Behaviour is slightly different between one or multiple roles. For one role, if it already exists then it is edited. If it doesn't exist then it is added and an editor opened for it. For multiple roles, they are ORed with the existing roles, and no prompt is given for the fields. """ roles = selected.values if len(roles) == 0: await selection.response.defer(thinking=False) else: # Check equipment validity and permissions for role in roles: await equippable_role(self.bot, role, selection.user) single = None to_create = {role.id: role for role in roles} for mrole in self.menu.roles: if to_create.pop(mrole.data.roleid, None) is not None: single = mrole if to_create: t = self.bot.translator.t # Check numbers if self.menu.data.menutype is MenuType.REACTION and len(self.menu.roles) + len(to_create) > 20: raise UserInputError(t(_p( 'ui:menu_editor|menu:add_roles|error:too_many_reactions', "Too many roles! Reaction role menus cannot exceed `20` roles." ))) if len(self.menu.roles) + len(to_create) > 25: raise UserInputError(t(_p( 'ui:menu_editor|menu:add_roles|error:too_many_roles', "Too many roles! Role menus cannot have more than `25` roles." ))) # Create roles emojis = self.menu.unused_emojis(include_defaults=(self.menu.data.menutype is MenuType.REACTION)) rows = await self.data.RoleMenuRole.table.insert_many( ('menuid', 'roleid', 'label', 'emoji'), *( (self.menu.data.menuid, role.id, role.name[:100], next(emojis, None)) for role in to_create.values() ) ).with_adapter(self.data.RoleMenuRole._make_rows) mroles = [RoleMenuRole(self.bot, row) for row in rows] single = single if single is not None else mroles[0] if len(roles) == 1: await self._edit_menu_role(selection, single) else: await selection.response.defer() await self.menu.reload_roles() if self.menu.data.name == 'Untitled': # Hack to name an anonymous menu # TODO: Formalise this await self.menu.data.update(name=roles[0].name) await self.refresh() await self.update_preview() await self.menu.update_message() if self.menu.data.menutype is MenuType.REACTION: try: await self.menu.update_reactons() except SafeCancellation as e: raise UserInputError(e.msg) async def add_roles_menu_refresh(self): t = self.bot.translator.t menu = self.add_roles_menu menu.placeholder = t(_p( 'ui:menu_editor|menu:add_roles|placeholder', "Add Roles" )) def _role_option(self, menurole: RoleMenuRole): return SelectOption( emoji=menurole.config.emoji.data or None, label=menurole.config.label.value, value=str(menurole.data.menuroleid), description=menurole.config.description.value, ) # Edit Roles Menu @select(cls=Select, placeholder="EDIT_ROLES_MENU_PLACEHOLDER", min_values=1, max_values=1) async def edit_roles_menu(self, selection: discord.Interaction, selected: Select): """ Edit a single selected role. """ menuroleid = int(selected.values[0]) menurole = next(menurole for menurole in self.menu.roles if menurole.data.menuroleid == menuroleid) await self._edit_menu_role(selection, menurole) async def edit_roles_menu_refresh(self): t = self.bot.translator.t menu = self.edit_roles_menu menu.placeholder = t(_p( 'ui:menu_editor|menu:edit_roles|placeholder', "Edit Roles" )) options = [self._role_option(menurole) for menurole in self.menu.roles] if options: menu.options = options menu.disabled = False else: menu.options = [SelectOption(label='DUMMY')] menu.disabled = True # Delete Roles Menu @select(cls=Select, placeholder="DEL_ROLE_MENU_PLACEHOLDER", min_values=0, max_values=25) async def del_role_menu(self, selection: discord.Interaction, selected: Select): """ Remove one or multiple menu roles. """ menuroleids = list(map(int, selected.values)) if menuroleids: await selection.response.defer(thinking=True, ephemeral=True) await self.data.RoleMenuRole.table.delete_where(menuroleid=menuroleids) await self.menu.reload_roles() await self.refresh(thinking=selection) await self.update_preview() await self.menu.update_message() if self.menu.data.menutype is MenuType.REACTION: try: await self.menu.update_reactons() except SafeCancellation: pass else: await selection.response.defer(thinking=False) async def del_role_menu_refresh(self): t = self.bot.translator.t menu = self.del_role_menu menu.placeholder = t(_p( 'ui:menu_editor|menu:del_role|placeholder', "Remove Roles" )) options = [self._role_option(menurole) for menurole in self.menu.roles] if options: menu.options = options menu.disabled = False else: menu.options = [SelectOption(label='DUMMY')] menu.disabled = True menu.max_values = len(menu.options) # -- Style Components -- # Menu Style Button @button(label="STYLE_BUTTON_PLACEHOLDER", style=ButtonStyle.grey) async def style_button(self, press: discord.Interaction, pressed: Button): """ Change mode to 'Style'. """ if self.menu.message and self.menu.message.author != self.menu.message.guild.me: t = self.bot.translator.t # Non-managed message, cannot change style raise UserInputError( t(_p( 'ui:menu_editor|button:style|error:non-managed', "Cannot change the style of a menu attached to a message I did not send! Please repost first." )) ) await press.response.defer() self.mode = EditorMode.STYLE await self.refresh() async def style_button_refresh(self): t = self.bot.translator.t button = self.style_button button.label = t(_p( 'ui:menu_editor|button:style|label', "Menu Style" )) if self.mode is EditorMode.STYLE: button.style = ButtonStyle.blurple else: button.style = ButtonStyle.grey # Style Menu @select(cls=Select, placeholder="STYLE_MENU_PLACEHOLDER", min_values=1, max_values=1) async def style_menu(self, selection: discord.Interaction, selected: Select): """ Select one of Reaction Roles / Dropdown / Button """ t = self.bot.translator.t value = selected.values[0] menutype = MenuType[value] if menutype is not self.menu.data.menutype: # A change is requested if menutype is MenuType.REACTION: # Some checks need to be done when moving to reaction roles menuroles = self.menu.roles if len(menuroles) > 20: raise UserInputError( t(_p( 'ui:menu_editor|menu:style|error:too_many_reactions', "Too many roles! The Reaction style is limited to `20` roles (Discord limitation)." )) ) emojis = [mrole.config.emoji.value for mrole in menuroles] emojis = [emoji for emoji in emojis if emoji] uniq_emojis = set(emojis) if len(uniq_emojis) != len(menuroles): raise UserInputError( t(_p( 'ui:menu_editor|menu:style|error:incomplete_emojis', "Cannot switch to the Reaction Role Style! Every role needs to have a distinct emoji first." )) ) await selection.response.defer(thinking=True, ephemeral=True) await self.menu.data.update(menutype=menutype) await self.refresh(thinking=selection) await self.update_preview() await self.menu.update_message() if menutype is MenuType.REACTION: await self.menu.update_reactons() else: await selection.response.defer() async def style_menu_refresh(self): t = self.bot.translator.t menu = self.style_menu menu.placeholder = t(_p( 'ui:menu_editor|menu:style|placeholder', "Select Menu Style" )) menu.options = [ SelectOption( label=t(_p('ui:menu_editor|menu:style|option:reaction|label', "Reaction Roles")), description=t(_p( 'ui:menu_editor|menu:style|option:reaction|desc', "Roles are represented compactly as clickable reactions on a message." )), value=str(MenuType.REACTION.name), default=(self.menu.data.menutype is MenuType.REACTION) ), SelectOption( label=t(_p('ui:menu_editor|menu:style|option:button|label', "Button Menu")), description=t(_p( 'ui:menu_editor|menu:style|option:button|desc', "Roles are represented in 5 rows of 5 buttons, each with an emoji and label." )), value=str(MenuType.BUTTON.name), default=(self.menu.data.menutype is MenuType.BUTTON) ), SelectOption( label=t(_p('ui:menu_editor|menu:style|option:dropdown|label', "Dropdown Menu")), description=t(_p( 'ui:menu_editor|menu:style|option:dropdown|desc', "Roles are selectable from a dropdown menu below the message." )), value=str(MenuType.DROPDOWN.name), default=(self.menu.data.menutype is MenuType.DROPDOWN) ) ] async def _editor_callback(self, new_data): raws = json.dumps(new_data) await self.menu.data.update(rawmessage=raws) await self.update_preview() await self.menu.update_message() async def _message_editor(self, interaction: discord.Interaction): # Spawn the message editor with the current rawmessage data. # If the rawmessage data is empty, use the current template instead. editor = MsgEditor( self.bot, json.loads(self.menu.data.rawmessage), callback=self._editor_callback, callerid=self._callerid ) self._slaves.append(editor) await editor.run(interaction, ephemeral=True) # Template/Custom Menu @select(cls=Select, placeholder="TEMPLATE_MENU_PLACEHOLDER", min_values=1, max_values=1) async def template_menu(self, selection: discord.Interaction, selected: Select): """ Select a template for the menu message, or create a custom message. If the custom message does not already exist, it will be based on the current template. """ templateid = int(selected.values[0]) if templateid != self.menu.data.templateid: # Changes requested await selection.response.defer(thinking=True, ephemeral=True) if templateid == -1: # Chosen a custom message # Initialise the custom message if needed update_args = {'templateid': None} if not self.menu.data.rawmessage: template = templates[self.menu.data.templateid] margs = await template.render_menu(self.menu) raw = { 'content': margs.kwargs.get('content', ''), } if 'embed' in margs.kwargs: raw['embed'] = margs.kwargs['embed'].to_dict() rawjson = json.dumps(raw) update_args['rawmessage'] = rawjson # Save choice to data await self.menu.data.update(**update_args) # Spawn editor await self._message_editor(selection) await self.refresh() await self.update_preview() await self.menu.update_message() else: await self.menu.data.update(templateid=templateid) await self.refresh(thinking=selection) await self.update_preview() await self.menu.update_message() else: await selection.response.defer() async def template_menu_refresh(self): t = self.bot.translator.t menu = self.template_menu menu.placeholder = t(_p( 'ui:menu_editor|menu:template|placeholder', "Select Message Template" )) options = [] for template in templates.values(): option = template.as_option() option.default = (self.menu.data.templateid == template.id) options.append(option) custom_option = SelectOption( label=t(_p( 'ui:menu_editor|menu:template|option:custom|label', "Custom Message" )), value='-1', description=t(_p( 'ui:menu_editor|menu:template|option:custom|description', "Entirely custom menu message (opens an interactive editor)." )), default=(self.menu.data.templateid is None) ) options.append(custom_option) menu.options = options # -- Common Components -- # Delete Menu Button @button(label="DELETE_BUTTON_PLACEHOLDER", style=ButtonStyle.red) async def delete_button(self, press: discord.Interaction, pressed: Button): """ Confirm menu deletion, and delete. """ t = self.bot.translator.t confirm_msg = t(_p( 'ui:menu_editor|button:delete|confirm|title', "Are you sure you want to delete this menu? This is not reversible!" )) confirm = Confirm(confirm_msg, self._callerid) confirm.confirm_button.label = t(_p( 'ui:menu_editor|button:delete|confirm|button:yes', "Yes, Delete Now" )) confirm.confirm_button.style = ButtonStyle.red confirm.cancel_button.label = t(_p( 'ui:menu_editor|button:delete|confirm|button:no', "No, Go Back" )) confirm.cancel_button.style = ButtonStyle.green try: result = await confirm.ask(press, ephemeral=True) except ResponseTimedOut: result = False if result: await self.menu.delete() await self.quit() async def delete_button_refresh(self): t = self.bot.translator.t button = self.delete_button button.label = t(_p( 'ui:menu_editor|button:delete|label', "Delete Menu" )) # Quit Button @button(emoji=conf.emojis.cancel, style=ButtonStyle.red) async def quit_button(self, press: discord.Interaction, pressed: Button): """ Close the UI. This should also close all children. """ await press.response.defer(thinking=False) await self.quit() # Page Buttons @button(emoji=conf.emojis.forward) async def next_page_button(self, press: discord.Interaction, pressed: Button): await press.response.defer() self.pagen += 1 await self.refresh() @button(emoji=conf.emojis.backward) async def prev_page_button(self, press: discord.Interaction, pressed: Button): await press.response.defer() self.pagen -= 1 await self.refresh() # Edit Message Button @button(label="EDIT_MSG_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple) async def edit_msg_button(self, press: discord.Interaction, pressed: Button): # Set the templateid to None if it isn't already # And initialise the rawmessage if it needs it. if (templateid := self.menu.data.templateid) is not None: update_args = {'templateid': None} if not self.menu.data.rawmessage: template = templates[templateid] margs = await template.render_menu(self.menu) raw = { 'content': margs.kwargs.get('content', ''), } if 'embed' in margs.kwargs: raw['embed'] = margs.kwargs['embed'].to_dict() rawjson = json.dumps(raw) update_args['rawmessage'] = rawjson await self.menu.data.update(**update_args) # At this point we are certain the menu is in custom mode and has a rawmessage # Spawn editor await self._message_editor(press) await self.refresh() await self.update_preview() await self.menu.update_message() async def edit_msg_button_refresh(self): t = self.bot.translator.t button = self.edit_msg_button button.label = t(_p( 'ui:menu_editor|button:edit_msg|label', "Edit Message" )) # Disable the button if we are on a non-managed message button.disabled = not self.menu.managed # Preview Button @button(label="PREVIEW_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple) async def preview_button(self, press: discord.Interaction, pressed: Button): """ Display or update the preview message. """ if self._preview is not None: try: await self._preview.delete_original_response() except discord.HTTPException: pass self._preview = None await press.response.defer(thinking=True, ephemeral=True) self._preview = press await self.update_preview() async def preview_button_refresh(self): t = self.bot.translator.t button = self.preview_button button.label = t(_p( 'ui:menu_editor|button:preview|label', "Preview" )) # Repost Menu Button @button(label="REPOST_BUTTON_PLACEHOLDER", style=ButtonStyle.blurple) async def repost_button(self, press: discord.Interaction, pressed: Button): """ Repost the menu in a selected channel. Pops up a minimal channel selection UI, asking where they want to post it. """ t = self.bot.translator.t @AChannelSelect( placeholder=t(_p( 'ui:menu_editor|button:repost|widget:repost|menu:channel|placeholder', "Select New Channel" )), channel_types=[discord.ChannelType.text, discord.ChannelType.voice], min_values=1, max_values=1 ) async def repost_widget(selection: discord.Interaction, selected: ChannelSelect): channel = selected.values[0].resolve() if selected.values else None if channel is None: await selection.response.defer() else: # Valid channel selected, do the repost await selection.response.defer(thinking=True, ephemeral=True) try: await self.menu.repost_to(channel) except discord.Forbidden: title = t(_p( 'ui:menu_editor|button:repost|widget:repost|error:perms|title', "Insufficient Permissions!" )) desc = t(_p( 'ui:menu_editor|button:repost|eidget:repost|error:perms|desc', "I lack the `EMBED_LINKS` or `SEND_MESSAGES` permission in this channel." )) embed = discord.Embed( colour=discord.Colour.brand_red(), title=title, description=desc ) await selection.edit_original_response(embed=embed) except discord.HTTPException as e: error = discord.Embed( colour=discord.Colour.brand_red(), description=t(_p( 'ui:menu_editor|button:repost|widget:repost|error:post_failed', "An unknown error ocurred while posting to {channel}!\n" "**Error:** `{exception}`" )).format(channel=channel.mention, exception=e.text) ) await selection.edit_original_response(embed=error) else: try: await press.delete_original_response() except discord.HTTPException: pass success_title = t(_p( 'ui:menu_editor|button:repost|widget:repost|success|title', "Role Menu Moved" )) desc_lines = [] desc_lines.append( t(_p( 'ui:menu_editor|button:repost|widget:repost|success|desc:general', "The role menu `{name}` is now available at {message_link}." )).format( name=self.menu.data.name, message_link=self.menu.message.jump_url, ) ) if self.menu.data.menutype is MenuType.REACTION: try: await self.menu.update_reactons() except SafeCancellation as e: desc_lines.append(e.msg) else: t(_p( 'ui:menu_editor|button:repost|widget:repost|success|desc:reactions', "Please check the message reactions are correct." )) await selection.edit_original_response( embed=discord.Embed( title=success_title, description='\n'.join(desc_lines), colour=discord.Colour.brand_green(), ) ) # Create the selection embed title = t(_p( 'ui:menu_editor|button:repost|widget:repost|title', "Repost Role Menu" )) desc = t(_p( 'ui:menu_editor|button:repost|widget:repost|description', "Please select the channel to which you want to resend this menu." )) embed = discord.Embed( colour=discord.Colour.orange(), title=title, description=desc ) # Send as response with the repost widget attached await press.response.send_message(embed=embed, view=AsComponents(repost_widget), ephemeral=True) async def repost_button_refresh(self): t = self.bot.translator.t button = self.repost_button if self.menu.message is not None: button.label = t(_p( 'ui:menu_editor|button:repost|label:repost', "Repost" )) else: button.label = t(_p( 'ui:menu_editor|button:repost|label:post', "Post" )) # ----- UI Flow ----- async def make_message(self) -> MessageArgs: t = self.bot.translator.t # TODO: Link to actual message title = t(_p( 'ui:menu_editor|embed|title', "Role Menu Editor" )).format(name=self.menu.config.name.value) table = await RoleMenuOptions().make_setting_table(self.menu.data.menuid) jump = self.menu.jump_link if jump: jump_text = t(_p( 'ui:menu_editor|embed|description|jump_text:attached', "Members may use this menu from {jump_url}" )).format(jump_url=jump) else: jump_text = t(_p( 'ui:menu_editor|embed|description|jump_text:unattached', "This menu is not currently active!\n" "Make it available by clicking `Post` below." )) embed = discord.Embed( colour=discord.Colour.orange(), title=title, description=jump_text + '\n' + table ) # Tip field embed.add_field( inline=False, name=t(_p( 'ui:menu_editor|embed|field:tips|name', "Command Tips" )), value=t(_p( 'ui:menu_editor|embed|field:tips|value', "Use the following commands for faster menu setup.\n" "{menuedit} to edit the above menu options.\n" "{addrole} to add new roles (recommended for roles with emojis).\n" "{editrole} to edit role options." )).format( menuedit=self.bot.core.mention_cmd('rolemenu editmenu'), addrole=self.bot.core.mention_cmd('rolemenu addrole'), editrole=self.bot.core.mention_cmd('rolemenu editrole'), ) ) # Compute and add the pages for mrole in self.page_block: name = f"{mrole.config.label.formatted}" prop_map = { mrole.config.emoji.display_name: mrole.config.emoji.formatted, mrole.config.price.display_name: mrole.config.price.formatted, mrole.config.duration.display_name: mrole.config.duration.formatted, mrole.config.description.display_name: mrole.config.description.formatted, } prop_table = '\n'.join(tabulate(*prop_map.items())) value = f"{mrole.config.role.formatted}\n{prop_table}" embed.add_field(name=name, value=value, inline=True) return MessageArgs(embed=embed) async def _handle_invalid_emoji(self, error: discord.HTTPException): t = self.bot.translator.t text = error.text splits = text.split('.') i = splits.index('emoji') role_index = int(splits[i-1]) mrole = self.menu.roles[role_index] embed = discord.Embed( colour=discord.Colour.brand_red(), title=t(_p( 'ui:menu_editor|error:invald_emoji|title', "Invalid emoji encountered." )), description=t(_p( 'ui:menu_editor|error:invalid_emoji|desc', "The emoji `{emoji}` for menu role `{label}` no longer exists, unsetting." )).format(emoji=mrole.config.emoji.data, label=mrole.config.label.data) ) await mrole.data.update(emoji=None) await self.channel.send(embed=embed) async def _redraw(self, args): try: await super()._redraw(args) except discord.HTTPException as e: if e.code == 50035 and 'Invalid emoji' in e.text: await self._handle_invalid_emoji(e) await self.refresh() await self.update_preview() await self.menu.update_message() else: raise e async def draw(self, *args, **kwargs): try: await super().draw(*args, **kwargs) except discord.HTTPException as e: if e.code == 50035 and 'Invalid emoji' in e.text: await self._handle_invalid_emoji(e) await self.draw(*args, **kwargs) await self.menu.update_message() else: raise e async def refresh_layout(self): to_refresh = ( self.options_button_refresh(), self.reqroles_menu_refresh(), self.sticky_button_refresh(), self.refunds_button_refresh(), self.bulk_edit_button_refresh(), self.modify_roles_button_refresh(), self.add_roles_menu_refresh(), self.edit_roles_menu_refresh(), self.del_role_menu_refresh(), self.style_button_refresh(), self.style_menu_refresh(), self.template_menu_refresh(), self.preview_button_refresh(), self.delete_button_refresh(), self.edit_msg_button_refresh(), self.repost_button_refresh(), ) await asyncio.gather(*to_refresh) line_last = ( self.options_button, self.modify_roles_button, self.style_button, self.delete_button, self.quit_button ) line_1 = ( self.preview_button, self.edit_msg_button, self.repost_button, ) if self.page_count > 1: line_1 = (self.prev_page_button, *line_1, self.next_page_button) if self.mode is EditorMode.OPTIONS: self.set_layout( line_1, (self.bulk_edit_button, self.sticky_button, self.refunds_button,), (self.reqroles_menu,), line_last, ) elif self.mode is EditorMode.ROLES: self.set_layout( line_1, (self.add_roles_menu,), (self.edit_roles_menu,), (self.del_role_menu,), line_last ) elif self.mode is EditorMode.STYLE: self.set_layout( line_1, (self.style_menu,), (self.template_menu,), line_last ) async def reload(self): mroles = self.menu.roles page_size = 6 blocks = [mroles[i:i+page_size] for i in range(0, len(mroles), page_size)] or [[]] self.page_count = len(blocks) self.pagen = self.pagen % self.page_count self.page_block = blocks[self.pagen] await self.menu.fetch_message() await self.menu.update_raw()