Ruby-Cogs/tickettool/ticket.py
2025-04-02 22:57:51 -04:00

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