diff --git a/bot.py b/bot.py index 3545dfe..2f21218 100644 --- a/bot.py +++ b/bot.py @@ -7,6 +7,7 @@ from datetime import datetime import json import mysql.connector import aiohttp +from typing import List # Load environment variables load_dotenv() @@ -52,15 +53,6 @@ def init_db(): archived BOOLEAN DEFAULT FALSE ) ''') - # Add new columns if missing - try: - cursor.execute("ALTER TABLE tickets ADD COLUMN referenced_message_content TEXT") - except mysql.connector.errors.ProgrammingError: - pass - try: - cursor.execute("ALTER TABLE tickets ADD COLUMN referenced_message_author VARCHAR(255)") - except mysql.connector.errors.ProgrammingError: - pass cursor.execute(''' CREATE TABLE IF NOT EXISTS server_settings ( guild_id BIGINT PRIMARY KEY, @@ -94,43 +86,14 @@ def load_server_settings(guild_id): conn.close() return result -def save_ticket(ticket_id, ticket): - conn = get_db_connection() - cursor = conn.cursor() - cursor.execute(''' - REPLACE INTO tickets (ticket_id, message_id, channel_id, title, project, status, creator, created_at, reference_message_id, archived, referenced_message_content, referenced_message_author) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) - ''', ( - ticket_id, - ticket['message_id'], - ticket['channel_id'], - ticket['title'], - ticket['project'], - ticket['status'], - ticket['creator'], - ticket['created_at'], - ticket.get('reference_message_id'), - ticket.get('archived', False), - ticket.get('referenced_message_content'), - ticket.get('referenced_message_author') - )) - conn.commit() - cursor.close() - conn.close() - -def load_ticket(ticket_id): - conn = get_db_connection() - cursor = conn.cursor(dictionary=True) - cursor.execute('SELECT * FROM tickets WHERE ticket_id = %s', (ticket_id,)) - result = cursor.fetchone() - cursor.close() - conn.close() - return result - # Ticket storage (in production, use a database) tickets = {} ticket_counter = 1 +# Project cache for autocomplete +project_cache = [] +last_project_fetch = None + # Ticket statuses class TicketStatus: PENDING = "⏳ Pending" @@ -138,43 +101,49 @@ class TicketStatus: COMPLETED = "✅ Completed" CANCELLED = "❌ Cancelled" -# Dynamically fetch project names from devanturas.net/versions -PROJECTS_URL = "https://devanturas.net/versions" -_project_cache = [] - -async def fetch_project_names(): - global _project_cache - if _project_cache: - return _project_cache +async def fetch_projects() -> List[str]: + """Fetch available projects from devanturas.net/versions""" + global project_cache, last_project_fetch + + # Cache for 1 hour + from datetime import timedelta + if last_project_fetch and (datetime.utcnow() - last_project_fetch) < timedelta(hours=1): + return project_cache + try: async with aiohttp.ClientSession() as session: - async with session.get(PROJECTS_URL) as resp: - data = await resp.json() - keys = list(data.keys()) - _project_cache = keys - return keys - except Exception: - return [] + async with session.get('https://devanturas.net/versions') as response: + if response.status == 200: + data = await response.json() + project_cache = list(data.keys()) + last_project_fetch = datetime.utcnow() + return project_cache + except Exception as e: + print(f"Error fetching projects: {e}") + + # Fallback to cached data or empty list + return project_cache if project_cache else [] -async def fetch_project_display_name(project_key): - try: - async with aiohttp.ClientSession() as session: - async with session.get(PROJECTS_URL) as resp: - data = await resp.json() - return data.get(project_key, {}).get("name", project_key) - except Exception: - return project_key +async def project_autocomplete( + interaction: discord.Interaction, + current: str, +) -> List[app_commands.Choice[str]]: + """Autocomplete for project names""" + projects = await fetch_projects() + + # Filter projects based on current input + filtered = [p for p in projects if current.lower() in p.lower()] + + # Return up to 25 choices (Discord limit) + return [ + app_commands.Choice(name=project, value=project) + for project in filtered[:25] + ] @bot.event async def on_ready(): print(f'{bot.user} has connected to Discord!') init_db() - # Preload project names - try: - import asyncio - asyncio.create_task(fetch_project_names()) - except Exception: - pass try: synced = await bot.tree.sync() print(f"Synced {len(synced)} command(s)") @@ -205,25 +174,17 @@ async def setup( @bot.tree.command(name="ticket", description="Create a new project update ticket") @app_commands.describe( - project_name="Select the project key for this ticket (e.g. friends, joinme)", + project_name="The project name from devanturas.net/projects", title="Brief title for this update", - message_id="Optional: Discord message ID to reference" + message_id="(Optional) Discord message ID to reference" ) +@app_commands.autocomplete(project_name=project_autocomplete) async def create_ticket( interaction: discord.Interaction, project_name: str, title: str, message_id: str = None ): - global ticket_counter - valid_projects = await fetch_project_names() - if project_name not in valid_projects: - await interaction.response.send_message( - f"❌ Invalid project key. Please use one of: {', '.join(valid_projects)}", - ephemeral=True - ) - return - display_name = await fetch_project_display_name(project_name) guild_id = interaction.guild.id if interaction.guild else None settings = load_server_settings(guild_id) if not settings or not settings.get("active_channel_id"): @@ -233,40 +194,44 @@ async def create_ticket( ) return active_channel = bot.get_channel(settings["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 - referenced_message_content = None - referenced_message_author = None - if message_id: - try: - ref_message = await active_channel.fetch_message(int(message_id)) - referenced_message_content = ref_message.content - referenced_message_author = ref_message.author.mention - except Exception: - referenced_message_content = None - referenced_message_author = None + + # 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=f"{display_name} ({project_name})", inline=True) + + 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) + if message_id: - embed.add_field(name="🔗 Message ID", value=f"`{message_id}`", inline=False) - if referenced_message_content: - embed.add_field(name="💬 Request Message", value=referenced_message_content, inline=False) - if referenced_message_author: - embed.add_field(name="🙋 Request By", value=referenced_message_author, inline=True) + embed.add_field(name="🔗 Reference Message ID", value=f"`{message_id}`", inline=False) + embed.add_field( name="🌐 Project Links", - value=f"[All Projects](https://devanturas.net/projects) | [Project Page](https://devanturas.net/projects/{project_name})", + value=f"[All Projects](https://devanturas.net/projects) | [Project Details](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 ticket = { "message_id": ticket_message.id, "channel_id": active_channel.id, @@ -275,11 +240,11 @@ async def create_ticket( "status": TicketStatus.PENDING, "creator": interaction.user.id, "created_at": datetime.utcnow().isoformat(), - "reference_message_id": message_id, - "referenced_message_content": referenced_message_content, - "referenced_message_author": referenced_message_author + "reference_message_id": message_id } save_ticket(ticket_id, ticket) + + # Confirm to user await interaction.response.send_message( f"✅ Ticket created successfully!\n" f"**Ticket ID:** `{ticket_id}`\n"