v2 refactor to label-based koans, and add discord

This commit is contained in:
2025-09-04 02:29:48 +10:00
parent fda6847671
commit 89173f1676
10 changed files with 679 additions and 76 deletions

View File

@@ -1,20 +1,30 @@
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(
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,
koanid 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,
deleted_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TRIGGER koans_timestamp BEFORE UPDATE ON koans
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;

View File

@@ -3,3 +3,4 @@ import logging
logger = logging.getLogger(__name__)
from .twitch import setup as twitch_setup
from .discord import setup

View File

@@ -1,32 +1,31 @@
from data import Registry, RowModel
from data.columns import String, Timestamp, Integer
from data import Registry, RowModel, Table
from data.columns import Bool, Integer, Timestamp, String
from weakref import WeakValueDictionary
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'
_cache_ = {}
_cache_ = WeakValueDictionary()
koanid = Integer(primary=True)
communityid = Integer()
name = String()
message = String()
content = String()
deleted_at = Timestamp()
created_by = Integer()
created_at = Timestamp()
_timestamp = Timestamp()
class KoanData(Registry):
VERSION = ('KOANS', 1)
class KoanInfo(Koan):
_tablename_ = 'koans_info'
_readonly_ = True
_cache_ = WeakValueDictionary()
koanlabel = Integer()
is_deleted = Bool()
class KoansData(Registry):
VERSION = ('KOANS', 2)
koans = Koan.table
koans_info = KoanInfo.table

View 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
View 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)

View File

@@ -0,0 +1 @@
from .koanlist import KoanListUI

View 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 [[]]

View File

@@ -1,34 +1,42 @@
from typing import Optional
from .data import Koan, KoanData
from .data import Koan, KoanInfo, KoansData
from .lib import utc_now
class KoanRegistry:
def __init__(self, data: KoanData):
self.data = data
def __init__(self, data: KoansData):
self.data = data
async def init(self):
await self.data.init()
async def get_community_koans(self, communityid: int) -> list[Koan]:
return await Koan.fetch_where(communityid=communityid)
async def get_community_koans(self, communityid: int) -> list[KoanInfo]:
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]:
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]:
name = name.lower()
koans = await Koan.fetch_where(communityid=communityid, name=name)
if koans:
return koans[0]
else:
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)
async def create_koan(
self,
communityid: int,
content: str,
created_by: Optional[int] = None
) -> KoanInfo:
koan = await Koan.create(
content=content,
communityid=communityid,
name=name,
message=message
created_by=created_by,
)
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
View 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)

View File

@@ -7,13 +7,14 @@ from meta import Bot, Context
from . import logger
from ..koans import KoanRegistry
from ..data import KoanData
from ..data import KoansData
from ..lib import minify
class KoanComponent(cmds.Component):
def __init__(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)
async def component_load(self):
@@ -25,6 +26,17 @@ class KoanComponent(cmds.Component):
async def event_message(self, payload: twitchio.ChatMessage) -> None:
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)
async def koans(self, ctx: cmds.Context) -> None:
"""
@@ -38,74 +50,100 @@ class KoanComponent(cmds.Component):
koans = await self.koans.get_community_koans(cid)
if koans:
names = ', '.join(koan.name for koan in koans)
await ctx.reply(
f"Koans: {names}"
)
count = len(koans)
parts = []
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:
await ctx.reply("No koans have been made in this channel!")
@koans.command(name='add', aliases=['new', 'create'])
@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.
!koans add wind This is a wind koan
!koans add This is a wind koan
"""
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
cid = community.communityid
profile = await self.bot.profiles.fetch_profile(ctx.chatter)
pid = profile.profileid
name = name.lower()
existing = await self.koans.get_koan_named(cid, name)
await self.koans.create_koan(cid, name, text)
koan = await self.koans.create_koan(
cid,
content,
created_by=pid,
)
if existing:
await ctx.reply(f"Updated the koan '{name}'")
await ctx.reply(f"Koan #{koan.koanlabel} created!")
# 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:
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'])
@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)
cid = community.communityid
name = name.lower()
koan = await self.koans.get_koan_named(cid, name)
koan = await self.koans.get_koan_label(cid, label)
if koan:
await koan.delete()
await ctx.reply(f"Deleted the koan '{name}'")
await ctx.reply(f"Deleted koan #{label}")
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')
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 wind
!koan 5
"""
community = await self.bot.profiles.fetch_community(ctx.broadcaster)
cid = community.communityid
if name is not None:
name = name.lower()
koan = await self.koans.get_koan_named(cid, name)
if label is not None:
koan = await self.koans.get_koan_label(cid, label)
if koan:
await ctx.reply(koan.message)
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:
koans = await self.koan.get_community_koans(communityid=cid)
if 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:
await ctx.reply("This channel doesn't have any koans!")