From 45e6ed4e2977f35edc856fbc47338f3fa62371f0 Mon Sep 17 00:00:00 2001 From: Interitio Date: Mon, 25 Aug 2025 19:33:48 +1000 Subject: [PATCH] Quotes data and cog skeleton. --- data/schema.sql | 28 +++++++++ plugin/discord/__init__.py | 0 plugin/twitch/__init__.py | 0 plugin/web/__init__.py | 0 quotes/__init__.py | 5 ++ quotes/data.py | 28 +++++++++ quotes/discord/__init__.py | 5 ++ quotes/discord/cog.py | 92 +++++++++++++++++++++++++++++ plugin/__init__.py => quotes/lib.py | 0 quotes/quotes.py | 41 +++++++++++++ 10 files changed, 199 insertions(+) delete mode 100644 plugin/discord/__init__.py delete mode 100644 plugin/twitch/__init__.py delete mode 100644 plugin/web/__init__.py create mode 100644 quotes/__init__.py create mode 100644 quotes/data.py create mode 100644 quotes/discord/__init__.py create mode 100644 quotes/discord/cog.py rename plugin/__init__.py => quotes/lib.py (100%) create mode 100644 quotes/quotes.py diff --git a/data/schema.sql b/data/schema.sql index e69de29..2a7f988 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -0,0 +1,28 @@ +BEGIN; + +INSERT INTO version_history (component, from_version, to_version, author) +VALUES ('QUOTES', 0, 1, 'Initial Creation'); + +CREATE TABLE quotes( + quoteid INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + communityid INTEGER NOT NULL REFERENCES communities(communityid) ON DELETE CASCADE ON UPDATE CASCADE, + content TEXT NOT NULL, + created_by INTEGER REFERENCES user_profiles(profileid) ON UPDATE CASCADE ON DELETE NO ACTION, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + _timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() +); +CREATE TRIGGER quotes_timestamp BEFORE UPDATE ON quotes + FOR EACH ROW EXECUTE FUNCTION update_timestamp_column(); + +CREATE VIEW + quotes_info +AS + SELECT + *, + row_number() + 1 OVER (PARITION BY communityid ORDER BY created_at ASC) + FROM + quotes + ORDER BY (communityid, created_at); + + +COMMIT; diff --git a/plugin/discord/__init__.py b/plugin/discord/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugin/twitch/__init__.py b/plugin/twitch/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/plugin/web/__init__.py b/plugin/web/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/quotes/__init__.py b/quotes/__init__.py new file mode 100644 index 0000000..7327446 --- /dev/null +++ b/quotes/__init__.py @@ -0,0 +1,5 @@ +import logging + +logger = logging.getLogger(__name__) + +from .discord import setup diff --git a/quotes/data.py b/quotes/data.py new file mode 100644 index 0000000..ab6ad21 --- /dev/null +++ b/quotes/data.py @@ -0,0 +1,28 @@ +from data import Registry, RowModel, Table +from data.columns import Integer, Timestamp, String + +from weakref import WeakValueDictionary + +class Quote(RowModel): + _tablename_ = 'quotes' + _cache_ = WeakValueDictionary() + + quoteid = Integer(primary=True) + communityid = Integer() + content = String() + created_by = Integer() + created_at = Timestamp() + _timestamp = Timestamp() + + +class QuoteInfo(Quote): + _tablename_ = 'quotes_info' + _readonly_ = True + _cache_ = WeakValueDictionary() + + quotelabel = Integer() + + +class QuotesData(Registry): + quotes = Quote.table + quotes_info = QuoteInfo.table diff --git a/quotes/discord/__init__.py b/quotes/discord/__init__.py new file mode 100644 index 0000000..769ab4c --- /dev/null +++ b/quotes/discord/__init__.py @@ -0,0 +1,5 @@ +from .. import logger + +async def setup(bot): + from .cog import QuoteCog + await bot.add_cog(QuoteCog(bot)) diff --git a/quotes/discord/cog.py b/quotes/discord/cog.py new file mode 100644 index 0000000..8034905 --- /dev/null +++ b/quotes/discord/cog.py @@ -0,0 +1,92 @@ +from typing import Optional +import asyncio + +import discord +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 . import logger +from ..data import QuotesData +from ..quotes import QuoteRegistry + + +class QuoteCog(LionCog): + def __init__(self, bot: LionBot): + 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): + # TODO + ... + + # ----- Commands ------ + @cmds.hybrid_command( + name='quote', + description="Display a random quote." + ) + async def quote_cmd(self, ctx: LionContext): + # TODO + ... + + @cmds.hybrid_group( + name='quotes', + description="Base command group for quotes management.", + ) + async def quotes_grp(self, ctx: LionContext): + # TODO + # Call the quotes list command + ... + + @cmds.hybrid_command( + name='addquote', + description="Create a new quote. Use without arguments to add a multiline quote." + ) + @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" + ) + async def quotes_add_cmd(self, ctx: LionContext, content: Optional[str]): + # TODO + ... + + @quotes_grp.command( + name='del', + description="Delete a saved quote (WARNING: This will change all quote numbers.)" + ) + @appcmds.describe( + quote="Select the quote to delete, or write the number." + ) + async def quotes_del_cmd(self, ctx: LionContext, quote: str): + # TODO + ... + + @quotes_grp.command( + name='list', + description="Display the community quotes. Quotes may also be added/edited/deleted here." + ) + async def quotes_list_cmd(self, ctx: LionContext): + # TODO + ... + + @quotes_grp.command( + name='edit', + description="Edit a saved quote." + ) + @appcmds.describe( + quote="Select the quote to delete, or write the number." + ) + async def quotes_edit_cmd(self, ctx: LionContext, quote: str): + # TODO: Move quote to QuoteConverter? + # TODO + ... diff --git a/plugin/__init__.py b/quotes/lib.py similarity index 100% rename from plugin/__init__.py rename to quotes/lib.py diff --git a/quotes/quotes.py b/quotes/quotes.py new file mode 100644 index 0000000..b2e1e4a --- /dev/null +++ b/quotes/quotes.py @@ -0,0 +1,41 @@ +from typing import Optional +from .data import Quote, QuoteInfo, QuotesData + + +class QuoteRegistry: + def __init__(self, data: QuotesData): + self.data = data + # TODO: For efficiency we could have a cache here + # Caching quotesinfo by community. + # Particularly efficient for autocomplete. + + async def init(self): + await self.data.init() + + async def get_community_quotes(self, communityid: int) -> list[QuoteInfo]: + return await QuoteInfo.fetch_where(communityid=communityid) + + async def get_quoteinfo(self, quoteid: int) -> Optional[QuoteInfo]: + return await QuoteInfo.fetch(quoteid) + + async def get_quote(self, quoteid: int) -> Optional[Quote]: + return await Quote.fetch(quoteid) + + async def get_quote_label(self, communityid: int, label: int) -> Optional[QuoteInfo]: + results = await QuoteInfo.fetch_where(communityid=communityid, quotelabel=label) + return results[0] if results else None + + async def create_quote( + self, + communityid: int, + content: str, + created_by: Optional[int] = None + ) -> QuoteInfo: + quote = await Quote.create( + content=content, + communityid=communityid, + created_by=created_by, + ) + info = await QuoteInfo.fetch(quote.quoteid) + assert info is not None + return info