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.logger import log_wrap
|
||||||
from meta.sharding import THIS_SHARD
|
from meta.sharding import THIS_SHARD
|
||||||
from core.data import CoreData
|
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 utils.ui import input
|
||||||
|
|
||||||
from wards import low_management_ward, high_management_ward, equippable_role, moderator_ward
|
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 .settingui import ModerationSettingUI
|
||||||
from .ticket import Ticket
|
from .ticket import Ticket
|
||||||
from .tickets import NoteTicket, WarnTicket
|
from .tickets import NoteTicket, WarnTicket
|
||||||
|
from .ticketui import TicketListUI, TicketFilter
|
||||||
|
|
||||||
_p, _np = babel._p, babel._np
|
_p, _np = babel._p, babel._np
|
||||||
|
|
||||||
@@ -472,6 +473,105 @@ class ModerationCog(LionCog):
|
|||||||
)
|
)
|
||||||
await interaction.edit_original_response(embed=embed)
|
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 -----
|
# ----- Configuration -----
|
||||||
@LionCog.placeholder_group
|
@LionCog.placeholder_group
|
||||||
@cmds.hybrid_group('configure', with_app_command=False)
|
@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