· 5 months ago · May 06, 2025, 02:30 AM
1# ===== FILE: config.py =====
2# config.py (final production values)
3
4# Server IDs
5MAIN_SERVER_ID = 1286803379150000148
6SUPPORT_SERVER_ID = 1046624035464810496
7
8# Bot Owners
9BOT_OWNERS = {
10 777345438495277076,
11 1165812036786196583,
12 448896936481652777,
13 1313967179628154922
14}
15
16# Role IDs
17ADMIN_ROLE_ID = 1286814304896548966
18PARTNER_ROLE_ID = 1338199385695195166
19
20# Channel IDs
21DEFAULT_PARTNER_CHANNEL = 1356654755899773029
22PING4PING_CHANNEL = 1351355751523680348
23BLACKLIST_CHANNEL = 1350585651736744028
24SCHEDULE_CHANNEL = 1356646460296269845
25
26# Timing (in seconds)
27DEFAULT_PARTNER_INTERVAL = 86400 # 24 hours
28# ===== FILE: bot.py =====
29# bot.py
30
31import discord
32from discord.ext import commands
33from discord import app_commands
34import os
35from dotenv import load_dotenv
36import aiosqlite
37from datetime import datetime
38import pytz
39
40load_dotenv()
41TOKEN = os.getenv("DISCORD_TOKEN")
42
43intents = discord.Intents.default()
44intents.guilds = True
45intents.messages = True
46intents.message_content = True
47intents.members = True
48
49class NyspiraBot(commands.Bot):
50 def __init__(self):
51 super().__init__(command_prefix="!", intents=intents)
52 self.initial_extensions = [
53 "cogs.partner",
54 "cogs.scheduler",
55 "cogs.admin",
56 "cogs.presence",
57 "cogs.dev",
58 "cogs.blacklist",
59 "cogs.logquery",
60 "cogs.metrics",
61 "cogs.info"
62 ]
63
64 async def setup_hook(self):
65 for extension in self.initial_extensions:
66 try:
67 await self.load_extension(extension)
68 print(f"🔧 Loaded: {extension}")
69 except Exception as e:
70 print(f"⚠ Failed to load {extension}: {e}")
71
72 # Global sync (safe but not always reliable)
73 try:
74 synced = await self.tree.sync()
75 print(f"✅ Synced {len(synced)} global slash command(s).")
76 except Exception as e:
77 print(f"❌ Global sync failed: {e}")
78
79 # Force sync to each connected guild
80 for guild in self.guilds:
81 try:
82 await self.tree.sync(guild=guild)
83 print(f"✅ Synced to {guild.name} ({guild.id})")
84 except Exception as e:
85 print(f"❌ Failed guild sync to {guild.name}: {e}")
86
87 async def on_ready(self):
88 print(f"🤖 Nyspira is online as {self.user}!")
89 print(f"Connected to {len(self.guilds)} server(s):")
90 for guild in self.guilds:
91 print(f" - {guild.name} (ID: {guild.id})")
92
93 await self.change_presence(
94 status=discord.Status.dnd,
95 activity=discord.Game(name="pinging partners 👀")
96 )
97
98 # Send "back online" message to a dev channel
99 chan = self.get_channel(1368400480346046605) # DEV_LOG_CHANNEL_ID
100 if chan:
101 cst = pytz.timezone("America/Chicago")
102 now = discord.utils.format_dt(datetime.now(cst), style='F')
103 await chan.send(f"✅ <:MCLex:1368780669374693456> Nyspira is back online as of **{now}**!")
104
105 async def on_app_command_completion(self, interaction: discord.Interaction, command: app_commands.Command):
106 print(f"{interaction.user} used /{command.name} in {interaction.guild}")
107
108 async def on_app_command_error(self, interaction: discord.Interaction, error):
109 if isinstance(error, app_commands.errors.CheckFailure):
110 await interaction.response.send_message("🚫 You are blacklisted from using this bot.", ephemeral=True)
111 elif isinstance(error, app_commands.errors.CommandOnCooldown):
112 await interaction.response.send_message(f"⏳ You're on cooldown! Try again in {round(error.retry_after, 1)}s.", ephemeral=True)
113 else:
114 await interaction.response.send_message(f"❌ Error: {str(error)}", ephemeral=True)
115
116bot = NyspiraBot()
117
118@bot.check
119async def global_blacklist_check(ctx):
120 async with aiosqlite.connect("data/blacklist.db") as db:
121 cursor = await db.execute("SELECT 1 FROM blacklist WHERE (type = 'user' AND target_id = ?) OR (type = 'guild' AND target_id = ?)", (ctx.author.id, ctx.guild.id if ctx.guild else 0))
122 result = await cursor.fetchone()
123 if result:
124 raise commands.CheckFailure("You or your server is blacklisted.")
125 return True
126
127# Optional fallback text command to sync if slash commands aren't showing up
128@bot.command(name="sync", hidden=True)
129@commands.is_owner()
130async def sync_slash(ctx):
131 try:
132 synced = await bot.tree.sync()
133 await ctx.send(f"✅ Slash commands synced globally. {ctx.author.mention}")
134 except Exception as e:
135 await ctx.send(f"❌ Failed to sync: {e}")
136
137bot.run(TOKEN)
138# ===== FILE: partner.py =====
139# partner.py (slash command version with cooldowns + error handler)
140
141import discord
142from discord.ext import commands
143from discord import app_commands
144import aiosqlite
145from config import PARTNER_ROLE_NAME, MAIN_SERVER_ID
146
147class PartnerCog(commands.GroupCog, name="partner"):
148 def __init__(self, bot):
149 self.bot = bot
150 super().__init__()
151
152 async def _init_db(self):
153 async with aiosqlite.connect("data/partners.db") as db:
154 await db.execute("""
155 CREATE TABLE IF NOT EXISTS partners (
156 id INTEGER PRIMARY KEY AUTOINCREMENT,
157 guild_id INTEGER,
158 name TEXT NOT NULL,
159 invite_link TEXT,
160 description TEXT,
161 active INTEGER DEFAULT 1
162 )
163 """)
164 await db.commit()
165
166 @commands.Cog.listener()
167 async def on_ready(self):
168 await self._init_db()
169
170 @app_commands.checks.cooldown(1, 15) # 1 use every 15 seconds
171 @app_commands.command(name="addpartner", description="Add a new partner server")
172 async def add_partner(self, interaction: discord.Interaction, name: str, invite: str, description: str):
173 guild_id = interaction.guild.id if interaction.guild else MAIN_SERVER_ID
174 async with aiosqlite.connect("data/partners.db") as db:
175 await db.execute(
176 "INSERT INTO partners (guild_id, name, invite_link, description) VALUES (?, ?, ?, ?)",
177 (guild_id, name, invite, description)
178 )
179 await db.commit()
180 await interaction.response.send_message(f"✅ Partner **{name}** added for this server.")
181
182 @app_commands.checks.cooldown(2, 10) # 2 uses every 10 seconds
183 @app_commands.command(name="listpartners", description="List active partners for this server")
184 async def list_partners(self, interaction: discord.Interaction):
185 guild_id = interaction.guild.id if interaction.guild else MAIN_SERVER_ID
186 async with aiosqlite.connect("data/partners.db") as db:
187 cursor = await db.execute("SELECT id, name, description FROM partners WHERE active = 1 AND guild_id = ?", (guild_id,))
188 rows = await cursor.fetchall()
189
190 if not rows:
191 return await interaction.response.send_message("No active partners found for this server.")
192
193 embed = discord.Embed(title="🤝 Active Partners", color=discord.Color.purple())
194 for pid, name, desc in rows:
195 embed.add_field(name=f"{name} (ID: {pid})", value=desc, inline=False)
196 await interaction.response.send_message(embed=embed)
197
198 @commands.Cog.listener()
199 async def on_app_command_error(self, interaction: discord.Interaction, error):
200 if isinstance(error, app_commands.errors.CommandOnCooldown):
201 await interaction.response.send_message(
202 f"⏳ You're on cooldown! Try again in {round(error.retry_after, 1)} seconds.", ephemeral=True
203 )
204 else:
205 raise error
206
207async def setup(bot):
208 await bot.add_cog(PartnerCog(bot))
209# ===== FILE: scheduler.py =====
210import discord
211from discord.ext import commands
212from discord import app_commands
213from apscheduler.schedulers.asyncio import AsyncIOScheduler
214from apscheduler.triggers.interval import IntervalTrigger
215import aiosqlite
216from datetime import datetime
217from config import DEFAULT_PARTNER_INTERVAL, MAIN_SERVER_ID
218
219PARTNER_ROLE_ID = 1338199385695195166
220DEFAULT_PARTNER_CHANNEL_ID = 1356654755899773029
221
222class SchedulerCog(commands.GroupCog, name="scheduler"):
223 def __init__(self, bot):
224 self.bot = bot
225 self.scheduler = AsyncIOScheduler()
226 super().__init__()
227
228 async def _init_db(self):
229 async with aiosqlite.connect("data/partners.db") as db:
230 await db.execute("""
231 CREATE TABLE IF NOT EXISTS schedules (
232 alias TEXT PRIMARY KEY,
233 guild_id INTEGER,
234 interval INTEGER,
235 last_ping TIMESTAMP,
236 channel_id INTEGER,
237 role_id INTEGER
238 )
239 """)
240 await db.commit()
241
242 async def _load_jobs(self):
243 async with aiosqlite.connect("data/partners.db") as db:
244 cursor = await db.execute("SELECT alias, interval, guild_id FROM schedules")
245 rows = await cursor.fetchall()
246 for alias, interval, guild_id in rows:
247 self.scheduler.add_job(
248 self.ping_partner,
249 IntervalTrigger(seconds=interval),
250 args=[alias, guild_id],
251 id=f"{alias}-{guild_id}"
252 )
253
254 @commands.Cog.listener()
255 async def on_ready(self):
256 await self._init_db()
257 await self._load_jobs()
258 self.scheduler.start()
259
260 @app_commands.command(name="schedule", description="Schedule pings for a partner")
261 @app_commands.checks.cooldown(1, 10)
262 async def schedule_partner(self, interaction: discord.Interaction, alias: str, interval: int = DEFAULT_PARTNER_INTERVAL):
263 guild_id = interaction.guild.id if interaction.guild else MAIN_SERVER_ID
264 channel_id = interaction.channel.id if interaction.channel else DEFAULT_PARTNER_CHANNEL_ID
265 role_id = PARTNER_ROLE_ID
266
267 async with aiosqlite.connect("data/partners.db") as db:
268 await db.execute("""
269 INSERT OR REPLACE INTO schedules (alias, guild_id, interval, last_ping, channel_id, role_id)
270 VALUES (?, ?, ?, ?, ?, ?)
271 """, (alias, guild_id, interval, datetime.utcnow().isoformat(), channel_id, role_id))
272 await db.commit()
273
274 self.scheduler.add_job(
275 self.ping_partner,
276 IntervalTrigger(seconds=interval),
277 args=[alias, guild_id],
278 id=f"{alias}-{guild_id}"
279 )
280
281 await interaction.response.send_message(f"✅ Scheduled pings for **{alias}** every {interval} seconds.")
282
283 @app_commands.command(name="unschedule", description="Unschedule a partner's pings")
284 @app_commands.checks.cooldown(1, 5)
285 async def unschedule_partner(self, interaction: discord.Interaction, alias: str):
286 guild_id = interaction.guild.id if interaction.guild else MAIN_SERVER_ID
287 job_id = f"{alias}-{guild_id}"
288 job = self.scheduler.get_job(job_id)
289 if job:
290 self.scheduler.remove_job(job_id)
291
292 async with aiosqlite.connect("data/partners.db") as db:
293 await db.execute("DELETE FROM schedules WHERE alias = ? AND guild_id = ?", (alias, guild_id))
294 await db.commit()
295
296 await interaction.response.send_message(f"❌ Unscheduling pings for **{alias}**.")
297
298 @app_commands.command(name="nextping", description="Check when the next ping will happen for a partner")
299 @app_commands.checks.cooldown(2, 5)
300 async def next_ping(self, interaction: discord.Interaction, alias: str):
301 guild_id = interaction.guild.id if interaction.guild else MAIN_SERVER_ID
302 job_id = f"{alias}-{guild_id}"
303 job = self.scheduler.get_job(job_id)
304 if job:
305 formatted = discord.utils.format_dt(job.next_run_time, style='R')
306 await interaction.response.send_message(f"⏱ Next ping for **{alias}**: {formatted}")
307 else:
308 await interaction.response.send_message(f"⚠ No job found for **{alias}**.")
309
310 async def ping_partner(self, alias, guild_id):
311 async with aiosqlite.connect("data/partners.db") as db:
312 cursor = await db.execute("""
313 SELECT name, invite_link, description FROM partners
314 WHERE (id = ? OR name = ?) AND guild_id = ?
315 """, (alias, alias, guild_id))
316 row = await cursor.fetchone()
317 if not row:
318 return
319 name, invite, desc = row
320 desc = desc or "No description available."
321 invite = invite or "No invite link provided."
322
323 cursor = await db.execute("SELECT channel_id, role_id FROM schedules WHERE alias = ? AND guild_id = ?", (alias, guild_id))
324 config = await cursor.fetchone()
325 if not config:
326 return
327 channel_id, role_id = config
328
329 channel = self.bot.get_channel(channel_id)
330 role_mention = f"<@&{role_id}>" if role_id else None
331
332 embed = discord.Embed(
333 title=f"🤝 Partner Spotlight: {name}",
334 description=desc,
335 color=discord.Color.green()
336 )
337 embed.add_field(name="Join Them!", value=invite)
338 await channel.send(content=role_mention, embed=embed)
339
340 print(f"[Scheduler] Pinged partner '{alias}' in guild {guild_id}")
341
342 @app_commands.command(name="list", description="List all currently scheduled partners (paginated)")
343 async def list_schedules(self, interaction: discord.Interaction):
344 guild_id = interaction.guild.id if interaction.guild else MAIN_SERVER_ID
345
346 async with aiosqlite.connect("data/partners.db") as db:
347 cursor = await db.execute("SELECT alias, interval FROM schedules WHERE guild_id = ?", (guild_id,))
348 rows = await cursor.fetchall()
349
350 if not rows:
351 await interaction.response.send_message("📭 No schedules found in this server.", ephemeral=True)
352 return
353
354 # Format each entry
355 entries = []
356 for alias, interval in rows:
357 job_id = f"{alias}-{guild_id}"
358 job = self.scheduler.get_job(job_id)
359 if job and job.next_run_time:
360 next_run = discord.utils.format_dt(job.next_run_time, style='R')
361 entries.append(f"🔹 **{alias}** — every `{interval}` seconds (next: {next_run})")
362 else:
363 entries.append(f"⚠️ **{alias}** — scheduled, but job inactive")
364
365 pages = [entries[i:i+5] for i in range(0, len(entries), 5)]
366 total_pages = len(pages)
367 current_page = 0
368
369 embed = discord.Embed(
370 title="📋 Scheduled Partners",
371 description="\n".join(pages[current_page]),
372 color=discord.Color.blue()
373 )
374 embed.set_footer(text=f"Page {current_page + 1}/{total_pages}")
375
376 view = View(timeout=60)
377
378 async def update_page(index):
379 embed.description = "\n".join(pages[index])
380 embed.set_footer(text=f"Page {index + 1}/{total_pages}")
381 await message.edit(embed=embed, view=view)
382
383 prev_btn = Button(label="◀️ Prev", style=discord.ButtonStyle.secondary)
384 next_btn = Button(label="Next ▶️", style=discord.ButtonStyle.secondary)
385
386 async def prev_callback(inter):
387 nonlocal current_page
388 if current_page > 0:
389 current_page -= 1
390 await update_page(current_page)
391 await inter.response.defer()
392
393 async def next_callback(inter):
394 nonlocal current_page
395 if current_page < total_pages - 1:
396 current_page += 1
397 await update_page(current_page)
398 await inter.response.defer()
399
400 prev_btn.callback = prev_callback
401 next_btn.callback = next_callback
402
403 view.add_item(prev_btn)
404 view.add_item(next_btn)
405
406 message = await interaction.response.send_message(embed=embed, view=view)
407
408 @commands.Cog.listener()
409 async def on_app_command_error(self, interaction: discord.Interaction, error):
410 if isinstance(error, app_commands.errors.CommandOnCooldown):
411 await interaction.response.send_message(
412 f"⏳ You're on cooldown! Try again in {round(error.retry_after, 1)} seconds.",
413 ephemeral=True
414 )
415 else:
416 raise error
417
418async def setup(bot):
419 await bot.add_cog(SchedulerCog(bot))
420# ===== FILE: presence.py =====
421# presence.py — dev command to change bot status or activity
422
423import discord
424from discord.ext import commands
425from discord import app_commands
426from config import BOT_OWNERS
427
428STATUS_MAP = {
429 "online": discord.Status.online,
430 "idle": discord.Status.idle,
431 "dnd": discord.Status.dnd,
432 "invisible": discord.Status.invisible
433}
434
435ACTIVITY_MAP = {
436 "playing": discord.Game,
437 "listening": lambda name: discord.Activity(type=discord.ActivityType.listening, name=name),
438 "watching": lambda name: discord.Activity(type=discord.ActivityType.watching, name=name),
439 "streaming": lambda name: discord.Streaming(name=name, url="https://twitch.tv/example")
440}
441
442class PresenceCog(commands.Cog):
443 def __init__(self, bot):
444 self.bot = bot
445
446 def is_owner():
447 async def predicate(interaction):
448 return interaction.user.id in BOT_OWNERS
449 return app_commands.check(predicate)
450
451 @app_commands.command(name="setpresence", description="Update the bot's visible status.")
452 @is_owner()
453 async def set_presence(self, interaction: discord.Interaction, status: str, activity_type: str, *, message: str):
454 status = status.lower()
455 activity_type = activity_type.lower()
456
457 if status not in STATUS_MAP or activity_type not in ACTIVITY_MAP:
458 return await interaction.response.send_message("❌ Invalid status or activity type.", ephemeral=True)
459
460 await self.bot.change_presence(
461 status=STATUS_MAP[status],
462 activity=ACTIVITY_MAP[activity_type](message)
463 )
464 await interaction.response.send_message(f"✅ Presence updated to `{status}` — {activity_type} `{message}`", ephemeral=True)
465
466async def setup(bot):
467 await bot.add_cog(PresenceCog(bot))