1232 lines
52 KiB
Python
1232 lines
52 KiB
Python
from redbot.core import commands # isort:skip
|
|
from redbot.core.i18n import Translator # isort:skip
|
|
from redbot.core.bot import Red # isort:skip
|
|
import discord # isort:skip
|
|
import typing # isort:skip
|
|
|
|
import datetime
|
|
import io
|
|
|
|
import chat_exporter
|
|
|
|
from .utils import utils
|
|
|
|
_: Translator = Translator("TicketTool", __file__)
|
|
|
|
|
|
class Ticket:
|
|
"""Representation of a Ticket."""
|
|
|
|
def __init__(
|
|
self,
|
|
bot,
|
|
cog,
|
|
profile,
|
|
id,
|
|
owner,
|
|
guild,
|
|
channel,
|
|
claim,
|
|
created_by,
|
|
opened_by,
|
|
closed_by,
|
|
deleted_by,
|
|
renamed_by,
|
|
locked_by,
|
|
unlocked_by,
|
|
members,
|
|
created_at,
|
|
opened_at,
|
|
closed_at,
|
|
deleted_at,
|
|
renamed_at,
|
|
locked_at,
|
|
unlocked_at,
|
|
status,
|
|
reason,
|
|
logs_messages,
|
|
save_data,
|
|
first_message,
|
|
):
|
|
self.bot: Red = bot
|
|
self.cog: commands.Cog = cog
|
|
|
|
self.profile: str = profile
|
|
self.id: int = id
|
|
|
|
self.owner: discord.Member = owner
|
|
self.guild: discord.Guild = guild
|
|
self.channel: typing.Union[discord.TextChannel, discord.Thread] = channel
|
|
self.claim: discord.Member = claim
|
|
|
|
self.created_by: discord.Member = created_by
|
|
self.opened_by: discord.Member = opened_by
|
|
self.closed_by: discord.Member = closed_by
|
|
self.deleted_by: discord.Member = deleted_by
|
|
self.renamed_by: discord.Member = renamed_by
|
|
self.locked_by: discord.Member = locked_by
|
|
self.unlocked_by: discord.Member = unlocked_by
|
|
|
|
self.members: typing.List[discord.Member] = members
|
|
|
|
self.created_at: datetime.datetime = created_at
|
|
self.opened_at: datetime.datetime = opened_at
|
|
self.closed_at: datetime.datetime = closed_at
|
|
self.deleted_at: datetime.datetime = deleted_at
|
|
self.renamed_at: datetime.datetime = renamed_at
|
|
self.locked_at: datetime.datetime = locked_at
|
|
self.unlocked_at: datetime.datetime = unlocked_at
|
|
|
|
self.status: str = status
|
|
self.reason: str = reason
|
|
|
|
self.first_message: discord.Message = first_message
|
|
self.logs_messages: bool = logs_messages
|
|
self.save_data: bool = save_data
|
|
|
|
@staticmethod
|
|
def instance(
|
|
ctx: commands.Context,
|
|
profile: str,
|
|
reason: str = "No reason provided.",
|
|
) -> typing.Any:
|
|
ticket: Ticket = Ticket(
|
|
bot=ctx.bot,
|
|
cog=ctx.cog,
|
|
profile=profile,
|
|
id=None,
|
|
owner=ctx.author,
|
|
guild=ctx.guild,
|
|
channel=None,
|
|
claim=None,
|
|
created_by=ctx.author,
|
|
opened_by=ctx.author,
|
|
closed_by=None,
|
|
deleted_by=None,
|
|
renamed_by=None,
|
|
locked_by=None,
|
|
unlocked_by=None,
|
|
members=[],
|
|
created_at=datetime.datetime.now(),
|
|
opened_at=None,
|
|
closed_at=None,
|
|
deleted_at=None,
|
|
renamed_at=None,
|
|
locked_at=None,
|
|
unlocked_at=None,
|
|
status="open",
|
|
reason=reason,
|
|
first_message=None,
|
|
logs_messages=True,
|
|
save_data=True,
|
|
)
|
|
return ticket
|
|
|
|
@staticmethod
|
|
def from_json(json: dict, bot: Red, cog: commands.Cog) -> typing.Any:
|
|
ticket: Ticket = Ticket(
|
|
bot=bot,
|
|
cog=cog,
|
|
profile=json["profile"],
|
|
id=json["id"],
|
|
owner=json["owner"],
|
|
guild=json["guild"],
|
|
channel=json["channel"],
|
|
claim=json.get("claim"),
|
|
created_by=json["created_by"],
|
|
opened_by=json.get("opened_by"),
|
|
closed_by=json.get("closed_by"),
|
|
deleted_by=json.get("deleted_by"),
|
|
renamed_by=json.get("renamed_by"),
|
|
locked_by=json.get("locked_by"),
|
|
unlocked_by=json.get("unlocked_by"),
|
|
members=json.get("members"),
|
|
created_at=json["created_at"],
|
|
opened_at=json.get("opened_at"),
|
|
closed_at=json.get("closed_at"),
|
|
deleted_at=json.get("deleted_at"),
|
|
renamed_at=json.get("renamed_at"),
|
|
locked_at=json.get("locked_at"),
|
|
unlocked_at=json.get("unlocked_at"),
|
|
status=json["status"],
|
|
reason=json["reason"],
|
|
first_message=json["first_message"],
|
|
logs_messages=json.get("logs_messages", True),
|
|
save_data=json.get("save_data", True),
|
|
)
|
|
return ticket
|
|
|
|
async def save(self, clean: bool = True) -> typing.Dict[str, typing.Any]:
|
|
if not self.save_data:
|
|
return
|
|
cog = self.cog
|
|
guild = self.guild
|
|
channel = self.channel
|
|
if self.owner is not None:
|
|
self.owner = int(getattr(self.owner, "id", self.owner))
|
|
if self.guild is not None:
|
|
self.guild = int(self.guild.id)
|
|
if self.channel is not None:
|
|
self.channel = int(self.channel.id)
|
|
if self.claim is not None:
|
|
self.claim = self.claim.id
|
|
if self.created_by is not None:
|
|
self.created_by = int(getattr(self.created_by, "id", self.created_by))
|
|
if self.opened_by is not None:
|
|
self.opened_by = int(getattr(self.opened_by, "id", self.opened_by))
|
|
if self.closed_by is not None:
|
|
self.closed_by = int(getattr(self.closed_by, "id", self.closed_by))
|
|
if self.deleted_by is not None:
|
|
self.deleted_by = int(getattr(self.deleted_by, "id", self.deleted_by))
|
|
if self.renamed_by is not None:
|
|
self.renamed_by = int(getattr(self.renamed_by, "id", self.renamed_by))
|
|
if self.locked_by is not None:
|
|
self.locked_by = int(getattr(self.locked_by, "id", self.locked_by))
|
|
if self.unlocked_by is not None:
|
|
self.unlocked_by = int(getattr(self.unlocked_by, "id", self.unlocked_by))
|
|
members = self.members
|
|
self.members = [int(m.id) for m in members]
|
|
if self.created_at is not None:
|
|
self.created_at = float(datetime.datetime.timestamp(self.created_at))
|
|
if self.opened_at is not None:
|
|
self.opened_at = float(datetime.datetime.timestamp(self.opened_at))
|
|
if self.closed_at is not None:
|
|
self.closed_at = float(datetime.datetime.timestamp(self.closed_at))
|
|
if self.deleted_at is not None:
|
|
self.deleted_at = float(datetime.datetime.timestamp(self.deleted_at))
|
|
if self.renamed_at is not None:
|
|
self.renamed_at = float(datetime.datetime.timestamp(self.renamed_at))
|
|
if self.locked_at is not None:
|
|
self.locked_at = float(datetime.datetime.timestamp(self.locked_at))
|
|
if self.unlocked_at is not None:
|
|
self.unlocked_at = float(datetime.datetime.timestamp(self.unlocked_at))
|
|
if self.first_message is not None:
|
|
self.first_message = int(self.first_message.id)
|
|
json = self.__dict__
|
|
for key in ("bot", "cog"):
|
|
del json[key]
|
|
if clean:
|
|
for key in (
|
|
"claim",
|
|
"opened_by",
|
|
"closed_by",
|
|
"deleted_by",
|
|
"renamed_by",
|
|
"locked_by",
|
|
"unlocked_by",
|
|
"opened_at",
|
|
"closed_at",
|
|
"deleted_at",
|
|
"renamed_at",
|
|
"locked_at",
|
|
"unlocked_at",
|
|
):
|
|
if json[key] is None:
|
|
del json[key]
|
|
if json["members"] == []:
|
|
del json["members"]
|
|
for key in ("logs_messages", "save_data"):
|
|
if json[key]:
|
|
del json[key]
|
|
data = await cog.config.guild(guild).tickets.all()
|
|
data[str(channel.id)] = json
|
|
await cog.config.guild(guild).tickets.set(data)
|
|
return json
|
|
|
|
async def create(self) -> typing.Any:
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
logschannel = config["logschannel"]
|
|
ping_roles = config["ping_roles"]
|
|
self.id = config["last_nb"] + 1
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=self.created_by,
|
|
reason=_("Creating the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
try:
|
|
to_replace = {
|
|
"ticket_id": str(self.id),
|
|
"owner_display_name": self.owner.display_name,
|
|
"owner_name": self.owner.name,
|
|
"owner_id": str(self.owner.id),
|
|
"guild_name": self.guild.name,
|
|
"guild_id": self.guild.id,
|
|
"bot_display_name": self.guild.me.display_name,
|
|
"bot_name": self.bot.user.name,
|
|
"bot_id": str(self.bot.user.id),
|
|
"shortdate": self.created_at.strftime("%m-%d"),
|
|
"longdate": self.created_at.strftime("%m-%d-%Y"),
|
|
"time": self.created_at.strftime("%I-%M-%p"),
|
|
"emoji": config["emoji_open"],
|
|
}
|
|
name = config["dynamic_channel_name"].format(**to_replace).replace(" ", "-")
|
|
except (KeyError, AttributeError):
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"The dynamic channel name does not contain correct variable names and must be re-configured with `[p]settickettool dynamicchannelname`."
|
|
)
|
|
)
|
|
|
|
view = self.cog.get_buttons(
|
|
buttons=[
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Close"),
|
|
"emoji": "🔒",
|
|
"custom_id": "close_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Claim"),
|
|
"emoji": "🙋♂️",
|
|
"custom_id": "claim_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Delete"),
|
|
"emoji": "⛔",
|
|
"custom_id": "delete_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
],
|
|
)
|
|
optionnal_ping = (
|
|
f" ||{' '.join(role.mention for role in ping_roles)}||"[:1500] if ping_roles else ""
|
|
)
|
|
embed = await self.cog.get_embed_important(
|
|
self,
|
|
False,
|
|
author=self.created_by,
|
|
title=_("Ticket Created"),
|
|
description=_("Thank you for creating a ticket on this server!"),
|
|
reason=self.reason,
|
|
)
|
|
if config["ticket_role"] is not None and self.owner:
|
|
try:
|
|
await self.owner.add_roles(config["ticket_role"], reason=_reason)
|
|
except discord.HTTPException:
|
|
pass
|
|
try:
|
|
if config["forum_channel"] is None:
|
|
overwrites = await utils().get_overwrites(self)
|
|
topic = _(
|
|
"🎟️ Ticket ID: {ticket.id}\n"
|
|
# "🔥 Channel ID: {ticket.channel.id}\n"
|
|
"🕵️ Ticket created by: @{ticket.created_by.display_name} ({ticket.created_by.id})\n"
|
|
"☢️ Ticket reason: {short_reason}\n"
|
|
# "👥 Ticket claimed by: Nobody."
|
|
).format(
|
|
ticket=self,
|
|
short_reason=(
|
|
f"{self.reason[:700]}...".replace("\n", " ")
|
|
if len(self.reason) > 700
|
|
else self.reason.replace("\n", " ")
|
|
),
|
|
)
|
|
self.channel: discord.TextChannel = await self.guild.create_text_channel(
|
|
name,
|
|
overwrites=overwrites,
|
|
category=config["category_open"],
|
|
topic=topic,
|
|
reason=_reason,
|
|
)
|
|
await self.channel.edit(topic=topic)
|
|
self.first_message = await self.channel.send(
|
|
f"{self.created_by.mention}{optionnal_ping}",
|
|
embed=embed,
|
|
view=view,
|
|
allowed_mentions=discord.AllowedMentions(users=True, roles=True),
|
|
)
|
|
self.cog.views[self.first_message] = view
|
|
else:
|
|
if isinstance(config["forum_channel"], discord.ForumChannel):
|
|
forum_channel: discord.ForumChannel = config["forum_channel"]
|
|
result: discord.channel.ThreadWithMessage = await forum_channel.create_thread(
|
|
name=name,
|
|
content=f"{self.created_by.mention}{optionnal_ping}",
|
|
embed=embed,
|
|
view=view,
|
|
allowed_mentions=discord.AllowedMentions(users=True, roles=True),
|
|
auto_archive_duration=10080,
|
|
reason=_reason,
|
|
)
|
|
self.channel: discord.Thread = result.thread
|
|
self.first_message: discord.Message = result.message
|
|
else: # isinstance(config["forum_channel"], discord.TextChannel)
|
|
forum_channel: discord.TextChannel = config["forum_channel"]
|
|
self.channel: discord.Thread = await forum_channel.create_thread(
|
|
name=name,
|
|
message=None, # Private thread.
|
|
type=discord.ChannelType.private_thread,
|
|
invitable=False,
|
|
auto_archive_duration=10080,
|
|
reason=_reason,
|
|
)
|
|
self.first_message = await self.channel.send(
|
|
f"{self.created_by.mention}{optionnal_ping}",
|
|
embed=embed,
|
|
view=view,
|
|
allowed_mentions=discord.AllowedMentions(users=True, roles=True),
|
|
)
|
|
self.cog.views[self.first_message] = view
|
|
members = [self.owner]
|
|
if self.claim is not None:
|
|
members.append(self.claim)
|
|
if config["admin_roles"]:
|
|
for role in config["admin_roles"]:
|
|
members.extend(role.members)
|
|
if config["support_roles"]:
|
|
for role in config["support_roles"]:
|
|
members.extend(role.members)
|
|
if config["view_roles"]:
|
|
for role in config["view_roles"]:
|
|
members.extend(role.members)
|
|
adding_error = False
|
|
for member in members:
|
|
try:
|
|
await self.channel.add_user(member)
|
|
except (
|
|
discord.HTTPException
|
|
): # The bot haven't the permission `manage_messages` in the parent text channel.
|
|
adding_error = True
|
|
if adding_error:
|
|
await self.channel.send(
|
|
_(
|
|
"⚠ At least one user (the ticket owner or a team member) could not be added to the ticket thread. Maybe the user doesn't have access to the parent forum/text channel. If the server uses private threads in a text channel, the bot does not have the `manage_messages` permission in this channel."
|
|
)
|
|
)
|
|
if config["create_modlog"]:
|
|
await self.cog.create_modlog(self, "ticket_created", _reason)
|
|
if config["custom_message"] is not None:
|
|
try:
|
|
embed: discord.Embed = discord.Embed()
|
|
embed.title = "Custom Message"
|
|
to_replace = {
|
|
"ticket_id": str(self.id),
|
|
"owner_display_name": self.owner.display_name,
|
|
"owner_name": self.owner.name,
|
|
"owner_id": str(self.owner.id),
|
|
"guild_name": self.guild.name,
|
|
"guild_id": self.guild.id,
|
|
"bot_display_name": self.guild.me.display_name,
|
|
"bot_name": self.bot.user.name,
|
|
"bot_id": str(self.bot.user.id),
|
|
"shortdate": self.created_at.strftime("%m-%d"),
|
|
"longdate": self.created_at.strftime("%m-%d-%Y"),
|
|
"time": self.created_at.strftime("%I-%M-%p"),
|
|
"emoji": config["emoji_open"],
|
|
}
|
|
embed.description = config["custom_message"].format(**to_replace)
|
|
await self.channel.send(embed=embed)
|
|
except (KeyError, AttributeError, discord.HTTPException):
|
|
pass
|
|
if logschannel is not None:
|
|
embed = await self.cog.get_embed_important(
|
|
self,
|
|
True,
|
|
author=self.created_by,
|
|
title=_("Ticket Created"),
|
|
description=_("The ticket was created by {ticket.created_by}.").format(
|
|
ticket=self
|
|
),
|
|
reason=self.reason,
|
|
)
|
|
await logschannel.send(
|
|
_("Report on the creation of the ticket {ticket.id}.").format(ticket=self),
|
|
embed=embed,
|
|
)
|
|
except discord.HTTPException:
|
|
if config["ticket_role"] is not None and self.owner:
|
|
try:
|
|
await self.owner.remove_roles(config["ticket_role"], reason=_reason)
|
|
except discord.HTTPException:
|
|
pass
|
|
raise
|
|
await self.cog.config.guild(self.guild).profiles.set_raw(
|
|
self.profile, "last_nb", value=self.id
|
|
)
|
|
await self.save()
|
|
return self
|
|
|
|
async def export(self) -> typing.Optional[discord.File]:
|
|
if self.channel:
|
|
transcript = await chat_exporter.export(
|
|
channel=self.channel,
|
|
limit=None,
|
|
tz_info="UTC",
|
|
guild=self.guild,
|
|
bot=self.bot,
|
|
)
|
|
if transcript is not None:
|
|
return discord.File(
|
|
io.BytesIO(transcript.encode()),
|
|
filename=f"transcript-ticket-{self.profile}-{self.id}.html",
|
|
)
|
|
return None
|
|
|
|
async def open(
|
|
self, author: typing.Optional[discord.Member] = None, reason: typing.Optional[str] = None
|
|
) -> typing.Any:
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_("Opening the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
logschannel = config["logschannel"]
|
|
emoji_open = config["emoji_open"]
|
|
emoji_close = config["emoji_close"]
|
|
self.status = "open"
|
|
self.opened_by = author
|
|
self.opened_at = datetime.datetime.now()
|
|
self.closed_by = None
|
|
self.closed_at = None
|
|
new_name = f"{self.channel.name}"
|
|
new_name = new_name.replace(f"{emoji_close}-", "", 1)
|
|
new_name = f"{emoji_open}-{new_name}"
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
members = [self.owner] + self.members
|
|
overwrites = self.channel.overwrites
|
|
for member in members:
|
|
if member in overwrites:
|
|
overwrites[member].send_messages = True
|
|
await self.channel.edit(
|
|
name=new_name,
|
|
category=config["category_open"],
|
|
overwrites=overwrites,
|
|
reason=_reason,
|
|
)
|
|
else:
|
|
await self.channel.edit(name=new_name, archived=False, reason=_reason)
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=self.opened_by, action=_("Ticket Opened"), reason=reason
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
if logschannel is not None:
|
|
embed = await self.cog.get_embed_important(
|
|
self,
|
|
True,
|
|
author=self.opened_by,
|
|
title=_("Ticket Opened"),
|
|
description=_("The ticket was opened by {ticket.opened_by}.").format(
|
|
ticket=self
|
|
),
|
|
reason=reason,
|
|
)
|
|
await logschannel.send(
|
|
_("Report on the close of the ticket {ticket.id}.").format(ticket=self),
|
|
embed=embed,
|
|
)
|
|
if self.first_message is not None:
|
|
view = self.cog.get_buttons(
|
|
buttons=[
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Close"),
|
|
"emoji": "🔒",
|
|
"custom_id": "close_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Claim"),
|
|
"emoji": "🙋♂️",
|
|
"custom_id": "claim_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Delete"),
|
|
"emoji": "⛔",
|
|
"custom_id": "delete_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
],
|
|
)
|
|
try:
|
|
self.first_message = await self.channel.fetch_message(int(self.first_message.id))
|
|
await self.first_message.edit(view=view)
|
|
except discord.HTTPException:
|
|
pass
|
|
if (
|
|
config["ticket_role"] is not None
|
|
and self.owner is not None
|
|
and isinstance(self.owner, discord.Member)
|
|
):
|
|
try:
|
|
await self.owner.add_roles(config["ticket_role"], reason=_reason)
|
|
except discord.HTTPException:
|
|
pass
|
|
await self.save()
|
|
return self
|
|
|
|
async def close(
|
|
self, author: typing.Optional[discord.Member] = None, reason: typing.Optional[str] = None
|
|
) -> typing.Any:
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=f"Closing the ticket {self.id}.",
|
|
)
|
|
logschannel = config["logschannel"]
|
|
emoji_open = config["emoji_open"]
|
|
emoji_close = config["emoji_close"]
|
|
self.status = "close"
|
|
self.closed_by = author
|
|
self.closed_at = datetime.datetime.now()
|
|
new_name = f"{self.channel.name}"
|
|
new_name = new_name.replace(f"{emoji_open}-", "", 1)
|
|
new_name = f"{emoji_close}-{new_name}"
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=self.closed_by, action="Ticket Closed", reason=reason
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
if logschannel is not None:
|
|
embed = await self.cog.get_embed_important(
|
|
self,
|
|
True,
|
|
author=self.closed_by,
|
|
title="Ticket Closed",
|
|
description=f"The ticket was closed by {self.closed_by}.",
|
|
reason=reason,
|
|
)
|
|
await logschannel.send(
|
|
_("Report on the close of the ticket {ticket.id}.").format(ticket=self),
|
|
embed=embed,
|
|
)
|
|
if self.first_message is not None:
|
|
view = self.cog.get_buttons(
|
|
buttons=[
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Re-open"),
|
|
"emoji": "🔓",
|
|
"custom_id": "open_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Claim"),
|
|
"emoji": "🙋♂️",
|
|
"custom_id": "claim_ticket_button",
|
|
"disabled": True,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Delete"),
|
|
"emoji": "⛔",
|
|
"custom_id": "delete_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
],
|
|
)
|
|
try:
|
|
self.first_message = await self.channel.fetch_message(int(self.first_message.id))
|
|
await self.first_message.edit(view=view)
|
|
except discord.HTTPException:
|
|
pass
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
allowed_members = []
|
|
if self.claim is not None:
|
|
allowed_members.append(self.claim)
|
|
if config["admin_roles"]:
|
|
for role in config["admin_roles"]:
|
|
allowed_members.extend(role.members)
|
|
if config["support_roles"]:
|
|
for role in config["support_roles"]:
|
|
allowed_members.extend(role.members)
|
|
members = filter(
|
|
lambda member: member not in allowed_members, [self.owner] + self.members
|
|
)
|
|
overwrites = self.channel.overwrites
|
|
for member in members:
|
|
if member in overwrites:
|
|
overwrites[member].send_messages = False
|
|
await self.channel.edit(
|
|
name=new_name,
|
|
category=config["category_close"],
|
|
overwrites=overwrites,
|
|
reason=_reason,
|
|
)
|
|
else:
|
|
await self.channel.edit(name=new_name, archived=True, locked=True, reason=_reason)
|
|
if (
|
|
config["ticket_role"] is not None
|
|
and self.owner is not None
|
|
and isinstance(self.owner, discord.Member)
|
|
):
|
|
try:
|
|
await self.owner.remove_roles(config["ticket_role"], reason=_reason)
|
|
except discord.HTTPException:
|
|
pass
|
|
await self.save()
|
|
return self
|
|
|
|
async def lock(
|
|
self, author: typing.Optional[discord.Member] = None, reason: typing.Optional[str] = None
|
|
) -> typing.Any:
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
raise commands.UserFeedbackCheckFailure(_("Cannot execute action on a text channel."))
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=f"Locking the ticket {self.id}.",
|
|
)
|
|
logschannel = config["logschannel"]
|
|
self.locked_by = author
|
|
self.locked_at = datetime.datetime.now()
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=self.locked_by, action="Ticket Locked", reason=reason
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
if logschannel is not None:
|
|
embed = await self.cog.get_embed_important(
|
|
self,
|
|
True,
|
|
author=self.locked_by,
|
|
title="Ticket Locked",
|
|
description=f"The ticket was locked by {self.closed_by}.",
|
|
reason=reason,
|
|
)
|
|
await logschannel.send(
|
|
_("Report on the lock of the ticket {ticket.id}."),
|
|
embed=embed,
|
|
)
|
|
await self.channel.edit(locked=True, reason=_reason)
|
|
await self.save()
|
|
return self
|
|
|
|
async def unlock(
|
|
self, author: typing.Optional[discord.Member] = None, reason: typing.Optional[str] = None
|
|
) -> typing.Any:
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
raise commands.UserFeedbackCheckFailure(_("Cannot execute action on a text channel."))
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=f"Unlocking the ticket {self.id}.",
|
|
)
|
|
logschannel = config["logschannel"]
|
|
self.unlocked_by = author
|
|
self.unlocked_at = datetime.datetime.now()
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=self.unlocked_by, action="Ticket Unlocked"
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
if logschannel is not None:
|
|
embed = await self.cog.get_embed_important(
|
|
self,
|
|
True,
|
|
author=self.unlocked_by,
|
|
title="Ticket Unlocked",
|
|
description=f"The ticket was unlocked by {self.closed_by}.",
|
|
reason=reason,
|
|
)
|
|
await logschannel.send(
|
|
_("Report on the unlock of the ticket {ticket.id}."),
|
|
embed=embed,
|
|
)
|
|
await self.channel.edit(locked=False, reason=_reason)
|
|
await self.save()
|
|
return self
|
|
|
|
async def rename(
|
|
self,
|
|
new_name: str,
|
|
author: typing.Optional[discord.Member] = None,
|
|
reason: typing.Optional[str] = None,
|
|
) -> typing.Any:
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_(
|
|
"Renaming the ticket {ticket.id}. (`{ticket.channel.name}` to `{new_name}`)"
|
|
).format(ticket=self, new_name=new_name),
|
|
)
|
|
await self.channel.edit(name=new_name, reason=_reason)
|
|
if author is not None:
|
|
self.renamed_by = author
|
|
self.renamed_at = datetime.datetime.now()
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=self.renamed_by, action=_("Ticket Renamed."), reason=reason
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
await self.save()
|
|
return self
|
|
|
|
async def delete(
|
|
self, author: typing.Optional[discord.Member] = None, reason: typing.Optional[str] = None
|
|
) -> typing.Any:
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
logschannel = config["logschannel"]
|
|
self.deleted_by = author
|
|
self.deleted_at = datetime.datetime.now()
|
|
if self.logs_messages and logschannel is not None:
|
|
embed = await self.cog.get_embed_important(
|
|
self,
|
|
True,
|
|
author=self.deleted_by,
|
|
title=_("Ticket Deleted"),
|
|
description=_("The ticket was deleted by {ticket.deleted_by}.").format(
|
|
ticket=self
|
|
),
|
|
reason=reason,
|
|
)
|
|
try:
|
|
transcript = await chat_exporter.export(
|
|
channel=self.channel,
|
|
limit=None,
|
|
tz_info="UTC",
|
|
guild=self.guild,
|
|
bot=self.bot,
|
|
)
|
|
except AttributeError:
|
|
transcript = None
|
|
if transcript is not None:
|
|
file = discord.File(
|
|
io.BytesIO(transcript.encode()),
|
|
filename=f"transcript-ticket-{self.id}.html",
|
|
)
|
|
else:
|
|
file = None
|
|
message = await logschannel.send(
|
|
_("Report on the deletion of the ticket {ticket.id}.").format(ticket=self),
|
|
embed=embed,
|
|
file=file,
|
|
)
|
|
embed = discord.Embed(
|
|
title="Transcript Link",
|
|
description=(
|
|
f"[Click here to view the transcript.](https://mahto.id/chat-exporter?url={message.attachments[0].url})"
|
|
),
|
|
color=discord.Color.red(),
|
|
)
|
|
await logschannel.send(embed=embed)
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_("Deleting the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
await self.channel.delete(reason=_reason)
|
|
else:
|
|
await self.channel.delete()
|
|
data = await self.cog.config.guild(self.guild).tickets.all()
|
|
try:
|
|
del data[str(self.channel.id)]
|
|
except KeyError:
|
|
pass
|
|
await self.cog.config.guild(self.guild).tickets.set(data)
|
|
return self
|
|
|
|
async def claim_ticket(
|
|
self,
|
|
member: discord.Member,
|
|
author: typing.Optional[discord.Member] = None,
|
|
reason: typing.Optional[str] = None,
|
|
) -> typing.Any:
|
|
if self.status != "open":
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("A ticket cannot be claimed if it is closed.")
|
|
)
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
if member.bot:
|
|
raise commands.UserFeedbackCheckFailure(_("A bot cannot claim a ticket."))
|
|
self.claim = member
|
|
# topic = _(
|
|
# "🎟️ Ticket ID: {ticket.id}\n"
|
|
# "🔥 Channel ID: {ticket.channel.id}\n"
|
|
# "🕵️ Ticket created by: @{ticket.created_by.display_name} ({ticket.created_by.id})\n"
|
|
# "☢️ Ticket reason: {short_reason}\n"
|
|
# "👥 Ticket claimed by: @{ticket.claim.display_name} (@{ticket.claim.id})."
|
|
# ).format(ticket=self, short_reason=f"{self.reason[:700]}...".replace("\n", " ") if len(self.reason) > 700 else self.reason.replace("\n", " "))
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_("Claiming the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
overwrites = self.channel.overwrites
|
|
overwrites[member] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
read_messages=True,
|
|
read_message_history=True,
|
|
send_messages=True,
|
|
attach_files=True,
|
|
use_application_commands=True,
|
|
)
|
|
if config["support_roles"]:
|
|
for role in config["support_roles"]:
|
|
overwrites[role] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
read_messages=True,
|
|
send_messages=False,
|
|
read_message_history=True,
|
|
attach_files=False,
|
|
use_application_commands=False,
|
|
)
|
|
await self.channel.edit(overwrites=overwrites, reason=_reason) # topic=topic,
|
|
if self.first_message is not None:
|
|
view = self.cog.get_buttons(
|
|
buttons=[
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Close"),
|
|
"emoji": "🔒",
|
|
"custom_id": "close_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Claim"),
|
|
"emoji": "🙋♂️",
|
|
"custom_id": "claim_ticket_button",
|
|
"disabled": True,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Delete"),
|
|
"emoji": "⛔",
|
|
"custom_id": "delete_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
],
|
|
)
|
|
try:
|
|
self.first_message = await self.channel.fetch_message(int(self.first_message.id))
|
|
await self.first_message.edit(view=view)
|
|
except discord.HTTPException:
|
|
pass
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=author, action=_("Ticket claimed."), reason=reason
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
await self.save()
|
|
return self
|
|
|
|
async def unclaim_ticket(
|
|
self,
|
|
member: discord.Member,
|
|
author: typing.Optional[discord.Member] = None,
|
|
reason: typing.Optional[str] = None,
|
|
) -> typing.Any:
|
|
if self.status != "open":
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("A ticket cannot be unclaimed if it is closed.")
|
|
)
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
self.claim = None
|
|
# topic = _(
|
|
# "🎟️ Ticket ID: {ticket.id}\n"
|
|
# "🔥 Channel ID: {ticket.channel.id}\n"
|
|
# "🕵️ Ticket created by: @{ticket.created_by.display_name} ({ticket.created_by.id})\n"
|
|
# "☢️ Ticket reason: {short_reason}\n"
|
|
# "👥 Ticket claimed by: Nobody."
|
|
# ).format(ticket=self, short_reason=f"{self.reason[:700]}...".replace("\n", " ") if len(self.reason) > 700 else self.reason.replace("\n", " "))
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_("Unclaiming the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
if config["support_roles"]:
|
|
overwrites = self.channel.overwrites
|
|
for role in config["support_roles"]:
|
|
overwrites[role] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
read_messages=True,
|
|
read_message_history=True,
|
|
send_messages=True,
|
|
attach_files=True,
|
|
use_application_commands=True,
|
|
)
|
|
await self.channel.edit(overwrites=overwrites, reason=_reason)
|
|
await self.channel.set_permissions(member, overwrite=None, reason=_reason)
|
|
# await self.channel.edit(topic=topic)
|
|
if self.first_message is not None:
|
|
view = self.cog.get_buttons(
|
|
buttons=[
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Close"),
|
|
"emoji": "🔒",
|
|
"custom_id": "close_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Claim"),
|
|
"emoji": "🙋♂️",
|
|
"custom_id": "claim_ticket_button",
|
|
"disabled": True,
|
|
},
|
|
{
|
|
"style": discord.ButtonStyle(2),
|
|
"label": _("Delete"),
|
|
"emoji": "⛔",
|
|
"custom_id": "delete_ticket_button",
|
|
"disabled": False,
|
|
},
|
|
],
|
|
)
|
|
try:
|
|
self.first_message = await self.channel.fetch_message(int(self.first_message.id))
|
|
await self.first_message.edit(view=view)
|
|
except discord.HTTPException:
|
|
pass
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=author, action=_("Ticket unclaimed."), reason=reason
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
await self.save()
|
|
return self
|
|
|
|
async def change_owner(
|
|
self,
|
|
member: discord.Member,
|
|
author: typing.Optional[discord.Member] = None,
|
|
reason: typing.Optional[str] = None,
|
|
) -> typing.Any:
|
|
if not isinstance(self.channel, discord.TextChannel):
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("Cannot execute action in a thread channel.")
|
|
)
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_("Changing owner of the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
if member.bot:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("You cannot transfer ownership of a ticket to a bot.")
|
|
)
|
|
if not isinstance(self.owner, int):
|
|
if config["ticket_role"] is not None:
|
|
try:
|
|
self.owner.remove_roles(config["ticket_role"], reason=_reason)
|
|
except discord.HTTPException:
|
|
pass
|
|
self.remove_member(self.owner, author=None)
|
|
self.add_member(self.owner, author=None)
|
|
self.owner = member
|
|
self.remove_member(self.owner, author=None)
|
|
overwrites = self.channel.overwrites
|
|
overwrites[member] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
read_messages=True,
|
|
read_message_history=True,
|
|
send_messages=True,
|
|
attach_files=True,
|
|
use_application_commands=True,
|
|
)
|
|
await self.channel.edit(overwrites=overwrites, reason=_reason)
|
|
if config["ticket_role"] is not None:
|
|
try:
|
|
self.owner.add_roles(config["ticket_role"], reason=_reason)
|
|
except discord.HTTPException:
|
|
pass
|
|
if self.logs_messages:
|
|
embed = await self.cog.get_embed_action(
|
|
self, author=author, action=_("Owner Modified."), reason=reason
|
|
)
|
|
await self.channel.send(embed=embed)
|
|
await self.save()
|
|
return self
|
|
|
|
async def add_member(
|
|
self, members: typing.List[discord.Member], author: typing.Optional[discord.Member] = None
|
|
) -> typing.Any:
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
admin_roles_members = []
|
|
if config["admin_roles"]:
|
|
for role in config["admin_roles"]:
|
|
admin_roles_members.extend(role.members)
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_("Adding a member to the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
overwrites = self.channel.overwrites
|
|
for member in members:
|
|
if author is not None:
|
|
if member.bot:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("You cannot add a bot to a ticket. ({member})").format(member=member)
|
|
)
|
|
if not isinstance(self.owner, int) and member == self.owner:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is already the owner of this ticket. ({member})"
|
|
).format(member=member)
|
|
)
|
|
if member in admin_roles_members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is an administrator for the tickets system. They will always have access to the ticket anyway. ({member})"
|
|
).format(member=member)
|
|
)
|
|
if member in self.members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("This member already has access to this ticket. ({member})").format(
|
|
member=member
|
|
)
|
|
)
|
|
if member not in self.members:
|
|
self.members.append(member)
|
|
overwrites[member] = discord.PermissionOverwrite(
|
|
view_channel=True,
|
|
read_messages=True,
|
|
read_message_history=True,
|
|
send_messages=True,
|
|
attach_files=True,
|
|
use_application_commands=True,
|
|
)
|
|
await self.channel.edit(overwrites=overwrites, reason=_reason)
|
|
else:
|
|
adding_error = False
|
|
for member in members:
|
|
if author is not None:
|
|
if member.bot:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("You cannot add a bot to a ticket. ({member})").format(member=member)
|
|
)
|
|
if not isinstance(self.owner, int) and member == self.owner:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is already the owner of this ticket. ({member})"
|
|
).format(member=member)
|
|
)
|
|
if member in admin_roles_members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is an administrator for the tickets system. They will always have access to the ticket anyway. ({member})"
|
|
).format(member=member)
|
|
)
|
|
if member in self.members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("This member already has access to this ticket. ({member})").format(
|
|
member=member
|
|
)
|
|
)
|
|
try:
|
|
await self.channel.add_user(member)
|
|
except (
|
|
discord.HTTPException
|
|
): # The bot haven't the permission `manage_messages` in the parent text channel.
|
|
adding_error = True
|
|
if member not in self.members:
|
|
self.members.append(member)
|
|
if adding_error:
|
|
await self.channel.send(
|
|
_(
|
|
"⚠ At least one user (the ticket owner or a team member) could not be added to the ticket thread. Maybe the user the user doesn't have access to the parent forum/text channel. If the server uses private threads in a text channel, the bot does not have the `manage_messages` permission in this channel."
|
|
)
|
|
)
|
|
await self.save()
|
|
return self
|
|
|
|
async def remove_member(
|
|
self, members: typing.List[discord.Member], author: typing.Optional[discord.Member] = None
|
|
) -> typing.Any:
|
|
config = await self.cog.get_config(self.guild, self.profile)
|
|
admin_roles_members = []
|
|
if config["admin_roles"]:
|
|
for role in config["admin_roles"]:
|
|
admin_roles_members.extend(role.members)
|
|
support_roles_members = []
|
|
if config["support_roles"]:
|
|
for role in config["support_roles"]:
|
|
support_roles_members.extend(role.members)
|
|
if isinstance(self.channel, discord.TextChannel):
|
|
_reason = await self.cog.get_audit_reason(
|
|
guild=self.guild,
|
|
profile=self.profile,
|
|
author=author,
|
|
reason=_("Removing a member to the ticket {ticket.id}.").format(ticket=self),
|
|
)
|
|
for member in members:
|
|
if author is not None:
|
|
if member.bot:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("You cannot remove a bot to a ticket ({member}).").format(
|
|
member=member
|
|
)
|
|
)
|
|
if not isinstance(self.owner, int) and member == self.owner:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("You cannot remove the owner of this ticket. ({member})").format(
|
|
member=member
|
|
)
|
|
)
|
|
if member in admin_roles_members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is an administrator for the tickets system. They will always have access to the ticket. ({member})"
|
|
).format(member=member)
|
|
)
|
|
if member not in self.members and member not in support_roles_members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is not in the list of those authorised to access the ticket. ({member})"
|
|
).format(member=member)
|
|
)
|
|
await self.channel.set_permissions(member, overwrite=None, reason=_reason)
|
|
if member in self.members:
|
|
self.members.remove(member)
|
|
else:
|
|
for member in members:
|
|
if author is not None:
|
|
if member.bot:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("You cannot remove a bot to a ticket ({member}).").format(
|
|
member=member
|
|
)
|
|
)
|
|
if not isinstance(self.owner, int) and member == self.owner:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_("You cannot remove the owner of this ticket. ({member})").format(
|
|
member=member
|
|
)
|
|
)
|
|
if member in admin_roles_members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is an administrator for the tickets system. They will always have access to the ticket. ({member})"
|
|
).format(member=member)
|
|
)
|
|
if member not in self.members and member not in support_roles_members:
|
|
raise commands.UserFeedbackCheckFailure(
|
|
_(
|
|
"This member is not in the list of those authorised to access the ticket. ({member})"
|
|
).format(member=member)
|
|
)
|
|
await self.channel.remove_user(member)
|
|
if member in self.members:
|
|
self.members.remove(member)
|
|
await self.save()
|
|
return self
|