Created
August 20, 2025 05:41
-
-
Save tempusthales/1da77bb645452807bee6894966a14752 to your computer and use it in GitHub Desktop.
A **discord.py 2.3+ bot** that tracks per-channel boss respawn timers with full interactive dashboards, dropdowns, and modals. Each channel has its own independent timers, bosses, and dashboards.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import discord | |
| from discord import app_commands | |
| from discord.ext import commands, tasks | |
| import os, json, asyncio | |
| from datetime import datetime | |
| from dotenv import load_dotenv | |
| # ---------------------------- | |
| # Setup | |
| # ---------------------------- | |
| load_dotenv() | |
| TOKEN = os.getenv("DISCORD_TOKEN") | |
| BOSSES_FILE = "bosses.json" # master defaults (global) | |
| CHANNEL_DATA_FILE = "channel_data.json" # per-channel bosses + timers | |
| DASHBOARDS_FILE = "dashboards.json" # {channel_id: message_id} | |
| TRACKING_FILE = "tracking.json" # optional: per-user filters | |
| # ---------------------------- | |
| # Async JSON I/O with locks | |
| # ---------------------------- | |
| _locks = {} | |
| def _get_lock(path: str) -> asyncio.Lock: | |
| if path not in _locks: | |
| _locks[path] = asyncio.Lock() | |
| return _locks[path] | |
| def _load_sync(path, default): | |
| if os.path.exists(path): | |
| with open(path, "r") as f: | |
| return json.load(f) | |
| return default | |
| async def load_json(path, default): | |
| async with _get_lock(path): | |
| return _load_sync(path, default) | |
| async def save_json(path, data): | |
| async with _get_lock(path): | |
| with open(path, "w") as f: | |
| json.dump(data, f, indent=4) | |
| # initial sync load (safe at startup) | |
| bosses_master = _load_sync(BOSSES_FILE, []) # [{name, respawn}] | |
| channel_data = _load_sync(CHANNEL_DATA_FILE, {}) # {cid: {...}} | |
| dashboards = _load_sync(DASHBOARDS_FILE, {}) # {cid: msg_id} | |
| tracking = _load_sync(TRACKING_FILE, {}) # {user_id: [names...]} | |
| # ---------------------------- | |
| # Bot | |
| # ---------------------------- | |
| intents = discord.Intents.default() | |
| intents.message_content = False | |
| bot = commands.Bot(command_prefix="!", intents=intents) | |
| # ---------------------------- | |
| # Helpers | |
| # ---------------------------- | |
| def find_master_boss(name: str): | |
| return next((b for b in bosses_master if b["name"].lower() == name.lower()), None) | |
| def fmt_hms(seconds: float) -> str: | |
| neg = seconds < 0 | |
| seconds = abs(int(seconds)) | |
| h = seconds // 3600 | |
| m = (seconds % 3600) // 60 | |
| s = seconds % 60 | |
| return f"{'-' if neg else ''}{h:02}:{m:02}:{s:02}" | |
| def now_ts() -> int: | |
| return int(datetime.utcnow().timestamp()) | |
| def ensure_channel_record(cid: str): | |
| if cid not in channel_data: | |
| channel_data[cid] = {"bosses": [], "timers": {}} | |
| if "bosses" not in channel_data[cid]: | |
| channel_data[cid]["bosses"] = [] | |
| if "timers" not in channel_data[cid]: | |
| channel_data[cid]["timers"] = {} | |
| def get_channel_bosses(cid: str): | |
| ensure_channel_record(cid) | |
| return channel_data[cid]["bosses"] | |
| def get_channel_timers(cid: str): | |
| ensure_channel_record(cid) | |
| return channel_data[cid]["timers"] | |
| def parse_hms(text: str) -> int: | |
| """Return seconds for 'HH:MM:SS'. Raises ValueError on bad format.""" | |
| parts = text.strip().split(":") | |
| if len(parts) != 3: | |
| raise ValueError("Use HH:MM:SS") | |
| h, m, s = [int(x) for x in parts] | |
| if m < 0 or m >= 60 or s < 0 or s >= 60 or h < 0: | |
| raise ValueError("Invalid time range") | |
| return h * 3600 + m * 60 + s | |
| async def reset_boss_timer(cid: str, boss_name: str): | |
| ensure_channel_record(cid) | |
| local = next((b for b in channel_data[cid]["bosses"] | |
| if b["name"].lower() == boss_name.lower()), None) | |
| base = local or find_master_boss(boss_name) | |
| if not base: | |
| return False | |
| channel_data[cid]["timers"][base["name"]] = now_ts() + int(base["respawn"]) | |
| await save_json(CHANNEL_DATA_FILE, channel_data) | |
| return True | |
| async def set_boss_remaining(cid: str, boss_name: str, remaining_seconds: int): | |
| ensure_channel_record(cid) | |
| channel_data[cid]["timers"][boss_name] = now_ts() + int(remaining_seconds) | |
| await save_json(CHANNEL_DATA_FILE, channel_data) | |
| async def refresh_all_dashboards(): | |
| for channel_id in list(dashboards.keys()): | |
| await update_dashboard_message(channel_id) | |
| # ---------------------------- | |
| # UI Components | |
| # ---------------------------- | |
| class EditTimeModal(discord.ui.Modal, title="Edit Boss Time (HH:MM:SS)"): | |
| def __init__(self, cid: str, boss_name: str): | |
| super().__init__() | |
| self.cid = cid | |
| self.boss_name = boss_name | |
| self.time_input = discord.ui.TextInput( | |
| label="New Remaining Time", | |
| placeholder="HH:MM:SS (e.g., 00:02:00)", | |
| required=True | |
| ) | |
| self.add_item(self.time_input) | |
| async def on_submit(self, interaction: discord.Interaction): | |
| try: | |
| secs = parse_hms(self.time_input.value) | |
| except Exception as e: | |
| await interaction.response.send_message(f"❌ {e}", ephemeral=True) | |
| return | |
| await set_boss_remaining(self.cid, self.boss_name, secs) | |
| await update_dashboard_message(self.cid) | |
| await interaction.response.send_message( | |
| f"⏱ Set **{self.boss_name}** to `{self.time_input.value}` remaining.", | |
| ephemeral=True | |
| ) | |
| class BossDropdown(discord.ui.Select): | |
| """Per-boss dropdown with actions: Killed or Edit Time.""" | |
| def __init__(self, cid: str, boss_name: str): | |
| self.cid = cid | |
| self.boss_name = boss_name | |
| super().__init__( | |
| placeholder=boss_name, | |
| min_values=1, | |
| max_values=1, | |
| options=[ | |
| discord.SelectOption(label="Killed", description=f"Reset {boss_name}"), | |
| discord.SelectOption(label="Edit Time", description=f"Manually set remaining time") | |
| ] | |
| ) | |
| async def callback(self, interaction: discord.Interaction): | |
| choice = self.values[0] | |
| if choice == "Killed": | |
| ok = await reset_boss_timer(self.cid, self.boss_name) | |
| await update_dashboard_message(self.cid) | |
| msg = "timer reset." if ok else "boss not found." | |
| await interaction.response.send_message( | |
| f"✅ **{self.boss_name}** {msg}", ephemeral=True | |
| ) | |
| elif choice == "Edit Time": | |
| await interaction.response.send_modal(EditTimeModal(self.cid, self.boss_name)) | |
| class AddBossModal(discord.ui.Modal, title="Add New Boss"): | |
| def __init__(self, cid: str): | |
| super().__init__() | |
| self.cid = cid | |
| self.boss_name = discord.ui.TextInput(label="Boss Name", required=True) | |
| self.respawn = discord.ui.TextInput(label="Respawn (seconds)", required=True) | |
| self.add_item(self.boss_name) | |
| self.add_item(self.respawn) | |
| async def on_submit(self, interaction: discord.Interaction): | |
| name = self.boss_name.value.strip() | |
| try: | |
| respawn_seconds = int(self.respawn.value.strip()) | |
| except ValueError: | |
| await interaction.response.send_message("❌ Respawn must be number of seconds.", ephemeral=True) | |
| return | |
| if not find_master_boss(name): | |
| bosses_master.append({"name": name, "respawn": respawn_seconds}) | |
| await save_json(BOSSES_FILE, bosses_master) | |
| ensure_channel_record(self.cid) | |
| if not any(b["name"].lower() == name.lower() for b in channel_data[self.cid]["bosses"]): | |
| channel_data[self.cid]["bosses"].append({"name": name, "respawn": respawn_seconds}) | |
| await save_json(CHANNEL_DATA_FILE, channel_data) | |
| await update_dashboard_message(self.cid) | |
| await interaction.response.send_message(f"✅ Boss '{name}' added ({respawn_seconds}s).", ephemeral=True) | |
| class AddBossButton(discord.ui.Button): | |
| def __init__(self, cid: str): | |
| super().__init__(label="➕ Add Boss", style=discord.ButtonStyle.green) | |
| self.cid = cid | |
| async def callback(self, interaction: discord.Interaction): | |
| await interaction.response.send_modal(AddBossModal(self.cid)) | |
| class RemoveBossDropdown(discord.ui.Select): | |
| def __init__(self, cid: str): | |
| self.cid = cid | |
| options = [discord.SelectOption(label=b["name"]) for b in get_channel_bosses(cid)] | |
| if not options: | |
| options = [discord.SelectOption(label="(No bosses)", default=True)] | |
| super().__init__(placeholder="Select boss to remove", options=options) | |
| async def callback(self, interaction: discord.Interaction): | |
| choice = self.values[0] | |
| if choice == "(No bosses)": | |
| await interaction.response.send_message("No bosses to remove.", ephemeral=True) | |
| return | |
| ensure_channel_record(self.cid) | |
| channel_data[self.cid]["bosses"] = [b for b in channel_data[self.cid]["bosses"] if b["name"] != choice] | |
| channel_data[self.cid]["timers"].pop(choice, None) | |
| await save_json(CHANNEL_DATA_FILE, channel_data) | |
| await update_dashboard_message(self.cid) | |
| await interaction.response.send_message(f"🗑 Removed '{choice}'.", ephemeral=True) | |
| class RemoveBossButton(discord.ui.Button): | |
| def __init__(self, cid: str): | |
| super().__init__(label="🗑 Remove Boss", style=discord.ButtonStyle.danger) | |
| self.cid = cid | |
| async def callback(self, interaction: discord.Interaction): | |
| view = discord.ui.View(timeout=60) | |
| view.add_item(RemoveBossDropdown(self.cid)) | |
| await interaction.response.send_message("Choose a boss to remove:", view=view, ephemeral=True) | |
| class DashboardView(discord.ui.View): | |
| def __init__(self, cid: str): | |
| super().__init__(timeout=None) | |
| self.cid = cid | |
| for b in get_channel_bosses(cid): | |
| self.add_item(BossDropdown(cid, b["name"])) | |
| self.add_item(AddBossButton(cid)) | |
| self.add_item(RemoveBossButton(cid)) | |
| # ---------------------------- | |
| # Dashboard render/update | |
| # ---------------------------- | |
| async def update_dashboard_message(channel_id: str): | |
| channel = bot.get_channel(int(channel_id)) | |
| if not channel or channel_id not in dashboards: | |
| return | |
| try: | |
| msg = await channel.fetch_message(int(dashboards[channel_id])) | |
| except discord.NotFound: | |
| return | |
| ensure_channel_record(channel_id) | |
| bosses = get_channel_bosses(channel_id) | |
| timers = get_channel_timers(channel_id) | |
| lines = [] | |
| for b in bosses: | |
| name = b["name"] | |
| if name in timers: | |
| remaining = timers[name] - now_ts() | |
| hms = fmt_hms(remaining) | |
| respawn_ts = int(timers[name]) | |
| lines.append(f"**{name}** — Respawns <t:{respawn_ts}:R> (`{hms}`)") | |
| else: | |
| lines.append(f"**{name}** — READY (`00:00:00`)") | |
| if not lines: | |
| lines = ["No bosses yet. Use ➕ **Add Boss** to get started."] | |
| embed = discord.Embed(title="Boss Timers", description="\n".join(lines), color=0x00ff00) | |
| files = [] | |
| logo_path = "mh_logo.png" | |
| if os.path.exists(logo_path): | |
| embed.set_thumbnail(url="attachment://mh_logo.png") | |
| files = [discord.File(logo_path, filename="mh_logo.png")] | |
| await msg.edit(embed=embed, view=DashboardView(channel_id), attachments=files) | |
| @tasks.loop(seconds=1) | |
| async def update_dashboards(): | |
| for channel_id in list(dashboards.keys()): | |
| await update_dashboard_message(channel_id) | |
| # ---------------------------- | |
| # Slash Commands | |
| # ---------------------------- | |
| @bot.event | |
| async def on_ready(): | |
| await bot.tree.sync() | |
| update_dashboards.start() | |
| print(f"Logged in as {bot.user}") | |
| @bot.tree.command(description="Create a boss dashboard in this channel.") | |
| async def timers_cmd(interaction: discord.Interaction): | |
| channel_id = str(interaction.channel.id) | |
| if channel_id in dashboards: | |
| msg_id = dashboards[channel_id] | |
| await interaction.response.send_message( | |
| f"Dashboard already exists: <https://discord.com/channels/{interaction.guild.id}/{channel_id}/{msg_id}>", | |
| ephemeral=True | |
| ) | |
| return | |
| ensure_channel_record(channel_id) | |
| bosses = get_channel_bosses(channel_id) | |
| timers = get_channel_timers(channel_id) | |
| lines = [] | |
| for b in bosses: | |
| name = b["name"] | |
| if name in timers: | |
| remaining = timers[name] - now_ts() | |
| hms = fmt_hms(remaining) | |
| respawn_ts = int(timers[name]) | |
| lines.append(f"**{name}** — Respawns <t:{respawn_ts}:R> (`{hms}`)") | |
| else: | |
| lines.append(f"**{name}** — READY (`00:00:00`)") | |
| if not lines: | |
| lines = ["No bosses yet. Use ➕ **Add Boss** to get started."] | |
| embed = discord.Embed(title="Boss Timers", description="\n".join(lines), color=0x00ff00) | |
| files = [] | |
| logo_path = "mh_logo.png" | |
| if os.path.exists(logo_path): | |
| embed.set_thumbnail(url="attachment://mh_logo.png") | |
| files = [discord.File(logo_path, filename="mh_logo.png")] | |
| msg = await interaction.channel.send(embed=embed, view=DashboardView(channel_id), files=files) | |
| dashboards[channel_id] = str(msg.id) | |
| await save_json(DASHBOARDS_FILE, dashboards) | |
| try: | |
| await msg.pin(reason="Boss Timers Dashboard") | |
| except (discord.Forbidden, discord.HTTPException): | |
| pass | |
| await interaction.response.send_message(f"Dashboard created: {msg.jump_url}", ephemeral=True) | |
| @bot.tree.command(description="Set remaining time for a boss in this channel (HH:MM:SS).") | |
| @app_commands.describe(name="Exact boss name", hhmmss="Time left, e.g. 00:02:00") | |
| async def settime(interaction: discord.Interaction, name: str, hhmmss: str): | |
| cid = str(interaction.channel.id) | |
| if not any(b["name"].lower() == name.lower() for b in get_channel_bosses(cid)): | |
| await interaction.response.send_message("❌ Boss not tracked in this channel.", ephemeral=True) | |
| return | |
| try: | |
| secs = parse_hms(hhmmss) | |
| except Exception as e: | |
| await interaction.response.send_message(f"❌ {e}", ephemeral=True) | |
| return | |
| await set_boss_remaining(cid, name, secs) | |
| await update_dashboard_message(cid) | |
| await interaction.response.send_message(f"⏱ Set **{name}** to `{hhmmss}` remaining.", ephemeral=True) | |
| @bot.tree.command(description="Add a boss (admin). Also updates master list if needed.") | |
| @app_commands.describe(name="Boss name", respawn_seconds="Default respawn time in seconds") | |
| @app_commands.checks.has_permissions(administrator=True) | |
| async def addboss(interaction: discord.Interaction, name: str, respawn_seconds: int): | |
| cid = str(interaction.channel.id) | |
| if not find_master_boss(name): | |
| bosses_master.append({"name": name, "respawn": respawn_seconds}) | |
| await save_json(BOSSES_FILE, bosses_master) | |
| ensure_channel_record(cid) | |
| if not any(b["name"].lower() == name.lower() for b in channel_data[cid]["bosses"]): | |
| channel_data[cid]["bosses"].append({"name": name, "respawn": respawn_seconds}) | |
| await save_json(CHANNEL_DATA_FILE, channel_data) | |
| await update_dashboard_message(cid) | |
| await interaction.response.send_message(f"✅ Boss '{name}' added ({respawn_seconds}s).", ephemeral=True) | |
| @bot.tree.command(description="Remove a boss from THIS channel only.") | |
| @app_commands.describe(name="Boss name to remove") | |
| @app_commands.checks.has_permissions(administrator=True) | |
| async def removeboss(interaction: discord.Interaction, name: str): | |
| cid = str(interaction.channel.id) | |
| ensure_channel_record(cid) | |
| before = len(channel_data[cid]["bosses"]) | |
| channel_data[cid]["bosses"] = [b for b in channel_data[cid]["bosses"] if b["name"].lower() != name.lower()] | |
| channel_data[cid]["timers"].pop(name, None) | |
| await save_json(CHANNEL_DATA_FILE, channel_data) | |
| await update_dashboard_message(cid) | |
| after = len(channel_data[cid]["bosses"]) | |
| if before == after: | |
| await interaction.response.send_message("❌ Boss not found in this channel.", ephemeral=True) | |
| else: | |
| await interaction.response.send_message(f"🗑 Removed '{name}' from this channel.", ephemeral=True) | |
| @bot.tree.command(description="Mark a boss as killed (uses default respawn).") | |
| @app_commands.describe(name="Exact boss name") | |
| async def kill(interaction: discord.Interaction, name: str): | |
| cid = str(interaction.channel.id) | |
| ok = await reset_boss_timer(cid, name) | |
| await update_dashboard_message(cid) | |
| await interaction.response.send_message( | |
| f"{'✅' if ok else '❌'} {name} {'timer reset.' if ok else 'not found.'}", | |
| ephemeral=True | |
| ) | |
| # ---------------------------- | |
| # Run | |
| # ---------------------------- | |
| bot.run(TOKEN) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment