feat(dreams): Add basic document viewer.

This commit is contained in:
2025-06-14 03:26:41 +10:00
parent 6ec500ec87
commit 445eccccd6
6 changed files with 598 additions and 1 deletions

View File

@@ -3,6 +3,7 @@ this_package = 'modules'
active = [
'.profiles',
'.sysadmin',
'.dreamspace',
]

View File

@@ -9,6 +9,7 @@ from meta.logger import log_wrap
from utils.lib import utc_now
from . import logger
from .ui.docviewer import DocumentViewer
class DreamCog(LionCog):
@@ -46,8 +47,24 @@ class DreamCog(LionCog):
# (Admin): View events/documents matching certain criteria
# (User): View own event cards with info?
# Let's make a demo viewer which lists their event cards and let's them open one via select?
# /documents -> Show a paged list of documents, select option displays the document in a viewer
@cmds.hybrid_command(
name='documents',
description="View your printer log!"
)
async def documents_cmd(self, ctx: LionContext):
profile = await self.bot.get_cog('ProfileCog').fetch_profile_discord(ctx.author)
events = await self.data.Events.fetch_where(user_id=profile.profileid)
docids = [event.document_id for event in events if event.document_id is not None]
if not docids:
await ctx.error_reply("You don't have any documents yet!")
return
# (User): View Specimen
view = DocumentViewer(self.bot, ctx.interaction.user.id, filter=(self.data.Document.document_id == docids))
await view.run(ctx.interaction)
# (User): View Specimen information

View File

@@ -0,0 +1,171 @@
import binascii
from itertools import chain
from typing import Optional
from dataclasses import dataclass
import asyncio
import datetime as dt
import discord
from discord.ui.select import select, Select, UserSelect
from discord.ui.button import button, Button
from discord.ui.text_input import TextInput
from discord.enums import ButtonStyle, TextStyle
from discord.components import SelectOption
from datamodels import DataModel
from meta import LionBot, conf
from meta.errors import ResponseTimedOut, SafeCancellation, UserInputError
from data import ORDER, Condition
from utils.ui import MessageUI, input
from utils.lib import MessageArgs, tabulate, utc_now
from .. import logger
class DocumentViewer(MessageUI):
"""
Simple pager which displays a filtered list of Documents.
"""
block_len = 5
def __init__(self, bot: LionBot, callerid: int, filter: Condition, **kwargs):
super().__init__(callerid=callerid, **kwargs)
self.bot = bot
self.data: DataModel = bot.core.datamodel
self.filter = filter
# Paging state
self._pagen = 0
self.blocks = [[]]
@property
def page_count(self):
return len(self.blocks)
@property
def pagen(self):
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]
# ----- UI Components -----
# Page 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)
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.
"""
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 = discord.Embed(title="Invalid page number, please try again!",
colour=discord.Colour.brand_red())
await interaction.response.send_message(embed=error, 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)
# Page forwards
@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()
# if self.child_viewer:
# await self.child_viewer.quit()
await self.quit()
# ----- UI Flow -----
async def make_message(self) -> MessageArgs:
files = []
embeds = []
for doc in self.current_page:
try:
imagedata = doc.to_bytes()
imagedata.seek(0)
except binascii.Error:
continue
fn = f"doc-{doc.document_id}.png"
file = discord.File(imagedata, fn)
embed = discord.Embed()
embed.set_image(url=f"attachment://{fn}")
files.append(file)
embeds.append(embed)
if not embeds:
embed = discord.Embed(description="You don't have any documents yet!")
embeds.append(embed)
print(f"FILES: {files}")
return MessageArgs(files=files, embeds=embeds)
async def refresh_layout(self):
to_refresh = (
self.jump_button_refresh(),
)
await asyncio.gather(*to_refresh)
if self.page_count > 1:
page_line = (
self.prev_button,
self.jump_button,
self.quit_button,
self.next_button,
)
else:
page_line = (self.quit_button,)
self.set_layout(page_line)
async def reload(self):
docs = await self.data.Document.fetch_where(self.filter).order_by('created_at', ORDER.DESC)
blocks = [
docs[i:i+self.block_len]
for i in range(0, len(docs), self.block_len)
]
self.blocks = blocks or [[]]

View File

@@ -0,0 +1,406 @@
from itertools import chain
from typing import Optional
from dataclasses import dataclass
import asyncio
import datetime as dt
import discord
from discord.ui.select import select, Select, UserSelect
from discord.ui.button import button, Button
from discord.ui.text_input import TextInput
from discord.enums import ButtonStyle, TextStyle
from discord.components import SelectOption
from datamodels import DataModel
from meta import LionBot, conf
from meta.errors import ResponseTimedOut, SafeCancellation, UserInputError
from data import ORDER, Condition
from utils.ui import MessageUI, input
from utils.lib import MessageArgs, tabulate, utc_now
from .. import logger
class EventsUI(MessageUI):
block_len = 10
def __init__(self, bot: LionBot, callerid: int, filter: Condition, **kwargs):
super().__init__(callerid=callerid, **kwargs)
self.bot = bot
self.data: DataModel = bot.core.datamodel
self.filter = Condition
# Paging state
self._pagen = 0
self.blocks = [[]]
@property
def page_count(self):
return len(self.blocks)
@property
def pagen(self):
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]
# ----- UI Components -----
# Page 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)
self.pagen -= 1
await self.refresh(thinking=press)
# Jump to page
# Page forwards
@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()
# if self.child_viewer:
# await self.child_viewer.quit()
await self.quit()
# ----- UI Flow -----
async def make_message(self) -> MessageArgs:
...
async def refresh_layout(self):
...
async def reload(self):
...
class TicketListUI(MessageUI):
# Select Ticket
@select(
cls=Select,
placeholder="TICKETS_MENU_PLACEHOLDER",
min_values=1, max_values=1
)
async def tickets_menu(self, selection: discord.Interaction, selected: Select):
await selection.response.defer(thinking=True, ephemeral=True)
if selected.values:
ticketid = int(selected.values[0])
ticket = await Ticket.fetch_ticket(self.bot, ticketid)
ticketui = TicketUI(self.bot, ticket, self._callerid)
if self.child_ticket:
await self.child_ticket.quit()
self.child_ticket = ticketui
await ticketui.run(selection)
async def tickets_menu_refresh(self):
menu = self.tickets_menu
t = self.bot.translator.t
menu.placeholder = t(_p(
'ui:tickets|menu:tickets|placeholder',
"Select Ticket"
))
options = []
for ticket in self.current_page:
option = SelectOption(
label=f"Ticket #{ticket.data.guild_ticketid}",
value=str(ticket.data.ticketid)
)
options.append(option)
menu.options = 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=t(_p(
'ui:tickets|button:jump|input:title',
"Jump to page"
)),
question=t(_p(
'ui:tickets|button:jump|input:question',
"Page number to jump to"
))
)
value = value.strip()
except asyncio.TimeoutError:
return
if not value.lstrip('- ').isdigit():
error_embed = discord.Embed(
title=t(_p(
'ui:tickets|button:jump|error:invalid_page',
"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()
if self.child_ticket:
await self.child_ticket.quit()
await self.quit()
# ----- UI Flow -----
def _format_ticket(self, ticket) -> str:
"""
Format a ticket into a single embed line.
"""
components = (
"[#{ticketid}]({link})",
"{created}",
"`{type}[{state}]`",
"<@{targetid}>",
"{content}",
)
formatstr = ' | '.join(components)
data = ticket.data
if not data.content:
content = 'No Content'
elif len(data.content) > 100:
content = data.content[:97] + '...'
else:
content = data.content
ticketstr = formatstr.format(
ticketid=data.guild_ticketid,
link=ticket.jump_url or 'https://lionbot.org',
created=discord.utils.format_dt(data.created_at, 'd'),
type=data.ticket_type.name,
state=data.ticket_state.name,
targetid=data.targetid,
content=content,
)
if data.ticket_state is TicketState.PARDONED:
ticketstr = f"~~{ticketstr}~~"
return ticketstr
async def make_message(self) -> MessageArgs:
t = self.bot.translator.t
embed = discord.Embed(
title=t(_p(
'ui:tickets|embed|title',
"Moderation Tickets in {guild}"
)).format(guild=self.guild.name),
timestamp=utc_now()
)
tickets = self.current_page
if tickets:
desc = '\n'.join(self._format_ticket(ticket) for ticket in tickets)
else:
desc = t(_p(
'ui:tickets|embed|desc:no_tickets',
"No tickets matching the given criteria!"
))
embed.description = desc
filterstr = self.filters.formatted()
if filterstr:
embed.add_field(
name=t(_p(
'ui:tickets|embed|field:filters|name',
"Filters"
)),
value=filterstr,
inline=False
)
return MessageArgs(embed=embed)
async def refresh_layout(self):
to_refresh = (
self.edit_filter_button_refresh(),
self.select_ticket_button_refresh(),
self.pardon_button_refresh(),
self.tickets_menu_refresh(),
self.filter_type_menu_refresh(),
self.filter_state_menu_refresh(),
self.filter_target_menu_refresh(),
self.jump_button_refresh(),
)
await asyncio.gather(*to_refresh)
action_line = (
self.edit_filter_button,
self.select_ticket_button,
self.pardon_button,
)
if self.page_count > 1:
page_line = (
self.prev_button,
self.jump_button,
self.quit_button,
self.next_button,
)
else:
page_line = ()
action_line = (*action_line, self.quit_button)
if self.show_filters:
menus = (
(self.filter_type_menu,),
(self.filter_state_menu,),
(self.filter_target_menu,),
)
elif self.show_tickets and self.current_page:
menus = ((self.tickets_menu,),)
else:
menus = ()
self.set_layout(
action_line,
*menus,
page_line,
)
async def reload(self):
tickets = await Ticket.fetch_tickets(
self.bot,
*self.filters.conditions(),
guildid=self.guild.id,
)
blocks = [
tickets[i:i+self.block_len]
for i in range(0, len(tickets), self.block_len)
]
self.blocks = blocks or [[]]
class TicketUI(MessageUI):
def __init__(self, bot: LionBot, ticket: Ticket, callerid: int, **kwargs):
super().__init__(callerid=callerid, **kwargs)
self.bot = bot
self.ticket = ticket
# ----- API -----
# ----- UI Components -----
# Pardon Ticket
@button(
label="PARDON_BUTTON_PLACEHOLDER",
style=ButtonStyle.red
)
async def pardon_button(self, press: discord.Interaction, pressed: Button):
t = self.bot.translator.t
modal_title = t(_p(
'ui:ticket|button:pardon|modal:reason|title',
"Pardon Moderation Ticket"
))
input_field = TextInput(
label=t(_p(
'ui:ticket|button:pardon|modal:reason|field|label',
"Why are you pardoning this ticket?"
)),
style=TextStyle.long,
min_length=0,
max_length=1024,
)
try:
interaction, reason = await input(
press, modal_title, field=input_field, timeout=300,
)
except asyncio.TimeoutError:
raise ResponseTimedOut
await interaction.response.defer(thinking=True, ephemeral=True)
await self.ticket.pardon(modid=press.user.id, reason=reason)
await self.refresh(thinking=interaction)
async def pardon_button_refresh(self):
button = self.pardon_button
t = self.bot.translator.t
button.label = t(_p(
'ui:ticket|button:pardon|label',
"Pardon"
))
button.disabled = (self.ticket.data.ticket_state is TicketState.PARDONED)
# 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:
return await self.ticket.make_message()
async def refresh_layout(self):
await self.pardon_button_refresh()
self.set_layout(
(self.pardon_button, self.quit_button,)
)
async def reload(self):
await self.ticket.data.refresh()

View File

View File

@@ -44,6 +44,7 @@ class TwitchLinkStatic(LeoUI):
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._embed: Optional[discord.Embed] = None
print("INITIALISATION")
async def interaction_check(self, interaction: discord.Interaction):
return True
@@ -76,6 +77,7 @@ class TwitchLinkStatic(LeoUI):
async def button_linker(self, interaction: discord.Interaction, btn: Button):
# Here we just reply to the interaction with the AuthFlow UI
# TODO
print("RESPONDING")
flowui = TwitchLinkFlow(interaction.client, interaction.user, callerid=interaction.user.id)
await flowui.run(interaction)
await flowui.wait()