feat(moderation): Implement ticket interface.
This commit is contained in:
@@ -12,7 +12,7 @@ from meta.errors import SafeCancellation, UserInputError
|
||||
from meta.logger import log_wrap
|
||||
from meta.sharding import THIS_SHARD
|
||||
from core.data import CoreData
|
||||
from utils.lib import utc_now, parse_ranges
|
||||
from utils.lib import utc_now, parse_ranges, parse_time_static
|
||||
from utils.ui import input
|
||||
|
||||
from wards import low_management_ward, high_management_ward, equippable_role, moderator_ward
|
||||
@@ -23,6 +23,7 @@ from .settings import ModerationSettings
|
||||
from .settingui import ModerationSettingUI
|
||||
from .ticket import Ticket
|
||||
from .tickets import NoteTicket, WarnTicket
|
||||
from .ticketui import TicketListUI, TicketFilter
|
||||
|
||||
_p, _np = babel._p, babel._np
|
||||
|
||||
@@ -472,6 +473,105 @@ class ModerationCog(LionCog):
|
||||
)
|
||||
await interaction.edit_original_response(embed=embed)
|
||||
|
||||
# View tickets
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:tickets', "tickets"),
|
||||
description=_p(
|
||||
'cmd:tickets|desc',
|
||||
"View moderation tickets in this server."
|
||||
)
|
||||
)
|
||||
@appcmds.rename(
|
||||
target_user=_p('cmd:tickets|param:target', "target"),
|
||||
ticket_type=_p('cmd:tickets|param:type', "type"),
|
||||
ticket_state=_p('cmd:tickets|param:state', "ticket_state"),
|
||||
include_pardoned=_p('cmd:tickets|param:pardoned', "include_pardoned"),
|
||||
acting_moderator=_p('cmd:tickets|param:moderator', "acting_moderator"),
|
||||
after=_p('cmd:tickets|param:after', "after"),
|
||||
before=_p('cmd:tickets|param:before', "before"),
|
||||
)
|
||||
@appcmds.describe(
|
||||
target_user=_p(
|
||||
'cmd:tickets|param:target|desc',
|
||||
"Filter by tickets acting on a given user."
|
||||
),
|
||||
ticket_type=_p(
|
||||
'cmd:tickets|param:type|desc',
|
||||
"Filter by ticket type."
|
||||
),
|
||||
ticket_state=_p(
|
||||
'cmd:tickets|param:state|desc',
|
||||
"Filter by ticket state."
|
||||
),
|
||||
include_pardoned=_p(
|
||||
'cmd:tickets|param:pardoned|desc',
|
||||
"Whether to only show active tickets, or also include pardoned."
|
||||
),
|
||||
acting_moderator=_p(
|
||||
'cmd:tickets|param:moderator|desc',
|
||||
"Filter by moderator responsible for the ticket."
|
||||
),
|
||||
after=_p(
|
||||
'cmd:tickets|param:after|desc',
|
||||
"Only show tickets after this date (YYY-MM-DD HH:MM)"
|
||||
),
|
||||
before=_p(
|
||||
'cmd:tickets|param:before|desc',
|
||||
"Only show tickets before this date (YYY-MM-DD HH:MM)"
|
||||
),
|
||||
)
|
||||
@appcmds.choices(
|
||||
ticket_type=[
|
||||
appcmds.Choice(name=typ.name, value=typ.name)
|
||||
for typ in (TicketType.NOTE, TicketType.WARNING, TicketType.STUDY_BAN)
|
||||
],
|
||||
ticket_state=[
|
||||
appcmds.Choice(name=state.name, value=state.name)
|
||||
for state in (
|
||||
TicketState.OPEN, TicketState.EXPIRING, TicketState.EXPIRED, TicketState.PARDONED,
|
||||
)
|
||||
]
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@appcmds.guild_only
|
||||
@moderator_ward
|
||||
async def tickets_cmd(self, ctx: LionContext,
|
||||
target_user: Optional[discord.User] = None,
|
||||
ticket_type: Optional[appcmds.Choice[str]] = None,
|
||||
ticket_state: Optional[appcmds.Choice[str]] = None,
|
||||
include_pardoned: Optional[bool] = None,
|
||||
acting_moderator: Optional[discord.User] = None,
|
||||
after: Optional[str] = None,
|
||||
before: Optional[str] = None,
|
||||
):
|
||||
if not ctx.guild:
|
||||
return
|
||||
if not ctx.interaction:
|
||||
return
|
||||
|
||||
filters = TicketFilter(self.bot)
|
||||
if target_user is not None:
|
||||
filters.targetids = [target_user.id]
|
||||
if ticket_type is not None:
|
||||
filters.types = [TicketType[ticket_type.value]]
|
||||
if ticket_state is not None:
|
||||
filters.states = [TicketState[ticket_state.value]]
|
||||
elif include_pardoned:
|
||||
filters.states = None
|
||||
else:
|
||||
filters.states = [TicketState.OPEN, TicketState.EXPIRING]
|
||||
if acting_moderator is not None:
|
||||
filters.moderatorids = [acting_moderator.id]
|
||||
if after is not None:
|
||||
filters.after = await parse_time_static(after, ctx.lguild.timezone)
|
||||
if before is not None:
|
||||
filters.before = await parse_time_static(before, ctx.lguild.timezone)
|
||||
|
||||
|
||||
ticketsui = TicketListUI(self.bot, ctx.guild, ctx.author.id, filters=filters)
|
||||
await ticketsui.run(ctx.interaction)
|
||||
await ticketsui.wait()
|
||||
|
||||
# ----- Configuration -----
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group('configure', with_app_command=False)
|
||||
|
||||
656
src/modules/moderation/ticketui.py
Normal file
656
src/modules/moderation/ticketui.py
Normal file
@@ -0,0 +1,656 @@
|
||||
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, SelectOption, UserSelect
|
||||
from discord.ui.button import button, Button, ButtonStyle
|
||||
from discord.ui.text_input import TextInput, TextStyle
|
||||
|
||||
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 babel, logger
|
||||
from .ticket import Ticket
|
||||
from .data import ModerationData, TicketType, TicketState
|
||||
|
||||
_p = babel._p
|
||||
|
||||
@dataclass
|
||||
class TicketFilter:
|
||||
bot: LionBot
|
||||
|
||||
after: Optional[dt.datetime] = None
|
||||
before: Optional[dt.datetime] = None
|
||||
targetids: Optional[list[int]] = None
|
||||
moderatorids: Optional[list[int]] = None
|
||||
types: Optional[list[TicketType]] = None
|
||||
states: Optional[list[TicketState]] = None
|
||||
|
||||
def conditions(self) -> list[Condition]:
|
||||
conditions = []
|
||||
Ticket = ModerationData.Ticket
|
||||
|
||||
if self.after is not None:
|
||||
conditions.append(Ticket.created_at >= self.after)
|
||||
if self.before is not None:
|
||||
conditions.append(Ticket.created_at < self.before)
|
||||
if self.targetids is not None:
|
||||
conditions.append(Ticket.targetid == self.targetids)
|
||||
if self.moderatorids is not None:
|
||||
conditions.append(Ticket.moderator_id == self.moderatorids)
|
||||
if self.types is not None:
|
||||
conditions.append(Ticket.ticket_type == self.types)
|
||||
if self.states is not None:
|
||||
conditions.append(Ticket.ticket_state == self.states)
|
||||
|
||||
return conditions
|
||||
|
||||
def formatted(self) -> str:
|
||||
t = self.bot.translator.t
|
||||
lines = []
|
||||
|
||||
if self.after is not None:
|
||||
name = t(_p(
|
||||
'ticketfilter|field:after|name',
|
||||
"Created After"
|
||||
))
|
||||
value = discord.utils.format_dt(self.after, 'd')
|
||||
lines.append((name, value))
|
||||
|
||||
if self.before is not None:
|
||||
name = t(_p(
|
||||
'ticketfilter|field:before|name',
|
||||
"Created Before"
|
||||
))
|
||||
value = discord.utils.format_dt(self.before, 'd')
|
||||
lines.append((name, value))
|
||||
|
||||
if self.targetids is not None:
|
||||
name = t(_p(
|
||||
'ticketfilter|field:targetids|name',
|
||||
"Targets"
|
||||
))
|
||||
value = ', '.join(f"<@{uid}>" for uid in self.targetids) or 'None'
|
||||
lines.append((name, value))
|
||||
|
||||
if self.moderatorids is not None:
|
||||
name = t(_p(
|
||||
'ticketfilter|field:moderatorids|name',
|
||||
"Moderators"
|
||||
))
|
||||
value = ', '.join(f"<@{uid}>" for uid in self.moderatorids) or 'None'
|
||||
lines.append((name, value))
|
||||
|
||||
if self.types is not None:
|
||||
name = t(_p(
|
||||
'ticketfilter|field:types|name',
|
||||
"Ticket Types"
|
||||
))
|
||||
value = ', '.join(typ.name for typ in self.types) or 'None'
|
||||
lines.append((name, value))
|
||||
|
||||
if self.states is not None:
|
||||
name = t(_p(
|
||||
'ticketfilter|field:states|name',
|
||||
"Ticket States"
|
||||
))
|
||||
value = ', '.join(state.name for state in self.states) or 'None'
|
||||
lines.append((name, value))
|
||||
|
||||
if lines:
|
||||
table = tabulate(*lines)
|
||||
filterstr = '\n'.join(table)
|
||||
else:
|
||||
filterstr = ''
|
||||
|
||||
return filterstr
|
||||
|
||||
|
||||
class TicketListUI(MessageUI):
|
||||
block_len = 10
|
||||
|
||||
def _init_children(self):
|
||||
# HACK to stop ViewWeights complaining that this UI has too many children
|
||||
# Children will be correctly initialised after parent init.
|
||||
return []
|
||||
|
||||
def __init__(self, bot: LionBot, guild: discord.Guild, callerid: int, filters=None, **kwargs):
|
||||
super().__init__(callerid=callerid, **kwargs)
|
||||
self._children = super()._init_children()
|
||||
|
||||
self.bot = bot
|
||||
self.data: ModerationData = bot.db.registries[ModerationData.__name__]
|
||||
self.guild = guild
|
||||
self.filters = filters or TicketFilter(bot)
|
||||
|
||||
# Paging state
|
||||
self._pagen = 0
|
||||
self.blocks = [[]]
|
||||
|
||||
# UI State
|
||||
self.show_filters = False
|
||||
self.show_tickets = False
|
||||
|
||||
self.child_ticket: Optional[TicketUI] = None
|
||||
|
||||
@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 -----
|
||||
# Edit Filters
|
||||
@button(
|
||||
label="EDIT_FILTER_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.blurple
|
||||
)
|
||||
async def edit_filter_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
self.show_filters = True
|
||||
self.show_tickets = False
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def edit_filter_button_refresh(self):
|
||||
button = self.edit_filter_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:tickets|button:edit_filter|label',
|
||||
"Edit Filters"
|
||||
))
|
||||
button.style = ButtonStyle.grey if not self.show_filters else ButtonStyle.blurple
|
||||
|
||||
# Select Ticket
|
||||
@button(
|
||||
label="SELECT_TICKET_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.blurple
|
||||
)
|
||||
async def select_ticket_button(self, press: discord.Interaction, pressed: Button):
|
||||
await press.response.defer(thinking=True, ephemeral=True)
|
||||
self.show_tickets = True
|
||||
self.show_filters = False
|
||||
await self.refresh(thinking=press)
|
||||
|
||||
async def select_ticket_button_refresh(self):
|
||||
button = self.select_ticket_button
|
||||
t = self.bot.translator.t
|
||||
button.label = t(_p(
|
||||
'ui:tickets|button:select_ticket|label',
|
||||
"Select Ticket"
|
||||
))
|
||||
button.style = ButtonStyle.grey if not self.show_tickets else ButtonStyle.blurple
|
||||
|
||||
# Pardon All
|
||||
@button(
|
||||
label="PARDON_BUTTON_PLACEHOLDER",
|
||||
style=ButtonStyle.red
|
||||
)
|
||||
async def pardon_button(self, press: discord.Interaction, pressed: Button):
|
||||
t = self.bot.translator.t
|
||||
|
||||
tickets = list(chain(*self.blocks))
|
||||
if not tickets:
|
||||
raise UserInputError(t(_p(
|
||||
'ui:tickets|button:pardon|error:no_tickets',
|
||||
"Not tickets matching the given criterial! Nothing to pardon."
|
||||
)))
|
||||
|
||||
# Request reason via modal
|
||||
modal_title = t(_p(
|
||||
'ui:tickets|button:pardon|modal:reason|title',
|
||||
"Pardon Tickets"
|
||||
))
|
||||
input_field = TextInput(
|
||||
label=t(_p(
|
||||
'ui:tickets|button:pardon|modal:reason|field|label',
|
||||
"Why are you pardoning these tickets?"
|
||||
)),
|
||||
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)
|
||||
|
||||
# Run pardon
|
||||
for ticket in tickets:
|
||||
await 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:tickets|button:pardon|label',
|
||||
"Pardon All"
|
||||
))
|
||||
button.disabled = not bool(self.current_page)
|
||||
|
||||
# Filter Ticket Type
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="FILTER_TYPE_MENU_PLACEHOLDER",
|
||||
min_values=1, max_values=3,
|
||||
)
|
||||
async def filter_type_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
self.filters.types = [TicketType[value] for value in selected.values] or None
|
||||
self.pagen = 0
|
||||
await self.refresh(thinking=selection)
|
||||
|
||||
async def filter_type_menu_refresh(self):
|
||||
menu = self.filter_type_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:tickets|menu:filter_type|placeholder',
|
||||
"Select Ticket Types"
|
||||
))
|
||||
|
||||
options = []
|
||||
descmap = {
|
||||
TicketType.NOTE: ('Notes',),
|
||||
TicketType.WARNING: ('Warnings',),
|
||||
TicketType.STUDY_BAN: ('Video Blacklists',),
|
||||
}
|
||||
filtered = self.filters.types
|
||||
for typ, (name,) in descmap.items():
|
||||
option = SelectOption(
|
||||
label=name,
|
||||
value=typ.name,
|
||||
default=(filtered is None or typ in filtered)
|
||||
)
|
||||
options.append(option)
|
||||
menu.options = options
|
||||
|
||||
# Filter Ticket State
|
||||
@select(
|
||||
cls=Select,
|
||||
placeholder="FILTER_STATE_MENU_PLACEHOLDER",
|
||||
min_values=1, max_values=4
|
||||
)
|
||||
async def filter_state_menu(self, selection: discord.Interaction, selected: Select):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
self.filters.states = [TicketState[value] for value in selected.values] or None
|
||||
self.pagen = 0
|
||||
await self.refresh(thinking=selection)
|
||||
|
||||
async def filter_state_menu_refresh(self):
|
||||
menu = self.filter_state_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:tickets|menu:filter_state|placeholder',
|
||||
"Select Ticket States"
|
||||
))
|
||||
|
||||
options = []
|
||||
descmap = {
|
||||
TicketState.OPEN: ('OPEN', ),
|
||||
TicketState.EXPIRING: ('EXPIRING', ),
|
||||
TicketState.EXPIRED: ('EXPIRED', ),
|
||||
TicketState.PARDONED: ('PARDONED', ),
|
||||
}
|
||||
filtered = self.filters.states
|
||||
for state, (name,) in descmap.items():
|
||||
option = SelectOption(
|
||||
label=name,
|
||||
value=state.name,
|
||||
default=(filtered is None or state in filtered)
|
||||
)
|
||||
options.append(option)
|
||||
menu.options = options
|
||||
|
||||
# Filter Ticket Target
|
||||
@select(
|
||||
cls=UserSelect,
|
||||
placeholder="FILTER_TARGET_MENU_PLACEHOLDER",
|
||||
min_values=0, max_values=10
|
||||
)
|
||||
async def filter_target_menu(self, selection: discord.Interaction, selected: UserSelect):
|
||||
await selection.response.defer(thinking=True, ephemeral=True)
|
||||
self.filters.targetids = [user.id for user in selected.values] or None
|
||||
self.pagen = 0
|
||||
await self.refresh(thinking=selection)
|
||||
|
||||
async def filter_target_menu_refresh(self):
|
||||
menu = self.filter_target_menu
|
||||
t = self.bot.translator.t
|
||||
menu.placeholder = t(_p(
|
||||
'ui:tickets|menu:filter_target|placeholder',
|
||||
"Select Ticket Targets"
|
||||
))
|
||||
|
||||
# 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()
|
||||
Reference in New Issue
Block a user