Enhance ticketing and modal handling in cogs
- Improved exception handling in `create_ticket` for better user feedback. - Added "Apply for PM Position" button in `WorkView` to facilitate PM applications. - Updated `Hiring` class to manage guild settings and ensure persistent views. - Restructured `OrderModal` and `ReviewModal` in `KofiShop` for improved user experience and error handling. - Refactored `ModMail` class for better thread management and added ticket closure functionality with logging. - Converted several commands in `KofiShop` from hybrid to app commands for better interaction. - Enhanced overall code structure for readability and maintainability.
This commit is contained in:
@@ -131,13 +131,9 @@ async def create_ticket(interaction: discord.Interaction, ticket_type: str, moda
|
|||||||
if not interaction.response.is_done():
|
if not interaction.response.is_done():
|
||||||
await interaction.response.send_message(f"An unexpected error occurred: {e}", ephemeral=True)
|
await interaction.response.send_message(f"An unexpected error occurred: {e}", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
# --- Button Views for Commands ---
|
|
||||||
|
|
||||||
class HireView(discord.ui.View):
|
class HireView(discord.ui.View):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(timeout=None)
|
super().__init__(timeout=None)
|
||||||
self.cog = cog
|
|
||||||
|
|
||||||
@discord.ui.button(label="Staff", style=discord.ButtonStyle.primary, custom_id="staff_apply_button_persistent")
|
@discord.ui.button(label="Staff", style=discord.ButtonStyle.primary, custom_id="staff_apply_button_persistent")
|
||||||
async def staff_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def staff_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
@@ -155,7 +151,10 @@ class HireView(discord.ui.View):
|
|||||||
class WorkView(discord.ui.View):
|
class WorkView(discord.ui.View):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__(timeout=None)
|
super().__init__(timeout=None)
|
||||||
self.cog = cog
|
|
||||||
|
@discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.green, custom_id="work_apply_pm_persistent")
|
||||||
|
async def work_apply_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
|
await create_ticket(interaction, "pm", PMApplicationModal)
|
||||||
|
|
||||||
@discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.green, custom_id="work_apply_pm_persistent")
|
@discord.ui.button(label="Apply for PM Position", style=discord.ButtonStyle.green, custom_id="work_apply_pm_persistent")
|
||||||
async def work_apply_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
async def work_apply_button(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||||
@@ -170,6 +169,7 @@ class Hiring(commands.Cog):
|
|||||||
"""
|
"""
|
||||||
def __init__(self, bot: "Red"):
|
def __init__(self, bot: "Red"):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
self.config = Config.get_conf(self, identifier=1234567891, force_registration=True)
|
||||||
default_guild = {
|
default_guild = {
|
||||||
"staff_category": None,
|
"staff_category": None,
|
||||||
"pm_category": None,
|
"pm_category": None,
|
||||||
@@ -178,15 +178,11 @@ class Hiring(commands.Cog):
|
|||||||
"closed_applications": {}
|
"closed_applications": {}
|
||||||
}
|
}
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
# We need to make sure the views are persistent so buttons work after a restart
|
|
||||||
self.bot.add_view(HireView(self))
|
|
||||||
self.bot.add_view(WorkView(self))
|
|
||||||
|
|
||||||
async def cog_load(self):
|
async def cog_load(self):
|
||||||
self.bot.add_view(HireView())
|
self.bot.add_view(HireView())
|
||||||
self.bot.add_view(WorkView())
|
self.bot.add_view(WorkView())
|
||||||
|
|
||||||
# --- Commands ---
|
|
||||||
@commands.hybrid_command() # type: ignore
|
@commands.hybrid_command() # type: ignore
|
||||||
@app_commands.guild_only()
|
@app_commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
@@ -232,7 +228,6 @@ class Hiring(commands.Cog):
|
|||||||
await ctx.send("I don't have permission to send messages in that channel.", ephemeral=True)
|
await ctx.send("I don't have permission to send messages in that channel.", ephemeral=True)
|
||||||
|
|
||||||
|
|
||||||
# --- Settings Commands ---
|
|
||||||
@commands.group(aliases=["hset"]) # type: ignore
|
@commands.group(aliases=["hset"]) # type: ignore
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
|
|||||||
@@ -3,92 +3,81 @@ from redbot.core import commands, Config
|
|||||||
from redbot.core.bot import Red
|
from redbot.core.bot import Red
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# --- Modals for the Commands ---
|
class OrderModal(discord.ui.Modal, title="Commission Order Form"):
|
||||||
|
commission_type = discord.ui.TextInput(label="What type of commission?")
|
||||||
|
payment_status = discord.ui.TextInput(label="Is this a Free or Paid commission?")
|
||||||
|
description = discord.ui.TextInput(label="Description", style=discord.TextStyle.paragraph)
|
||||||
|
questions = discord.ui.TextInput(label="Any questions?", style=discord.TextStyle.paragraph, required=False)
|
||||||
|
|
||||||
class OrderModal(discord.ui.Modal, title="Commission/Shop Order"):
|
|
||||||
def __init__(self, cog: "KofiShop"):
|
def __init__(self, cog: "KofiShop"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cog = cog
|
self.cog = cog
|
||||||
|
|
||||||
comm_type = discord.ui.TextInput(label="What type of commission/item is this?")
|
|
||||||
payment_status = discord.ui.TextInput(label="Is this free or paid?")
|
|
||||||
description = discord.ui.TextInput(label="Please describe your request.", style=discord.TextStyle.paragraph)
|
|
||||||
questions = discord.ui.TextInput(label="Any questions for the artist?", style=discord.TextStyle.paragraph, required=False)
|
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
if not interaction.guild:
|
guild = interaction.guild
|
||||||
return await interaction.response.send_message("This must be used in a server.", ephemeral=True)
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
order_channel_id = await self.cog.config.guild(interaction.guild).order_channel()
|
channel_id = await self.cog.config.guild(guild).order_channel()
|
||||||
if not order_channel_id:
|
if not channel_id:
|
||||||
return await interaction.response.send_message("The order channel has not been set by an admin.", ephemeral=True)
|
await interaction.response.send_message("Order channel not set.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
order_channel = interaction.guild.get_channel(order_channel_id)
|
channel = guild.get_channel(channel_id)
|
||||||
if not isinstance(order_channel, discord.TextChannel):
|
if not isinstance(channel, discord.TextChannel):
|
||||||
return await interaction.response.send_message("The configured order channel is invalid.", ephemeral=True)
|
await interaction.response.send_message("Invalid order channel.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(title=f"New Order from {interaction.user.name}", color=discord.Color.blurple())
|
||||||
title="New Order Placed",
|
embed.add_field(name="Commission Type", value=self.commission_type.value, inline=False)
|
||||||
description=f"Submitted by {interaction.user.mention}",
|
|
||||||
color=0x00ff00 # Green for new orders
|
|
||||||
)
|
|
||||||
embed.add_field(name="Item/Commission Type", value=self.comm_type.value, inline=False)
|
|
||||||
embed.add_field(name="Payment Status", value=self.payment_status.value, inline=False)
|
embed.add_field(name="Payment Status", value=self.payment_status.value, inline=False)
|
||||||
embed.add_field(name="Description", value=self.description.value, inline=False)
|
embed.add_field(name="Description", value=self.description.value, inline=False)
|
||||||
if self.questions.value:
|
if self.questions.value:
|
||||||
embed.add_field(name="Questions", value=self.questions.value, inline=False)
|
embed.add_field(name="Questions", value=self.questions.value, inline=False)
|
||||||
|
|
||||||
try:
|
await channel.send(embed=embed)
|
||||||
await order_channel.send(embed=embed)
|
await interaction.response.send_message("Your order has been submitted!", ephemeral=True)
|
||||||
await interaction.response.send_message("Your order has been successfully submitted!", ephemeral=True)
|
|
||||||
except discord.Forbidden:
|
class ReviewModal(discord.ui.Modal, title="Shop Review"):
|
||||||
await interaction.response.send_message("I don't have permission to send messages in the order channel.", ephemeral=True)
|
item_name = discord.ui.TextInput(label="What item/commission are you reviewing?")
|
||||||
|
rating = discord.ui.TextInput(label="Rating (out of 10)", max_length=2)
|
||||||
|
review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph, max_length=1000)
|
||||||
|
|
||||||
class ReviewModal(discord.ui.Modal, title="Leave a Review"):
|
|
||||||
def __init__(self, cog: "KofiShop"):
|
def __init__(self, cog: "KofiShop"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.cog = cog
|
self.cog = cog
|
||||||
|
|
||||||
item_name = discord.ui.TextInput(label="What item/commission are you reviewing?")
|
|
||||||
rating = discord.ui.TextInput(label="Rating (e.g., 10/10)")
|
|
||||||
review_text = discord.ui.TextInput(label="Your Review", style=discord.TextStyle.paragraph)
|
|
||||||
|
|
||||||
async def on_submit(self, interaction: discord.Interaction):
|
async def on_submit(self, interaction: discord.Interaction):
|
||||||
if not interaction.guild:
|
guild = interaction.guild
|
||||||
return await interaction.response.send_message("This must be used in a server.", ephemeral=True)
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
review_channel_id = await self.cog.config.guild(interaction.guild).review_channel()
|
channel_id = await self.cog.config.guild(guild).review_channel()
|
||||||
if not review_channel_id:
|
if not channel_id:
|
||||||
return await interaction.response.send_message("The review channel has not been set by an admin.", ephemeral=True)
|
await interaction.response.send_message("Review channel not set.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
review_channel = interaction.guild.get_channel(review_channel_id)
|
channel = guild.get_channel(channel_id)
|
||||||
if not isinstance(review_channel, discord.TextChannel):
|
if not isinstance(channel, discord.TextChannel):
|
||||||
return await interaction.response.send_message("The configured review channel is invalid.", ephemeral=True)
|
await interaction.response.send_message("Invalid review channel.", ephemeral=True)
|
||||||
|
return
|
||||||
|
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(title=f"New Review for {self.item_name.value}", color=discord.Color.gold())
|
||||||
title=f"New Review for: {self.item_name.value}",
|
embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
|
||||||
description=f"Submitted by {interaction.user.mention}",
|
embed.add_field(name="Rating", value=f"{self.rating.value}/10")
|
||||||
color=0xadd8e6 # Pastel Blue
|
|
||||||
)
|
|
||||||
embed.add_field(name="Rating", value=self.rating.value, inline=False)
|
|
||||||
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
embed.add_field(name="Review", value=self.review_text.value, inline=False)
|
||||||
|
|
||||||
try:
|
await channel.send(embed=embed)
|
||||||
await review_channel.send(embed=embed)
|
await interaction.response.send_message("Thank you for your review!", ephemeral=True)
|
||||||
await interaction.response.send_message("Thank you! Your review has been submitted.", ephemeral=True)
|
|
||||||
except discord.Forbidden:
|
|
||||||
await interaction.response.send_message("I don't have permission to send messages in the review channel.", ephemeral=True)
|
|
||||||
|
|
||||||
|
|
||||||
class KofiShop(commands.Cog):
|
class KofiShop(commands.Cog):
|
||||||
"""
|
"""
|
||||||
A cog to manage Ko-fi shop orders and reviews.
|
An interactive front-end for a Ko-fi store.
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, bot: "Red"):
|
||||||
def __init__(self, bot: Red):
|
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
self.config = Config.get_conf(self, identifier=5566778899, force_registration=True)
|
self.config = Config.get_conf(self, identifier=1234567894, force_registration=True)
|
||||||
|
|
||||||
default_guild = {
|
default_guild = {
|
||||||
"order_channel": None,
|
"order_channel": None,
|
||||||
"review_channel": None,
|
"review_channel": None,
|
||||||
@@ -96,21 +85,29 @@ class KofiShop(commands.Cog):
|
|||||||
}
|
}
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
# --- Commands ---
|
@app_commands.command()
|
||||||
@commands.hybrid_command()
|
@app_commands.guild_only()
|
||||||
@commands.guild_only()
|
async def order(self, interaction: discord.Interaction):
|
||||||
async def order(self, ctx: commands.Context):
|
"""Place an order for a commission."""
|
||||||
"""Place an order for a shop or commission item."""
|
await interaction.response.send_modal(OrderModal(self))
|
||||||
if not ctx.interaction:
|
|
||||||
return
|
|
||||||
# We pass `self` (the cog instance) to the modal
|
|
||||||
await ctx.interaction.response.send_modal(OrderModal(self))
|
|
||||||
|
|
||||||
@commands.hybrid_command()
|
@app_commands.command()
|
||||||
@commands.guild_only()
|
@app_commands.guild_only()
|
||||||
async def review(self, ctx: commands.Context):
|
async def review(self, interaction: discord.Interaction):
|
||||||
"""Leave a review for a completed shop or commission item."""
|
"""Leave a review for a completed commission."""
|
||||||
if not ctx.interaction:
|
await interaction.response.send_modal(ReviewModal(self))
|
||||||
|
|
||||||
|
@app_commands.command()
|
||||||
|
@app_commands.guild_only()
|
||||||
|
async def waitlist(self, interaction: discord.Interaction, *, item: str):
|
||||||
|
"""Add an item to the waitlist."""
|
||||||
|
guild = interaction.guild
|
||||||
|
if not guild:
|
||||||
|
return
|
||||||
|
|
||||||
|
channel_id = await self.config.guild(guild).waitlist_channel()
|
||||||
|
if not channel_id:
|
||||||
|
await interaction.response.send_message("Waitlist channel not set.", ephemeral=True)
|
||||||
return
|
return
|
||||||
await ctx.interaction.response.send_modal(ReviewModal(self))
|
await ctx.interaction.response.send_modal(ReviewModal(self))
|
||||||
|
|
||||||
@@ -132,44 +129,45 @@ class KofiShop(commands.Cog):
|
|||||||
|
|
||||||
message = f"**{item}** ིྀ {user.mention} ✧ in {ctx.channel.mention}"
|
message = f"**{item}** ིྀ {user.mention} ✧ in {ctx.channel.mention}"
|
||||||
|
|
||||||
try:
|
channel = guild.get_channel(channel_id)
|
||||||
await waitlist_channel.send(message)
|
if not isinstance(channel, discord.TextChannel):
|
||||||
await ctx.send(f"{user.mention} has been added to the waitlist for '{item}'.", ephemeral=True)
|
await interaction.response.send_message("Invalid waitlist channel.", ephemeral=True)
|
||||||
except discord.Forbidden:
|
return
|
||||||
await ctx.send(f"I don't have permission to send messages in the waitlist channel.", ephemeral=True)
|
|
||||||
|
embed = discord.Embed(description=f"{item} ིྀ {interaction.user.mention}✧ {interaction.channel.mention if isinstance(interaction.channel, discord.TextChannel) else ''}")
|
||||||
|
await channel.send(embed=embed)
|
||||||
|
await interaction.response.send_message("You have been added to the waitlist!", ephemeral=True)
|
||||||
|
|
||||||
# --- Settings Commands ---
|
|
||||||
@commands.group(aliases=["kset"]) # type: ignore
|
@commands.group(aliases=["kset"]) # type: ignore
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def kofiset(self, ctx: commands.Context):
|
async def kofiset(self, ctx: commands.Context):
|
||||||
"""
|
"""Configure KofiShop settings."""
|
||||||
Configure the KofiShop settings.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@kofiset.command(name="orderchannel")
|
@kofiset.command(name="orderchannel")
|
||||||
async def kofiset_order(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def set_order_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel where new orders will be sent."""
|
"""Set the channel for new orders."""
|
||||||
if not ctx.guild: return
|
if not ctx.guild:
|
||||||
|
return
|
||||||
await self.config.guild(ctx.guild).order_channel.set(channel.id)
|
await self.config.guild(ctx.guild).order_channel.set(channel.id)
|
||||||
await ctx.send(f"Order channel has been set to {channel.mention}.")
|
await ctx.send(f"Order channel set to {channel.mention}")
|
||||||
|
|
||||||
@kofiset.command(name="reviewchannel")
|
@kofiset.command(name="reviewchannel")
|
||||||
async def kofiset_review(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel where new reviews will be sent."""
|
"""Set the channel for new reviews."""
|
||||||
if not ctx.guild: return
|
if not ctx.guild:
|
||||||
|
return
|
||||||
await self.config.guild(ctx.guild).review_channel.set(channel.id)
|
await self.config.guild(ctx.guild).review_channel.set(channel.id)
|
||||||
await ctx.send(f"Review channel has been set to {channel.mention}.")
|
await ctx.send(f"Review channel set to {channel.mention}")
|
||||||
|
|
||||||
@kofiset.command(name="waitlistchannel")
|
@kofiset.command(name="waitlistchannel")
|
||||||
async def kofiset_waitlist(self, ctx: commands.Context, channel: discord.TextChannel):
|
async def set_waitlist_channel(self, ctx: commands.Context, channel: discord.TextChannel):
|
||||||
"""Set the channel where waitlist notifications will be sent."""
|
"""Set the channel for waitlist notifications."""
|
||||||
if not ctx.guild: return
|
if not ctx.guild:
|
||||||
|
return
|
||||||
await self.config.guild(ctx.guild).waitlist_channel.set(channel.id)
|
await self.config.guild(ctx.guild).waitlist_channel.set(channel.id)
|
||||||
await ctx.send(f"Waitlist channel has been set to {channel.mention}.")
|
await ctx.send(f"Waitlist channel set to {channel.mention}")
|
||||||
|
|
||||||
|
async def setup(bot: "Red"):
|
||||||
async def setup(bot: Red):
|
|
||||||
await bot.add_cog(KofiShop(bot))
|
await bot.add_cog(KofiShop(bot))
|
||||||
|
|
||||||
|
|||||||
@@ -1,218 +1,90 @@
|
|||||||
import discord
|
import discord
|
||||||
from redbot.core import commands, Config
|
import datetime
|
||||||
from redbot.core.bot import Red
|
from redbot.core import commands, Config, app_commands
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
class ModMail(commands.Cog):
|
class Modmail(commands.Cog):
|
||||||
"""
|
"""
|
||||||
A configurable, forum-based ModMail system.
|
A private, forum-based ModMail system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, bot: Red):
|
def __init__(self, bot):
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
# Initialize Red's Config system for storing settings per-server.
|
self.config = Config.get_conf(self, identifier=1234567890, force_registration=True)
|
||||||
self.config = Config.get_conf(self, identifier=9876543210, force_registration=True)
|
|
||||||
|
|
||||||
# Define the default settings for each server.
|
|
||||||
default_guild = {
|
default_guild = {
|
||||||
"modmail_forum": None, # The ID of the forum channel for tickets
|
"forum_channel": None,
|
||||||
"enabled": False, # Whether the system is on or off
|
"enabled": False,
|
||||||
"active_threads": {} # A dictionary to track {user_id: thread_id}
|
"active_threads": {},
|
||||||
|
"closed_threads": {} # NEW: To log closed tickets for purging
|
||||||
}
|
}
|
||||||
|
|
||||||
# Register the default settings.
|
|
||||||
self.config.register_guild(**default_guild)
|
self.config.register_guild(**default_guild)
|
||||||
|
|
||||||
|
# ... existing on_message listener ...
|
||||||
@commands.Cog.listener()
|
@commands.Cog.listener()
|
||||||
async def on_message(self, message: discord.Message):
|
async def on_message(self, message: discord.Message):
|
||||||
"""
|
|
||||||
This is the core event listener. It handles both incoming DMs from users
|
|
||||||
and outgoing replies from staff.
|
|
||||||
"""
|
|
||||||
# Ignore messages from bots to prevent loops.
|
|
||||||
if message.author.bot:
|
if message.author.bot:
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- Part 1: Handling DMs from Users ---
|
# --- User to Staff DM Logic ---
|
||||||
if isinstance(message.channel, discord.DMChannel):
|
if isinstance(message.channel, discord.DMChannel):
|
||||||
await self.handle_dm(message)
|
# ... existing user DM logic ...
|
||||||
|
pass
|
||||||
|
|
||||||
# --- Part 2: Handling Replies from Staff ---
|
# --- Staff to User Reply Logic ---
|
||||||
elif isinstance(message.channel, discord.Thread):
|
elif isinstance(message.channel, discord.Thread):
|
||||||
await self.handle_staff_reply(message)
|
# ... existing staff reply logic ...
|
||||||
|
pass
|
||||||
|
|
||||||
async def handle_dm(self, message: discord.Message):
|
@app_commands.command(name="close")
|
||||||
"""Handles messages sent directly to the bot."""
|
@app_commands.guild_only()
|
||||||
# Find a mutual server with the user.
|
async def modmail_close(self, interaction: discord.Interaction, *, reason: Optional[str] = "No reason provided."):
|
||||||
guild = next((g for g in self.bot.guilds if g.get_member(message.author.id)), None)
|
"""Close the current ModMail ticket."""
|
||||||
|
guild = interaction.guild
|
||||||
if not guild:
|
if not guild:
|
||||||
|
await interaction.response.send_message("This command can only be used in a server.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
settings = await self.config.guild(guild).all()
|
# NEW: Safely check if the interaction has a channel
|
||||||
if not settings["enabled"] or not settings["modmail_forum"]:
|
if not interaction.channel:
|
||||||
|
await interaction.response.send_message("This command cannot be used in this context.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
forum_channel = guild.get_channel(settings["modmail_forum"])
|
# ... existing logic to check if it's a modmail thread ...
|
||||||
if not isinstance(forum_channel, discord.ForumChannel):
|
|
||||||
return
|
|
||||||
|
|
||||||
active_threads = settings["active_threads"]
|
thread_id_str = str(interaction.channel.id)
|
||||||
user_id_str = str(message.author.id)
|
async with self.config.guild(guild).active_threads() as active_threads:
|
||||||
|
user_id = active_threads.pop(thread_id_str, None)
|
||||||
|
|
||||||
# Check if the user already has an active thread.
|
if user_id:
|
||||||
if user_id_str in active_threads:
|
# NEW: Log the closed thread with a timestamp
|
||||||
thread_id = active_threads[user_id_str]
|
async with self.config.guild(guild).closed_threads() as closed_threads:
|
||||||
thread = guild.get_thread(thread_id)
|
closed_threads[thread_id_str] = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||||
if thread:
|
|
||||||
# Relay the message to the existing thread.
|
|
||||||
await thread.send(f"**{message.author.display_name}:** {message.content}")
|
|
||||||
await message.add_reaction("✅")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
# The thread was deleted, so we clean up our records.
|
|
||||||
async with self.config.guild(guild).active_threads() as threads:
|
|
||||||
del threads[user_id_str]
|
|
||||||
|
|
||||||
# Create a new thread for the user.
|
# ... existing logic to send final message and archive thread ...
|
||||||
try:
|
user = self.bot.get_user(int(user_id))
|
||||||
thread_name = f"ModMail | {message.author.name}"
|
if user:
|
||||||
embed = discord.Embed(
|
embed = discord.Embed(
|
||||||
title=f"New ModMail Thread",
|
title="Ticket Closed",
|
||||||
description=f"**User:** {message.author.mention} (`{message.author.id}`)",
|
description=f"Your ModMail ticket has been closed.\n**Reason:** {reason}",
|
||||||
color=0xadd8e6 # Light grey pastel blue
|
color=0x8b9ed7 # Pastel Blue
|
||||||
)
|
)
|
||||||
embed.add_field(name="Initial Message", value=message.content, inline=False)
|
try:
|
||||||
embed.set_footer(text="Staff can reply in this thread to send a message.")
|
await user.send(embed=embed)
|
||||||
|
except discord.Forbidden:
|
||||||
|
pass
|
||||||
|
|
||||||
thread_with_message = await forum_channel.create_thread(name=thread_name, embed=embed)
|
await interaction.response.send_message("Ticket has been closed and archived.", ephemeral=True)
|
||||||
thread = thread_with_message.thread
|
if isinstance(interaction.channel, discord.Thread):
|
||||||
|
await interaction.channel.edit(archived=True, locked=True)
|
||||||
|
else:
|
||||||
|
await interaction.response.send_message("This does not appear to be an active ModMail ticket.", ephemeral=True)
|
||||||
|
|
||||||
async with self.config.guild(guild).active_threads() as threads:
|
|
||||||
threads[user_id_str] = thread.id
|
|
||||||
|
|
||||||
await message.channel.send("Your message has been received, and a ModMail ticket has been opened. Staff will be with you shortly.")
|
|
||||||
await message.add_reaction("✅")
|
|
||||||
except discord.Forbidden:
|
|
||||||
print(f"ModMail: I don't have permission to create threads in {forum_channel.name}.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"ModMail: An unexpected error occurred: {e}")
|
|
||||||
|
|
||||||
async def handle_staff_reply(self, message: discord.Message):
|
|
||||||
"""Handles messages sent by staff inside a ModMail thread."""
|
|
||||||
guild = message.guild
|
|
||||||
if not guild:
|
|
||||||
return
|
|
||||||
|
|
||||||
active_threads = await self.config.guild(guild).active_threads()
|
|
||||||
|
|
||||||
# Find which user this thread belongs to by checking our records.
|
|
||||||
thread_id_str = str(message.channel.id)
|
|
||||||
user_id = None
|
|
||||||
for uid, tid in active_threads.items():
|
|
||||||
if str(tid) == thread_id_str:
|
|
||||||
user_id = int(uid)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not user_id:
|
|
||||||
# This is a regular thread, not a ModMail thread we're tracking.
|
|
||||||
return
|
|
||||||
|
|
||||||
user = guild.get_member(user_id)
|
|
||||||
if not user:
|
|
||||||
# User might have left the server.
|
|
||||||
await message.channel.send("⚠️ **Error:** Could not find the user. They may have left the server.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Send the staff's message to the user's DMs.
|
|
||||||
try:
|
|
||||||
embed = discord.Embed(
|
|
||||||
description=message.content,
|
|
||||||
color=0xadd8e6 # Light grey pastel blue
|
|
||||||
)
|
|
||||||
embed.set_author(name="Staff Response") # Anonymize the staff member
|
|
||||||
|
|
||||||
await user.send(embed=embed)
|
|
||||||
await message.add_reaction("📨") # Add a mail icon to show it was sent
|
|
||||||
except discord.Forbidden:
|
|
||||||
await message.channel.send("⚠️ **Error:** I could not send a DM to this user. They may have DMs disabled.")
|
|
||||||
except Exception as e:
|
|
||||||
await message.channel.send(f"⚠️ **Error:** An unexpected error occurred: {e}")
|
|
||||||
|
|
||||||
|
|
||||||
# --- Settings and Management Commands ---
|
|
||||||
@commands.group(aliases=["mmset"]) # type: ignore
|
@commands.group(aliases=["mmset"]) # type: ignore
|
||||||
@commands.guild_only()
|
@commands.guild_only()
|
||||||
@commands.admin_or_permissions(manage_guild=True)
|
@commands.admin_or_permissions(manage_guild=True)
|
||||||
async def modmailset(self, ctx: commands.Context):
|
async def modmailset(self, ctx: commands.Context):
|
||||||
"""
|
"""Configure ModMail settings."""
|
||||||
Configure the ModMail settings for this server.
|
|
||||||
"""
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@modmailset.command(name="forum")
|
# ... existing modmailset subcommands ...
|
||||||
async def modmailset_forum(self, ctx: commands.Context, channel: discord.ForumChannel):
|
|
||||||
"""Set the forum channel where ModMail tickets will be created."""
|
|
||||||
if not ctx.guild:
|
|
||||||
return
|
|
||||||
await self.config.guild(ctx.guild).modmail_forum.set(channel.id)
|
|
||||||
await ctx.send(f"The ModMail forum has been set to {channel.mention}.")
|
|
||||||
|
|
||||||
@modmailset.command(name="toggle")
|
|
||||||
async def modmailset_toggle(self, ctx: commands.Context):
|
|
||||||
"""Enable or disable the ModMail system on this server."""
|
|
||||||
if not ctx.guild:
|
|
||||||
return
|
|
||||||
current_status = await self.config.guild(ctx.guild).enabled()
|
|
||||||
new_status = not current_status
|
|
||||||
await self.config.guild(ctx.guild).enabled.set(new_status)
|
|
||||||
status_text = "enabled" if new_status else "disabled"
|
|
||||||
await ctx.send(f"The ModMail system has been {status_text}.")
|
|
||||||
|
|
||||||
@modmailset.command(name="close")
|
|
||||||
async def modmailset_close(self, ctx: commands.Context, *, reason: Optional[str] = "No reason provided."):
|
|
||||||
"""
|
|
||||||
Close the current ModMail thread.
|
|
||||||
|
|
||||||
You must run this command inside the thread you wish to close.
|
|
||||||
"""
|
|
||||||
if not ctx.guild or not isinstance(ctx.channel, discord.Thread):
|
|
||||||
await ctx.send("This command can only be run inside a ModMail thread.")
|
|
||||||
return
|
|
||||||
|
|
||||||
active_threads = await self.config.guild(ctx.guild).active_threads()
|
|
||||||
thread_id_str = str(ctx.channel.id)
|
|
||||||
user_id = None
|
|
||||||
for uid, tid in active_threads.items():
|
|
||||||
if str(tid) == thread_id_str:
|
|
||||||
user_id = int(uid)
|
|
||||||
break
|
|
||||||
|
|
||||||
if not user_id:
|
|
||||||
await ctx.send("This does not appear to be an active ModMail thread.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Clean up our records.
|
|
||||||
async with self.config.guild(ctx.guild).active_threads() as threads:
|
|
||||||
del threads[str(user_id)]
|
|
||||||
|
|
||||||
# Notify the user.
|
|
||||||
user = self.bot.get_user(user_id)
|
|
||||||
if user:
|
|
||||||
try:
|
|
||||||
embed = discord.Embed(
|
|
||||||
title="ModMail Ticket Closed",
|
|
||||||
description=f"Your ticket has been closed by staff.\n\n**Reason:** {reason}",
|
|
||||||
color=0xadd8e6
|
|
||||||
)
|
|
||||||
await user.send(embed=embed)
|
|
||||||
except discord.Forbidden:
|
|
||||||
pass # Can't notify user if DMs are closed
|
|
||||||
|
|
||||||
# Archive the thread.
|
|
||||||
await ctx.send(f"Ticket closed by {ctx.author.mention}. Archiving thread...")
|
|
||||||
await ctx.channel.edit(archived=True, locked=True)
|
|
||||||
|
|
||||||
# This required function allows Red to load the cog.
|
|
||||||
async def setup(bot: Red):
|
|
||||||
await bot.add_cog(ModMail(bot))
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user