diff --git a/bot.py b/bot.py index db1f2b7..5362c1c 100644 --- a/bot.py +++ b/bot.py @@ -1,8 +1,9 @@ import discord from discord import app_commands from discord.ext import commands, tasks -from datetime import datetime, timedelta +from datetime import datetime, timedelta, time as dt_time import json +import logging import random import re import pytz @@ -11,13 +12,18 @@ from dotenv import load_dotenv load_dotenv() TOKEN = os.getenv('DISCORD_BOT_TOKEN') +GUILD_IDS = [int(gid.strip()) for gid in os.getenv('GUILD_IDS', '').split(',') if gid.strip()] + +logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') +log = logging.getLogger(__name__) 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] +TZ_UTC = pytz.UTC +TZ_LOCAL = pytz.timezone('America/Edmonton') def load_tickets_data(): @@ -25,10 +31,10 @@ def load_tickets_data(): with open(TICKETS_FILE, 'r') as f: try: data = json.load(f) - print("Loaded tickets data:", data) + log.info("Loaded tickets data: %s", data) return data except json.JSONDecodeError as e: - print(f"Error loading JSON data: {e}") + log.error("Error loading JSON data: %s", e) return {} return {} @@ -38,12 +44,13 @@ 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) + log.info("Saved tickets data: %s", tickets_data) -def reload_tickets_data(): - global tickets_data - tickets_data = load_tickets_data() +def _format_ticket_expiry(ticket: dict) -> str: + exp_utc = datetime.fromisoformat(ticket["expiration"]).astimezone(TZ_UTC) + exp_local = exp_utc.astimezone(TZ_LOCAL) + return f"- Expires on {exp_utc.strftime('%Y-%m-%d %H:%M:%S')} UTC / {exp_local.strftime('%Y-%m-%d %H:%M:%S')} {exp_local.strftime('%Z')}" async def ticket_type_autocomplete(interaction: discord.Interaction, current: str): @@ -51,7 +58,7 @@ async def ticket_type_autocomplete(interaction: discord.Interaction, current: st return [ app_commands.Choice(name=t, value=t) for t in ticket_types - if current.lower() in t + if t.startswith(current.lower()) ] @@ -74,16 +81,17 @@ async def assign_ticket(interaction: discord.Interaction, member: discord.Member if member_id not in tickets_data: tickets_data[member_id] = {"tickets": []} - expiration_date = datetime.utcnow() + timedelta(days=365) + expiration_date = datetime.now(TZ_UTC) + 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) + await interaction.response.send_message( + f"Assigned a ticket to {member.mention}, which expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC.", + ephemeral=True + ) @tpa.command(name="redeem_ticket", description="Redeem a ticket for a member") @@ -103,15 +111,15 @@ async def redeem_ticket(interaction: discord.Interaction, member: discord.Member 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 + tickets = tickets_data[member_id]["tickets"] + ticket_to_remove = next((t for t in tickets if t["type"] == ticket_type), None) + if ticket_to_remove is None: + await interaction.response.send_message(f"{member.mention} does not have any {ticket_type} tickets to redeem.", ephemeral=True) + return - await interaction.response.send_message(f"{member.mention} does not have any {ticket_type} tickets to redeem.", ephemeral=True) + tickets.remove(ticket_to_remove) + save_tickets_data() + await interaction.response.send_message(f"Redeemed a {ticket_type} ticket for {member.mention}.", ephemeral=True) @tpa.command(name="check_ticket", description="Check a member's tickets") @@ -127,18 +135,14 @@ async def check_ticket(interaction: discord.Interaction, member: discord.Member) 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') + lines = [] + if normal_tickets: + lines.append(f"{member.mention} has {len(normal_tickets)} normal ticket(s):") + lines.extend(_format_ticket_expiry(t) for t in normal_tickets) + if golden_tickets: + lines.append(f"{member.mention} has {len(golden_tickets)} golden ticket(s).") - 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) + await interaction.response.send_message("\n".join(lines), ephemeral=True) @tp.command(name="tickets", description="Check your own tickets") @@ -153,12 +157,10 @@ async def tickets(interaction: discord.Interaction): golden_tickets = [t for t in ticket_info if t["type"] == "golden"] lines = [] - if len(normal_tickets) > 0: + if normal_tickets: 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.extend(_format_ticket_expiry(t) for t in normal_tickets) + if golden_tickets: lines.append(f"You have {len(golden_tickets)} golden ticket(s).") await interaction.response.send_message("\n".join(lines), ephemeral=True) @@ -182,7 +184,6 @@ def parse_and_roll(notation: str): 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}**") @@ -220,34 +221,33 @@ async def handle_roll(interaction: discord.Interaction, notation: str): @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) +@app_commands.describe(dice="Dice notation, e.g. 2d20+1d4+3") +async def roll_cmd(interaction: discord.Interaction, dice: str): + await handle_roll(interaction, dice) @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) +@app_commands.describe(dice="Dice notation, e.g. 2d20+1d4+3") +async def r_cmd(interaction: discord.Interaction, dice: str): + await handle_roll(interaction, dice) -@tasks.loop(hours=24) +@tasks.loop(time=dt_time(hour=0, minute=0, tzinfo=TZ_UTC)) async def check_expired_tickets(): - tz = pytz.UTC - current_time = datetime.now(tz) + now = datetime.now(TZ_UTC) 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 ticket["type"] == "golden" or now < datetime.fromisoformat(ticket["expiration"]).astimezone(TZ_UTC) ] 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}") + log.info("Removing expired tickets for user ID: %s", member_id) del tickets_data[member_id] save_tickets_data() @@ -261,12 +261,12 @@ async def setup_hook(): 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}") + log.info("Synced commands to guild %s", guild_id) @bot.event async def on_ready(): - print(f"Logged in as {bot.user.name}") + log.info("Logged in as %s", bot.user.name) check_expired_tickets.start()