Compare commits
8 Commits
384820370a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| d307726a58 | |||
| 25c64bb338 | |||
| d4e3c49ef2 | |||
| d17da09572 | |||
| 48a5d83e8b | |||
| 0395a9b4d3 | |||
| 47c50c29b0 | |||
| ff78f7d5bf |
@@ -19,3 +19,6 @@
|
||||
[submodule "src/modules/pluscampaign"]
|
||||
path = src/modules/pluscampaign
|
||||
url = https://git.thewisewolf.dev/CarmiCoven/pluscampaign-plugin.git
|
||||
[submodule "src/modules/tracker"]
|
||||
path = src/modules/tracker
|
||||
url = https://git.thewisewolf.dev/HoloTech/twitch-eventtracker-plugin.git
|
||||
|
||||
@@ -6,7 +6,9 @@ active = [
|
||||
".voicefix",
|
||||
".messagelogger",
|
||||
".voicelog",
|
||||
".yarn"
|
||||
".yarn",
|
||||
".tracker",
|
||||
".pluscampaign",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+1
-1
Submodule src/modules/pluscampaign updated: 0923f32a82...97bdf8e8e3
Submodule
+1
Submodule src/modules/tracker added at 2f4778d0b8
+194
-2
@@ -1,4 +1,5 @@
|
||||
from typing import Literal
|
||||
from pathlib import Path
|
||||
from typing import Literal, Optional
|
||||
from collections import defaultdict
|
||||
import datetime as dt
|
||||
from datetime import datetime, timedelta, UTC
|
||||
@@ -6,7 +7,7 @@ from datetime import datetime, timedelta, UTC
|
||||
from data.queries import ORDER
|
||||
import discord
|
||||
from discord.ext import commands as cmds
|
||||
from discord import app_commands as appcmds
|
||||
from discord import Attachment, app_commands as appcmds
|
||||
|
||||
from meta import LionBot, LionCog, LionContext
|
||||
from meta.logger import log_wrap
|
||||
@@ -132,3 +133,194 @@ class YarnCog(LionCog):
|
||||
pages = paginate_list(lb_strings, block_length=page_len, title=title)
|
||||
|
||||
await pager(ctx, pages)
|
||||
|
||||
@cmds.hybrid_command(name="topvoice")
|
||||
async def topvoice_cmd(self, ctx):
|
||||
"""
|
||||
Show top voice members by total time.
|
||||
"""
|
||||
target_channelid = 1383707078740279366
|
||||
since_stamp = 1769832959
|
||||
|
||||
voicelogger = ctx.bot.get_cog("VoiceLogCog")
|
||||
session_data = voicelogger.data.voicelog_sessions
|
||||
|
||||
query = (
|
||||
session_data.select_where(
|
||||
VoiceLogSession.joined_at
|
||||
>= datetime.fromtimestamp(since_stamp, tz=UTC),
|
||||
guildid=ctx.guild.id,
|
||||
channelid=target_channelid,
|
||||
)
|
||||
.select(
|
||||
userid="userid",
|
||||
total_time="SUM(COALESCE(duration, EXTRACT(EPOCH FROM (NOW() - joined_at))))",
|
||||
)
|
||||
.order_by("total_time", ORDER.DESC)
|
||||
.group_by("userid")
|
||||
.with_no_adapter()
|
||||
)
|
||||
leaderboard = [(row["userid"], int(row["total_time"])) for row in await query]
|
||||
|
||||
# Format for display and pager
|
||||
# First collect names
|
||||
names = {}
|
||||
for uid, _ in leaderboard:
|
||||
user = ctx.guild.get_member(uid)
|
||||
if user is None:
|
||||
try:
|
||||
user = await ctx.guild.fetch_member(uid)
|
||||
except discord.NotFound:
|
||||
user = None
|
||||
names[uid] = user.display_name if user else str(uid)
|
||||
|
||||
lb_strings = []
|
||||
max_name_len = min((30, max(len(name) for name in names.values())))
|
||||
for i, (uid, total) in enumerate(leaderboard):
|
||||
lb_strings.append(
|
||||
"{:<{}}\t{:<9}".format(
|
||||
names[uid], max_name_len, strfdur(total, short=False)
|
||||
)
|
||||
)
|
||||
|
||||
page_len = 20
|
||||
title = "Voice Leaderboard"
|
||||
pages = paginate_list(lb_strings, block_length=page_len, title=title)
|
||||
|
||||
await pager(ctx, pages)
|
||||
|
||||
@cmds.hybrid_command(name="topmoomin")
|
||||
async def topmoomin_cmd(self, ctx):
|
||||
target_channelid = 1383707078740279366
|
||||
since_stamp = 1777960800
|
||||
|
||||
# Build timestamps of all moomin times since the start
|
||||
# Reverse time order
|
||||
moomin_periods = []
|
||||
last_moomin_start = 1785823200
|
||||
moomin_dur = 60 * 30
|
||||
|
||||
mstart = last_moomin_start
|
||||
while mstart >= since_stamp:
|
||||
period = (mstart, mstart + moomin_dur)
|
||||
moomin_periods.append(period)
|
||||
mstart -= 24 * 60 * 60
|
||||
|
||||
# Get all voice sessions
|
||||
|
||||
voicelogger = ctx.bot.get_cog("VoiceLogCog")
|
||||
session_data = voicelogger.data.voicelog_sessions
|
||||
|
||||
query = (
|
||||
session_data.select_where(
|
||||
VoiceLogSession.joined_at
|
||||
>= datetime.fromtimestamp(since_stamp, tz=UTC),
|
||||
guildid=ctx.guild.id,
|
||||
channelid=target_channelid,
|
||||
)
|
||||
.select("userid", "joined_at", "duration")
|
||||
.order_by("joined_at", ORDER.DESC)
|
||||
.with_no_adapter()
|
||||
)
|
||||
rows = await query
|
||||
|
||||
# For each session, intersect it with all the moomin times, sum the intersection seconds. Key by userid
|
||||
moomin_leaderboard = defaultdict(int)
|
||||
for row in rows:
|
||||
start_stamp = row["joined_at"].timestamp()
|
||||
if row["duration"]:
|
||||
end_stamp = start_stamp + row["duration"]
|
||||
else:
|
||||
end_stamp = utc_now().timestamp()
|
||||
|
||||
# Intersection
|
||||
for mstart, mend in moomin_periods:
|
||||
if mend < start_stamp:
|
||||
# Moomin periods go backwards
|
||||
# Stop looking if the period ended before this voice session started
|
||||
break
|
||||
# Guarantee: mend >= start_stamp
|
||||
if mstart <= end_stamp:
|
||||
# Period starts before the end of the voice session
|
||||
# and ends after the start of the session
|
||||
intersection_start = max(start_stamp, mstart)
|
||||
intersection_end = min(end_stamp, mend)
|
||||
diff = intersection_end - intersection_start
|
||||
moomin_leaderboard[row["userid"]] += int(diff)
|
||||
else:
|
||||
# Period starts after the end of the voice session, ignore
|
||||
pass
|
||||
|
||||
# Format for display and pager
|
||||
leaderboard = sorted(
|
||||
moomin_leaderboard.items(), key=lambda p: p[1], reverse=True
|
||||
)
|
||||
|
||||
# First collect names
|
||||
names = {}
|
||||
users = {}
|
||||
for uid, _ in leaderboard:
|
||||
user = ctx.guild.get_member(uid)
|
||||
if user is None:
|
||||
try:
|
||||
user = await ctx.guild.fetch_member(uid)
|
||||
except discord.NotFound:
|
||||
user = None
|
||||
names[uid] = user.display_name if user else str(uid)
|
||||
users[uid] = user
|
||||
|
||||
lb_strings = []
|
||||
max_name_len = min((30, max(len(name) for name in names.values())))
|
||||
for i, (uid, total) in enumerate(leaderboard):
|
||||
lb_strings.append(
|
||||
"{:<{}}\t{:<9}".format(
|
||||
names[uid], max_name_len, strfdur(total, short=False)
|
||||
)
|
||||
)
|
||||
|
||||
page_len = 20
|
||||
title = "Moomin Leaderboard"
|
||||
pages = paginate_list(lb_strings, block_length=page_len, title=title)
|
||||
await pager(ctx, pages)
|
||||
|
||||
if ctx.author.guild_permissions.administrator:
|
||||
threshold = 20 * 60
|
||||
|
||||
role = ctx.guild.get_role(1534089417612857424)
|
||||
if not role:
|
||||
await ctx.reply("Couldn't find the role!")
|
||||
return
|
||||
earned_users = []
|
||||
for uid, dur in leaderboard:
|
||||
if dur >= threshold:
|
||||
if user := users.get(uid):
|
||||
earned_users.append(user)
|
||||
if role not in user.roles:
|
||||
await user.add_roles(role, reason=f"Watched moomin for {dur}")
|
||||
await ctx.reply(
|
||||
f"Added moomin role to: {', '.join(u.mention for u in earned_users)} ",
|
||||
allowed_mentions=None,
|
||||
)
|
||||
|
||||
async def _upload_file(self, file: discord.Attachment, filename: str):
|
||||
filedir = Path("/extension/www/lilac")
|
||||
target = filedir / filename
|
||||
|
||||
await file.save(target)
|
||||
return f"https://lilac.thewisewolf.dev/provides/{filename}"
|
||||
|
||||
@cmds.hybrid_group(name="upload")
|
||||
@appcmds.default_permissions(administrator=True)
|
||||
async def upload_group(self, ctx):
|
||||
pass
|
||||
|
||||
@upload_group.command(name="file")
|
||||
@appcmds.default_permissions(administrator=True)
|
||||
async def upload_file_group(
|
||||
self, ctx, file: discord.Attachment, filename: Optional[str] = None
|
||||
):
|
||||
await ctx.interaction.response.defer(thinking=True, ephemeral=True)
|
||||
url = await self._upload_file(file, filename or file.filename)
|
||||
await ctx.reply(
|
||||
embed=discord.Embed(description=f"File now available at: {url}")
|
||||
)
|
||||
|
||||
+11
-11
@@ -56,7 +56,7 @@ class LeoUI(View):
|
||||
Currently exposes a hidden attribute of the underlying View.
|
||||
May be reimplemented in future.
|
||||
"""
|
||||
return self._View__stopped
|
||||
return self._BaseView__stopped
|
||||
|
||||
def to_components(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -138,7 +138,7 @@ class LeoUI(View):
|
||||
to include a pre_timeout task
|
||||
which may optionally refresh and hence cancel the timeout.
|
||||
"""
|
||||
if self._View__stopped.done():
|
||||
if self._BaseView__stopped.done():
|
||||
# We are already stopped, nothing to do
|
||||
return
|
||||
|
||||
@@ -160,17 +160,17 @@ class LeoUI(View):
|
||||
# The timeout was removed entirely, silently walk away
|
||||
return
|
||||
|
||||
if self._View__stopped.done():
|
||||
if self._BaseView__stopped.done():
|
||||
# We stopped while waiting for the pre timeout.
|
||||
# Or maybe another thread timed us out
|
||||
# Either way, we are done here
|
||||
return
|
||||
|
||||
now = time.monotonic()
|
||||
if self._View__timeout_expiry is not None and now < self._View__timeout_expiry:
|
||||
if self._BaseView__timeout_expiry is not None and now < self._BaseView__timeout_expiry:
|
||||
# The timeout was extended, make sure the timeout task is running then fade away
|
||||
if self._View__timeout_task is None or self._View__timeout_task.done():
|
||||
self._View__timeout_task = asyncio.create_task(self._View__timeout_task_impl())
|
||||
if self._BaseView__timeout_task is None or self._BaseView__timeout_task.done():
|
||||
self._BaseView__timeout_task = asyncio.create_task(self._BaseView__timeout_task_impl())
|
||||
else:
|
||||
# Actually timeout, and call the post-timeout task for cleanup.
|
||||
self._really_timeout()
|
||||
@@ -189,14 +189,14 @@ class LeoUI(View):
|
||||
This copies View._dispatch_timeout, apart from the `on_timeout` dispatch,
|
||||
which is now handled by `__dispatch_timeout`.
|
||||
"""
|
||||
if self._View__stopped.done():
|
||||
if self._BaseView__stopped.done():
|
||||
return
|
||||
|
||||
if self._View__cancel_callback:
|
||||
self._View__cancel_callback(self)
|
||||
self._View__cancel_callback = None
|
||||
if self._BaseView__cancel_callback:
|
||||
self._BaseView__cancel_callback(self)
|
||||
self._BaseView__cancel_callback = None
|
||||
|
||||
self._View__stopped.set_result(True)
|
||||
self._BaseView__stopped.set_result(True)
|
||||
|
||||
def _dispatch_item(self, *args, **kwargs):
|
||||
"""Extending event dispatch to run in the instantiation context."""
|
||||
|
||||
@@ -1,456 +0,0 @@
|
||||
from typing import List, Coroutine, Optional, Any, Type, TypeVar, Callable, Dict
|
||||
from collections import defaultdict
|
||||
|
||||
import discord
|
||||
from discord.ui.button import Button, button
|
||||
from discord import app_commands as appcmds
|
||||
|
||||
from meta.logger import log_action_stack, logging_context
|
||||
from meta.errors import SafeCancellation
|
||||
from meta.config import conf
|
||||
|
||||
from babel.translator import ctx_translator
|
||||
|
||||
from ..lib import MessageArgs, error_embed
|
||||
from .. import util_babel
|
||||
|
||||
from .leo import LeoUI
|
||||
|
||||
_p = util_babel._p
|
||||
|
||||
|
||||
__all__ = (
|
||||
'BasePager',
|
||||
'Pager',
|
||||
)
|
||||
|
||||
|
||||
class BasePager(LeoUI):
|
||||
"""
|
||||
An ABC describing the common interface for a Paging UI.
|
||||
|
||||
A paging UI represents a sequence of pages, accessible by `next` and `previous` buttons,
|
||||
and possibly by a dropdown (not implemented).
|
||||
|
||||
A `Page` is represented as a `MessageArgs` object, which is passable to `send` and `edit` methods as required.
|
||||
|
||||
Each page of a paging UI is accessed through the coroutine `get_page`.
|
||||
This allows for more complex paging schemes where the pages are expensive to compute,
|
||||
and not generally needed simultaneously.
|
||||
In general, `get_page` should cache expensive pages,
|
||||
perhaps simply with a `cached` decorator, but this is not enforced.
|
||||
|
||||
The state of the base UI is represented as the current `page_num` and the `current_page`.
|
||||
|
||||
This class also maintains an `active_pagers` cache,
|
||||
representing all `BasePager`s that are currently running.
|
||||
This allows access from external page controlling utilities, e.g. the `/page` command.
|
||||
"""
|
||||
# List of valid keys indicating movement to the next page
|
||||
next_list = _p('cmd:page|pager:Pager|options:next', "n, nxt, next, forward, +")
|
||||
|
||||
# List of valid keys indicating movement to the previous page
|
||||
prev_list = _p('cmd:page|pager:Pager|options:prev', "p, prev, back, -")
|
||||
|
||||
# List of valid keys indicating movement to the first page
|
||||
first_list = _p('cmd:page|pager:Pager|options:first', "f, first, one, start")
|
||||
|
||||
# List of valid keys indicating movement to the last page
|
||||
last_list = _p('cmd:page|pager:Pager|options:last', "l, last, end")
|
||||
# channelid -> pager.id -> list of active pagers in this channel
|
||||
active_pagers: dict[int, dict[int, 'BasePager']] = defaultdict(dict)
|
||||
|
||||
page_num: int
|
||||
current_page: MessageArgs
|
||||
_channelid: Optional[int]
|
||||
|
||||
@classmethod
|
||||
def get_active_pager(self, channelid, userid):
|
||||
"""
|
||||
Get the last active pager in the `destinationid`, which may be accessed by `userid`.
|
||||
Returns None if there are no matching pagers.
|
||||
"""
|
||||
for pager in reversed(self.active_pagers[channelid].values()):
|
||||
if pager.access_check(userid):
|
||||
return pager
|
||||
|
||||
def set_active(self):
|
||||
if self._channelid is None:
|
||||
raise ValueError("Cannot set active without a channelid.")
|
||||
self.active_pagers[self._channelid][self.id] = self
|
||||
|
||||
def set_inactive(self):
|
||||
self.active_pagers[self._channelid].pop(self.id, None)
|
||||
|
||||
def access_check(self, userid):
|
||||
"""
|
||||
Check whether the given userid is allowed to use this UI.
|
||||
Must be overridden by subclasses.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def get_page(self, page_id) -> MessageArgs:
|
||||
"""
|
||||
`get_page` returns the specified page number, starting from 0.
|
||||
An implementation of `get_page` must:
|
||||
- Always return a page (if no data is a valid state, must return a placeholder page).
|
||||
- Always accept out-of-range `page_id` values.
|
||||
- There is no behaviour specified for these, although they will usually be modded into the correct
|
||||
range.
|
||||
- In some cases (e.g. stream data where we don't have a last page),
|
||||
they may simply return the last correct page instead.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
async def page_cmd(self, interaction: discord.Interaction, value: str):
|
||||
"""
|
||||
Command implementation for the paging command.
|
||||
Pager subclasses should override this if they use `active_pagers`.
|
||||
Default implementation is essentially a no-op,
|
||||
simply replying to the interaction.
|
||||
"""
|
||||
await interaction.response.defer()
|
||||
return
|
||||
|
||||
async def page_acmpl(self, interaction: discord.Interaction, partial: str):
|
||||
"""
|
||||
Command autocompletion for the paging command.
|
||||
Pager subclasses should override this if they use `active_pagers`.
|
||||
"""
|
||||
return []
|
||||
|
||||
@button(emoji=conf.emojis.getemoji('forward'))
|
||||
async def next_page_button(self, interaction: discord.Interaction, press):
|
||||
await interaction.response.defer()
|
||||
self.page_num += 1
|
||||
await self.redraw()
|
||||
|
||||
@button(emoji=conf.emojis.getemoji('backward'))
|
||||
async def prev_page_button(self, interaction: discord.Interaction, press):
|
||||
await interaction.response.defer()
|
||||
self.page_num -= 1
|
||||
await self.redraw()
|
||||
|
||||
async def refresh(self):
|
||||
"""
|
||||
Recalculate current computed state.
|
||||
(E.g. fetch current page, set layout, disable components, etc.)
|
||||
"""
|
||||
self.current_page = await self.get_page(self.page_num)
|
||||
|
||||
async def redraw(self):
|
||||
"""
|
||||
This should refresh the current state and redraw the UI.
|
||||
Not implemented here, as the implementation depends on whether this is a reaction response ephemeral UI
|
||||
or a message=based one.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Pager(BasePager):
|
||||
"""
|
||||
MicroUI to display a sequence of static pages,
|
||||
supporting paging reaction and paging commands.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
pages: list[MessageArgs]
|
||||
A non-empty list of message arguments to page.
|
||||
start_from: int
|
||||
The page number to display first.
|
||||
Default: 0
|
||||
locked: bool
|
||||
Whether to only allow the author to use the paging interface.
|
||||
"""
|
||||
|
||||
def __init__(self, pages: list[MessageArgs],
|
||||
start_from=0,
|
||||
show_cancel=False, delete_on_cancel=True, delete_after=False, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self._pages = pages
|
||||
self.page_num = start_from
|
||||
self.current_page = pages[self.page_num]
|
||||
|
||||
self._locked = True
|
||||
self._ownerid: Optional[int] = None
|
||||
self._channelid: Optional[int] = None
|
||||
|
||||
if not pages:
|
||||
raise ValueError("Cannot run Pager with no pages.")
|
||||
|
||||
self._original: Optional[discord.Interaction] = None
|
||||
self._is_followup: bool = False
|
||||
self._message: Optional[discord.Message] = None
|
||||
|
||||
self.show_cancel = show_cancel
|
||||
self._delete_on_cancel = delete_on_cancel
|
||||
self._delete_after = delete_after
|
||||
|
||||
@property
|
||||
def ownerid(self):
|
||||
if self._ownerid is not None:
|
||||
return self._ownerid
|
||||
elif self._original:
|
||||
return self._original.user.id
|
||||
else:
|
||||
return None
|
||||
|
||||
def access_check(self, userid):
|
||||
return not self._locked or (userid == self.ownerid)
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction):
|
||||
return self.access_check(interaction.user.id)
|
||||
|
||||
@button(emoji=conf.emojis.getemoji('cancel'))
|
||||
async def cancel_button(self, interaction: discord.Interaction, press: Button):
|
||||
await interaction.response.defer()
|
||||
if self._delete_on_cancel:
|
||||
self._delete_after = True
|
||||
await self.close()
|
||||
|
||||
async def cleanup(self):
|
||||
self.set_inactive()
|
||||
|
||||
# If we still have a message, delete it or clear the view
|
||||
try:
|
||||
if self._is_followup:
|
||||
if self._message:
|
||||
if self._delete_after:
|
||||
await self._message.delete()
|
||||
else:
|
||||
await self._message.edit(view=None)
|
||||
else:
|
||||
if self._original and not self._original.is_expired():
|
||||
if self._delete_after:
|
||||
await self._original.delete_original_response()
|
||||
else:
|
||||
await self._original.edit_original_response(view=None)
|
||||
except discord.HTTPException:
|
||||
# Nothing we can do here
|
||||
pass
|
||||
|
||||
async def get_page(self, page_id):
|
||||
page_id %= len(self._pages)
|
||||
return self._pages[page_id]
|
||||
|
||||
def page_count(self):
|
||||
return len(self.pages)
|
||||
|
||||
async def page_cmd(self, interaction: discord.Interaction, value: str):
|
||||
"""
|
||||
`/page` command for the `Pager` MicroUI.
|
||||
"""
|
||||
await interaction.response.defer(ephemeral=True)
|
||||
t = ctx_translator.get().t
|
||||
nexts = {word.strip() for word in t(self.next_list).split(',')}
|
||||
prevs = {word.strip() for word in t(self.prev_list).split(',')}
|
||||
firsts = {word.strip() for word in t(self.first_list).split(',')}
|
||||
lasts = {word.strip() for word in t(self.last_list).split(',')}
|
||||
|
||||
if value:
|
||||
value = value.lower().strip()
|
||||
if value.isdigit():
|
||||
# Assume value is page number
|
||||
self.page_num = int(value) - 1
|
||||
if self.page_num == -1:
|
||||
self.page_num = 0
|
||||
elif value in firsts:
|
||||
self.page_num = 0
|
||||
elif value in nexts:
|
||||
self.page_num += 1
|
||||
elif value in prevs:
|
||||
self.page_num -= 1
|
||||
elif value in lasts:
|
||||
self.page_num = -1
|
||||
elif value.startswith('-') and value[1:].isdigit():
|
||||
self.page_num = - int(value[1:])
|
||||
else:
|
||||
await interaction.edit_original_response(
|
||||
embed=error_embed(
|
||||
t(_p(
|
||||
'cmd:page|pager:Pager|error:parse',
|
||||
"Could not understand page specification `{value}`."
|
||||
)).format(value=value)
|
||||
)
|
||||
)
|
||||
return
|
||||
await interaction.delete_original_response()
|
||||
await self.redraw()
|
||||
|
||||
async def page_acmpl(self, interaction: discord.Interaction, partial: str):
|
||||
"""
|
||||
`/page` command autocompletion for the `Pager` MicroUI.
|
||||
"""
|
||||
t = ctx_translator.get().t
|
||||
nexts = {word.strip() for word in t(self.next_list).split(',')}
|
||||
prevs = {word.strip() for word in t(self.prev_list).split(',')}
|
||||
firsts = {word.strip() for word in t(self.first_list).split(',')}
|
||||
lasts = {word.strip() for word in t(self.last_list).split(',')}
|
||||
|
||||
total = len(self._pages)
|
||||
num = self.page_num
|
||||
page_choices: dict[int, str] = {}
|
||||
|
||||
# TODO: Support page names and hints?
|
||||
|
||||
if len(self._pages) > 10:
|
||||
# First add the general choices
|
||||
if num < total-1:
|
||||
page_choices[total-1] = t(_p(
|
||||
'cmd:page|acmpl|pager:Pager|choice:last',
|
||||
"Last: Page {page}/{total}"
|
||||
)).format(page=total, total=total)
|
||||
|
||||
page_choices[num] = t(_p(
|
||||
'cmd:page|acmpl|pager:Pager|choice:current',
|
||||
"Current: Page {page}/{total}"
|
||||
)).format(page=num+1, total=total)
|
||||
choices = [
|
||||
appcmds.Choice(name=string[:100], value=str(num+1))
|
||||
for num, string in sorted(page_choices.items(), key=lambda t: t[0])
|
||||
]
|
||||
else:
|
||||
# Particularly support page names here
|
||||
choices = [
|
||||
appcmds.Choice(
|
||||
name=('> ' * (i == num) + t(_p(
|
||||
'cmd:page|acmpl|pager:Pager|choice:general',
|
||||
"Page {page}"
|
||||
)).format(page=i+1))[:100],
|
||||
value=str(i+1)
|
||||
)
|
||||
for i in range(0, total)
|
||||
]
|
||||
|
||||
partial = partial.strip()
|
||||
|
||||
if partial:
|
||||
value = partial.lower().strip()
|
||||
if value.isdigit():
|
||||
# Assume value is page number
|
||||
page_num = int(value) - 1
|
||||
if page_num == -1:
|
||||
page_num = 0
|
||||
elif value in firsts:
|
||||
page_num = 0
|
||||
elif value in nexts:
|
||||
page_num = self.page_num + 1
|
||||
elif value in prevs:
|
||||
page_num = self.page_num - 1
|
||||
elif value in lasts:
|
||||
page_num = -1
|
||||
elif value.startswith('-') and value[1:].isdigit():
|
||||
page_num = - int(value[1:])
|
||||
else:
|
||||
page_num = None
|
||||
|
||||
if page_num is not None:
|
||||
page_num %= total
|
||||
choice = appcmds.Choice(
|
||||
name=t(_p(
|
||||
'cmd:page|acmpl|pager:Page|choice:select',
|
||||
"Selected: Page {page}/{total}"
|
||||
)).format(page=page_num+1, total=total)[:100],
|
||||
value=str(page_num + 1)
|
||||
)
|
||||
return [choice, *choices]
|
||||
else:
|
||||
return [
|
||||
appcmds.Choice(
|
||||
name=t(_p(
|
||||
'cmd:page|acmpl|pager:Page|error:parse',
|
||||
"No matching pages!"
|
||||
)).format(page=page_num, total=total)[:100],
|
||||
value=partial
|
||||
)
|
||||
]
|
||||
else:
|
||||
return choices
|
||||
|
||||
@property
|
||||
def page_row(self):
|
||||
if self.show_cancel:
|
||||
if len(self._pages) > 1:
|
||||
return (self.prev_page_button, self.cancel_button, self.next_page_button)
|
||||
else:
|
||||
return (self.cancel_button,)
|
||||
else:
|
||||
if len(self._pages) > 1:
|
||||
return (self.prev_page_button, self.next_page_button)
|
||||
else:
|
||||
return ()
|
||||
|
||||
async def refresh(self):
|
||||
await super().refresh()
|
||||
self.set_layout(self.page_row)
|
||||
|
||||
async def redraw(self):
|
||||
await self.refresh()
|
||||
|
||||
if not self._original:
|
||||
raise ValueError("Running run pager manually without interaction.")
|
||||
|
||||
try:
|
||||
if self._message:
|
||||
await self._message.edit(**self.current_page.edit_args, view=self)
|
||||
else:
|
||||
if self._original.is_expired():
|
||||
raise SafeCancellation("This interface has expired, please try again.")
|
||||
await self._original.edit_original_response(**self.current_page.edit_args, view=self)
|
||||
except discord.HTTPException:
|
||||
raise SafeCancellation("Could not page your results! Please try again.")
|
||||
|
||||
async def run(self, interaction: discord.Interaction, ephemeral=False, locked=True, ownerid=None, **kwargs):
|
||||
"""
|
||||
Display the UI.
|
||||
Attempts to reply to the interaction if it has not already been replied to,
|
||||
otherwise send a follow-up.
|
||||
|
||||
An ephemeral response must be sent as an initial interaction response.
|
||||
On the other hand, a semi-persistent response (expected to last longer than the lifetime of the interaction)
|
||||
must be sent as a followup.
|
||||
|
||||
Extra kwargs are combined with the first page arguments and given to the relevant send method.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction: discord.Interaction
|
||||
The interaction to send the pager in response to.
|
||||
ephemeral: bool
|
||||
Whether to send the interaction ephemerally.
|
||||
If this is true, the interaction *must* be fresh (i.e. no response done).
|
||||
Default: False
|
||||
locked: bool
|
||||
Whether this interface is locked to the user `self.ownerid`.
|
||||
Irrelevant for ephemeral messages.
|
||||
Use `ownerid` to override the default owner id.
|
||||
Defaults to true for fail-safety.
|
||||
Default: True
|
||||
ownerid: Optional[int]
|
||||
The userid allowed to use this interaction.
|
||||
By default, this will be the `interaction.user.id`,
|
||||
presuming that this is the user which originally triggered this message.
|
||||
An override may be useful if a user triggers a paging UI for someone else.
|
||||
"""
|
||||
if not interaction.channel_id:
|
||||
raise ValueError("Cannot run pager on a channelless interaction.")
|
||||
|
||||
self._original = interaction
|
||||
self._ownerid = ownerid
|
||||
self._locked = locked
|
||||
self._channelid = interaction.channel_id
|
||||
|
||||
await self.refresh()
|
||||
args = self.current_page.send_args | kwargs
|
||||
|
||||
if interaction.response.is_done():
|
||||
if ephemeral:
|
||||
raise ValueError("Ephemeral response requires fres interaction.")
|
||||
self._message = await interaction.followup.send(**args, view=self)
|
||||
self._is_followup = True
|
||||
else:
|
||||
self._is_followup = False
|
||||
await interaction.response.send_message(**args, view=self)
|
||||
|
||||
self.set_active()
|
||||
Reference in New Issue
Block a user