Advertisement
R2dTOO

Multi Server Bot

Jun 18th, 2025 (edited)
546
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 16.09 KB | None | 0 0
  1. import discord
  2. from discord.ext import commands
  3. from discord import ui, SelectOption
  4. from discord import app_commands
  5. from datetime import datetime, timedelta
  6. from pytz import timezone, utc
  7. import gspread
  8. from oauth2client.service_account import ServiceAccountCredentials
  9. from collections import defaultdict
  10. import os, io, json, logging, asyncio
  11.  
  12. # ————— Logging Setup —————
  13. logging.basicConfig(filename="fitness_bot.log", level=logging.INFO,
  14.                     format="%(asctime)s [%(levelname)s] %(message)s")
  15. log = logging.getLogger()
  16. log.info("=== Bot startup ===")
  17.  
  18. # ————— Config Utilities —————
  19. CONFIG_FILE = "server_configs.json"
  20. COMMON_TIMEZONES = [
  21.     "UTC", "US/Eastern", "US/Central", "US/Mountain", "US/Pacific",
  22.     "Europe/London", "Europe/Berlin", "Asia/Tokyo", "Australia/Sydney"
  23. ]
  24.  
  25. def load_configs():
  26.     try:
  27.         data = json.load(open(CONFIG_FILE))
  28.         return data if isinstance(data, dict) else {}
  29.     except (FileNotFoundError, json.JSONDecodeError):
  30.         return {}
  31.  
  32. def save_configs(cfg):
  33.     with open(CONFIG_FILE, "w") as f:
  34.         json.dump(cfg, f, indent=2)
  35.  
  36. server_configs = load_configs()
  37. daily_log_cache = defaultdict(set)
  38.  
  39. # ————— Google Sheets Setup —————
  40. CREDENTIALS_FILE = '/home/r23dprinting/fitness-challenge-462612-78786b6edf2e.json'
  41. scope = ['https://spreadsheets.google.com/feeds','https://www.googleapis.com/auth/drive']
  42. creds = ServiceAccountCredentials.from_json_keyfile_name(CREDENTIALS_FILE, scope)
  43. client = gspread.authorize(creds)
  44.  
  45. # ————— Helpers —————
  46. def is_admin(ctx, cfg):
  47.     return ctx.user.guild_permissions.administrator or any(
  48.         r.name == cfg.get("admin_role") for r in ctx.user.roles
  49.     )
  50.  
  51. # ————— Config Flow Modals —————
  52. CONFIG_KEYS = [
  53.     ("sheet_name", "Google Sheet name"),
  54.     ("thread_name", "Exact thread name"),
  55.     ("channel_name", "Parent channel name"),
  56.     ("admin_role", "Admin role name"),
  57.     ("hashtag", "Hashtag (#include, or leave blank)"),
  58.     ("timezone", "Timezone"),
  59.     ("start_date", "Challenge start date (YYYY-MM-DD)"),
  60.     ("end_date", "Challenge end date (YYYY-MM-DD)"),
  61.     ("goal_days", "Goal days (number)"),
  62.     ("auto_summaries", "Send auto-summaries? (yes/no)")
  63. ]
  64.  
  65. class ConfigFlow:
  66.     def __init__(self, guild_id, existing=None):
  67.         self.guild_id = guild_id
  68.         self.data = existing.copy() if existing else {}
  69.         self.step = 0
  70.  
  71. class InputModal(ui.Modal):
  72.     def __init__(self, flow: ConfigFlow, key, label):
  73.         super().__init__(title=f"Set {label}")
  74.         self.flow = flow
  75.         self.key = key
  76.         default_val = self.flow.data.get(self.key, "")
  77.         self.add_item(ui.TextInput(label=label, style=discord.TextStyle.short, default=default_val))
  78.  
  79.     async def on_submit(self, interaction):
  80.         val = self.children[0].value.strip()
  81.         if self.key == "goal_days" and not val.isdigit():
  82.             return await interaction.response.send_message("❌ Must be a number.", ephemeral=True)
  83.         if self.key == "auto_summaries":
  84.             lv = val.lower()
  85.             if lv not in ("yes", "no"):
  86.                 return await interaction.response.send_message("❌ Enter yes or no.", ephemeral=True)
  87.             val = (lv == "yes")
  88.         self.flow.data[self.key] = val
  89.         self.flow.step += 1
  90.         if self.flow.step < len(CONFIG_KEYS):
  91.             nk, nl = CONFIG_KEYS[self.flow.step]
  92.             if nk == "timezone":
  93.                 await interaction.response.send_modal(TimezoneModal(self.flow))
  94.             else:
  95.                 await interaction.response.send_modal(InputModal(self.flow, nk, nl))
  96.         else:
  97.             server_configs[str(self.flow.guild_id)] = self.flow.data
  98.             save_configs(server_configs)
  99.             await interaction.response.send_message("✅ Configuration saved & reloaded!", ephemeral=True)
  100.  
  101. class TimezoneModal(ui.Modal):
  102.     def __init__(self, flow: ConfigFlow):
  103.         super().__init__(title="Enter Timezone (e.g. US/Eastern)")
  104.         self.flow = flow
  105.         self.add_item(ui.TextInput(label="Timezone", placeholder="e.g. US/Eastern"))
  106.  
  107.     async def on_submit(self, interaction: discord.Interaction):
  108.         val = self.children[0].value.strip()
  109.         if val not in COMMON_TIMEZONES:
  110.             return await interaction.response.send_message(
  111.                 "❌ Invalid timezone. Must be one of:\n" +
  112.                 ", ".join(COMMON_TIMEZONES), ephemeral=True
  113.             )
  114.         self.flow.data["timezone"] = val
  115.         self.flow.step += 1
  116.         nk, nl = CONFIG_KEYS[self.flow.step]
  117.         await interaction.response.send_modal(InputModal(self.flow, nk, nl))
  118.  
  119. # ————— Bot Init —————
  120. intents = discord.Intents.default()
  121. intents.message_content = True
  122. intents.guilds = True
  123. intents.members = True
  124. bot = commands.Bot(command_prefix="!", intents=intents)
  125.  
  126. @bot.event
  127. async def on_ready():
  128.     log.info("Bot is online and ready")
  129.     try:
  130.         synced = await bot.tree.sync()
  131.         log.info(f"Synced {len(synced)} application commands")
  132.     except Exception as e:
  133.         log.error(f"Slash sync failed: {e}")
  134.     asyncio.create_task(summary_scheduler())
  135.  
  136. @bot.event
  137. async def on_message(message: discord.Message):
  138.     if message.author.bot or not message.guild:
  139.         return
  140.     cfg = server_configs.get(str(message.guild.id))
  141.     if not cfg:
  142.         return
  143.     thread = message.channel if isinstance(message.channel, discord.Thread) else None
  144.     if not thread or thread.name != cfg["thread_name"] or thread.parent.name != cfg["channel_name"]:
  145.         return
  146.     if not (message.attachments or cfg.get("hashtag", "").lower() in message.content.lower()):
  147.         return
  148.  
  149.     tz = timezone(cfg["timezone"])
  150.     now = utc.localize(datetime.utcnow()).astimezone(tz)
  151.     try:
  152.         sd = datetime.fromisoformat(cfg["start_date"]).date()
  153.         ed = datetime.fromisoformat(cfg["end_date"]).date()
  154.     except:
  155.         log.error(f"Invalid dates for guild {message.guild.id}")
  156.         return
  157.  
  158.     if not (sd <= now.date() <= ed):
  159.         try:
  160.             await message.author.send(
  161.                 f"💪 Good job staying active!\nYour activity in **{message.guild.name}** wasn’t logged — "
  162.                 f"challenge runs **{sd.strftime('%b %d')} to {ed.strftime('%b %d')} ({tz.zone})**."
  163.             )
  164.             log.info(f"Sent DM to {message.author}")
  165.         except discord.Forbidden:
  166.             log.warning(f"Cannot DM {message.author}")
  167.         return
  168.  
  169.     dstr = now.date().isoformat()
  170.     uid = str(message.author.id)
  171.     if uid not in daily_log_cache[dstr]:
  172.         try:
  173.             client.open(cfg["sheet_name"]).sheet1.append_row([uid, str(message.author), now.isoformat()])
  174.             daily_log_cache[dstr].add(uid)
  175.             log.info(f"Logged {message.author}")
  176.         except Exception as e:
  177.             log.error(f"Sheet append failed: {e}")
  178.  
  179.     await bot.process_commands(message)
  180.  
  181.  
  182. # ————— Slash (Tree) Commands —————
  183. @bot.tree.command(name="progress", description="Your monthly activity count")
  184. async def progress(interaction: discord.Interaction):
  185.     cfg = server_configs[str(interaction.guild.id)]
  186.     sheet = client.open(cfg["sheet_name"]).sheet1
  187.     days = {r['Timestamp'].split("T")[0] for r in sheet.get_all_records() if r['UserID'] == str(interaction.user.id)}
  188.     await interaction.response.send_message(f"You’ve logged {len(days)} day(s) this challenge! 💪", ephemeral=True)
  189.  
  190. @bot.tree.command(name="streak", description="Your current activity streak")
  191. async def streak(interaction: discord.Interaction):
  192.     cfg = server_configs[str(interaction.guild.id)]
  193.     sheet = client.open(cfg["sheet_name"]).sheet1
  194.     recs = sorted(sheet.get_all_records(), key=lambda r: r['Timestamp'])
  195.     days = sorted({r['Timestamp'].split("T")[0] for r in recs if r['UserID'] == str(interaction.user.id)}, reverse=True)
  196.     streak_count = 0
  197.     today = datetime.utcnow().date()
  198.     for d in days:
  199.         if datetime.fromisoformat(d).date() == today - timedelta(days=streak_count):
  200.             streak_count += 1
  201.         else:
  202.             break
  203.     await interaction.response.send_message(f"🔥 Your current streak: {streak_count} day(s)", ephemeral=True)
  204.  
  205. @bot.tree.command(name="check", description="ADMIN: Check another member’s log days")
  206. async def check(interaction: discord.Interaction, member: discord.Member):
  207.     cfg = server_configs[str(interaction.guild.id)]
  208.     if not is_admin(interaction, cfg):
  209.         return await interaction.response.send_message("❌ No permission", ephemeral=True)
  210.     sheet = client.open(cfg["sheet_name"]).sheet1
  211.     days = {r['Timestamp'].split("T")[0] for r in sheet.get_all_records() if r['UserID'] == str(member.id)}
  212.     await interaction.response.send_message(f"{member.display_name} has logged {len(days)} day(s).", ephemeral=True)
  213.  
  214. @bot.tree.command(name="leaderboard", description="ADMIN: View top participants")
  215. async def leaderboard(interaction: discord.Interaction):
  216.     cfg = server_configs[str(interaction.guild.id)]
  217.     if not is_admin(interaction, cfg):
  218.         return await interaction.response.send_message("❌ No permission", ephemeral=True)
  219.     sheet = client.open(cfg["sheet_name"]).sheet1
  220.     counts = defaultdict(set)
  221.     for r in sheet.get_all_records():
  222.         counts[r['UserID']].add(r['Timestamp'].split("T")[0])
  223.     sorted_lb = sorted(counts.items(), key=lambda x: len(x[1]), reverse=True)
  224.     msg = "🏆 Leaderboard:\n"
  225.     for i, (uid, ds) in enumerate(sorted_lb[:10]):
  226.         member = interaction.guild.get_member(int(uid))
  227.         name = member.display_name if member else uid
  228.         msg += f"{i+1}. {name} – {len(ds)} day(s)\n"
  229.     await interaction.response.send_message(msg, ephemeral=True)
  230.  
  231. @bot.tree.command(name="export", description="ADMIN: Export logs to CSV")
  232. async def export(interaction: discord.Interaction):
  233.     cfg = server_configs[str(interaction.guild.id)]
  234.     if not is_admin(interaction, cfg):
  235.         return await interaction.response.send_message("❌ No permission", ephemeral=True)
  236.     csv_content = "\n".join(",".join(r) for r in client.open(cfg["sheet_name"]).sheet1.get_all_values())
  237.     await interaction.response.send_message(file=discord.File(fp=io.StringIO(csv_content), filename="fitness_log.csv"), ephemeral=True)
  238.  
  239. @bot.tree.command(name="reset_cache", description="ADMIN: Reset today’s in-memory log cache")
  240. async def reset_cache(interaction: discord.Interaction):
  241.     cfg = server_configs[str(interaction.guild.id)]
  242.     if not is_admin(interaction, cfg):
  243.         return await interaction.response.send_message("❌ No permission", ephemeral=True)
  244.     today = datetime.utcnow().date().isoformat()
  245.     if today in daily_log_cache:
  246.         del daily_log_cache[today]
  247.         await interaction.response.send_message("♻️ Today’s log cache has been reset.", ephemeral=True)
  248.     else:
  249.         await interaction.response.send_message("ℹ️ No cached entries today.", ephemeral=True)
  250.  
  251. @bot.tree.command(name="reload_config", description="ADMIN: Reload JSON config file")
  252. async def reload_config(interaction: discord.Interaction):
  253.     global server_configs  # <- move this line to the top
  254.     cfg = server_configs.get(str(interaction.guild.id))
  255.     if not is_admin(interaction, cfg):
  256.         return await interaction.response.send_message("❌ No permission", ephemeral=True)
  257.     server_configs = load_configs()
  258.     await interaction.response.send_message("🔄 Configuration reloaded.", ephemeral=True)
  259.  
  260. class ConfigPromptView(ui.View):
  261.     def __init__(self, interaction, cfg):
  262.         super().__init__(timeout=60)
  263.         self.cfg = cfg
  264.  
  265.     @ui.button(label="Edit Settings", style=discord.ButtonStyle.primary)
  266.     async def edit(self, interaction: discord.Interaction, button: discord.ui.Button):
  267.         flow = ConfigFlow(interaction.guild.id, self.cfg)
  268.         nk, nl = CONFIG_KEYS[0]
  269.         await interaction.response.send_modal(InputModal(flow, nk, nl))
  270.         self.stop()
  271.  
  272.     @ui.button(label="Keep Current Settings", style=discord.ButtonStyle.secondary)
  273.     async def keep(self, interaction: discord.Interaction, button: discord.ui.Button):
  274.         await interaction.response.send_message("✅ Keeping existing configuration.", ephemeral=True)
  275.         self.stop()
  276.  
  277. @bot.tree.command(name="configure", description="ADMIN: Setup or update this server")
  278. async def configure(interaction: discord.Interaction):
  279.     if not interaction.user.guild_permissions.administrator:
  280.         return await interaction.response.send_message("❌ Admins only.", ephemeral=True)
  281.  
  282.     cfg = server_configs.get(str(interaction.guild.id))
  283.     if not cfg:
  284.         await interaction.response.send_message("🆕 Starting new server setup...", ephemeral=True)
  285.         flow = ConfigFlow(interaction.guild.id)
  286.         nk, nl = CONFIG_KEYS[0]
  287.         return await interaction.response.send_modal(InputModal(flow, nk, nl))
  288.  
  289.     summary = (
  290.         f"**Current Configuration:**\n"
  291.         f"- Sheet: `{cfg.get('sheet_name', 'N/A')}`\n"
  292.         f"- Channel: `{cfg.get('channel_name', 'N/A')}`\n"
  293.         f"- Thread: `{cfg.get('thread_name', 'N/A')}`\n"
  294.         f"- Admin role: `{cfg.get('admin_role', 'N/A')}`\n"
  295.         f"- Hashtag: `{cfg.get('hashtag', 'None')}`\n"
  296.         f"- Timezone: `{cfg.get('timezone', 'N/A')}`\n"
  297.         f"- Challenge: {cfg.get('start_date', 'N/A')} → {cfg.get('end_date', 'N/A')} "
  298.         f"(Goal: {cfg.get('goal_days','n/a')}, AutoSummaries: {cfg.get('auto_summaries','yes')})\n\n"
  299.         f"🔧 Do you want to edit this configuration?"
  300.     )
  301.     await interaction.response.send_message(summary, view=ConfigPromptView(interaction, cfg), ephemeral=True)
  302.  
  303. @bot.tree.command(name="view_config", description="ADMIN: View current configuration")
  304. async def view_config(interaction: discord.Interaction):
  305.     cfg = server_configs.get(str(interaction.guild.id))
  306.     if not cfg or not interaction.user.guild_permissions.administrator:
  307.         return await interaction.response.send_message("❌ No permission or not configured.", ephemeral=True)
  308.  
  309.     await interaction.response.send_message(
  310.         f"**Configuration for {interaction.guild.name}:**\n"
  311.         f"- Sheet: `{cfg.get('sheet_name', 'N/A')}`\n"
  312.         f"- Channel: `{cfg.get('channel_name', 'N/A')}`\n"
  313.         f"- Thread: `{cfg.get('thread_name', 'N/A')}`\n"
  314.         f"- Admin role: `{cfg.get('admin_role', 'N/A')}`\n"
  315.         f"- Hashtag: `{cfg.get('hashtag', 'None')}`\n"
  316.         f"- Timezone: `{cfg.get('timezone', 'N/A')}`\n"
  317.         f"- Challenge: {cfg.get('start_date', 'N/A')} → {cfg.get('end_date', 'N/A')} "
  318.         f"(Goal: {cfg.get('goal_days','n/a')}, AutoSummaries: {cfg.get('auto_summaries','yes')})\n\n"
  319.         f"🔑 Make sure your sheet is shared with `{creds.service_account_email}` as Editor.",
  320.         ephemeral=True
  321.     )
  322.  
  323. @bot.tree.command(name="help", description="Show available commands")
  324. async def _help(interaction: discord.Interaction):
  325.     cfg = server_configs.get(str(interaction.guild.id))
  326.     admin = cfg and is_admin(interaction, cfg)
  327.     lines = ["**Available Commands:**",
  328.              "• /progress – Your monthly log count",
  329.              "• /streak – Your current activity streak"]
  330.     if admin:
  331.         lines += [
  332.             "", "**Admin Commands:**",
  333.             "• /check @user – View another user’s stats",
  334.             "• /leaderboard – View top participants",
  335.             "• /export – Export logs to CSV",
  336.             "• /reset_cache – Clear today’s cache",
  337.             "• /reload_config – Reload config file",
  338.             "• /configure – Setup/update this server",
  339.             "• /view_config – View current settings"
  340.         ]
  341.     lines.append("• /help – Show this help message")
  342.     await interaction.response.send_message("\n".join(lines), ephemeral=True)
  343.  
  344. # ————— Summary Scheduler (stub) —————
  345. async def summary_scheduler():
  346.     await bot.wait_until_ready()
  347.     while True:
  348.         await asyncio.sleep(86400)
  349.         # future summary/notification logic happens here
  350.  
  351. # ————— Run the Bot —————
  352. bot.run(os.getenv("DISCORD_BOT_TOKEN"))
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement