6 Commits

Author SHA1 Message Date
acc7eaa861 Update info.json for RPG cog metadata clarity
Enhanced the metadata in `info.json` for the RPG cog by updating the install message, short description, and adding a detailed description of its functionality. Simplified requirements and permissions fields, expanded tags for better discoverability, adjusted the min Python version format, and clarified the end user data statement for transparency.
2025-09-23 03:45:27 -04:00
5a31115458 Improve modals and waitlist handling in KofiShop
Updated modal handling for order and review submissions with more user-friendly error messages. Added `waitlist_entries` to manage waitlist data. Refactored the `waitlist` command for better user mention handling and added error handling for permission issues. Overall enhancements improve functionality and user experience.
2025-09-23 03:26:32 -04:00
b35ecb52b7 Refactor imports in kofishop.py
Updated import statements for clarity and correctness.
Removed unnecessary imports and ensured `Red` is imported properly.
Added `Optional` from `typing` for potential future use.
2025-09-23 03:23:17 -04:00
7ea6a52a6c Refactor cogs and update author formatting
Updated `__init__.py` to import specific cog classes
and added asynchronous `setup` functions for each.
Modified `info.json` to improve the formatting of the
"author" field for better readability.
2025-09-23 03:18:09 -04:00
65ddb244fe Refactor work_apply_button method in WorkView
Re-added the work_apply_button method in the WorkView class without any changes to its functionality. The button for applying for the PM position retains its original properties, including label, style, and custom ID.
2025-09-23 02:43:57 -04:00
500c7daaae 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.
2025-09-23 02:40:35 -04:00
9 changed files with 203 additions and 314 deletions

View File

@@ -1,4 +1,4 @@
from .hiring import setup from .hiring import Hiring
# This function is required for the cog to be loaded by Red. async def setup(bot):
# It simply imports the setup function from the main cog file. await bot.add_cog(Hiring(bot))

View File

@@ -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,6 @@ 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") @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 +165,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 +174,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 +224,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)

View File

@@ -1 +1,4 @@
from .kofishop import setup from .kofishop import KofiShop
async def setup(bot):
await bot.add_cog(KofiShop(bot))

View File

@@ -1,126 +1,121 @@
import discord import discord
from redbot.core import commands, Config from redbot.core import commands, Config, app_commands
from typing import Optional, TYPE_CHECKING
import datetime
if TYPE_CHECKING:
from redbot.core.bot import Red from redbot.core.bot import Red
from typing import Optional
# --- Modals for the Commands --- # --- Modals for the Forms ---
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("The order channel has not been set by an admin.", 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("The configured order channel is invalid.", 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.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url)
description=f"Submitted by {interaction.user.mention}", embed.add_field(name="Commission Type", value=self.commission_type.value, inline=False)
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:
await interaction.response.send_message("I don't have permission to send messages in the order channel.", ephemeral=True) class ReviewModal(discord.ui.Modal, title="Shop Review"):
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("The review channel has not been set by an admin.", 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("The configured review channel is invalid.", 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)
# --- Main Cog Class ---
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,
"waitlist_channel": None "waitlist_channel": None,
"waitlist_entries": {} # For DataManager
} }
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(name="rev")
@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))
return
await ctx.interaction.response.send_modal(ReviewModal(self))
@commands.hybrid_command() # type: ignore @commands.hybrid_command() # type: ignore
@commands.guild_only() @app_commands.guild_only()
@commands.admin_or_permissions(manage_guild=True) async def waitlist(self, ctx: commands.Context, user: Optional[discord.Member], *, item: str):
async def waitlist(self, ctx: commands.Context, user: discord.Member, *, item: str):
"""Add a user and their requested item to the waitlist.""" """Add a user and their requested item to the waitlist."""
if not ctx.guild or not isinstance(ctx.channel, discord.TextChannel): if not ctx.guild:
return await ctx.send("This command must be used in a server's text channel.", ephemeral=True) return
target_user = user or ctx.author
waitlist_channel_id = await self.config.guild(ctx.guild).waitlist_channel() waitlist_channel_id = await self.config.guild(ctx.guild).waitlist_channel()
if not waitlist_channel_id: if not waitlist_channel_id:
@@ -130,46 +125,54 @@ class KofiShop(commands.Cog):
if not isinstance(waitlist_channel, discord.TextChannel): if not isinstance(waitlist_channel, discord.TextChannel):
return await ctx.send("The configured waitlist channel is invalid.", ephemeral=True) return await ctx.send("The configured waitlist channel is invalid.", ephemeral=True)
message = f"**{item}** ིྀ {user.mention} in {ctx.channel.mention}" message_to_send = f"**{item}** ིྀ {target_user.mention} ✧ in {ctx.channel.mention if isinstance(ctx.channel, discord.TextChannel) else 'this ticket'}"
try: try:
await waitlist_channel.send(message) sent_message = await waitlist_channel.send(message_to_send)
await ctx.send(f"{user.mention} has been added to the waitlist for '{item}'.", ephemeral=True) # For DataManager
except discord.Forbidden: async with self.config.guild(ctx.guild).waitlist_entries() as entries:
await ctx.send(f"I don't have permission to send messages in the waitlist channel.", ephemeral=True) entries[str(sent_message.id)] = datetime.datetime.now(datetime.timezone.utc).isoformat()
await ctx.send(f"{target_user.mention} has been added to the waitlist for '{item}'.", ephemeral=True)
# Also send confirmation to user's ticket if it's a ticket channel
if isinstance(ctx.channel, discord.TextChannel) and "ticket" in ctx.channel.name.lower():
await ctx.channel.send(f"You have been added to the waitlist for **{item}**.")
except discord.Forbidden:
await ctx.send("I do not have permission to send messages in the waitlist channel.", 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))

View File

@@ -1 +1,4 @@
from .modmail import setup from .modmail import Modmail
async def setup(bot):
await bot.add_cog(Modmail(bot))

View File

@@ -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)
embed.set_footer(text="Staff can reply in this thread to send a message.")
thread_with_message = await forum_channel.create_thread(name=thread_name, embed=embed)
thread = thread_with_message.thread
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: 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 user.send(embed=embed)
await message.add_reaction("📨") # Add a mail icon to show it was sent
except discord.Forbidden: except discord.Forbidden:
await message.channel.send("⚠️ **Error:** I could not send a DM to this user. They may have DMs disabled.") pass
except Exception as e:
await message.channel.send(f"⚠️ **Error:** An unexpected error occurred: {e}")
await interaction.response.send_message("Ticket has been closed and archived.", ephemeral=True)
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)
# --- 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))

View File

@@ -1,5 +1,6 @@
{ {
"author": ["kitsunic"], "author": [ "kitsunic" ],
"name": "PP",
"description": "A cog for password protected channels", "description": "A cog for password protected channels",
"end_user_data_statement": "This cog stores user, avatar, and guild data. A simple delete request to bot to remove your data or guild data", "end_user_data_statement": "This cog stores user, avatar, and guild data. A simple delete request to bot to remove your data or guild data",
"install_msg": "Thanks for installing & testing.", "install_msg": "Thanks for installing & testing.",

View File

@@ -1,11 +1,24 @@
{ {
"author" : ["UnstableKitsune (unstablekitsune)"], "author": [
"install _msg" : "Oh you installed me! How dare you obtain me! return me right now!", "UnstableKitsune (unstablekitsune)"
"name" : "RPG", ],
"short" : "Its a TTRPG", "install_msg": "Thank you for installing the RPG cog! Get ready for an adventure. Use the help command to see available actions.",
"requirements" : [""], "name": "RPG",
"permissions" : [""], "short": "A text-based tabletop role-playing game cog.",
"tags" : [""], "description": "A comprehensive TTRPG system allowing users to create characters, manage inventory, go on adventures, and interact with a game world, all within Discord.",
"min_python_version" : [3, 1, 1], "requirements": [],
"end_user_data_statement" : "" "permissions": [],
"tags": [
"rpg",
"game",
"ttrpg",
"fun",
"adventure"
],
"min_python_version": [
3,
1,
1
],
"end_user_data_statement": "This cog stores user data persistently. This includes character sheets (stats, inventory, etc.) and game progress tied to your Discord User ID."
} }

View File

@@ -1 +1,4 @@
from .welcomer import setup from .welcomer import Welcomer
async def setup(bot):
await bot.add_cog(Welcomer(bot))