Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import discord
- from discord.ext import commands
- from discord import ui, SelectOption
- from discord import app_commands
- from datetime import datetime, timedelta
- from pytz import timezone, utc
- import gspread
- from oauth2client.service_account import ServiceAccountCredentials
- from collections import defaultdict
- import os, io, json, logging, asyncio
- # ————— Logging Setup —————
- logging.basicConfig(filename="fitness_bot.log", level=logging.INFO,
- format="%(asctime)s [%(levelname)s] %(message)s")
- log = logging.getLogger()
- log.info("=== Bot startup ===")
- # ————— Config Utilities —————
- CONFIG_FILE = "server_configs.json"
- COMMON_TIMEZONES = [
- "UTC", "US/Eastern", "US/Central", "US/Mountain", "US/Pacific",
- "Europe/London", "Europe/Berlin", "Asia/Tokyo", "Australia/Sydney"
- ]
- def load_configs():
- try:
- data = json.load(open(CONFIG_FILE))
- return data if isinstance(data, dict) else {}
- except (FileNotFoundError, json.JSONDecodeError):
- return {}
- def save_configs(cfg):
- with open(CONFIG_FILE, "w") as f:
- json.dump(cfg, f, indent=2)
- server_configs = load_configs()
- daily_log_cache = defaultdict(set)
- # ————— Google Sheets Setup —————
- CREDENTIALS_FILE = '/home/r23dprinting/fitness-challenge-462612-78786b6edf2e.json'
- scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
- creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope)
- client = gspread.authorize(creds)
- # ————— Helpers —————
- def is_admin(ctx, cfg):
- return ctx.user.guild_permissions.administrator or any(
- r.name == cfg.get("admin_role") for r in ctx.user.roles
- )
- # ————— Config Flow Modals —————
- CONFIG_KEYS = [
- ("sheet_name", "Google Sheet name"),
- ("thread_name", "Exact thread name"),
- ("channel_name", "Parent channel name"),
- ("admin_role", "Admin role name"),
- ("hashtag", "Hashtag (#include, or leave blank)"),
- ("timezone", "Timezone"),
- ("start_date", "Challenge start date (YYYY-MM-DD)"),
- ("end_date", "Challenge end date (YYYY-MM-DD)"),
- ("goal_days", "Goal days (number)"),
- ("auto_summaries", "Send auto-summaries? (yes/no)")
- ]
- class ConfigFlow:
- def __init__(self, guild_id, existing=None):
- self.guild_id = guild_id
- self.data = existing.copy() if existing else {}
- self.step = 0
- class InputModal(ui.Modal):
- def __init__(self, flow: ConfigFlow, key, label):
- super().__init__(title=f"Set {label}")
- self.flow = flow
- self.key = key
- default_val = self.flow.data.get(self.key, "")
- self.add_item(ui.TextInput(label=label, style=discord.TextStyle.short, default=default_val))
- async def on_submit(self, interaction):
- val = self.children[0].value.strip()
- if self.key == "goal_days" and not val.isdigit():
- return await interaction.response.send_message("❌ Must be a number.", ephemeral=True)
- if self.key == "auto_summaries":
- lv = val.lower()
- if lv not in ("yes", "no"):
- return await interaction.response.send_message("❌ Enter yes or no.", ephemeral=True)
- val = (lv == "yes")
- self.flow.data[self.key] = val
- self.flow.step += 1
- if self.flow.step < len(CONFIG_KEYS):
- nk, nl = CONFIG_KEYS[self.flow.step]
- if nk == "timezone":
- await interaction.response.send_modal(TimezoneModal(self.flow))
- else:
- await interaction.response.send_modal(InputModal(self.flow, nk, nl))
- else:
- server_configs[str(self.flow.guild_id)] = self.flow.data
- save_configs(server_configs)
- await interaction.response.send_message("✅ Configuration saved & reloaded!", ephemeral=True)
- class TimezoneModal(ui.Modal):
- def __init__(self, flow: ConfigFlow):
- super().__init__(title="Enter Timezone (e.g. US/Eastern)")
- self.flow = flow
- self.add_item(ui.TextInput(label="Timezone", placeholder="e.g. US/Eastern"))
- async def on_submit(self, interaction: discord.Interaction):
- val = self.children[0].value.strip()
- if val not in COMMON_TIMEZONES:
- return await interaction.response.send_message(
- "❌ Invalid timezone. Must be one of:\n" +
- ", ".join(COMMON_TIMEZONES), ephemeral=True
- )
- self.flow.data["timezone"] = val
- self.flow.step += 1
- nk, nl = CONFIG_KEYS[self.flow.step]
- await interaction.response.send_modal(InputModal(self.flow, nk, nl))
- # ————— Bot Init —————
- intents = discord.Intents.default()
- intents.message_content = True
- intents.guilds = True
- intents.members = True
- bot = commands.Bot(command_prefix="!", intents=intents)
- @bot.event
- async def on_ready():
- log.info("Bot is online and ready")
- try:
- synced = await bot.tree.sync()
- log.info(f"Synced {len(synced)} application commands")
- except Exception as e:
- log.error(f"Slash sync failed: {e}")
- asyncio.create_task(summary_scheduler())
- @bot.event
- async def on_message(message: discord.Message):
- if message.author.bot or not message.guild:
- return
- cfg = server_configs.get(str(message.guild.id))
- if not cfg:
- return
- thread = message.channel if isinstance(message.channel, discord.Thread) else None
- if not thread or thread.name != cfg["thread_name"] or thread.parent.name != cfg["channel_name"]:
- return
- if not (message.attachments or cfg.get("hashtag", "").lower() in message.content.lower()):
- return
- tz = timezone(cfg["timezone"])
- now = utc.localize(datetime.utcnow()).astimezone(tz)
- try:
- sd = datetime.fromisoformat(cfg["start_date"]).date()
- ed = datetime.fromisoformat(cfg["end_date"]).date()
- except:
- log.error(f"Invalid dates for guild {message.guild.id}")
- return
- if not (sd <= now.date() <= ed):
- try:
- await message.author.send(
- f"💪 Good job staying active!\nYour activity in **{message.guild.name}** wasn’t logged — "
- f"challenge runs **{sd.strftime('%b %d')} to {ed.strftime('%b %d')} ({tz.zone})**."
- )
- log.info(f"Sent DM to {message.author}")
- except discord.Forbidden:
- log.warning(f"Cannot DM {message.author}")
- return
- dstr = now.date().isoformat()
- uid = str(message.author.id)
- if uid not in daily_log_cache[dstr]:
- try:
- client.open(cfg["sheet_name"]).sheet1.append_row([uid, str(message.author), now.isoformat()])
- daily_log_cache[dstr].add(uid)
- log.info(f"Logged {message.author}")
- except Exception as e:
- log.error(f"Sheet append failed: {e}")
- await bot.process_commands(message)
- # ————— Slash (Tree) Commands —————
- @bot.tree.command(name="progress", description="Your monthly activity count")
- async def progress(interaction: discord.Interaction):
- cfg = server_configs[str(interaction.guild.id)]
- sheet = client.open(cfg["sheet_name"]).sheet1
- days = {r['Timestamp'].split("T")[0] for r in sheet.get_all_records() if r['UserID'] == str(interaction.user.id)}
- await interaction.response.send_message(f"You’ve logged {len(days)} day(s) this challenge! 💪", ephemeral=True)
- @bot.tree.command(name="streak", description="Your current activity streak")
- async def streak(interaction: discord.Interaction):
- cfg = server_configs[str(interaction.guild.id)]
- sheet = client.open(cfg["sheet_name"]).sheet1
- recs = sorted(sheet.get_all_records(), key=lambda r: r['Timestamp'])
- days = sorted({r['Timestamp'].split("T")[0] for r in recs if r['UserID'] == str(interaction.user.id)}, reverse=True)
- streak_count = 0
- today = datetime.utcnow().date()
- for d in days:
- if datetime.fromisoformat(d).date() == today - timedelta(days=streak_count):
- streak_count += 1
- else:
- break
- await interaction.response.send_message(f"🔥 Your current streak: {streak_count} day(s)", ephemeral=True)
- @bot.tree.command(name="check", description="ADMIN: Check another member’s log days")
- async def check(interaction: discord.Interaction, member: discord.Member):
- cfg = server_configs[str(interaction.guild.id)]
- if not is_admin(interaction, cfg):
- return await interaction.response.send_message("❌ No permission", ephemeral=True)
- sheet = client.open(cfg["sheet_name"]).sheet1
- days = {r['Timestamp'].split("T")[0] for r in sheet.get_all_records() if r['UserID'] == str(member.id)}
- await interaction.response.send_message(f"{member.display_name} has logged {len(days)} day(s).", ephemeral=True)
- @bot.tree.command(name="leaderboard", description="ADMIN: View top participants")
- async def leaderboard(interaction: discord.Interaction):
- cfg = server_configs[str(interaction.guild.id)]
- if not is_admin(interaction, cfg):
- return await interaction.response.send_message("❌ No permission", ephemeral=True)
- sheet = client.open(cfg["sheet_name"]).sheet1
- counts = defaultdict(set)
- for r in sheet.get_all_records():
- counts[r['UserID']].add(r['Timestamp'].split("T")[0])
- sorted_lb = sorted(counts.items(), key=lambda x: len(x[1]), reverse=True)
- msg = "🏆 Leaderboard:\n"
- for i, (uid, ds) in enumerate(sorted_lb[:10]):
- member = interaction.guild.get_member(int(uid))
- name = member.display_name if member else uid
- msg += f"{i+1}. {name} – {len(ds)} day(s)\n"
- await interaction.response.send_message(msg, ephemeral=True)
- @bot.tree.command(name="export", description="ADMIN: Export logs to CSV")
- async def export(interaction: discord.Interaction):
- cfg = server_configs[str(interaction.guild.id)]
- if not is_admin(interaction, cfg):
- return await interaction.response.send_message("❌ No permission", ephemeral=True)
- csv_content = "\n".join(",".join(r) for r in client.open(cfg["sheet_name"]).sheet1.get_all_values())
- await interaction.response.send_message(file=discord.File(fp=io.StringIO(csv_content), filename="fitness_log.csv"), ephemeral=True)
- @bot.tree.command(name="reset_cache", description="ADMIN: Reset today’s in-memory log cache")
- async def reset_cache(interaction: discord.Interaction):
- cfg = server_configs[str(interaction.guild.id)]
- if not is_admin(interaction, cfg):
- return await interaction.response.send_message("❌ No permission", ephemeral=True)
- today = datetime.utcnow().date().isoformat()
- if today in daily_log_cache:
- del daily_log_cache[today]
- await interaction.response.send_message("♻️ Today’s log cache has been reset.", ephemeral=True)
- else:
- await interaction.response.send_message("ℹ️ No cached entries today.", ephemeral=True)
- @bot.tree.command(name="reload_config", description="ADMIN: Reload JSON config file")
- async def reload_config(interaction: discord.Interaction):
- global server_configs # <- move this line to the top
- cfg = server_configs.get(str(interaction.guild.id))
- if not is_admin(interaction, cfg):
- return await interaction.response.send_message("❌ No permission", ephemeral=True)
- server_configs = load_configs()
- await interaction.response.send_message("🔄 Configuration reloaded.", ephemeral=True)
- class ConfigPromptView(ui.View):
- def __init__(self, interaction, cfg):
- super().__init__(timeout=60)
- self.cfg = cfg
- @ui.button(label="Edit Settings", style=discord.ButtonStyle.primary)
- async def edit(self, interaction: discord.Interaction, button: discord.ui.Button):
- flow = ConfigFlow(interaction.guild.id, self.cfg)
- nk, nl = CONFIG_KEYS[0]
- await interaction.response.send_modal(InputModal(flow, nk, nl))
- self.stop()
- @ui.button(label="Keep Current Settings", style=discord.ButtonStyle.secondary)
- async def keep(self, interaction: discord.Interaction, button: discord.ui.Button):
- await interaction.response.send_message("✅ Keeping existing configuration.", ephemeral=True)
- self.stop()
- @bot.tree.command(name="configure", description="ADMIN: Setup or update this server")
- async def configure(interaction: discord.Interaction):
- if not interaction.user.guild_permissions.administrator:
- return await interaction.response.send_message("❌ Admins only.", ephemeral=True)
- cfg = server_configs.get(str(interaction.guild.id))
- if not cfg:
- await interaction.response.send_message("🆕 Starting new server setup...", ephemeral=True)
- flow = ConfigFlow(interaction.guild.id)
- nk, nl = CONFIG_KEYS[0]
- return await interaction.response.send_modal(InputModal(flow, nk, nl))
- summary = (
- f"**Current Configuration:**\n"
- f"- Sheet: `{cfg.get('sheet_name', 'N/A')}`\n"
- f"- Channel: `{cfg.get('channel_name', 'N/A')}`\n"
- f"- Thread: `{cfg.get('thread_name', 'N/A')}`\n"
- f"- Admin role: `{cfg.get('admin_role', 'N/A')}`\n"
- f"- Hashtag: `{cfg.get('hashtag', 'None')}`\n"
- f"- Timezone: `{cfg.get('timezone', 'N/A')}`\n"
- f"- Challenge: {cfg.get('start_date', 'N/A')} → {cfg.get('end_date', 'N/A')} "
- f"(Goal: {cfg.get('goal_days','n/a')}, AutoSummaries: {cfg.get('auto_summaries','yes')})\n\n"
- f"🔧 Do you want to edit this configuration?"
- )
- await interaction.response.send_message(summary, view=ConfigPromptView(interaction, cfg), ephemeral=True)
- @bot.tree.command(name="view_config", description="ADMIN: View current configuration")
- async def view_config(interaction: discord.Interaction):
- cfg = server_configs.get(str(interaction.guild.id))
- if not cfg or not interaction.user.guild_permissions.administrator:
- return await interaction.response.send_message("❌ No permission or not configured.", ephemeral=True)
- await interaction.response.send_message(
- f"**Configuration for {interaction.guild.name}:**\n"
- f"- Sheet: `{cfg.get('sheet_name', 'N/A')}`\n"
- f"- Channel: `{cfg.get('channel_name', 'N/A')}`\n"
- f"- Thread: `{cfg.get('thread_name', 'N/A')}`\n"
- f"- Admin role: `{cfg.get('admin_role', 'N/A')}`\n"
- f"- Hashtag: `{cfg.get('hashtag', 'None')}`\n"
- f"- Timezone: `{cfg.get('timezone', 'N/A')}`\n"
- f"- Challenge: {cfg.get('start_date', 'N/A')} → {cfg.get('end_date', 'N/A')} "
- f"(Goal: {cfg.get('goal_days','n/a')}, AutoSummaries: {cfg.get('auto_summaries','yes')})\n\n"
- f"🔑 Make sure your sheet is shared with `{creds.service_account_email}` as Editor.",
- ephemeral=True
- )
- @bot.tree.command(name="help", description="Show available commands")
- async def _help(interaction: discord.Interaction):
- cfg = server_configs.get(str(interaction.guild.id))
- admin = cfg and is_admin(interaction, cfg)
- lines = ["**Available Commands:**",
- "• /progress – Your monthly log count",
- "• /streak – Your current activity streak"]
- if admin:
- lines += [
- "", "**Admin Commands:**",
- "• /check @user – View another user’s stats",
- "• /leaderboard – View top participants",
- "• /export – Export logs to CSV",
- "• /reset_cache – Clear today’s cache",
- "• /reload_config – Reload config file",
- "• /configure – Setup/update this server",
- "• /view_config – View current settings"
- ]
- lines.append("• /help – Show this help message")
- await interaction.response.send_message("\n".join(lines), ephemeral=True)
- # ————— Summary Scheduler (stub) —————
- async def summary_scheduler():
- await bot.wait_until_ready()
- while True:
- await asyncio.sleep(86400)
- # future summary/notification logic happens here
- # ————— Run the Bot —————
- bot.run(os.getenv("DISCORD_BOT_TOKEN"))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement