feat(moderation): Impl ticket pardon.
This commit is contained in:
@@ -8,11 +8,11 @@ from discord import app_commands as appcmds
|
||||
from discord.ui.text_input import TextInput, TextStyle
|
||||
|
||||
from meta import LionCog, LionBot, LionContext
|
||||
from meta.errors import SafeCancellation
|
||||
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
|
||||
from utils.lib import utc_now, parse_ranges
|
||||
from utils.ui import input
|
||||
|
||||
from wards import low_management_ward, high_management_ward, equippable_role, moderator_ward
|
||||
@@ -352,6 +352,126 @@ class ModerationCog(LionCog):
|
||||
)
|
||||
await interaction.edit_original_response(embed=embed)
|
||||
|
||||
# Pardon user command
|
||||
@cmds.hybrid_command(
|
||||
name=_p('cmd:pardon', "pardon"),
|
||||
description=_p(
|
||||
'cmd:pardon|desc',
|
||||
"Pardon moderation tickets to mark them as no longer in effect."
|
||||
)
|
||||
)
|
||||
@appcmds.rename(
|
||||
ticketids=_p(
|
||||
'cmd:pardon|param:ticketids',
|
||||
"tickets"
|
||||
),
|
||||
reason=_p(
|
||||
'cmd:pardon|param:reason',
|
||||
"reason"
|
||||
)
|
||||
)
|
||||
@appcmds.describe(
|
||||
ticketids=_p(
|
||||
'cmd:pardon|param:ticketids|desc',
|
||||
"Comma separated list of ticket numbers to pardon."
|
||||
),
|
||||
reason=_p(
|
||||
'cmd:pardon|param:reason',
|
||||
"Why these tickets are being pardoned."
|
||||
)
|
||||
)
|
||||
@appcmds.default_permissions(manage_guild=True)
|
||||
@appcmds.guild_only
|
||||
@moderator_ward
|
||||
async def cmd_pardon(self, ctx: LionContext,
|
||||
ticketids: str,
|
||||
reason: Optional[appcmds.Range[str, 0, 1024]] = None,
|
||||
):
|
||||
if not ctx.guild:
|
||||
return
|
||||
if not ctx.interaction:
|
||||
return
|
||||
t = self.bot.translator.t
|
||||
|
||||
# Prompt for pardon reason if not given
|
||||
# Note we can't parse first since we need to do first response with the modal
|
||||
if reason is None:
|
||||
modal_title = t(_p(
|
||||
'cmd:pardon|modal:reason|title',
|
||||
"Pardon Tickets"
|
||||
))
|
||||
input_field = TextInput(
|
||||
label=t(_p(
|
||||
'cmd: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(
|
||||
ctx.interaction, modal_title, field=input_field, timeout=300,
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
raise SafeCancellation
|
||||
else:
|
||||
interaction = ctx.interaction
|
||||
|
||||
await interaction.response.defer(thinking=True)
|
||||
|
||||
# Parse provided ticketids
|
||||
try:
|
||||
parsed_ids = parse_ranges(ticketids)
|
||||
errored = False
|
||||
except ValueError:
|
||||
errored = True
|
||||
parsed_ids = []
|
||||
|
||||
if errored or not parsed_ids:
|
||||
raise UserInputError(t(_p(
|
||||
'cmd:pardon|error:parse_ticketids',
|
||||
"Could not parse provided tickets as a list of ticket ids!"
|
||||
" Please enter tickets as a comma separated list of ticket numbers,"
|
||||
" for example `1, 2, 3`."
|
||||
)))
|
||||
|
||||
# Now find these tickets
|
||||
tickets = await Ticket.fetch_tickets(
|
||||
bot=self.bot,
|
||||
guildid=ctx.guild.id,
|
||||
guild_ticketid=parsed_ids,
|
||||
)
|
||||
if not tickets:
|
||||
raise UserInputError(t(_p(
|
||||
'cmd:pardon|error:no_matching',
|
||||
"No matching moderation tickets found to pardon!"
|
||||
)))
|
||||
|
||||
# Pardon each ticket
|
||||
for ticket in tickets:
|
||||
await ticket.pardon(
|
||||
modid=ctx.author.id,
|
||||
reason=reason
|
||||
)
|
||||
|
||||
# Now ack the pardon
|
||||
count = len(tickets)
|
||||
ticketstr = ', '.join(
|
||||
f"[#{ticket.data.guild_ticketid}]({ticket.jump_url})" for ticket in tickets
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.brand_green(),
|
||||
description=t(_np(
|
||||
'cmd:pardon|embed:success|title',
|
||||
"Ticket {ticketstr} has been pardoned.",
|
||||
"The following tickets have been pardoned:\n{ticketstr}",
|
||||
count
|
||||
)).format(ticketstr=ticketstr)
|
||||
)
|
||||
await interaction.edit_original_response(embed=embed)
|
||||
|
||||
# ----- Configuration -----
|
||||
@LionCog.placeholder_group
|
||||
@cmds.hybrid_group('configure', with_app_command=False)
|
||||
|
||||
@@ -105,6 +105,6 @@ class ModerationData(Registry):
|
||||
file_data = String()
|
||||
expiry = Timestamp()
|
||||
pardoned_by = Integer()
|
||||
pardoned_at = Integer()
|
||||
pardoned_at = Timestamp()
|
||||
pardoned_reason = String()
|
||||
created_at = Timestamp()
|
||||
|
||||
@@ -227,10 +227,10 @@ class Ticket:
|
||||
name=t(_p('ticket|field:pardoned|name', "Pardoned")),
|
||||
value=t(_p(
|
||||
'ticket|field:pardoned|value',
|
||||
"Pardoned by <&{moderator}> at {timestamp}.\n{reason}"
|
||||
"Pardoned by <@{moderator}> at {timestamp}.\n{reason}"
|
||||
)).format(
|
||||
moderator=data.pardoned_by,
|
||||
timestamp=discord.utils.format_dt(timestamp),
|
||||
timestamp=discord.utils.format_dt(data.pardoned_at) if data.pardoned_at else 'Unknown',
|
||||
reason=data.pardoned_reason or ''
|
||||
),
|
||||
inline=False
|
||||
@@ -297,9 +297,6 @@ class Ticket:
|
||||
self.expiring.cancel_tasks(self.data.ticketid)
|
||||
await self.post()
|
||||
|
||||
async def _revert(self):
|
||||
raise NotImplementedError
|
||||
|
||||
async def _expire(self):
|
||||
"""
|
||||
Actual expiry method.
|
||||
@@ -321,11 +318,16 @@ class Ticket:
|
||||
await self.post()
|
||||
# TODO: Post an extra note to the modlog about the expiry.
|
||||
|
||||
async def revert(self):
|
||||
async def revert(self, reason: Optional[str] = None, **kwargs):
|
||||
"""
|
||||
Revert this ticket.
|
||||
|
||||
By default this is a no-op.
|
||||
Ticket types should override to implement any required revert logic.
|
||||
|
||||
The optional `reason` paramter is intended for any auditable actions.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
return
|
||||
|
||||
async def expire(self):
|
||||
"""
|
||||
@@ -336,5 +338,31 @@ class Ticket:
|
||||
"""
|
||||
await self._expire()
|
||||
|
||||
async def pardon(self):
|
||||
raise NotImplementedError
|
||||
async def pardon(self, modid: int, reason: str):
|
||||
"""
|
||||
Pardon a ticket.
|
||||
|
||||
Specifically, set the state of the ticket to `PARDONED`,
|
||||
with the given moderator and reason,
|
||||
and revert the ticket if applicable.
|
||||
|
||||
If the ticket is already pardoned, this is a no-op.
|
||||
"""
|
||||
if self.data.ticket_state != TicketState.PARDONED:
|
||||
# Cancel expiry if it was scheduled
|
||||
self.expiring.cancel_tasks(self.data.ticketid)
|
||||
|
||||
# Revert the ticket if it is currently active
|
||||
if self.data.ticket_state in (TicketState.OPEN, TicketState.EXPIRING):
|
||||
await self.revert(reason=f"Pardoned by {modid}")
|
||||
|
||||
# Set pardoned state
|
||||
await self.data.update(
|
||||
ticket_state=TicketState.PARDONED,
|
||||
pardoned_at=utc_now(),
|
||||
pardoned_by=modid,
|
||||
pardoned_reason=reason
|
||||
)
|
||||
|
||||
# Update ticket log message
|
||||
await self.post()
|
||||
|
||||
Reference in New Issue
Block a user