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 discord.ui.text_input import TextInput, TextStyle
|
||||||
|
|
||||||
from meta import LionCog, LionBot, LionContext
|
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.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
|
from utils.lib import utc_now, parse_ranges
|
||||||
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
|
||||||
@@ -352,6 +352,126 @@ class ModerationCog(LionCog):
|
|||||||
)
|
)
|
||||||
await interaction.edit_original_response(embed=embed)
|
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 -----
|
# ----- Configuration -----
|
||||||
@LionCog.placeholder_group
|
@LionCog.placeholder_group
|
||||||
@cmds.hybrid_group('configure', with_app_command=False)
|
@cmds.hybrid_group('configure', with_app_command=False)
|
||||||
|
|||||||
@@ -105,6 +105,6 @@ class ModerationData(Registry):
|
|||||||
file_data = String()
|
file_data = String()
|
||||||
expiry = Timestamp()
|
expiry = Timestamp()
|
||||||
pardoned_by = Integer()
|
pardoned_by = Integer()
|
||||||
pardoned_at = Integer()
|
pardoned_at = Timestamp()
|
||||||
pardoned_reason = String()
|
pardoned_reason = String()
|
||||||
created_at = Timestamp()
|
created_at = Timestamp()
|
||||||
|
|||||||
@@ -227,10 +227,10 @@ class Ticket:
|
|||||||
name=t(_p('ticket|field:pardoned|name', "Pardoned")),
|
name=t(_p('ticket|field:pardoned|name', "Pardoned")),
|
||||||
value=t(_p(
|
value=t(_p(
|
||||||
'ticket|field:pardoned|value',
|
'ticket|field:pardoned|value',
|
||||||
"Pardoned by <&{moderator}> at {timestamp}.\n{reason}"
|
"Pardoned by <@{moderator}> at {timestamp}.\n{reason}"
|
||||||
)).format(
|
)).format(
|
||||||
moderator=data.pardoned_by,
|
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 ''
|
reason=data.pardoned_reason or ''
|
||||||
),
|
),
|
||||||
inline=False
|
inline=False
|
||||||
@@ -297,9 +297,6 @@ class Ticket:
|
|||||||
self.expiring.cancel_tasks(self.data.ticketid)
|
self.expiring.cancel_tasks(self.data.ticketid)
|
||||||
await self.post()
|
await self.post()
|
||||||
|
|
||||||
async def _revert(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
async def _expire(self):
|
async def _expire(self):
|
||||||
"""
|
"""
|
||||||
Actual expiry method.
|
Actual expiry method.
|
||||||
@@ -321,11 +318,16 @@ class Ticket:
|
|||||||
await self.post()
|
await self.post()
|
||||||
# TODO: Post an extra note to the modlog about the expiry.
|
# 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.
|
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):
|
async def expire(self):
|
||||||
"""
|
"""
|
||||||
@@ -336,5 +338,31 @@ class Ticket:
|
|||||||
"""
|
"""
|
||||||
await self._expire()
|
await self._expire()
|
||||||
|
|
||||||
async def pardon(self):
|
async def pardon(self, modid: int, reason: str):
|
||||||
raise NotImplementedError
|
"""
|
||||||
|
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