diff --git a/bot.py b/bot.py index 48ecfe8..db1f2b7 100644 --- a/bot.py +++ b/bot.py @@ -1,186 +1,273 @@ -import discord -from discord.ext import commands, tasks -from datetime import datetime, timedelta -import json -import pytz -import os -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -# Retrieve the bot token from the environment variable -TOKEN = os.getenv('DISCORD_BOT_TOKEN') - -# Set up intents -intents = discord.Intents.default() -intents.members = True -intents.message_content = True -bot = commands.Bot(command_prefix='/tp ', intents=intents) - -# File path for tickets data -TICKETS_FILE = 'tickets.json' - -# Load or initialize tickets data -def load_tickets_data(): - if os.path.exists(TICKETS_FILE): - with open(TICKETS_FILE, 'r') as f: - try: - data = json.load(f) - print("Loaded tickets data:", data) # Debug log - return data - except json.JSONDecodeError as e: - print(f"Error loading JSON data: {e}") - return {} - return {} - -tickets_data = load_tickets_data() - -# Helper function to save tickets data -def save_tickets_data(): - with open(TICKETS_FILE, 'w') as f: - json.dump(tickets_data, f, indent=4) - print("Saved tickets data:", tickets_data) # Debug log - -# Helper function to reload tickets data from the file -def reload_tickets_data(): - global tickets_data - tickets_data = load_tickets_data() - -# Command to assign a ticket -@bot.command() -@commands.has_permissions(administrator=True) -async def assign_ticket(ctx, member: discord.Member, email: str, ticket_type: str = "normal"): - if ticket_type not in ["normal", "golden"]: - await ctx.send("Invalid ticket type. Use 'normal' or 'golden'.") - return - - member_id = str(member.id) # Ensure ID is a string - if member_id not in tickets_data: - tickets_data[member_id] = {"email": email, "tickets": []} - - if ticket_type == "normal": - # Assign an individual expiration date for the normal ticket - expiration_date = datetime.utcnow() + timedelta(days=365) - tickets_data[member_id]["tickets"].append({ - "type": "normal", - "expiration": expiration_date.isoformat() - }) - expiration_message = f", which expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC." - else: - # Golden tickets do not expire - tickets_data[member_id]["tickets"].append({ - "type": "golden", - "expiration": None - }) - expiration_message = "" - - save_tickets_data() - reload_tickets_data() # Reload the tickets data to ensure consistency - await ctx.send(f"Assigned a {ticket_type} ticket to {member.mention}{expiration_message}.") - -# Command to redeem a ticket -@bot.command() -@commands.has_permissions(administrator=True) -async def redeem_ticket(ctx, member: discord.Member, ticket_type: str): - if ticket_type not in ["normal", "golden"]: - await ctx.send("Invalid ticket type. Use 'normal' or 'golden'.") - return - - member_id = str(member.id) # Ensure ID is a string - if member_id not in tickets_data or not tickets_data[member_id]["tickets"]: - await ctx.send(f"{member.mention} does not have any tickets.") - return - - for i, ticket in enumerate(tickets_data[member_id]["tickets"]): - if ticket["type"] == ticket_type: - del tickets_data[member_id]["tickets"][i] - save_tickets_data() - reload_tickets_data() # Reload the tickets data to ensure consistency - await ctx.send(f"Redeemed a {ticket_type} ticket for {member.mention}.") - return - - await ctx.send(f"{member.mention} does not have any {ticket_type} tickets to redeem.") - -# Command to check a user's tickets (admins only) -@bot.command() -@commands.has_permissions(administrator=True) -async def check_ticket(ctx, member: discord.Member): - member_id = str(member.id) # Ensure ID is a string - if member_id not in tickets_data or not tickets_data[member_id]["tickets"]: - await ctx.send(f"{member.mention} does not have any tickets.") - return - - ticket_info = tickets_data[member_id]["tickets"] - normal_tickets = [t for t in ticket_info if t["type"] == "normal"] - golden_tickets = [t for t in ticket_info if t["type"] == "golden"] - - # Timezone definitions - tz_utc = pytz.UTC - tz_mst = pytz.timezone('America/Edmonton') - - normal_message = f"{member.mention} has {len(normal_tickets)} normal tickets:\n" - for ticket in normal_tickets: - expiration_date_utc = datetime.fromisoformat(ticket["expiration"]).astimezone(tz_utc) - expiration_date_mst = expiration_date_utc.astimezone(tz_mst) - normal_message += f"- Expires on {expiration_date_utc.strftime('%Y-%m-%d %H:%M:%S')} UTC / {expiration_date_mst.strftime('%Y-%m-%d %H:%M:%S')} MST\n" - - golden_message = f"{member.mention} has {len(golden_tickets)} golden tickets." - - await ctx.send(f"{normal_message}{golden_message}") - -# Command to check your own tickets (all users) -@bot.command() -async def tickets(ctx): - member_id = str(ctx.author.id) # Ensure ID is a string - if member_id not in tickets_data or not tickets_data[member_id]["tickets"]: - # Send a DM if no tickets are found - await ctx.author.send("You do not have any tickets.") - await ctx.send("I have sent you a DM with your ticket information.") - return - - ticket_info = tickets_data[member_id]["tickets"] - normal_tickets = [t for t in ticket_info if t["type"] == "normal"] - golden_tickets = [t for t in ticket_info if t["type"] == "golden"] - - normal_message = f"You have {len(normal_tickets)} normal tickets:\n" - for ticket in normal_tickets: - expiration_date = datetime.fromisoformat(ticket["expiration"]) - normal_message += f"- Expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC\n" - - golden_message = f"You have {len(golden_tickets)} golden tickets." - - # Send the result to the user via DM - await ctx.author.send(f"{normal_message}\n{golden_message}") - await ctx.send("I have sent you a DM with your ticket information.") - -# Background task to check and remove expired tickets -@tasks.loop(hours=24) -async def check_expired_tickets(): - tz = pytz.UTC - current_time = datetime.now(tz) - to_remove = [] - - for member_id, ticket_info in tickets_data.items(): - tickets_data[member_id]["tickets"] = [ - ticket for ticket in ticket_info["tickets"] - if ticket["type"] == "golden" or current_time < datetime.fromisoformat(ticket["expiration"]).astimezone(tz) - ] - - # Remove the member if they have no tickets left - if not tickets_data[member_id]["tickets"]: - to_remove.append(member_id) - - for member_id in to_remove: - print(f"Removing expired tickets for user ID: {member_id}") # Debug log - del tickets_data[member_id] - - save_tickets_data() - -@bot.event -async def on_ready(): - print(f'Logged in as {bot.user.name}') - check_expired_tickets.start() - -# Run the bot -bot.run(TOKEN) \ No newline at end of file +import discord +from discord import app_commands +from discord.ext import commands, tasks +from datetime import datetime, timedelta +import json +import random +import re +import pytz +import os +from dotenv import load_dotenv + +load_dotenv() +TOKEN = os.getenv('DISCORD_BOT_TOKEN') + +intents = discord.Intents.default() +intents.members = True +bot = commands.Bot(command_prefix=commands.when_mentioned, intents=intents) + +TICKETS_FILE = 'tickets.json' +GUILD_IDS = [922622021110730753, 988258730061725738] + + +def load_tickets_data(): + if os.path.exists(TICKETS_FILE): + with open(TICKETS_FILE, 'r') as f: + try: + data = json.load(f) + print("Loaded tickets data:", data) + return data + except json.JSONDecodeError as e: + print(f"Error loading JSON data: {e}") + return {} + return {} + +tickets_data = load_tickets_data() + + +def save_tickets_data(): + with open(TICKETS_FILE, 'w') as f: + json.dump(tickets_data, f, indent=4) + print("Saved tickets data:", tickets_data) + + +def reload_tickets_data(): + global tickets_data + tickets_data = load_tickets_data() + + +async def ticket_type_autocomplete(interaction: discord.Interaction, current: str): + ticket_types = ["normal", "golden"] + return [ + app_commands.Choice(name=t, value=t) + for t in ticket_types + if current.lower() in t + ] + + +class TP(app_commands.Group): + pass + +class TPA(app_commands.Group): + pass + +tp = TP(name="tp", description="Tickets Please commands") +tpa = TPA(name="tpa", description="Tickets Please admin commands", default_permissions=discord.Permissions(administrator=True)) + + +@tpa.command(name="assign_ticket", description="Assign a ticket to a member") +@app_commands.describe( + member="The member to assign the ticket to" +) +async def assign_ticket(interaction: discord.Interaction, member: discord.Member): + member_id = str(member.id) + if member_id not in tickets_data: + tickets_data[member_id] = {"tickets": []} + + expiration_date = datetime.utcnow() + timedelta(days=365) + tickets_data[member_id]["tickets"].append({ + "type": "normal", + "expiration": expiration_date.isoformat() + }) + expiration_message = f", which expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC." + + save_tickets_data() + reload_tickets_data() + await interaction.response.send_message(f"Assigned a ticket to {member.mention}{expiration_message}.", ephemeral=True) + + +@tpa.command(name="redeem_ticket", description="Redeem a ticket for a member") +@app_commands.describe( + member="The member to redeem the ticket for", + ticket_type="The type of ticket to redeem" +) +@app_commands.autocomplete(ticket_type=ticket_type_autocomplete) +@app_commands.default_permissions(administrator=True) +async def redeem_ticket(interaction: discord.Interaction, member: discord.Member, ticket_type: str): + if ticket_type not in ["normal", "golden"]: + await interaction.response.send_message("Invalid ticket type. Use 'normal' or 'golden'.", ephemeral=True) + return + + member_id = str(member.id) + if member_id not in tickets_data or not tickets_data[member_id]["tickets"]: + await interaction.response.send_message(f"{member.mention} does not have any tickets.", ephemeral=True) + return + + for i, ticket in enumerate(tickets_data[member_id]["tickets"]): + if ticket["type"] == ticket_type: + del tickets_data[member_id]["tickets"][i] + save_tickets_data() + reload_tickets_data() + await interaction.response.send_message(f"Redeemed a {ticket_type} ticket for {member.mention}.", ephemeral=True) + return + + await interaction.response.send_message(f"{member.mention} does not have any {ticket_type} tickets to redeem.", ephemeral=True) + + +@tpa.command(name="check_ticket", description="Check a member's tickets") +@app_commands.describe(member="The member to check tickets for") +@app_commands.default_permissions(administrator=True) +async def check_ticket(interaction: discord.Interaction, member: discord.Member): + member_id = str(member.id) + if member_id not in tickets_data or not tickets_data[member_id]["tickets"]: + await interaction.response.send_message(f"{member.mention} does not have any tickets.", ephemeral=True) + return + + ticket_info = tickets_data[member_id]["tickets"] + normal_tickets = [t for t in ticket_info if t["type"] == "normal"] + golden_tickets = [t for t in ticket_info if t["type"] == "golden"] + + tz_utc = pytz.UTC + tz_mst = pytz.timezone('America/Edmonton') + + normal_message = f"{member.mention} has {len(normal_tickets)} normal tickets:\n" + for ticket in normal_tickets: + expiration_date_utc = datetime.fromisoformat(ticket["expiration"]).astimezone(tz_utc) + expiration_date_mst = expiration_date_utc.astimezone(tz_mst) + normal_message += f"- Expires on {expiration_date_utc.strftime('%Y-%m-%d %H:%M:%S')} UTC / {expiration_date_mst.strftime('%Y-%m-%d %H:%M:%S')} MST\n" + + golden_message = f"{member.mention} has {len(golden_tickets)} golden tickets." + + await interaction.response.send_message(f"{normal_message}{golden_message}", ephemeral=True) + + +@tp.command(name="tickets", description="Check your own tickets") +async def tickets(interaction: discord.Interaction): + member_id = str(interaction.user.id) + if member_id not in tickets_data or not tickets_data[member_id]["tickets"]: + await interaction.response.send_message("You do not have any tickets.", ephemeral=True) + return + + ticket_info = tickets_data[member_id]["tickets"] + normal_tickets = [t for t in ticket_info if t["type"] == "normal"] + golden_tickets = [t for t in ticket_info if t["type"] == "golden"] + + lines = [] + if len(normal_tickets) > 0: + lines.append(f"You have {len(normal_tickets)} normal ticket(s):") + for ticket in normal_tickets: + expiration_date = datetime.fromisoformat(ticket["expiration"]) + lines.append(f"- Expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC") + if len(golden_tickets) > 0: + lines.append(f"You have {len(golden_tickets)} golden ticket(s).") + + await interaction.response.send_message("\n".join(lines), ephemeral=True) + + +def parse_and_roll(notation: str): + notation = notation.replace(' ', '').lower() + matches = re.findall(r'([+-]?)(adv|dis|\d*d\d+|\d+)', notation) + if not matches: + return None, None + + if len(matches) > 20: + return None, "Too many terms (max 20)." + + lines = [] + total = 0 + + for sign, term in matches: + multiplier = -1 if sign == '-' else 1 + prefix = '-' if multiplier == -1 else ('+' if lines else '') + if term in ('adv', 'dis'): + rolls = [random.randint(1, 20), random.randint(1, 20)] + kept = max(rolls) if term == 'adv' else min(rolls) + dropped = min(rolls) if term == 'adv' else max(rolls) + subtotal = kept * multiplier + rolls_str = ', '.join(f"**{r}**" if r == kept else f"~~{r}~~" for r in rolls) + lines.append(f"`{prefix}{term}:` [{rolls_str}] = **{subtotal}**") + total += subtotal + elif 'd' in term: + parts = term.split('d') + num = int(parts[0]) if parts[0] else 1 + sides = int(parts[1]) + if num < 1 or num > 100: + return None, f"Number of dice must be between 1 and 100 (got {num})." + if sides < 2 or sides > 1000: + return None, f"Die sides must be between 2 and 1000 (got {sides})." + rolls = [random.randint(1, sides) for _ in range(num)] + subtotal = sum(rolls) * multiplier + lines.append(f"`{prefix}{term}:` [{', '.join(str(r) for r in rolls)}] = **{subtotal}**") + total += subtotal + else: + flat = int(term) * multiplier + lines.append(f"`{prefix}{flat}`") + total += flat + + return lines, total + + +async def handle_roll(interaction: discord.Interaction, notation: str): + lines, result = parse_and_roll(notation) + if lines is None: + await interaction.response.send_message( + result or "Invalid dice notation. Try something like `2d20+1d4+3`.", + ephemeral=True + ) + return + lines.append(f"{'─' * 20}\n**Total: {result}**") + await interaction.response.send_message("\n".join(lines)) + + +@bot.tree.command(name="roll", description="Roll dice using dice notation (e.g. 2d20+1d4+3)") +@app_commands.describe(notation="Dice notation, e.g. 2d20+1d4+3") +async def roll_cmd(interaction: discord.Interaction, notation: str): + await handle_roll(interaction, notation) + + +@bot.tree.command(name="r", description="Roll dice using dice notation (e.g. 2d20+1d4+3)") +@app_commands.describe(notation="Dice notation, e.g. 2d20+1d4+3") +async def r_cmd(interaction: discord.Interaction, notation: str): + await handle_roll(interaction, notation) + + +@tasks.loop(hours=24) +async def check_expired_tickets(): + tz = pytz.UTC + current_time = datetime.now(tz) + to_remove = [] + + for member_id, ticket_info in tickets_data.items(): + tickets_data[member_id]["tickets"] = [ + ticket for ticket in ticket_info["tickets"] + if ticket["type"] == "golden" or current_time < datetime.fromisoformat(ticket["expiration"]).astimezone(tz) + ] + + if not tickets_data[member_id]["tickets"]: + to_remove.append(member_id) + + for member_id in to_remove: + print(f"Removing expired tickets for user ID: {member_id}") + del tickets_data[member_id] + + save_tickets_data() + + +@bot.event +async def setup_hook(): + bot.tree.add_command(tp) + bot.tree.add_command(tpa) + for guild_id in GUILD_IDS: + guild = discord.Object(id=guild_id) + bot.tree.copy_global_to(guild=guild) + await bot.tree.sync(guild=guild) + print(f"Synced commands to guild {guild_id}") + + +@bot.event +async def on_ready(): + print(f"Logged in as {bot.user.name}") + check_expired_tickets.start() + + +bot.run(TOKEN)