v2 refactor to label-based koans, and add discord
This commit is contained in:
@@ -1,20 +1,30 @@
|
|||||||
BEGIN;
|
BEGIN;
|
||||||
-- Koan data {{{
|
|
||||||
INSERT INTO version_history (component, from_version, to_version, author)
|
|
||||||
VALUES ('KOANS', 0, 1, 'Initial Creation');
|
|
||||||
|
|
||||||
---- !koans lists koans. !koan gives a random koan. !koans add name ... !koans del name ...
|
INSERT INTO version_history (component, from_version, to_version, author)
|
||||||
|
VALUES ('KOANS', 0, 2, 'Initial Creation');
|
||||||
|
|
||||||
CREATE TABLE koans(
|
CREATE TABLE koans(
|
||||||
koanid INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
koanid INTEGER NOT NULL PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
|
||||||
communityid INTEGER NOT NULL REFERENCES communities ON UPDATE CASCADE ON DELETE CASCADE,
|
communityid INTEGER NOT NULL REFERENCES communities(communityid) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
name TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
message TEXT NOT NULL,
|
created_by INTEGER REFERENCES user_profiles(profileid) ON UPDATE CASCADE ON DELETE NO ACTION,
|
||||||
|
deleted_at TIMESTAMPTZ,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
);
|
);
|
||||||
CREATE TRIGGER koans_timestamp BEFORE UPDATE ON koans
|
CREATE TRIGGER koans_timestamp BEFORE UPDATE ON koans
|
||||||
FOR EACH ROW EXECUTE FUNCTION update_timestamp_column();
|
FOR EACH ROW EXECUTE FUNCTION update_timestamp_column();
|
||||||
|
|
||||||
-- }}}
|
CREATE VIEW
|
||||||
|
koans_info
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
(deleted_at is not NULL) AS is_deleted,
|
||||||
|
(row_number() OVER (PARTITION BY communityid ORDER BY created_at ASC)) as koanlabel
|
||||||
|
FROM
|
||||||
|
koans
|
||||||
|
ORDER BY (communityid, created_at);
|
||||||
|
|
||||||
|
|
||||||
COMMIT;
|
COMMIT;
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from .twitch import setup as twitch_setup
|
from .twitch import setup as twitch_setup
|
||||||
|
from .discord import setup
|
||||||
|
|||||||
@@ -1,32 +1,31 @@
|
|||||||
from data import Registry, RowModel
|
from data import Registry, RowModel, Table
|
||||||
from data.columns import String, Timestamp, Integer
|
from data.columns import Bool, Integer, Timestamp, String
|
||||||
|
|
||||||
|
from weakref import WeakValueDictionary
|
||||||
|
|
||||||
class Koan(RowModel):
|
class Koan(RowModel):
|
||||||
"""
|
|
||||||
Schema
|
|
||||||
======
|
|
||||||
CREATE TABLE koans(
|
|
||||||
koanid INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
||||||
communityid INTEGER NOT NULL REFERENCES communities ON UPDATE CASCADE ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
||||||
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
||||||
);
|
|
||||||
"""
|
|
||||||
_tablename_ = 'koans'
|
_tablename_ = 'koans'
|
||||||
_cache_ = {}
|
_cache_ = WeakValueDictionary()
|
||||||
|
|
||||||
koanid = Integer(primary=True)
|
koanid = Integer(primary=True)
|
||||||
communityid = Integer()
|
communityid = Integer()
|
||||||
name = String()
|
content = String()
|
||||||
message = String()
|
deleted_at = Timestamp()
|
||||||
|
created_by = Integer()
|
||||||
created_at = Timestamp()
|
created_at = Timestamp()
|
||||||
_timestamp = Timestamp()
|
_timestamp = Timestamp()
|
||||||
|
|
||||||
|
|
||||||
class KoanData(Registry):
|
class KoanInfo(Koan):
|
||||||
VERSION = ('KOANS', 1)
|
_tablename_ = 'koans_info'
|
||||||
|
_readonly_ = True
|
||||||
|
_cache_ = WeakValueDictionary()
|
||||||
|
|
||||||
|
koanlabel = Integer()
|
||||||
|
is_deleted = Bool()
|
||||||
|
|
||||||
|
|
||||||
|
class KoansData(Registry):
|
||||||
|
VERSION = ('KOANS', 2)
|
||||||
koans = Koan.table
|
koans = Koan.table
|
||||||
|
koans_info = KoanInfo.table
|
||||||
|
|||||||
5
koans/discord/__init__.py
Normal file
5
koans/discord/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
from .. import logger
|
||||||
|
|
||||||
|
async def setup(bot):
|
||||||
|
from .cog import KoanCog
|
||||||
|
await bot.add_cog(KoanCog(bot))
|
||||||
251
koans/discord/cog.py
Normal file
251
koans/discord/cog.py
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
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 KoanInfo, KoansData
|
||||||
|
from ..koans import KoanRegistry
|
||||||
|
from ..lib import minify
|
||||||
|
|
||||||
|
from . import logger
|
||||||
|
from .ui import KoanListUI
|
||||||
|
|
||||||
|
|
||||||
|
class KoanCog(LionCog):
|
||||||
|
def __init__(self, bot: LionBot):
|
||||||
|
self.bot = bot
|
||||||
|
self.data = bot.db.load_registry(KoansData())
|
||||||
|
self.koans = KoanRegistry(self.data)
|
||||||
|
|
||||||
|
async def cog_load(self):
|
||||||
|
await self.data.init()
|
||||||
|
await self.koans.init()
|
||||||
|
|
||||||
|
# ----- API -----
|
||||||
|
async def koan_acmpl(self, interaction: discord.Interaction, partial: str):
|
||||||
|
"""
|
||||||
|
Autocomplete for a community-local koan selection.
|
||||||
|
"""
|
||||||
|
if not interaction.guild:
|
||||||
|
return []
|
||||||
|
|
||||||
|
choices = []
|
||||||
|
community = await self.bot.profiles.fetch_community(
|
||||||
|
interaction.guild, interaction=interaction,
|
||||||
|
touch=False
|
||||||
|
)
|
||||||
|
koans = await self.koans.get_community_koans(community.communityid)
|
||||||
|
if not koans:
|
||||||
|
nullchoice = appcmds.Choice(
|
||||||
|
name="No koans have been created!",
|
||||||
|
value="0",
|
||||||
|
)
|
||||||
|
choices.append(nullchoice)
|
||||||
|
else:
|
||||||
|
for koan in koans:
|
||||||
|
labelstr = f"#{koan.koanlabel}:"
|
||||||
|
if partial.lower() in labelstr + koan.content.lower():
|
||||||
|
minified = minify(koan.content, 100 - len(labelstr) - 1, strip=' >')
|
||||||
|
displayed = f"{labelstr} {minified}"
|
||||||
|
choice = appcmds.Choice(
|
||||||
|
name=displayed,
|
||||||
|
value=str(koan.koanlabel),
|
||||||
|
)
|
||||||
|
choices.append(choice)
|
||||||
|
if not choices:
|
||||||
|
nullchoice = appcmds.Choice(
|
||||||
|
name="No koans matching your input!",
|
||||||
|
value="0",
|
||||||
|
)
|
||||||
|
choices.append(nullchoice)
|
||||||
|
|
||||||
|
return choices
|
||||||
|
|
||||||
|
async def resolve_koan(self, ctx: LionContext, koanstr: str) -> KoanInfo:
|
||||||
|
"""
|
||||||
|
Resolve a koan string provided as an argument.
|
||||||
|
|
||||||
|
Essentially only accepts integer koan labels.
|
||||||
|
"""
|
||||||
|
koanstr = koanstr.strip('# ')
|
||||||
|
if not koanstr.isdigit():
|
||||||
|
raise UserInputError(
|
||||||
|
"Could not parse desired koan! Please enter the number or select from autocomplete options."
|
||||||
|
)
|
||||||
|
elif (label := int(koanstr)) == 0:
|
||||||
|
raise UserInputError(
|
||||||
|
"Invalid option selected!"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
koan = await self.koans.get_koan_label(ctx.community.communityid, label)
|
||||||
|
if not koan:
|
||||||
|
raise UserInputError(
|
||||||
|
f"Koan #{label} does not exist!"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return koan
|
||||||
|
|
||||||
|
# ----- Commands ------
|
||||||
|
@cmds.hybrid_command(
|
||||||
|
name='koan',
|
||||||
|
description="Display a random koan."
|
||||||
|
)
|
||||||
|
@cmds.guild_only()
|
||||||
|
async def koan_cmd(self, ctx: LionContext):
|
||||||
|
koans = await self.koans.get_community_koans(ctx.community.communityid)
|
||||||
|
if koans:
|
||||||
|
# Select a random koan
|
||||||
|
koan = random.choice(koans)
|
||||||
|
if '\n' in koan.content:
|
||||||
|
formatted = f"**#{koan.koanlabel}:**\n{koan.content}"
|
||||||
|
else:
|
||||||
|
formatted = f"**#{koan.koanlabel}:** {koan.content}"
|
||||||
|
await ctx.reply(formatted)
|
||||||
|
else:
|
||||||
|
await ctx.reply("There are no koans to display!")
|
||||||
|
|
||||||
|
@cmds.hybrid_group(
|
||||||
|
name='koans',
|
||||||
|
description="Base command group for koans management.",
|
||||||
|
)
|
||||||
|
@cmds.has_permissions(manage_guild=True)
|
||||||
|
@cmds.guild_only()
|
||||||
|
async def koans_grp(self, ctx: LionContext):
|
||||||
|
await self.koans_list_cmd(ctx)
|
||||||
|
|
||||||
|
@koans_grp.command(
|
||||||
|
name='add',
|
||||||
|
description="Create a new koan. Use without arguments to add a multiline koan."
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
content="Content of the koan to add"
|
||||||
|
)
|
||||||
|
@cmds.has_permissions(manage_guild=True)
|
||||||
|
async def koans_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 Koan",
|
||||||
|
question="Koan 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
|
||||||
|
|
||||||
|
koan = await self.koans.create_koan(
|
||||||
|
communityid=ctx.community.communityid,
|
||||||
|
content=to_create,
|
||||||
|
created_by=ctx.profile.profileid,
|
||||||
|
)
|
||||||
|
await sender(f"Koan #{koan.koanlabel} created!")
|
||||||
|
|
||||||
|
@koans_grp.command(
|
||||||
|
name='del',
|
||||||
|
description="Delete a saved koan."
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
koanstr="Select the koan to delete, or write the number."
|
||||||
|
)
|
||||||
|
@cmds.has_permissions(manage_guild=True)
|
||||||
|
@appcmds.rename(koanstr='koan')
|
||||||
|
async def koans_del_cmd(self, ctx: LionContext, koanstr: str):
|
||||||
|
koan = await self.resolve_koan(ctx, koanstr)
|
||||||
|
label = koan.koanlabel
|
||||||
|
await self.koans.delete_koan(koan.koanid)
|
||||||
|
await ctx.reply(f"Koan #{label} was deleted.")
|
||||||
|
|
||||||
|
koans_del_cmd.autocomplete('koanstr')(koan_acmpl)
|
||||||
|
|
||||||
|
@koans_grp.command(
|
||||||
|
name='list',
|
||||||
|
description="Display the community koans. Koans may also be added/edited/deleted here."
|
||||||
|
)
|
||||||
|
@cmds.has_permissions(manage_guild=True)
|
||||||
|
async def koans_list_cmd(self, ctx: LionContext):
|
||||||
|
view = KoanListUI(self.bot, self.koans, 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()
|
||||||
|
|
||||||
|
@koans_grp.command(
|
||||||
|
name='edit',
|
||||||
|
description="Edit a saved koan."
|
||||||
|
)
|
||||||
|
@appcmds.describe(
|
||||||
|
koanstr="Select the koan to edit, or write the number.",
|
||||||
|
new_content="New content for the koan. Leave unselected to edit in multiline modal."
|
||||||
|
)
|
||||||
|
@appcmds.rename(koanstr='koan')
|
||||||
|
@cmds.has_permissions(manage_guild=True)
|
||||||
|
async def koans_edit_cmd(self, ctx: LionContext, koanstr: str, *, new_content: Optional[str] = None):
|
||||||
|
koan = await self.resolve_koan(ctx, koanstr)
|
||||||
|
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} <koanid> <new content>".format(
|
||||||
|
prefix=ctx.prefix,
|
||||||
|
cmd=invoked
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise SafeCancellation
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
interaction, new_content = await input(
|
||||||
|
interaction=ctx.interaction,
|
||||||
|
title="Edit Koan",
|
||||||
|
question="Koan Content",
|
||||||
|
timeout=300,
|
||||||
|
style=TextStyle.long,
|
||||||
|
default=koan.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.koans.update_where(koanid=koan.koanid).set(content=new_content)
|
||||||
|
await sender(f"Koan #{koan.koanlabel} Updated!")
|
||||||
|
|
||||||
|
koans_edit_cmd.autocomplete('koanstr')(koan_acmpl)
|
||||||
1
koans/discord/ui/__init__.py
Normal file
1
koans/discord/ui/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
from .koanlist import KoanListUI
|
||||||
271
koans/discord/ui/koanlist.py
Normal file
271
koans/discord/ui/koanlist.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
import asyncio
|
||||||
|
|
||||||
|
import discord
|
||||||
|
from discord.ui.select import select, Select
|
||||||
|
from discord.components import SelectOption
|
||||||
|
from discord.enums import ButtonStyle, TextStyle
|
||||||
|
from discord.ui.button import button, Button
|
||||||
|
from discord.ui.text_input import TextInput
|
||||||
|
|
||||||
|
from meta import LionBot, conf
|
||||||
|
from meta.errors import SafeCancellation
|
||||||
|
|
||||||
|
from utils.ui import MessageUI, input
|
||||||
|
from utils.lib import MessageArgs
|
||||||
|
|
||||||
|
from .. import logger
|
||||||
|
from ...koans import KoanRegistry
|
||||||
|
from ...lib import minify
|
||||||
|
|
||||||
|
class KoanListUI(MessageUI):
|
||||||
|
block_len = 10
|
||||||
|
def __init__(self, bot: LionBot, koans: KoanRegistry, communityid: int, profileid: int, callerid: int, **kwargs):
|
||||||
|
super().__init__(callerid=callerid, **kwargs)
|
||||||
|
|
||||||
|
self.bot = bot
|
||||||
|
self.koans = koans
|
||||||
|
self.communityid = communityid
|
||||||
|
self.caller_profileid = profileid
|
||||||
|
|
||||||
|
# Paging state
|
||||||
|
self._pagen = 0
|
||||||
|
self.blocks = [[]]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def page_count(self):
|
||||||
|
return len(self.blocks)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pagen(self):
|
||||||
|
self._pagen = self._pagen % self.page_count
|
||||||
|
return self._pagen
|
||||||
|
|
||||||
|
@pagen.setter
|
||||||
|
def pagen(self, value):
|
||||||
|
self._pagen = value % self.page_count
|
||||||
|
|
||||||
|
@property
|
||||||
|
def current_page(self):
|
||||||
|
return self.blocks[self.pagen]
|
||||||
|
|
||||||
|
# ----- API -----
|
||||||
|
|
||||||
|
# ----- UI Components -----
|
||||||
|
@button(
|
||||||
|
label="New Koan",
|
||||||
|
style=ButtonStyle.green
|
||||||
|
)
|
||||||
|
async def new_koan_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
# Show the create koan modal
|
||||||
|
# Create koan flow.
|
||||||
|
try:
|
||||||
|
interaction, to_create = await input(
|
||||||
|
interaction=press,
|
||||||
|
title="Create Koan",
|
||||||
|
question="Koan Content",
|
||||||
|
timeout=300,
|
||||||
|
style=TextStyle.long,
|
||||||
|
min_length=1,
|
||||||
|
max_length=2000 - 8
|
||||||
|
)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise SafeCancellation
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
|
||||||
|
koan = await self.koans.create_koan(
|
||||||
|
communityid=self.communityid,
|
||||||
|
content=to_create,
|
||||||
|
created_by=self.caller_profileid
|
||||||
|
)
|
||||||
|
self.pagen = -1
|
||||||
|
await self.refresh(thinking=interaction)
|
||||||
|
|
||||||
|
# Koan edit and delete selectors
|
||||||
|
async def make_page_options(self):
|
||||||
|
options = []
|
||||||
|
for koan in self.current_page:
|
||||||
|
labelstr = f"#{koan.koanlabel}: "
|
||||||
|
minified = minify(koan.content, 100 - len(labelstr) - 1, strip=' >')
|
||||||
|
option = SelectOption(
|
||||||
|
label=f"{labelstr} {minified}",
|
||||||
|
value=str(koan.koanid)
|
||||||
|
)
|
||||||
|
options.append(option)
|
||||||
|
return options
|
||||||
|
|
||||||
|
@select(
|
||||||
|
cls=Select,
|
||||||
|
placeholder="Select to delete",
|
||||||
|
min_values=1, max_values=1
|
||||||
|
)
|
||||||
|
async def delete_menu(self, selection: discord.Interaction, selected: Select):
|
||||||
|
await selection.response.defer(thinking=True, ephemeral=True)
|
||||||
|
if selected.values:
|
||||||
|
koanid = int(selected.values[0])
|
||||||
|
await self.koans.delete_koan(koanid)
|
||||||
|
await self.refresh(thinking=selection)
|
||||||
|
|
||||||
|
async def refresh_delete_menu(self):
|
||||||
|
self.delete_menu.options = await self.make_page_options()
|
||||||
|
|
||||||
|
@select(
|
||||||
|
cls=Select,
|
||||||
|
placeholder="Select to edit",
|
||||||
|
min_values=1, max_values=1
|
||||||
|
)
|
||||||
|
async def edit_menu(self, selection: discord.Interaction, selected: Select):
|
||||||
|
if selected.values:
|
||||||
|
koanid = int(selected.values[0])
|
||||||
|
koan = await self.koans.get_koan(koanid)
|
||||||
|
assert koan is not None
|
||||||
|
try:
|
||||||
|
interaction, new_content = await input(
|
||||||
|
interaction=selection,
|
||||||
|
title="Edit Koan",
|
||||||
|
question="Koan Content",
|
||||||
|
timeout=300,
|
||||||
|
style=TextStyle.long,
|
||||||
|
default=koan.content,
|
||||||
|
min_length=1,
|
||||||
|
max_length=2000 - 8
|
||||||
|
)
|
||||||
|
await interaction.response.defer(thinking=True, ephemeral=True)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
raise SafeCancellation
|
||||||
|
|
||||||
|
await koan.update(content=new_content)
|
||||||
|
await self.refresh(thinking=interaction)
|
||||||
|
|
||||||
|
async def refresh_edit_menu(self):
|
||||||
|
self.edit_menu.options = await self.make_page_options()
|
||||||
|
|
||||||
|
# Backwards
|
||||||
|
@button(emoji=conf.emojis.backward, style=ButtonStyle.grey)
|
||||||
|
async def prev_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True, ephemeral=True)
|
||||||
|
self.pagen -= 1
|
||||||
|
await self.refresh(thinking=press)
|
||||||
|
|
||||||
|
# Jump to page
|
||||||
|
@button(label="JUMP_PLACEHOLDER", style=ButtonStyle.blurple)
|
||||||
|
async def jump_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
"""
|
||||||
|
Jump-to-page button.
|
||||||
|
Loads a page-switch dialogue.
|
||||||
|
"""
|
||||||
|
t = self.bot.translator.t
|
||||||
|
try:
|
||||||
|
interaction, value = await input(
|
||||||
|
press,
|
||||||
|
title="Jump to page",
|
||||||
|
question="Page number to jump to"
|
||||||
|
)
|
||||||
|
value = value.strip()
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not value.lstrip('- ').isdigit():
|
||||||
|
error_embed = discord.Embed(
|
||||||
|
title="Invalid page number, please try again!",
|
||||||
|
colour=discord.Colour.brand_red()
|
||||||
|
)
|
||||||
|
await interaction.response.send_message(embed=error_embed, ephemeral=True)
|
||||||
|
else:
|
||||||
|
await interaction.response.defer(thinking=True)
|
||||||
|
pagen = int(value.lstrip('- '))
|
||||||
|
if value.startswith('-'):
|
||||||
|
pagen = -1 * pagen
|
||||||
|
elif pagen > 0:
|
||||||
|
pagen = pagen - 1
|
||||||
|
self.pagen = pagen
|
||||||
|
await self.refresh(thinking=interaction)
|
||||||
|
|
||||||
|
async def jump_button_refresh(self):
|
||||||
|
component = self.jump_button
|
||||||
|
component.label = f"{self.pagen + 1}/{self.page_count}"
|
||||||
|
component.disabled = (self.page_count <= 1)
|
||||||
|
|
||||||
|
# Forward
|
||||||
|
@button(emoji=conf.emojis.forward, style=ButtonStyle.grey)
|
||||||
|
async def next_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
await press.response.defer(thinking=True)
|
||||||
|
self.pagen += 1
|
||||||
|
await self.refresh(thinking=press)
|
||||||
|
|
||||||
|
# Quit
|
||||||
|
@button(emoji=conf.emojis.cancel, style=ButtonStyle.red)
|
||||||
|
async def quit_button(self, press: discord.Interaction, pressed: Button):
|
||||||
|
"""
|
||||||
|
Quit the UI.
|
||||||
|
"""
|
||||||
|
await press.response.defer()
|
||||||
|
await self.quit()
|
||||||
|
|
||||||
|
# ----- UI Flow -----
|
||||||
|
async def make_message(self) -> MessageArgs:
|
||||||
|
if self.current_page:
|
||||||
|
lines = []
|
||||||
|
for koan in self.current_page:
|
||||||
|
labelstr = f"**#{koan.koanlabel}:** "
|
||||||
|
minified = minify(koan.content, 100 - len(labelstr) - 1, strip=' >')
|
||||||
|
lines.append(f"{labelstr} {minified}")
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Community Koans",
|
||||||
|
description='\n'.join(lines)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
embed = discord.Embed(
|
||||||
|
title="Community Koans",
|
||||||
|
description="No koans have been created! Use `/koans add` or the 'New Koan' button below to make a koan!"
|
||||||
|
)
|
||||||
|
return MessageArgs(embed=embed)
|
||||||
|
|
||||||
|
async def refresh_layout(self):
|
||||||
|
# Refresh menus
|
||||||
|
to_refresh = (
|
||||||
|
self.refresh_edit_menu(),
|
||||||
|
self.refresh_delete_menu(),
|
||||||
|
self.jump_button_refresh(),
|
||||||
|
)
|
||||||
|
await asyncio.gather(*to_refresh)
|
||||||
|
|
||||||
|
if self.page_count > 1:
|
||||||
|
# Multiple page case
|
||||||
|
# < Add Jump Close >
|
||||||
|
# edit
|
||||||
|
# delete
|
||||||
|
self.set_layout(
|
||||||
|
(self.prev_button,
|
||||||
|
self.new_koan_button,
|
||||||
|
self.jump_button,
|
||||||
|
self.quit_button,
|
||||||
|
self.next_button),
|
||||||
|
(self.edit_menu,),
|
||||||
|
(self.delete_menu,),
|
||||||
|
)
|
||||||
|
elif self.current_page:
|
||||||
|
# Single page case
|
||||||
|
# Add Close
|
||||||
|
# Edit
|
||||||
|
# Delete
|
||||||
|
self.set_layout(
|
||||||
|
(self.new_koan_button,
|
||||||
|
self.quit_button),
|
||||||
|
(self.edit_menu,),
|
||||||
|
(self.delete_menu,),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Add Close
|
||||||
|
self.set_layout(
|
||||||
|
(self.new_koan_button,
|
||||||
|
self.quit_button),
|
||||||
|
)
|
||||||
|
|
||||||
|
async def reload(self):
|
||||||
|
koans = await self.koans.get_community_koans(self.communityid)
|
||||||
|
blocks = [
|
||||||
|
koans[i:i+self.block_len]
|
||||||
|
for i in range(0, len(koans), self.block_len)
|
||||||
|
]
|
||||||
|
self.blocks = blocks or [[]]
|
||||||
|
|
||||||
@@ -1,34 +1,42 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from .data import Koan, KoanData
|
from .data import Koan, KoanInfo, KoansData
|
||||||
|
from .lib import utc_now
|
||||||
|
|
||||||
|
|
||||||
class KoanRegistry:
|
class KoanRegistry:
|
||||||
def __init__(self, data: KoanData):
|
def __init__(self, data: KoansData):
|
||||||
self.data = data
|
self.data = data
|
||||||
|
|
||||||
async def init(self):
|
async def init(self):
|
||||||
await self.data.init()
|
await self.data.init()
|
||||||
|
|
||||||
async def get_community_koans(self, communityid: int) -> list[Koan]:
|
async def get_community_koans(self, communityid: int) -> list[KoanInfo]:
|
||||||
return await Koan.fetch_where(communityid=communityid)
|
return await KoanInfo.fetch_where(communityid=communityid, is_deleted=False)
|
||||||
|
|
||||||
|
async def get_koaninfo(self, koanid: int) -> Optional[KoanInfo]:
|
||||||
|
return await KoanInfo.fetch(koanid)
|
||||||
|
|
||||||
async def get_koan(self, koanid: int) -> Optional[Koan]:
|
async def get_koan(self, koanid: int) -> Optional[Koan]:
|
||||||
return await Koan.fetch(koanid)
|
return await Koan.fetch(koanid)
|
||||||
|
|
||||||
|
async def get_koan_label(self, communityid: int, label: int) -> Optional[KoanInfo]:
|
||||||
|
results = await KoanInfo.fetch_where(communityid=communityid, koanlabel=label, is_deleted=False)
|
||||||
|
return results[0] if results else None
|
||||||
|
|
||||||
async def get_koan_named(self, communityid: int, name: str) -> Optional[Koan]:
|
async def create_koan(
|
||||||
name = name.lower()
|
self,
|
||||||
koans = await Koan.fetch_where(communityid=communityid, name=name)
|
communityid: int,
|
||||||
if koans:
|
content: str,
|
||||||
return koans[0]
|
created_by: Optional[int] = None
|
||||||
else:
|
) -> KoanInfo:
|
||||||
return None
|
|
||||||
|
|
||||||
async def create_koan(self, communityid: int, name: str, message: str) -> Koan:
|
|
||||||
name = name.lower()
|
|
||||||
await Koan.table.delete_where(communityid=communityid, name=name)
|
|
||||||
koan = await Koan.create(
|
koan = await Koan.create(
|
||||||
|
content=content,
|
||||||
communityid=communityid,
|
communityid=communityid,
|
||||||
name=name,
|
created_by=created_by,
|
||||||
message=message
|
|
||||||
)
|
)
|
||||||
return koan
|
info = await KoanInfo.fetch(koan.koanid)
|
||||||
|
assert info is not None
|
||||||
|
return info
|
||||||
|
|
||||||
|
async def delete_koan(self, koanid: int):
|
||||||
|
await self.data.koans.update_where(koanid=koanid).set(deleted_at=utc_now())
|
||||||
|
|||||||
19
koans/lib.py
Normal file
19
koans/lib.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from typing import Optional
|
||||||
|
import datetime as dt
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
|
||||||
|
def minify(content: str, maxlength: int, strip: Optional[str] = ' ', newlines: str = ' '):
|
||||||
|
content.replace('\n', newlines)
|
||||||
|
if strip:
|
||||||
|
content = content.strip(strip)
|
||||||
|
|
||||||
|
if len(content) > maxlength:
|
||||||
|
new_content = content[maxlength-3] + '...'
|
||||||
|
else:
|
||||||
|
new_content = content
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def utc_now():
|
||||||
|
return datetime.now(dt.UTC)
|
||||||
@@ -7,13 +7,14 @@ from meta import Bot, Context
|
|||||||
|
|
||||||
from . import logger
|
from . import logger
|
||||||
from ..koans import KoanRegistry
|
from ..koans import KoanRegistry
|
||||||
from ..data import KoanData
|
from ..data import KoansData
|
||||||
|
from ..lib import minify
|
||||||
|
|
||||||
|
|
||||||
class KoanComponent(cmds.Component):
|
class KoanComponent(cmds.Component):
|
||||||
def __init__(self, bot: Bot):
|
def __init__(self, bot: Bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.data = bot.dbconn.load_registry(KoanData())
|
self.data = bot.dbconn.load_registry(KoansData())
|
||||||
self.koans = KoanRegistry(self.data)
|
self.koans = KoanRegistry(self.data)
|
||||||
|
|
||||||
async def component_load(self):
|
async def component_load(self):
|
||||||
@@ -25,6 +26,17 @@ class KoanComponent(cmds.Component):
|
|||||||
async def event_message(self, payload: twitchio.ChatMessage) -> None:
|
async def event_message(self, payload: twitchio.ChatMessage) -> None:
|
||||||
print(f"[{payload.broadcaster.name}] - {payload.chatter.name}: {payload.text}")
|
print(f"[{payload.broadcaster.name}] - {payload.chatter.name}: {payload.text}")
|
||||||
|
|
||||||
|
async def component_command_error(self, payload):
|
||||||
|
try:
|
||||||
|
raise payload.exception
|
||||||
|
except cmds.ArgumentError:
|
||||||
|
if cmd := payload.context.command:
|
||||||
|
usage = f"{cmd.qualified_name} {cmd.signature}"
|
||||||
|
await payload.context.reply(f"USAGE: {usage}")
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
@cmds.group(invoke_fallback=True)
|
@cmds.group(invoke_fallback=True)
|
||||||
async def koans(self, ctx: cmds.Context) -> None:
|
async def koans(self, ctx: cmds.Context) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -38,74 +50,100 @@ class KoanComponent(cmds.Component):
|
|||||||
koans = await self.koans.get_community_koans(cid)
|
koans = await self.koans.get_community_koans(cid)
|
||||||
|
|
||||||
if koans:
|
if koans:
|
||||||
names = ', '.join(koan.name for koan in koans)
|
count = len(koans)
|
||||||
await ctx.reply(
|
parts = []
|
||||||
f"Koans: {names}"
|
for koan in koans[-10:]:
|
||||||
)
|
minified = minify(koan.content, 20)
|
||||||
|
formatted = f"#{koan.koanlabel}: {minified}"
|
||||||
|
parts.append(formatted)
|
||||||
|
partstr = '; '.join(parts)
|
||||||
|
laststr = "Last 10: " if count > 10 else ""
|
||||||
|
message = f"We have {count} koans! {laststr}{partstr}"
|
||||||
|
await ctx.reply(message)
|
||||||
else:
|
else:
|
||||||
await ctx.reply("No koans have been made in this channel!")
|
await ctx.reply("No koans have been made in this channel!")
|
||||||
|
|
||||||
@koans.command(name='add', aliases=['new', 'create'])
|
@koans.command(name='add', aliases=['new', 'create'])
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def koans_add(self, ctx: cmds.Context, name: str, *, text: str):
|
async def koans_add(self, ctx: cmds.Context, *, content: str):
|
||||||
"""
|
"""
|
||||||
Add or overwrite a koan to this channel.
|
Add or overwrite a koan to this channel.
|
||||||
|
|
||||||
!koans add wind This is a wind koan
|
!koans add This is a wind koan
|
||||||
"""
|
"""
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
|
profile = await self.bot.profiles.fetch_profile(ctx.chatter)
|
||||||
|
pid = profile.profileid
|
||||||
|
|
||||||
name = name.lower()
|
koan = await self.koans.create_koan(
|
||||||
existing = await self.koans.get_koan_named(cid, name)
|
cid,
|
||||||
await self.koans.create_koan(cid, name, text)
|
content,
|
||||||
|
created_by=pid,
|
||||||
|
)
|
||||||
|
|
||||||
if existing:
|
await ctx.reply(f"Koan #{koan.koanlabel} created!")
|
||||||
await ctx.reply(f"Updated the koan '{name}'")
|
|
||||||
|
# TODO: Error message on signature failure
|
||||||
|
|
||||||
|
@koans.command(name='edit', aliases=['update',])
|
||||||
|
@cmds.is_moderator()
|
||||||
|
async def koans_edit(self, ctx: cmds.Context, label: int, *, new_content: str):
|
||||||
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
|
cid = community.communityid
|
||||||
|
|
||||||
|
koan = await self.koans.get_koan_label(cid, label)
|
||||||
|
|
||||||
|
if koan:
|
||||||
|
await self.data.koans.update_where(koanid=koan.koanid).set(content=new_content)
|
||||||
|
await ctx.reply(f"Updated koan #{label}")
|
||||||
else:
|
else:
|
||||||
await ctx.reply(f"Created the new koan '{name}'")
|
await ctx.reply(f"Koan #{label}' does not exist to delete!")
|
||||||
|
|
||||||
|
|
||||||
@koans.command(name='del', aliases=['delete', 'rm', 'remove'])
|
@koans.command(name='del', aliases=['delete', 'rm', 'remove'])
|
||||||
@cmds.is_moderator()
|
@cmds.is_moderator()
|
||||||
async def koans_del(self, ctx: cmds.Context, name: str):
|
async def koans_del(self, ctx: cmds.Context, label: int):
|
||||||
"""
|
"""
|
||||||
Remove a koan from this channel by name.
|
Remove a koan from this channel by number.
|
||||||
|
|
||||||
!koans del wind
|
!koans del 5
|
||||||
"""
|
"""
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
|
|
||||||
name = name.lower()
|
koan = await self.koans.get_koan_label(cid, label)
|
||||||
koan = await self.koans.get_koan_named(cid, name)
|
|
||||||
if koan:
|
if koan:
|
||||||
await koan.delete()
|
await koan.delete()
|
||||||
await ctx.reply(f"Deleted the koan '{name}'")
|
await ctx.reply(f"Deleted koan #{label}")
|
||||||
else:
|
else:
|
||||||
await ctx.reply(f"The koan '{name}' does not exist to delete!")
|
await ctx.reply(f"Koan #{label}' does not exist to delete!")
|
||||||
|
|
||||||
@cmds.command(name='koan')
|
@cmds.command(name='koan')
|
||||||
async def koan(self, ctx: cmds.Context, name: Optional[str] = None):
|
async def koan(self, ctx: cmds.Context, label: Optional[int] = None):
|
||||||
"""
|
"""
|
||||||
Show a koan from this channel. Optionally by name.
|
Show a koan from this channel. Optionally by number.
|
||||||
|
|
||||||
!koan
|
!koan
|
||||||
!koan wind
|
!koan 5
|
||||||
"""
|
"""
|
||||||
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
|
||||||
cid = community.communityid
|
cid = community.communityid
|
||||||
|
|
||||||
if name is not None:
|
if label is not None:
|
||||||
name = name.lower()
|
koan = await self.koans.get_koan_label(cid, label)
|
||||||
koan = await self.koans.get_koan_named(cid, name)
|
|
||||||
if koan:
|
if koan:
|
||||||
await ctx.reply(koan.message)
|
await ctx.reply(koan.message)
|
||||||
else:
|
else:
|
||||||
await ctx.reply(f"The requested koan '{name}' does not exist! Use '{ctx.prefix}koans' to see all the koans.")
|
await ctx.reply(f"Koan #{label} does not exist!")
|
||||||
else:
|
else:
|
||||||
koans = await self.koan.get_community_koans(communityid=cid)
|
koans = await self.koan.get_community_koans(communityid=cid)
|
||||||
if koans:
|
if koans:
|
||||||
koan = random.choice(koans)
|
koan = random.choice(koans)
|
||||||
await ctx.reply(koan.message)
|
formatted = f"Koan #{koan.koanlabel}: {koan.message}"
|
||||||
|
parts = [formatted[i: i:500] for i in range(0, len(formatted), 500)]
|
||||||
|
for part in parts:
|
||||||
|
await ctx.reply(part)
|
||||||
else:
|
else:
|
||||||
await ctx.reply("This channel doesn't have any koans!")
|
await ctx.reply("This channel doesn't have any koans!")
|
||||||
|
|||||||
Reference in New Issue
Block a user