from typing import Optional import asyncio import random import discord from discord.enums import TextStyle from discord.ext import commands as cmds from discord import app_commands as appcmds from meta import LionBot, LionCog, LionContext from meta.errors import ResponseTimedOut, SafeCancellation, UserInputError from utils.ui import input from ..data import QuoteInfo, QuotesData from ..quotes import QuoteRegistry from ..lib import minify from . import logger from .ui import QuoteListUI class QuoteCog(LionCog): def __init__(self, bot: LionBot): self.bot = bot self.data = bot.db.load_registry(QuotesData()) self.quotes = QuoteRegistry(self.data) async def cog_load(self): await self.data.init() await self.quotes.init() # ----- API ----- async def quote_acmpl(self, interaction: discord.Interaction, partial: str): """ Autocomplete for a community-local quote selection. """ if not interaction.guild: return [] choices = [] community = await self.bot.profiles.fetch_community( interaction.guild, interaction=interaction, touch=False ) quotes = await self.quotes.get_community_quotes(community.communityid) if not quotes: nullchoice = appcmds.Choice( name="No quotes have been created!", value="0", ) choices.append(nullchoice) else: for quote in quotes: labelstr = f"#{quote.quotelabel}:" if partial.lower() in labelstr + quote.content.lower(): minified = minify(quote.content, 100 - len(labelstr) - 1, strip=' >') displayed = f"{labelstr} {minified}" choice = appcmds.Choice( name=displayed, value=str(quote.quotelabel), ) choices.append(choice) if not choices: nullchoice = appcmds.Choice( name="No quotes matching your input!", value="0", ) choices.append(nullchoice) return choices async def resolve_quote(self, ctx: LionContext, quotestr: str) -> QuoteInfo: """ Resolve a quote string provided as an argument. Essentially only accepts integer quote labels. """ quotestr = quotestr.strip('# ') if not quotestr.isdigit(): raise UserInputError( "Could not parse desired quote! Please enter the number or select from autocomplete options." ) elif (label := int(quotestr)) == 0: raise UserInputError( "Invalid option selected!" ) else: quote = await self.quotes.get_quote_label(ctx.community.communityid, label) if not quote: raise UserInputError( f"Quote #{label} does not exist!" ) else: return quote # ----- Commands ------ @cmds.hybrid_command( name='quote', description="Display a random quote." ) @cmds.guild_only() async def quote_cmd(self, ctx: LionContext): quotes = await self.quotes.get_community_quotes(ctx.community.communityid) if quotes: # Select a random quote quote = random.choice(quotes) if '\n' in quote.content: formatted = f"**#{quote.quotelabel}:**\n{quote.content}" else: formatted = f"**#{quote.quotelabel}:** {quote.content}" await ctx.reply(formatted) else: await ctx.reply("There are no quotes to display!") @cmds.hybrid_group( name='quotes', description="Base command group for quotes management.", ) @cmds.has_permissions(manage_guild=True) @cmds.guild_only() async def quotes_grp(self, ctx: LionContext): await self.quotes_list_cmd(ctx) @cmds.hybrid_command( name='addquote', description="Create a new quote. Use without arguments to add a multiline quote." ) @appcmds.describe( content="Content of the quote to add" ) @cmds.has_permissions(manage_guild=True) async def addquote_cmd(self, ctx: LionContext, *, content: Optional[str] = None): await self.quotes_add_cmd(ctx, content=content) @quotes_grp.command( name='add', description="Create a new quote. Use without arguments to add a multiline quote." ) @appcmds.describe( content="Content of the quote to add" ) @cmds.has_permissions(manage_guild=True) async def quotes_add_cmd(self, ctx: LionContext, *, content: Optional[str] = None): if content is not None: interaction = ctx.interaction to_create = content elif not ctx.interaction: invoked = ' '.join( (*ctx.invoked_parents, ctx.invoked_with or '') ) await ctx.error_reply( "**USAGE:** {prefix}{cmd} ".format( prefix=ctx.prefix, cmd=invoked ) ) raise SafeCancellation else: try: interaction, to_create = await input( interaction=ctx.interaction, title="Create Quote", question="Quote Content", timeout=300, style=TextStyle.long, min_length=1, max_length=2000 - 8 ) except asyncio.TimeoutError: raise SafeCancellation if interaction: sender = interaction.response.send_message else: sender = ctx.reply quote = await self.quotes.create_quote( communityid=ctx.community.communityid, content=to_create, created_by=ctx.profile.profileid, ) await sender(f"Quote #{quote.quotelabel} created!") @quotes_grp.command( name='del', description="Delete a saved quote." ) @appcmds.describe( quotestr="Select the quote to delete, or write the number." ) @cmds.has_permissions(manage_guild=True) @appcmds.rename(quotestr='quote') async def quotes_del_cmd(self, ctx: LionContext, quotestr: str): quote = await self.resolve_quote(ctx, quotestr) label = quote.quotelabel await self.quotes.delete_quote(quote.quoteid) await ctx.reply(f"Quote #{label} was deleted.") quotes_del_cmd.autocomplete('quotestr')(quote_acmpl) @quotes_grp.command( name='list', description="Display the community quotes. Quotes may also be added/edited/deleted here." ) @cmds.has_permissions(manage_guild=True) async def quotes_list_cmd(self, ctx: LionContext): view = QuoteListUI(self.bot, self.quotes, ctx.community.communityid, ctx.profile.profileid, ctx.author.id) if ctx.interaction is None: await view.send(ctx.channel) else: await view.run(ctx.interaction) await view.wait() @quotes_grp.command( name='edit', description="Edit a saved quote." ) @appcmds.describe( quotestr="Select the quote to edit, or write the number.", new_content="New content for the quote. Leave unselected to edit in multiline modal." ) @appcmds.rename(quotestr='quote') @cmds.has_permissions(manage_guild=True) async def quotes_edit_cmd(self, ctx: LionContext, quotestr: str, *, new_content: Optional[str] = None): quote = await self.resolve_quote(ctx, quotestr) if new_content is not None: interaction = ctx.interaction elif not ctx.interaction: invoked = ' '.join( (*ctx.invoked_parents, ctx.invoked_with or '') ) await ctx.error_reply( "**USAGE:** {prefix}{cmd} ".format( prefix=ctx.prefix, cmd=invoked ) ) raise SafeCancellation else: try: interaction, new_content = await input( interaction=ctx.interaction, title="Edit Quote", question="Quote Content", timeout=300, style=TextStyle.long, default=quote.content, min_length=1, max_length=2000 - 8 ) except asyncio.TimeoutError: raise SafeCancellation if interaction: sender = interaction.response.send_message else: sender = ctx.reply await self.data.quotes.update_where(quoteid=quote.quoteid).set(content=new_content) await sender(f"Quote #{quote.quotelabel} Updated!") quotes_edit_cmd.autocomplete('quotestr')(quote_acmpl)