import discord from discord import app_commands from discord.ext import commands import os from dotenv import load_dotenv from datetime import datetime import json # Load environment variables load_dotenv() TOKEN = os.getenv('DISCORD_TOKEN') # Bot setup intents = discord.Intents.default() intents.message_content = True intents.guilds = True bot = commands.Bot(command_prefix='!', intents=intents) # Ticket storage (in production, use a database) tickets = {} ticket_counter = 1 # Ticket statuses class TicketStatus: PENDING = "⏳ Pending" IN_PROGRESS = "🔄 In Progress" COMPLETED = "✅ Completed" CANCELLED = "❌ Cancelled" # Channel IDs (configure these in config.json) config = {} def load_config(): global config try: with open('config.json', 'r') as f: config = json.load(f) except FileNotFoundError: config = { "active_channel_id": None, "archive_channel_id": None } save_config() def save_config(): with open('config.json', 'w') as f: json.dump(config, f, indent=4) @bot.event async def on_ready(): print(f'{bot.user} has connected to Discord!') load_config() try: synced = await bot.tree.sync() print(f"Synced {len(synced)} command(s)") except Exception as e: print(f"Error syncing commands: {e}") @bot.tree.command(name="setup", description="Setup the bot channels") @app_commands.describe( active_channel="The channel for active tickets", archive_channel="The channel for archived tickets" ) async def setup( interaction: discord.Interaction, active_channel: discord.TextChannel, archive_channel: discord.TextChannel ): """Setup the channels for ticket management""" config["active_channel_id"] = active_channel.id config["archive_channel_id"] = archive_channel.id save_config() await interaction.response.send_message( f"✅ Configuration saved!\n" f"Active tickets: {active_channel.mention}\n" f"Archive: {archive_channel.mention}", ephemeral=True ) @bot.tree.command(name="ticket", description="Create a new project update ticket") @app_commands.describe( message_id="The Discord message ID to reference", project_name="The project name from devanturas.net/versions", title="Brief title for this update" ) async def create_ticket( interaction: discord.Interaction, message_id: str, project_name: str, title: str ): """Create a new ticket for project updates""" global ticket_counter # Check if channels are configured if not config.get("active_channel_id"): await interaction.response.send_message( "❌ Bot not configured! Please use `/setup` first.", ephemeral=True ) return # Get the active channel active_channel = bot.get_channel(config["active_channel_id"]) if not active_channel: await interaction.response.send_message( "❌ Active channel not found! Please reconfigure with `/setup`.", ephemeral=True ) return # Create ticket ID ticket_id = f"TICKET-{ticket_counter:04d}" ticket_counter += 1 # Create embed for the ticket embed = discord.Embed( title=f"🎫 {ticket_id}: {title}", description=f"**Project Update Request**", color=discord.Color.blue(), timestamp=datetime.utcnow() ) embed.add_field(name="📋 Project", value=project_name, inline=True) embed.add_field(name="📊 Status", value=TicketStatus.PENDING, inline=True) embed.add_field(name="👤 Created By", value=interaction.user.mention, inline=True) embed.add_field(name="🔗 Message ID", value=f"`{message_id}`", inline=False) embed.add_field( name="🌐 Project Links", value=f"[All Projects](https://devanturas.net/versions) | [Project Page](https://devanturas.net/projects/{project_name})", inline=False ) embed.set_footer(text=f"Ticket ID: {ticket_id}") # Send to active channel ticket_message = await active_channel.send(embed=embed) # Store ticket data tickets[ticket_id] = { "message_id": ticket_message.id, "channel_id": active_channel.id, "title": title, "project": project_name, "status": TicketStatus.PENDING, "creator": interaction.user.id, "created_at": datetime.utcnow().isoformat(), "reference_message_id": message_id } # Confirm to user await interaction.response.send_message( f"✅ Ticket created successfully!\n" f"**Ticket ID:** `{ticket_id}`\n" f"**Channel:** {active_channel.mention}\n" f"Use `/status {ticket_id}` to update the status.", ephemeral=True ) @bot.tree.command(name="status", description="Update ticket status") @app_commands.describe( ticket_id="The ticket ID (e.g., TICKET-0001)", new_status="The new status for the ticket" ) @app_commands.choices(new_status=[ app_commands.Choice(name="⏳ Pending", value="pending"), app_commands.Choice(name="🔄 In Progress", value="in_progress"), app_commands.Choice(name="✅ Completed", value="completed"), app_commands.Choice(name="❌ Cancelled", value="cancelled"), ]) async def update_status( interaction: discord.Interaction, ticket_id: str, new_status: str ): """Update the status of a ticket""" # Convert to uppercase for consistency ticket_id = ticket_id.upper() # Check if ticket exists if ticket_id not in tickets: await interaction.response.send_message( f"❌ Ticket `{ticket_id}` not found!", ephemeral=True ) return ticket = tickets[ticket_id] # Map status choice to display text status_map = { "pending": TicketStatus.PENDING, "in_progress": TicketStatus.IN_PROGRESS, "completed": TicketStatus.COMPLETED, "cancelled": TicketStatus.CANCELLED } old_status = ticket["status"] new_status_text = status_map[new_status] ticket["status"] = new_status_text # Get the ticket message channel = bot.get_channel(ticket["channel_id"]) if not channel: await interaction.response.send_message( "❌ Channel not found!", ephemeral=True ) return try: message = await channel.fetch_message(ticket["message_id"]) embed = message.embeds[0] # Update the status field for i, field in enumerate(embed.fields): if field.name == "📊 Status": embed.set_field_at(i, name="📊 Status", value=new_status_text, inline=True) break # Change color based on status if new_status == "completed": embed.color = discord.Color.green() elif new_status == "in_progress": embed.color = discord.Color.orange() elif new_status == "cancelled": embed.color = discord.Color.red() # Add status update log embed.add_field( name=f"📝 Status Updated", value=f"{interaction.user.mention} changed status from {old_status} to {new_status_text}", inline=False ) await message.edit(embed=embed) # If completed, archive the ticket if new_status == "completed": await archive_ticket(interaction, ticket_id, message, channel) else: await interaction.response.send_message( f"✅ Ticket `{ticket_id}` status updated to {new_status_text}", ephemeral=True ) except discord.NotFound: await interaction.response.send_message( "❌ Ticket message not found!", ephemeral=True ) except Exception as e: await interaction.response.send_message( f"❌ Error updating ticket: {str(e)}", ephemeral=True ) async def archive_ticket(interaction, ticket_id, message, current_channel): """Archive a completed ticket""" archive_channel_id = config.get("archive_channel_id") if not archive_channel_id: await interaction.response.send_message( "⚠️ Status updated, but no archive channel configured. Use `/setup` to configure.", ephemeral=True ) return archive_channel = bot.get_channel(archive_channel_id) if not archive_channel: await interaction.response.send_message( "⚠️ Archive channel not found!", ephemeral=True ) return # Copy message to archive embed = message.embeds[0] embed.add_field( name="📦 Archived", value=f"Moved to archive on {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}", inline=False ) await archive_channel.send(embed=embed) # Delete from active channel await message.delete() # Update ticket storage tickets[ticket_id]["channel_id"] = archive_channel.id tickets[ticket_id]["archived"] = True await interaction.response.send_message( f"✅ Ticket `{ticket_id}` marked as completed and archived to {archive_channel.mention}", ephemeral=True ) @bot.tree.command(name="list", description="List all active tickets") async def list_tickets(interaction: discord.Interaction): """List all active tickets""" active_tickets = [ (tid, t) for tid, t in tickets.items() if not t.get("archived", False) ] if not active_tickets: await interaction.response.send_message( "📭 No active tickets found.", ephemeral=True ) return embed = discord.Embed( title="🎫 Active Tickets", color=discord.Color.blue(), timestamp=datetime.utcnow() ) for ticket_id, ticket in active_tickets[:25]: # Discord limit embed.add_field( name=f"{ticket_id}: {ticket['title']}", value=f"Project: {ticket['project']}\nStatus: {ticket['status']}", inline=False ) if len(active_tickets) > 25: embed.set_footer(text=f"Showing 25 of {len(active_tickets)} tickets") await interaction.response.send_message(embed=embed, ephemeral=True) @bot.tree.command(name="info", description="Get detailed information about a ticket") @app_commands.describe(ticket_id="The ticket ID (e.g., TICKET-0001)") async def ticket_info(interaction: discord.Interaction, ticket_id: str): """Get detailed information about a specific ticket""" ticket_id = ticket_id.upper() if ticket_id not in tickets: await interaction.response.send_message( f"❌ Ticket `{ticket_id}` not found!", ephemeral=True ) return ticket = tickets[ticket_id] creator = await bot.fetch_user(ticket["creator"]) embed = discord.Embed( title=f"🎫 {ticket_id} Details", description=ticket["title"], color=discord.Color.blue() ) embed.add_field(name="📋 Project", value=ticket["project"], inline=True) embed.add_field(name="📊 Status", value=ticket["status"], inline=True) embed.add_field(name="👤 Creator", value=creator.mention, inline=True) embed.add_field(name="📅 Created", value=ticket["created_at"][:10], inline=True) embed.add_field(name="🔗 Reference Message", value=f"`{ticket['reference_message_id']}`", inline=False) embed.add_field( name="📦 Archived", value="Yes" if ticket.get("archived") else "No", inline=True ) await interaction.response.send_message(embed=embed, ephemeral=True) # Run the bot if __name__ == "__main__": if not TOKEN: print("ERROR: DISCORD_TOKEN not found in .env file!") else: bot.run(TOKEN)