diff --git a/hiring/hiring.py b/hiring/hiring.py index d60d027..94b9dba 100644 --- a/hiring/hiring.py +++ b/hiring/hiring.py @@ -131,13 +131,9 @@ async def create_ticket(interaction: discord.Interaction, ticket_type: str, moda if not interaction.response.is_done(): await interaction.response.send_message(f"An unexpected error occurred: {e}", ephemeral=True) - -# --- Button Views for Commands --- - class HireView(discord.ui.View): def __init__(self): super().__init__(timeout=None) - self.cog = cog @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): @@ -155,7 +151,10 @@ class HireView(discord.ui.View): class WorkView(discord.ui.View): def __init__(self): 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") 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"): self.bot = bot + self.config = Config.get_conf(self, identifier=1234567891, force_registration=True) default_guild = { "staff_category": None, "pm_category": None, @@ -178,15 +178,11 @@ class Hiring(commands.Cog): "closed_applications": {} } 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): self.bot.add_view(HireView()) self.bot.add_view(WorkView()) - # --- Commands --- @commands.hybrid_command() # type: ignore @app_commands.guild_only() @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) - # --- Settings Commands --- @commands.group(aliases=["hset"]) # type: ignore @commands.guild_only() @commands.admin_or_permissions(manage_guild=True) diff --git a/kofishop/kofishop.py b/kofishop/kofishop.py index 04334d4..d31d1b6 100644 --- a/kofishop/kofishop.py +++ b/kofishop/kofishop.py @@ -3,92 +3,81 @@ from redbot.core import commands, Config from redbot.core.bot import Red 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"): super().__init__() 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): - if not interaction.guild: - return await interaction.response.send_message("This must be used in a server.", ephemeral=True) + guild = interaction.guild + if not guild: + return - order_channel_id = await self.cog.config.guild(interaction.guild).order_channel() - if not order_channel_id: - return await interaction.response.send_message("The order channel has not been set by an admin.", ephemeral=True) + channel_id = await self.cog.config.guild(guild).order_channel() + if not channel_id: + await interaction.response.send_message("Order channel not set.", ephemeral=True) + return - order_channel = interaction.guild.get_channel(order_channel_id) - if not isinstance(order_channel, discord.TextChannel): - return await interaction.response.send_message("The configured order channel is invalid.", ephemeral=True) + channel = guild.get_channel(channel_id) + if not isinstance(channel, discord.TextChannel): + await interaction.response.send_message("Invalid order channel.", ephemeral=True) + return - embed = discord.Embed( - title="New Order Placed", - 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 = discord.Embed(title=f"New Order from {interaction.user.name}", color=discord.Color.blurple()) + embed.add_field(name="Commission Type", value=self.commission_type.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) if self.questions.value: embed.add_field(name="Questions", value=self.questions.value, inline=False) - try: - await order_channel.send(embed=embed) - 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) + await channel.send(embed=embed) + await interaction.response.send_message("Your order has been submitted!", 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"): super().__init__() 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): - if not interaction.guild: - return await interaction.response.send_message("This must be used in a server.", ephemeral=True) + guild = interaction.guild + if not guild: + return - review_channel_id = await self.cog.config.guild(interaction.guild).review_channel() - if not review_channel_id: - return await interaction.response.send_message("The review channel has not been set by an admin.", ephemeral=True) + channel_id = await self.cog.config.guild(guild).review_channel() + if not channel_id: + await interaction.response.send_message("Review channel not set.", ephemeral=True) + return - review_channel = interaction.guild.get_channel(review_channel_id) - if not isinstance(review_channel, discord.TextChannel): - return await interaction.response.send_message("The configured review channel is invalid.", ephemeral=True) + channel = guild.get_channel(channel_id) + if not isinstance(channel, discord.TextChannel): + await interaction.response.send_message("Invalid review channel.", ephemeral=True) + return - embed = discord.Embed( - title=f"New Review for: {self.item_name.value}", - description=f"Submitted by {interaction.user.mention}", - color=0xadd8e6 # Pastel Blue - ) - embed.add_field(name="Rating", value=self.rating.value, inline=False) + embed = discord.Embed(title=f"New Review for {self.item_name.value}", color=discord.Color.gold()) + embed.set_author(name=interaction.user.name, icon_url=interaction.user.display_avatar.url) + embed.add_field(name="Rating", value=f"{self.rating.value}/10") embed.add_field(name="Review", value=self.review_text.value, inline=False) - try: - await review_channel.send(embed=embed) - 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) + await channel.send(embed=embed) + await interaction.response.send_message("Thank you for your review!", ephemeral=True) 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.config = Config.get_conf(self, identifier=5566778899, force_registration=True) - + self.config = Config.get_conf(self, identifier=1234567894, force_registration=True) default_guild = { "order_channel": None, "review_channel": None, @@ -96,21 +85,29 @@ class KofiShop(commands.Cog): } self.config.register_guild(**default_guild) - # --- Commands --- - @commands.hybrid_command() - @commands.guild_only() - async def order(self, ctx: commands.Context): - """Place an order for a shop or commission item.""" - if not ctx.interaction: - return - # We pass `self` (the cog instance) to the modal - await ctx.interaction.response.send_modal(OrderModal(self)) + @app_commands.command() + @app_commands.guild_only() + async def order(self, interaction: discord.Interaction): + """Place an order for a commission.""" + await interaction.response.send_modal(OrderModal(self)) - @commands.hybrid_command() - @commands.guild_only() - async def review(self, ctx: commands.Context): - """Leave a review for a completed shop or commission item.""" - if not ctx.interaction: + @app_commands.command() + @app_commands.guild_only() + async def review(self, interaction: discord.Interaction): + """Leave a review for a completed commission.""" + 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 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}" - try: - await waitlist_channel.send(message) - await ctx.send(f"{user.mention} has been added to the waitlist for '{item}'.", ephemeral=True) - except discord.Forbidden: - await ctx.send(f"I don't have permission to send messages in the waitlist channel.", ephemeral=True) + channel = guild.get_channel(channel_id) + if not isinstance(channel, discord.TextChannel): + await interaction.response.send_message("Invalid waitlist channel.", ephemeral=True) + return + + 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.guild_only() @commands.admin_or_permissions(manage_guild=True) async def kofiset(self, ctx: commands.Context): - """ - Configure the KofiShop settings. - """ + """Configure KofiShop settings.""" pass @kofiset.command(name="orderchannel") - async def kofiset_order(self, ctx: commands.Context, channel: discord.TextChannel): - """Set the channel where new orders will be sent.""" - if not ctx.guild: return + async def set_order_channel(self, ctx: commands.Context, channel: discord.TextChannel): + """Set the channel for new orders.""" + if not ctx.guild: + return 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") - async def kofiset_review(self, ctx: commands.Context, channel: discord.TextChannel): - """Set the channel where new reviews will be sent.""" - if not ctx.guild: return + async def set_review_channel(self, ctx: commands.Context, channel: discord.TextChannel): + """Set the channel for new reviews.""" + if not ctx.guild: + return 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") - async def kofiset_waitlist(self, ctx: commands.Context, channel: discord.TextChannel): - """Set the channel where waitlist notifications will be sent.""" - if not ctx.guild: return + async def set_waitlist_channel(self, ctx: commands.Context, channel: discord.TextChannel): + """Set the channel for waitlist notifications.""" + if not ctx.guild: + return 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)) - diff --git a/modmail/modmail.py b/modmail/modmail.py index 8c84793..f319e36 100644 --- a/modmail/modmail.py +++ b/modmail/modmail.py @@ -1,218 +1,90 @@ import discord -from redbot.core import commands, Config -from redbot.core.bot import Red +import datetime +from redbot.core import commands, Config, app_commands 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 - # Initialize Red's Config system for storing settings per-server. - self.config = Config.get_conf(self, identifier=9876543210, force_registration=True) - - # Define the default settings for each server. + self.config = Config.get_conf(self, identifier=1234567890, force_registration=True) default_guild = { - "modmail_forum": None, # The ID of the forum channel for tickets - "enabled": False, # Whether the system is on or off - "active_threads": {} # A dictionary to track {user_id: thread_id} + "forum_channel": None, + "enabled": False, + "active_threads": {}, + "closed_threads": {} # NEW: To log closed tickets for purging } - - # Register the default settings. self.config.register_guild(**default_guild) + # ... existing on_message listener ... @commands.Cog.listener() 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: return - # --- Part 1: Handling DMs from Users --- + # --- User to Staff DM Logic --- if isinstance(message.channel, discord.DMChannel): - await self.handle_dm(message) - - # --- Part 2: Handling Replies from Staff --- + # ... existing user DM logic ... + pass + + # --- Staff to User Reply Logic --- 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): - """Handles messages sent directly to the bot.""" - # Find a mutual server with the user. - guild = next((g for g in self.bot.guilds if g.get_member(message.author.id)), None) + @app_commands.command(name="close") + @app_commands.guild_only() + async def modmail_close(self, interaction: discord.Interaction, *, reason: Optional[str] = "No reason provided."): + """Close the current ModMail ticket.""" + guild = interaction.guild if not guild: + await interaction.response.send_message("This command can only be used in a server.", ephemeral=True) return - - settings = await self.config.guild(guild).all() - if not settings["enabled"] or not settings["modmail_forum"]: - return - - forum_channel = guild.get_channel(settings["modmail_forum"]) - if not isinstance(forum_channel, discord.ForumChannel): - return - - active_threads = settings["active_threads"] - user_id_str = str(message.author.id) - - # Check if the user already has an active thread. - if user_id_str in active_threads: - thread_id = active_threads[user_id_str] - thread = guild.get_thread(thread_id) - 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. - try: - thread_name = f"ModMail | {message.author.name}" - embed = discord.Embed( - title=f"New ModMail Thread", - description=f"**User:** {message.author.mention} (`{message.author.id}`)", - color=0xadd8e6 # Light grey 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 + # NEW: Safely check if the interaction has a channel + if not interaction.channel: + await interaction.response.send_message("This command cannot be used in this context.", ephemeral=True) + return - if not user_id: - # This is a regular thread, not a ModMail thread we're tracking. - return + # ... existing logic to check if it's a modmail thread ... - 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 + thread_id_str = str(interaction.channel.id) + async with self.config.guild(guild).active_threads() as active_threads: + user_id = active_threads.pop(thread_id_str, None) + + if user_id: + # NEW: Log the closed thread with a timestamp + async with self.config.guild(guild).closed_threads() as closed_threads: + closed_threads[thread_id_str] = datetime.datetime.now(datetime.timezone.utc).isoformat() - 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}") + # ... existing logic to send final message and archive thread ... + user = self.bot.get_user(int(user_id)) + if user: + embed = discord.Embed( + title="Ticket Closed", + description=f"Your ModMail ticket has been closed.\n**Reason:** {reason}", + color=0x8b9ed7 # Pastel Blue + ) + try: + await user.send(embed=embed) + except discord.Forbidden: + pass + + 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.guild_only() @commands.admin_or_permissions(manage_guild=True) async def modmailset(self, ctx: commands.Context): - """ - Configure the ModMail settings for this server. - """ + """Configure ModMail settings.""" pass - @modmailset.command(name="forum") - 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)) + # ... existing modmailset subcommands ...