From a0f35c24dd47739919ec0918d74e4468896dcb0f Mon Sep 17 00:00:00 2001 From: SimolZimol <70102430+SimolZimol@users.noreply.github.com> Date: Thu, 8 Jan 2026 19:01:30 +0100 Subject: [PATCH] new file: .env.example new file: .gitignore new file: Dockerfile new file: README.md new file: bot.py new file: requirements.txt --- .env.example | 3 + .gitignore | 26 ++++ Dockerfile | 26 ++++ README.md | 198 +++++++++++++++++++++++++ bot.py | 377 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 6 files changed, 633 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 bot.py create mode 100644 requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..2c93cad --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +# Discord Bot Token +# Get your token from https://discord.com/developers/applications +DISCORD_TOKEN=your_bot_token_here diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f072f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Environment variables +.env + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +env/ +ENV/ + +# Bot data +config.json +tickets.json + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..531bc9b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Base image with Python +FROM python:3.11-slim + +# Set working directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + libffi-dev \ + libnacl-dev \ + python3-dev \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy project files +COPY . . + +# Environment variables +ENV DISCORD_TOKEN=$DISCORD_TOKEN + +# Start the bot +CMD ["python", "bot.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..a9d4ab1 --- /dev/null +++ b/README.md @@ -0,0 +1,198 @@ +# Discord Ticket Management Bot + +A Discord bot for creating and managing project update tickets with integration to devanturas.net projects. + +## Features + +- ✅ Create tickets with slash commands +- 📋 Track project updates from devanturas.net +- 🔄 Update ticket status (Pending, In Progress, Completed, Cancelled) +- 📦 Automatic archiving of completed tickets +- 🎫 List and view all active tickets +- 📊 Beautiful embed messages with ticket information + +## Setup Instructions + +### 1. Create a Discord Bot + +1. Go to [Discord Developer Portal](https://discord.com/developers/applications) +2. Click "New Application" and give it a name +3. Go to the "Bot" section +4. Click "Add Bot" +5. Under "Privileged Gateway Intents", enable: + - MESSAGE CONTENT INTENT + - SERVER MEMBERS INTENT +6. Click "Reset Token" and copy your bot token + +### 2. Install Dependencies + +```bash +pip install -r requirements.txt +``` + +### 3. Configure the Bot + +1. Copy `.env.example` to `.env`: + ```bash + copy .env.example .env + ``` + +2. Edit `.env` and add your bot token: + ``` + DISCORD_TOKEN=your_bot_token_here + ``` + +### 4. Invite the Bot to Your Server + +1. In the Discord Developer Portal, go to "OAuth2" > "URL Generator" +2. Select scopes: + - `bot` + - `applications.commands` +3. Select bot permissions: + - Send Messages + - Embed Links + - Read Message History + - Manage Messages +4. Copy the generated URL and open it in your browser +5. Select your server and authorize + +### 5. Run the Bot + +```bash +python bot.py +``` + +### 6. Configure Channels + +In Discord, use the `/setup` command to configure the channels: + +``` +/setup active_channel:#active-tickets archive_channel:#archived-tickets +``` + +## Commands + +### `/ticket` - Create a New Ticket + +Create a new project update ticket. + +**Parameters:** +- `message_id`: Discord message ID to reference +- `project_name`: Project name from devanturas.net/versions +- `title`: Brief description of the update + +**Example:** +``` +/ticket message_id:123456789 project_name:MyProject title:Version 2.0 Update +``` + +### `/status` - Update Ticket Status + +Update the status of an existing ticket. + +**Parameters:** +- `ticket_id`: The ticket ID (e.g., TICKET-0001) +- `new_status`: Choose from: + - ⏳ Pending + - 🔄 In Progress + - ✅ Completed + - ❌ Cancelled + +**Example:** +``` +/status ticket_id:TICKET-0001 new_status:In Progress +``` + +**Note:** When a ticket is marked as "Completed", it will automatically be moved to the archive channel. + +### `/list` - View Active Tickets + +Lists all currently active (non-archived) tickets. + +**Example:** +``` +/list +``` + +### `/info` - Get Ticket Details + +Get detailed information about a specific ticket. + +**Parameters:** +- `ticket_id`: The ticket ID to view + +**Example:** +``` +/info ticket_id:TICKET-0001 +``` + +### `/setup` - Configure Bot Channels + +Configure the active and archive channels for tickets (Admin only). + +**Parameters:** +- `active_channel`: Channel for active tickets +- `archive_channel`: Channel for completed tickets + +**Example:** +``` +/setup active_channel:#active-tickets archive_channel:#archived-tickets +``` + +## Ticket Workflow + +1. **Create**: Use `/ticket` to create a new ticket in the active channel +2. **Update**: Use `/status` to update the ticket status as you work +3. **Complete**: Mark ticket as "Completed" to automatically archive it +4. **Archive**: Completed tickets are moved to the archive channel + +## Project Integration + +The bot integrates with devanturas.net: +- All projects: https://devanturas.net/versions +- Specific project: https://devanturas.net/projects/{project_name} + +Each ticket embed includes direct links to the project pages. + +## Data Storage + +Currently, tickets are stored in memory. For production use, consider implementing: +- Database storage (SQLite, PostgreSQL, etc.) +- Persistent ticket data between restarts +- Backup and recovery mechanisms + +## Docker Support + +Build and run with Docker: + +```bash +# Build the image +docker build -t discord-ticket-bot . + +# Run the container +docker run -d --name ticket-bot --env-file .env discord-ticket-bot +``` + +## Troubleshooting + +### Bot doesn't respond to commands +- Make sure the bot is online +- Check that you've enabled the correct intents in the Developer Portal +- Verify the bot has permissions in your server + +### Commands don't appear +- Wait a few minutes for commands to sync +- Try kicking and re-inviting the bot +- Check the bot logs for sync errors + +### Tickets aren't archiving +- Verify you've run `/setup` to configure channels +- Check that the bot has permissions in both channels + +## License + +MIT License - feel free to modify and use for your projects! + +## Author + +Created for SimolZimol diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..e0accfa --- /dev/null +++ b/bot.py @@ -0,0 +1,377 @@ +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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..876aed8 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +discord.py>=2.3.0 +python-dotenv>=1.0.0 +aiohttp>=3.9.0 \ No newline at end of file