Files
mrtuxie-quotes-module/quotes/discord/cog.py

263 lines
8.9 KiB
Python

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} <content>".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} <quoteid> <new content>".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)