from copy import deepcopy import json from json.decoder import JSONDecodeError import logging from naff import ( Client, Extension, slash_command, InteractionContext, Modal, ParagraphText, ModalContext, context_menu, Permissions, CommandTypes, Message, ) from database import BotMessages as BotMessagesModel # Template modal for creating/editing bot messages. message_creation_modal = Modal( custom_id="bot-message-create", title="Create a message as the bot", components=[ ParagraphText( custom_id="embeds", label="Embeds", placeholder="Embeds as JSON", required=False, ), ParagraphText( custom_id="content", label="Text", placeholder="Lorem ipsum dolor sit amet and so on", required=False, ), ] ) class BotMessages(Extension): def __init__(self, client: Client) -> None: self.client = client # Create a new bot message. @slash_command( name="bot-message-create", description="Create a message as the bot.", dm_permission=False, default_member_permissions=Permissions.MANAGE_GUILD, ) async def bot_message_create_command(self, ctx: InteractionContext): # Respond with the template modal. No values have been set in the modal, as it is # a new message. await ctx.send_modal(message_creation_modal) # Wait for the user to submit the modal, and ensure that we are receiving the # correct modal. modal_ctx: ModalContext = await self.client.wait_for_modal(message_creation_modal, author=ctx.author) if modal_ctx.custom_id != "bot-message-create": return # Retrieve the values from the modal. # Ensure that at least one of the fields is filled in, as a message cannot be # empty. embeds_string: str = modal_ctx.responses["embeds"] content_string: str = modal_ctx.responses["content"] if ( (embeds_string is None or embeds_string == "") and (content_string is None or content_string == "") ): await modal_ctx.send( "You must provide either an embed or text.", ephemeral=True, ) return # Attempt to parse the embed(s) JSON into a Python object. # Try loading it as a single or multiple embeds depending on if the object # returned is a list or a dictionary. # If the JSON is invalid, return an error. embed: dict | None = None embeds: list | None = None try: if embeds_string: embeds_temp = json.loads(embeds_string) if isinstance(embeds_temp, list): embeds = embeds_temp elif isinstance(embeds_temp, dict): embed = embeds_temp except JSONDecodeError: await modal_ctx.send( "The embeds were not valid JSON.", ephemeral=True, ) return # Send the bot message in the channel. msg = await ctx.channel.send( content=content_string if content_string else None, embed=embed, embeds=embeds, ) # Add an entry of the message in the database. BotMessagesModel.create( guild_id=msg.guild.id, channel_id=msg.channel.id, message_id=msg.id, ) # Send a confirmation message, as Discord requires us to respond to the interaction. await modal_ctx.send( "Message created!", ephemeral=True, ) # A context menu to allow moderators to edit a bot message. @context_menu( name="Edit bot message", context_type=CommandTypes.MESSAGE, dm_permission=False, default_member_permissions=Permissions.MANAGE_GUILD, ) async def edit_bot_message_context_menu(self, ctx: InteractionContext): message: Message = ctx.target # Ensure that the target message is from the bot. # If it is not, return an error. if message.author.id != self.client.user.id: await ctx.send( "This is not a bot message.", ephemeral=True, ) return # Retrieve the bot message from the database, if any. bot_message: BotMessagesModel | None = BotMessagesModel.get_or_none( BotMessagesModel.guild_id == message.channel.guild.id, BotMessagesModel.channel_id == message.channel.id, BotMessagesModel.message_id == message.id, ) # If there is no bot message, return an error. if bot_message is None: await ctx.send( "This is not an editable bot message.", ephemeral=True, ) return # Create a copy of the template modal, and insert the contents of the bot message. modal = deepcopy(message_creation_modal) modal.title = "Edit bot message" modal.custom_id = "bot-message-edit" modal.components[0].value = json.dumps( [e.to_dict() for e in message.embeds] if message.embeds else "", indent=4, ) modal.components[1].value = message.content # Send the modal to the user await ctx.send_modal(modal) # Wait for the user to submit the modal, and ensure that we are receiving the # correct modal. modal_ctx: ModalContext = await self.client.wait_for_modal(modal, author=ctx.author) if modal_ctx.custom_id != "bot-message-edit": return embeds_string: str = modal_ctx.responses["embeds"] content_string: str = modal_ctx.responses["content"] if ( (embeds_string is None or embeds_string == "") and (content_string is None or content_string == "") ): await modal_ctx.send( "You must provide either an embed or text.", ephemeral=True, ) return embed: dict | None = None embeds: list | None = None try: if embeds_string: embeds_temp = json.loads(embeds_string) if isinstance(embeds_temp, list): embeds = embeds_temp elif isinstance(embeds_temp, dict): embed = embeds_temp except JSONDecodeError: await modal_ctx.send( "The embeds were not valid JSON.", ephemeral=True, ) return await message.edit( content=content_string if content_string else None, embed=embed, embeds=embeds, ) await modal_ctx.send( "Message edited!", ephemeral=True, ) def setup(client: Client): BotMessagesModel.create_table() BotMessages(client) logging.info("BotMessages extension loaded")