From 7064f0a1f72e4c1181b898ad4fe756765108f245 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Wed, 7 Feb 2024 23:24:31 +1100 Subject: [PATCH] feat: Add ticketing --- bot.py | 109 ++++++++++++++++++++- support.py | 279 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+), 3 deletions(-) create mode 100644 support.py diff --git a/bot.py b/bot.py index 6487b3d..e399fb1 100644 --- a/bot.py +++ b/bot.py @@ -17,6 +17,8 @@ import tools from tools import parse_time, read_reminders, store_reminder, write_reminders import asyncio from discord.ext import tasks, commands +from discord.ext.commands import has_permissions, MissingPermissions +import support @@ -233,9 +235,9 @@ async def ssl(ctx, domain: str, showcert: bool = False, notifymeonexpiry: bool = expiry_date = cert_obj.not_valid_after # Check if expiry date is past - if expiry_date < datetime.datetime.now(): + if expiry_date < datetime.datetime.utcnow(): message = message + "\n## Expiry Date:\n:x: Certificate has expired\n" - elif expiry_date < datetime.datetime.now() + datetime.timedelta(days=7): + elif expiry_date < datetime.datetime.utcnow() + datetime.timedelta(days=7): message = message + "\n## Expiry Date:\n:warning: Certificate expires soon\n" else: message = message + "\n## Expiry Date:\n:white_check_mark: Certificate is valid\n" @@ -363,7 +365,7 @@ async def checkForSSLExpiry(): # Get expiry date cert_obj = x509.load_pem_x509_certificate(cert.encode("utf-8"), default_backend()) expiry_date = cert_obj.not_valid_after - if expiry_date < datetime.datetime.now() + datetime.timedelta(days=7): + if expiry_date < datetime.datetime.utcnow() + datetime.timedelta(days=7): user = await client.fetch_user(int(userid)) if user: await user.send(f"SSL certificate for {domain} expires soon") @@ -507,6 +509,105 @@ async def timestamp(ctx, when: str): else: await ctx.response.send_message("Invalid time format. Please use something like `1d 3h` or `4hr`. End with `ago` to convert to past time",ephemeral=True) +#region Tickets +@tree.command(name="ticket", description="Create a ticket") +async def ticket(ctx): + if (ctx.guild == None): + await ctx.response.send_message("This command can only be used in a server",ephemeral=True) + return + + server = ctx.guild.id + if not support.is_server_valid(str(server)): + await ctx.response.send_message("This server is not registered",ephemeral=True) + return + await ctx.response.send_message("Creating ticket...",ephemeral=True) + await support.create_ticket(str(ctx.user.id), str(ctx.guild.id)) + + +@tree.command(name="ticketaddserver", description="Add a server to the ticket system") +@commands.has_permissions(administrator=True) +async def ticketaddserver(ctx, category: str, modrole: discord.Role, closedcategory: str): + if (ctx.user.id != ADMINID): + await log("User: " + str(ctx.user.name) + " tried to use the ticketAddServer command") + await ctx.response.send_message("You don't have permission to use this command",ephemeral=True) + else: + await ctx.response.send_message("Adding server to ticket system",ephemeral=True) + result = await support.ticketAddServer(ctx.guild.id, category, modrole.id,closedcategory) + await ctx.channel.send(result) + + +@tree.command(name="adduser", description="Add a user to a ticket") +async def adduser(ctx, user: discord.User): + if (ctx.guild == None): + await ctx.response.send_message("This command can only be used in a server",ephemeral=True) + return + + server = ctx.guild.id + if not support.is_server_valid(str(server)): + await ctx.response.send_message("This server is not registered",ephemeral=True) + return + result = await support.addMemberToTicket(user,str(ctx.channel.id), str(ctx.guild.id)) + await ctx.response.send_message(result,ephemeral=True) + +@tree.command(name="removeuser", description="Remove a user from a ticket") +async def removeuser(ctx, user: discord.User): + if (ctx.guild == None): + await ctx.response.send_message("This command can only be used in a server",ephemeral=True) + return + + server = ctx.guild.id + if not support.is_server_valid(str(server)): + await ctx.response.send_message("This server is not registered",ephemeral=True) + return + result = await support.removeMemberFromTicket(user,str(ctx.channel.id), str(ctx.guild.id)) + await ctx.response.send_message(result,ephemeral=True) + + +@tree.command(name="closeticket", description="Close a ticket") +async def closeticket(ctx): + if (ctx.guild == None): + await ctx.response.send_message("This command can only be used in a server",ephemeral=True) + return + + server = ctx.guild.id + if not support.is_server_valid(str(server)): + await ctx.response.send_message("This server is not registered",ephemeral=True) + return + await ctx.response.send_message("Closing ticket",ephemeral=True) + await support.close_ticket(str(ctx.user.id),str(ctx.channel.id), str(ctx.guild.id)) + +@tree.command(name="reopenticket", description="Reopen a ticket") +async def reopenticket(ctx): + if (ctx.guild == None): + await ctx.response.send_message("This command can only be used in a server",ephemeral=True) + return + + server = ctx.guild.id + if not support.is_server_valid(str(server)): + await ctx.response.send_message("This server is not registered",ephemeral=True) + return + await ctx.response.send_message("Reopening ticket",ephemeral=True) + await support.reopen_ticket(str(ctx.user.id),str(ctx.channel.id), str(ctx.guild.id)) + +@tree.command(name="renameticket", description="Rename a ticket") +async def renameticket(ctx, name: str): + if (ctx.guild == None): + await ctx.response.send_message("This command can only be used in a server",ephemeral=True) + return + + server = ctx.guild.id + if not support.is_server_valid(str(server)): + await ctx.response.send_message("This server is not registered",ephemeral=True) + return + + result = await support.rename_ticket(str(ctx.user.id),str(ctx.channel.id), str(ctx.guild.id),name) + await ctx.response.send_message(result,ephemeral=True) + +#endregion + + + + @tasks.loop(seconds=10) async def check_reminders(): now = datetime.datetime.now() @@ -529,6 +630,7 @@ async def check_reminders(): async def on_ready(): global ADMINID ADMINID = client.application.owner.id + support.set_client(client) await tree.sync() updateStatus() check_reminders.start() @@ -538,3 +640,4 @@ async def on_ready(): client.run(TOKEN) + diff --git a/support.py b/support.py new file mode 100644 index 0000000..eb2135d --- /dev/null +++ b/support.py @@ -0,0 +1,279 @@ +import datetime +import re +import discord +import json +import os +import dotenv + +dotenv.load_dotenv() + + +TICKETS_FILE_PATH = '/mnt/tickets.json' +DISCORD_TOKEN = os.getenv('DISCORD_TOKEN') + + +intents = discord.Intents.default() +client = discord.Client(intents=intents) + +def set_client(c): + global client + client = c + print("Client set") + + +if not os.path.exists(TICKETS_FILE_PATH): + with open(TICKETS_FILE_PATH, 'w') as f: + json.dump({"server": {}}, f) + +def is_server_valid(server:str): + server = server + with open(TICKETS_FILE_PATH, 'r') as f: + ticketsData = json.load(f) + + if server in ticketsData['server']: + return True + return False + + +async def create_ticket(user_id, server:str): + with open(TICKETS_FILE_PATH, 'r') as f: + ticketsData = json.load(f) + + if server not in ticketsData['server']: + return "Server not found" + + tickets = ticketsData['server'][server]['tickets'] + if user_id != "admin": + for ticket in tickets: + if ticket['user_id'] == user_id and ticket['status'] == "open": + return "You already have an open ticket" + + ticketNum = len(tickets) + 1 + # Generate a ticket name + user = await client.fetch_user(int(user_id)) + if user == None: + return "User not found" + + # Create a new ticket channel + guild = await client.fetch_guild(int(server)) + guild = client.get_guild(int(server)) + + + ticketName = f"{ticketNum}-{user.name}-ticket" + + category = discord.utils.get(guild.categories, id=int(ticketsData['server'][server]['category'])) + if category == None: + return "Category not found" + # Create the ticket channel + ticketChannel = await guild.create_text_channel(ticketName, category=category) + # Remove the default permissions + await ticketChannel.set_permissions(guild.default_role, read_messages=False, send_messages=False) + # Add the user to the ticket channel + await ticketChannel.set_permissions(user, read_messages=True, send_messages=True) + # Add the modRole to the ticket channel + admin = guild.get_role(int(ticketsData['server'][server]['adminRole'])) + await ticketChannel.set_permissions(admin, read_messages=True, send_messages=True) + # Add the ticket to the tickets list + tickets.append({ + 'user_id': user_id, + 'channel_id': ticketChannel.id, + 'status': "open", + 'members': [user_id] + }) + ticketsData['server'][server]['tickets'] = tickets + with open(TICKETS_FILE_PATH, 'w') as f: + json.dump(ticketsData, f) + + await ticketChannel.send(f"G'day <@{user_id}>, your ticket has been created. Please let us know how we can help you.") + await ticketChannel.send(f"Commands: \n - Add a user to the ticket\n - Remove a user from the ticket\n - Rename the ticket\n - Close the ticket") + + return f"Ticket <#{ticketChannel.id}> created" + + +async def ticketAddServer(server, category, adminRole,closedCategory): + with open(TICKETS_FILE_PATH, 'r') as f: + ticketsData = json.load(f) + ticketsData['server'][server] = { + 'category': category, + 'closedCategory': closedCategory, + 'adminRole': adminRole, + 'tickets': [] + } + with open(TICKETS_FILE_PATH, 'w') as f: + json.dump(ticketsData, f) + return "Server added" + + +async def close_ticket(user_id,channel_id, server): + with open(TICKETS_FILE_PATH, 'r') as f: + ticketsData = json.load(f) + tickets = ticketsData['server'][server]['tickets'] + validTicket = False + for ticket in tickets: + if str(ticket['channel_id']) == channel_id: + ticket['status'] = "closed" + validTicket = True + break + if not validTicket: + return "This ticket does not exist" + ticketsData['server'][server]['tickets'] = tickets + with open(TICKETS_FILE_PATH, 'w') as f: + json.dump(ticketsData, f) + + # Remove read and send permissions for everyone + guild = await client.fetch_guild(int(server)) + guild = client.get_guild(int(server)) + ticketChannel = guild.get_channel(int(channel_id)) + + await ticketChannel.set_permissions(guild.default_role, read_messages=False, send_messages=False) + + for member in ticket['members']: + user = await client.fetch_user(int(member)) + await ticketChannel.set_permissions(user, read_messages=False, send_messages=False) + + await ticketChannel.send(f"This ticket has been closed by <@{user_id}>") + # Move to the closed category + closedCategory = discord.utils.get(guild.categories, id=int(ticketsData['server'][server]['closedCategory'])) + if closedCategory == None: + return "Category not found" + await ticketChannel.edit(category=closedCategory) + +async def addMemberToTicket(user_id, channel_id, server): + guild = await client.fetch_guild(int(server)) + guild = client.get_guild(int(server)) + user = await fuzzyUser(user_id, guild) + if user == False: + return "User not found" + user_id = str(user.id) + + with open(TICKETS_FILE_PATH, 'r') as f: + ticketsData = json.load(f) + tickets = ticketsData['server'][server]['tickets'] + validTicket = False + for ticket in tickets: + if str(ticket['channel_id']) == channel_id: + if user_id in ticket['members']: + return "User already in ticket" + + ticket['members'].append(user_id) + validTicket = True + break + if not validTicket: + return "This ticket does not exist" + ticketsData['server'][server]['tickets'] = tickets + with open(TICKETS_FILE_PATH, 'w') as f: + json.dump(ticketsData, f) + + ticketChannel = guild.get_channel(int(channel_id)) + + await ticketChannel.set_permissions(user, read_messages=True, send_messages=True) + await ticketChannel.send(f"{user.mention} has been added to the ticket") + + return "User added to ticket" + +async def removeMemberFromTicket(user_id, channel_id, server): + guild = await client.fetch_guild(int(server)) + guild = client.get_guild(int(server)) + user = await fuzzyUser(user_id, guild) + if user == False: + return "User not found" + user_id = str(user.id) + + with open(TICKETS_FILE_PATH, 'r') as f: + ticketsData = json.load(f) + tickets = ticketsData['server'][server]['tickets'] + validTicket = False + for ticket in tickets: + if str(ticket['channel_id']) == channel_id: + if user_id not in ticket['members']: + return "User not found in ticket" + ticket['members'].remove(user_id) + validTicket = True + break + if not validTicket: + return "This ticket does not exist" + ticketsData['server'][server]['tickets'] = tickets + with open(TICKETS_FILE_PATH, 'w') as f: + json.dump(ticketsData, f) + + ticketChannel = guild.get_channel(int(channel_id)) + + await ticketChannel.set_permissions(user, read_messages=False, send_messages=False) + await ticketChannel.send(f"{user.mention} has been removed from the ticket") + + return "User removed from ticket" + +async def reopen_ticket(user_id,channel_id,guild_id): + with open(TICKETS_FILE_PATH, 'r') as f: + ticketsData = json.load(f) + tickets = ticketsData['server'][guild_id]['tickets'] + validTicket = False + for ticket in tickets: + if str(ticket['channel_id']) == channel_id: + if ticket['status'] == "open": + return "Ticket is already open" + ticket['status'] = "open" + validTicket = True + break + if not validTicket: + return "This ticket does not exist" + ticketsData['server'][guild_id]['tickets'] = tickets + with open(TICKETS_FILE_PATH, 'w') as f: + json.dump(ticketsData, f) + + guild = await client.fetch_guild(int(guild_id)) + guild = client.get_guild(int(guild_id)) + channel = guild.get_channel(int(channel_id)) + + # Remove read and send permissions for everyone + await channel.set_permissions(guild.default_role, read_messages=False, send_messages=False) + + for member in ticket['members']: + user = await client.fetch_user(int(member)) + await channel.set_permissions(user, read_messages=True, send_messages=True) + + await channel.send(f"This ticket has been reopened by <@{user_id}>") + # Move to the closed category + category = discord.utils.get(guild.categories, id=int(ticketsData['server'][guild_id]['category'])) + if category == None: + return "Category not found" + await channel.edit(category=category) + + return "Ticket reopened" + +async def rename_ticket(user_id,channel_id,server_id,newName): + try: + guild = await client.fetch_guild(int(server_id)) + guild = client.get_guild(int(server_id)) + channel = guild.get_channel(int(channel_id)) + await channel.edit(name=newName) + return "Ticket renamed" + except: + return "Error renaming ticket" + + +async def fuzzyUser(search, guild): + user = None + try: + user = await client.fetch_user(int(search)) + except: + pass + if user == None: + user = discord.utils.get(guild.members, name=search) + if user == None: + user = discord.utils.get(guild.members, nick=search) + if user == None: + user = discord.utils.get(guild.members, global_name=search) + if user == None: + user = discord.utils.get(guild.members, global_name=search) + if user == None: + user = guild.get_member_named(search) + if user == None: + user = await guild.query_members(search) + if len(user) > 0: + user = user[0] + else: + return False + if user == None: + return False + return user \ No newline at end of file