From e81b4a55ffce2ab93f179eb5d1969af600ded1ec Mon Sep 17 00:00:00 2001 From: youngcoder45 Date: Wed, 20 May 2026 16:31:19 +0530 Subject: [PATCH 1/2] Counting bug fix + ?pat command --- cogs/counting.py | 261 ++++++++++++------ cogs/fun.py | 9 + cogs/misc.py | 516 +++++++++++++++++++++--------------- utils/codebuddy_database.py | 282 ++++++++++++++------ 4 files changed, 703 insertions(+), 365 deletions(-) diff --git a/cogs/counting.py b/cogs/counting.py index a156dcd..b9641c5 100644 --- a/cogs/counting.py +++ b/cogs/counting.py @@ -1,22 +1,26 @@ +import ast +import asyncio +import math +import operator +import time +from typing import Optional + +import aiosqlite import discord -from discord.ext import commands from discord import app_commands -import aiosqlite -from utils.codebuddy_database import DB_PATH +from discord.ext import commands + from utils.codebuddy_database import ( + DB_PATH, add_guild_save_units, get_guild_save_units, get_user_save_units, + increment_guild_daily_count, increment_quest_counting_count, try_use_guild_save, try_use_user_save, ) -import ast -import operator -import math -import asyncio -import time -from typing import Optional + class Counting(commands.Cog): def __init__(self, bot): @@ -27,7 +31,9 @@ def __init__(self, bot): # Key: message_id, Value: monotonic timestamp self._recent_message_ids: dict[int, float] = {} # Throttle reaction API calls to avoid Discord rate limits in fast counting channels. - self._reaction_queue: asyncio.Queue[tuple[discord.Message, str]] = asyncio.Queue() + self._reaction_queue: asyncio.Queue[tuple[discord.Message, str]] = ( + asyncio.Queue() + ) self._pending_reactions: set[tuple[int, str]] = set() self._reaction_worker_task: Optional[asyncio.Task[None]] = None @@ -97,13 +103,17 @@ async def cog_load(self): await db.commit() try: - async with db.execute("SELECT guild_id, channel_id FROM counting_config") as cursor: + async with db.execute( + "SELECT guild_id, channel_id FROM counting_config" + ) as cursor: rows = await cursor.fetchall() for guild_id, channel_id in rows: self.counting_channels[guild_id] = channel_id print(f"Loaded {len(self.counting_channels)} counting channels") except aiosqlite.OperationalError: - print("counting_config table not found during cog load (likely first run)") + print( + "counting_config table not found during cog load (likely first run)" + ) except Exception as e: print(f"Error loading counting channels: {e}") @@ -116,7 +126,9 @@ async def _get_warning_count(self, guild_id: int, user_id: int) -> int: row = await cursor.fetchone() return int(row[0]) if row else 0 - async def _set_warning_count(self, guild_id: int, user_id: int, warnings: int) -> None: + async def _set_warning_count( + self, guild_id: int, user_id: int, warnings: int + ) -> None: async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: if warnings <= 0: await db.execute( @@ -136,7 +148,9 @@ async def _set_warning_count(self, guild_id: int, user_id: int, warnings: int) - async def _clear_all_warnings(self, guild_id: int) -> None: async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: - await db.execute("DELETE FROM counting_warnings WHERE guild_id = ?", (guild_id,)) + await db.execute( + "DELETE FROM counting_warnings WHERE guild_id = ?", (guild_id,) + ) await db.commit() async def _get_active_highscore_message_id(self, guild_id: int) -> Optional[int]: @@ -148,10 +162,15 @@ async def _get_active_highscore_message_id(self, guild_id: int) -> Optional[int] row = await cursor.fetchone() return int(row[0]) if row and row[0] is not None else None - async def _set_active_highscore_message_id(self, guild_id: int, message_id: Optional[int]) -> None: + async def _set_active_highscore_message_id( + self, guild_id: int, message_id: Optional[int] + ) -> None: async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: if message_id is None: - await db.execute("DELETE FROM counting_active_highscore WHERE guild_id = ?", (guild_id,)) + await db.execute( + "DELETE FROM counting_active_highscore WHERE guild_id = ?", + (guild_id,), + ) else: await db.execute( """ @@ -163,7 +182,9 @@ async def _set_active_highscore_message_id(self, guild_id: int, message_id: Opti ) await db.commit() - async def _remove_bot_reactions(self, channel: discord.TextChannel, message_id: int) -> None: + async def _remove_bot_reactions( + self, channel: discord.TextChannel, message_id: int + ) -> None: try: msg = await channel.fetch_message(message_id) except Exception: @@ -213,18 +234,30 @@ async def _mark_highscore_message( INSERT INTO counting_highscore_history (guild_id, score, user_id, message_id, timestamp) VALUES (?, ?, ?, ?, ?) """, - (guild_id, new_count, message.author.id, message.id, int(time.time())), + ( + guild_id, + new_count, + message.author.id, + message.id, + int(time.time()), + ), ) await db.commit() - async def _clear_highscore_marker_if_any(self, guild_id: int, channel: discord.TextChannel) -> None: + async def _clear_highscore_marker_if_any( + self, guild_id: int, channel: discord.TextChannel + ) -> None: marker_id = await self._get_active_highscore_message_id(guild_id) if marker_id: await self._set_active_highscore_message_id(guild_id, None) - @app_commands.command(name="setcountingchannel", description="Set the channel for the counting game") + @app_commands.command( + name="setcountingchannel", description="Set the channel for the counting game" + ) @app_commands.checks.has_permissions(administrator=True) - async def setcountingchannel(self, interaction: discord.Interaction, channel: discord.TextChannel): + async def setcountingchannel( + self, interaction: discord.Interaction, channel: discord.TextChannel + ): # Slash command interactions must be acknowledged quickly. # DB operations can take >3s (locks, slow disks), so defer immediately. if interaction.response.is_done(): @@ -234,7 +267,9 @@ async def setcountingchannel(self, interaction: discord.Interaction, channel: di await interaction.response.defer(ephemeral=True) if interaction.guild_id is None: - await interaction.followup.send("This command can only be used in a server.", ephemeral=True) + await interaction.followup.send( + "This command can only be used in a server.", ephemeral=True + ) return retries = 3 @@ -261,7 +296,9 @@ async def setcountingchannel(self, interaction: discord.Interaction, channel: di # Update cache self.counting_channels[interaction.guild_id] = channel.id - await interaction.followup.send(f"Counting channel set to {channel.mention}", ephemeral=True) + await interaction.followup.send( + f"Counting channel set to {channel.mention}", ephemeral=True + ) def safe_eval(self, expr): operators = { @@ -270,8 +307,8 @@ def safe_eval(self, expr): ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, - ast.BitXor: operator.pow, # Allow ^ for power - ast.USub: operator.neg + ast.BitXor: operator.pow, # Allow ^ for power + ast.USub: operator.neg, } constants = { @@ -315,7 +352,7 @@ def eval_node(node): left = eval_node(node.left) right = eval_node(node.right) if op in (ast.Pow, ast.BitXor): - if right > 100: # Limit exponent + if right > 100: # Limit exponent raise ValueError("Exponent too large") return operators[op](left, right) elif isinstance(node, ast.UnaryOp): @@ -327,7 +364,7 @@ def eval_node(node): if node.func.id in functions: args = [eval_node(a) for a in node.args] return functions[node.func.id](*args) - + elif isinstance(node, ast.Name): if node.id in constants: return constants[node.id] @@ -335,7 +372,7 @@ def eval_node(node): raise TypeError("Unsupported type") try: - tree = ast.parse(expr, mode='eval') + tree = ast.parse(expr, mode="eval") return eval_node(tree.body) except Exception: return None @@ -395,7 +432,7 @@ async def on_message(self, message): # 1. OPTIMIZATION: Check cache first before touching DB if message.guild.id not in self.counting_channels: return - + if message.channel.id != self.counting_channels[message.guild.id]: return @@ -408,7 +445,9 @@ async def on_message(self, message): self._recent_message_ids[message.id] = now if len(self._recent_message_ids) > 5000: cutoff = now - 120 - self._recent_message_ids = {mid: ts for mid, ts in self._recent_message_ids.items() if ts >= cutoff} + self._recent_message_ids = { + mid: ts for mid, ts in self._recent_message_ids.items() if ts >= cutoff + } # 2. Process the message logic # Wrap DB operations in retry loop for robustness @@ -416,9 +455,12 @@ async def on_message(self, message): while retries > 0: try: async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: - async with db.execute("SELECT current_count, last_user_id, high_score FROM counting_config WHERE guild_id = ?", (message.guild.id,)) as cursor: + async with db.execute( + "SELECT current_count, last_user_id, high_score FROM counting_config WHERE guild_id = ?", + (message.guild.id,), + ) as cursor: config = await cursor.fetchone() - + if not config: # Should not happen if in cache, but possible if DB was manually cleared return @@ -437,7 +479,7 @@ async def on_message(self, message): # Check rules next_count = current_count + 1 - + if number != next_count: await self.fail_count(message, current_count, "Wrong number!") return @@ -462,7 +504,11 @@ async def on_message(self, message): await db.commit() if warnings >= 3: - await self.fail_count(message, current_count, "Too many warnings (counted twice in a row 3 times)!") + await self.fail_count( + message, + current_count, + "Too many warnings (counted twice in a row 3 times)!", + ) return self._enqueue_reaction(message, "⚠️") @@ -476,27 +522,38 @@ async def on_message(self, message): # Valid count - Update DB new_high_score = max(high_score, next_count) - + # Update configuration tables - await db.execute(""" - UPDATE counting_config + await db.execute( + """ + UPDATE counting_config SET current_count = ?, last_user_id = ?, high_score = ? WHERE guild_id = ? - """, (next_count, message.author.id, new_high_score, message.guild.id)) - + """, + ( + next_count, + message.author.id, + new_high_score, + message.guild.id, + ), + ) + # Update user stats - await db.execute(""" + await db.execute( + """ INSERT INTO counting_stats (user_id, guild_id, total_counts, ruined_counts) VALUES (?, ?, 1, 0) ON CONFLICT(user_id, guild_id) DO UPDATE SET total_counts = total_counts + 1 - """, (message.author.id, message.guild.id)) + """, + (message.author.id, message.guild.id), + ) # Reset warnings for this user on a valid count (in the same transaction). await db.execute( "DELETE FROM counting_warnings WHERE guild_id = ? AND user_id = ?", (message.guild.id, message.author.id), ) - + await db.commit() # Side effects after commit to avoid duplicate reactions on retries. @@ -504,7 +561,9 @@ async def on_message(self, message): # Daily quest progress: count 5 numbers (best-effort). try: - quest_completed = await increment_quest_counting_count(message.author.id) + quest_completed = await increment_quest_counting_count( + message.author.id + ) if quest_completed: await message.channel.send( f"Daily quest completed, {message.author.mention}! " @@ -515,22 +574,54 @@ async def on_message(self, message): except Exception: pass + # Guild daily counting: increment the per-guild daily counter and award + # a server save for every 10 valid counts (counts may come from multiple users). + try: + awarded, new_daily_count = await increment_guild_daily_count( + message.guild.id + ) + if awarded: + # Try to get current server save units for display (best-effort). + try: + guild_units = await get_guild_save_units( + message.guild.id + ) + except Exception: + guild_units = None + + if guild_units is not None: + await message.channel.send( + f"Server reached **{new_daily_count}** counting numbers today — awarded **1.0** server save!\n" + f"Server saves: **{guild_units / 10:.1f}**" + ) + else: + await message.channel.send( + f"Server reached **{new_daily_count}** counting numbers today — awarded **1.0** server save!" + ) + except Exception: + # Best-effort only; do not break counting if this fails. + pass + # Highscore marker: react ✅+🏆 when reaching/topping the record if next_count >= high_score: - await self._mark_highscore_message(message, next_count, high_score) - return # Success - + await self._mark_highscore_message( + message, next_count, high_score + ) + return # Success + except aiosqlite.OperationalError as e: # If specifically locked, retry if "locked" in str(e): retries -= 1 if retries == 0: - print(f"Database locked repeatedly in counting for msg {message.id}") + print( + f"Database locked repeatedly in counting for msg {message.id}" + ) # Don't crash bot, just ignore or log return - await asyncio.sleep(0.1 * (4 - retries)) # backoff + await asyncio.sleep(0.1 * (4 - retries)) # backoff else: - raise # Re-raise other operational errors + raise # Re-raise other operational errors @commands.Cog.listener() async def on_message_delete(self, message: discord.Message): @@ -611,7 +702,9 @@ async def fail_count(self, message, current_count, reason): except Exception: pass - def _check(reaction: discord.Reaction, user: discord.abc.User) -> bool: + def _check( + reaction: discord.Reaction, user: discord.abc.User + ) -> bool: if user.bot: return False if user.id != message.author.id: @@ -651,7 +744,7 @@ def _check(reaction: discord.Reaction, user: discord.abc.User) -> bool: f"{reason} {message.author.mention} messed up at **{current_count}**, " f"but {source} save was used — the count is **saved**.\n" f"Next number is **{current_count + 1}**.\n" - f"Your saves: **{remaining_user_units/10:.1f}** • Server saves: **{remaining_guild_units/10:.1f}**" + f"Your saves: **{remaining_user_units / 10:.1f}** • Server saves: **{remaining_guild_units / 10:.1f}**" ) return @@ -700,7 +793,6 @@ def _check(reaction: discord.Reaction, user: discord.abc.User) -> bool: await self._clear_all_warnings(guild_id) await self._clear_highscore_marker_if_any(guild_id, message.channel) - @commands.command(name="donateguild", aliases=["dg"]) async def donate_guild(self, ctx: commands.Context): """Donate 1 personal save to the guild pool (guild receives 0.5 save).""" @@ -714,7 +806,7 @@ async def donate_guild(self, ctx: commands.Context): user_units = await get_user_save_units(user_id) if user_units < 10: return await ctx.send( - f"You need **1.0** save to donate. Your saves: **{user_units/10:.1f}**" + f"You need **1.0** save to donate. Your saves: **{user_units / 10:.1f}**" ) # Consume 1.0 personal save @@ -729,10 +821,9 @@ async def donate_guild(self, ctx: commands.Context): new_guild_units = await get_guild_save_units(guild_id) await ctx.send( f"Donated **1.0** save to the server pool. Server gained **0.5** save.\n" - f"Your saves: **{new_user_units/10:.1f}** • Server saves: **{new_guild_units/10:.1f}**" + f"Your saves: **{new_user_units / 10:.1f}** • Server saves: **{new_guild_units / 10:.1f}**" ) - @commands.command(name="guildsaves", aliases=["gsaves", "serversaves", "ssaves"]) async def guild_saves(self, ctx: commands.Context): """Show the server save pool used to protect counting mistakes.""" @@ -741,11 +832,15 @@ async def guild_saves(self, ctx: commands.Context): units = await get_guild_save_units(ctx.guild.id) await ctx.send( - f"Server saves: **{units/10:.1f}**\n" + f"Server saves: **{units / 10:.1f}**\n" "(Needs **1.0** server save to protect a ruined count.)" ) - @commands.hybrid_command(name="highscoretable", aliases=["highscores"], help="Show recent counting highscores") + @commands.hybrid_command( + name="highscoretable", + aliases=["highscores"], + help="Show recent counting highscores", + ) async def highscore_table(self, ctx: commands.Context): if not ctx.guild: return await ctx.send("Server only command.") @@ -790,20 +885,25 @@ async def highscore_table(self, ctx: commands.Context): @commands.command(name="mcl", aliases=["tc"]) async def most_count_leaderboard(self, ctx): async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: - async with db.execute(""" - SELECT user_id, total_counts - FROM counting_stats - WHERE guild_id = ? - ORDER BY total_counts DESC + async with db.execute( + """ + SELECT user_id, total_counts + FROM counting_stats + WHERE guild_id = ? + ORDER BY total_counts DESC LIMIT 10 - """, (ctx.guild.id,)) as cursor: + """, + (ctx.guild.id,), + ) as cursor: rows = await cursor.fetchall() - + if not rows: await ctx.send("No counting stats yet.") return - embed = discord.Embed(title="Most Count Leaderboard", color=discord.Color.blue()) + embed = discord.Embed( + title="Most Count Leaderboard", color=discord.Color.blue() + ) description = "" for i, (user_id, count) in enumerate(rows, 1): description += f"{i}. <@{user_id}>: {count}\n" @@ -813,20 +913,25 @@ async def most_count_leaderboard(self, ctx): @commands.command(name="mrl") async def most_ruined_leaderboard(self, ctx): async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: - async with db.execute(""" - SELECT user_id, ruined_counts - FROM counting_stats - WHERE guild_id = ? - ORDER BY ruined_counts DESC + async with db.execute( + """ + SELECT user_id, ruined_counts + FROM counting_stats + WHERE guild_id = ? + ORDER BY ruined_counts DESC LIMIT 10 - """, (ctx.guild.id,)) as cursor: + """, + (ctx.guild.id,), + ) as cursor: rows = await cursor.fetchall() - + if not rows: await ctx.send("No ruined stats yet.") return - embed = discord.Embed(title="Most Ruined Leaderboard", color=discord.Color.red()) + embed = discord.Embed( + title="Most Ruined Leaderboard", color=discord.Color.red() + ) description = "" for i, (user_id, count) in enumerate(rows, 1): description += f"{i}. <@{user_id}>: {count}\n" @@ -836,18 +941,22 @@ async def most_ruined_leaderboard(self, ctx): @commands.command(name="scs") async def server_count_stats(self, ctx): async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: - async with db.execute("SELECT current_count, high_score FROM counting_config WHERE guild_id = ?", (ctx.guild.id,)) as cursor: + async with db.execute( + "SELECT current_count, high_score FROM counting_config WHERE guild_id = ?", + (ctx.guild.id,), + ) as cursor: row = await cursor.fetchone() - + if not row: await ctx.send("Counting channel not set up or no data.") return - + current, high = row embed = discord.Embed(title="Server Count Stats", color=discord.Color.green()) embed.add_field(name="Current Count", value=str(current)) embed.add_field(name="High Score", value=str(high)) await ctx.send(embed=embed) + async def setup(bot): await bot.add_cog(Counting(bot)) diff --git a/cogs/fun.py b/cogs/fun.py index 9636e45..b5fef8a 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -226,6 +226,14 @@ async def fridge(self, ctx: commands.Context): embed.set_image(url="attachment://fridge.png") await ctx.reply(embed=embed, file=file, mention_author=False) + @commands.command(name="pat", help="Send a wholesome pat gif (prefix-only).") + async def pat(self, ctx: commands.Context): + """Send a wholesome pat gif (prefix-only).""" + gif_url = "https://tenor.com/bQSNq.gif" + embed = discord.Embed(color=0xFFB6C1) + embed.set_image(url=gif_url) + await ctx.reply(embed=embed, mention_author=False) + @commands.hybrid_command( name="compliment", help="Receive a professional programming compliment" ) @@ -349,6 +357,7 @@ async def choose(self, ctx: commands.Context, *, choices: str): ) embed.set_footer(text="CodeVerse Bot | Decision Helper") await ctx.reply(embed=embed, mention_author=False) + @app_commands.describe(text="Text to replace 'cinema' with") async def absolute(self, ctx: commands.Context, *, text: str): diff --git a/cogs/misc.py b/cogs/misc.py index 43387d0..a76930a 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -2,18 +2,34 @@ Misc commands cog. """ -import discord -from discord import app_commands -from discord.ext import commands -from typing import Optional, Any, Union -from datetime import datetime, timezone, timedelta import calendar import re +from datetime import datetime, timedelta, timezone +from typing import Any, Optional, Union + import aiosqlite -from better_profanity import profanity +import discord +from discord import app_commands +from discord.ext import commands + +# better_profanity is optional. If it's not installed, provide a no-op fallback so the cog can still load. +try: + from better_profanity import profanity +except Exception: + + class _DummyProfanity: + @staticmethod + def censor(text): + try: + # Keep behavior safe for non-str inputs + return str(text) + except Exception: + return text + + profanity = _DummyProfanity() -from utils.config import Config from utils.codebuddy_database import DB_PATH +from utils.config import Config class _EditSayModal(discord.ui.Modal, title="Edit Bot Message"): @@ -88,7 +104,9 @@ async def cog_load(self): except Exception as e: print(f"[Misc] Error ensuring say_messages table: {e}") - async def _is_say_message(self, guild_id: int, message_id: int) -> Optional[tuple[int, int]]: + async def _is_say_message( + self, guild_id: int, message_id: int + ) -> Optional[tuple[int, int]]: """Return (guild_id, channel_id) if message is tracked as /say.""" try: async with aiosqlite.connect(DB_PATH, timeout=30.0) as db: @@ -131,7 +149,9 @@ async def _record_say_message( except Exception as e: print(f"[Misc] Error recording /say message: {e}") - @commands.hybrid_command(name='join-vc', description='Join your voice channel for fun') + @commands.hybrid_command( + name="join-vc", description="Join your voice channel for fun" + ) async def join_vc(self, ctx: commands.Context): """Join the invoker's voice channel (only if it is not empty).""" if ctx.guild is None: @@ -153,7 +173,9 @@ async def join_vc(self, ctx: commands.Context): try: if isinstance(vc, discord.VoiceClient) and vc.is_connected(): if vc.is_playing() or vc.is_paused(): - return await ctx.send("I am currently playing audio in a voice channel.") + return await ctx.send( + "I am currently playing audio in a voice channel." + ) if vc.channel == channel: return await ctx.send("I am already in your voice channel.") @@ -163,7 +185,9 @@ async def join_vc(self, ctx: commands.Context): await channel.connect() except discord.Forbidden: - return await ctx.send("I do not have permission to join that voice channel.") + return await ctx.send( + "I do not have permission to join that voice channel." + ) except discord.ClientException: return await ctx.send("I could not connect to that voice channel.") @@ -180,8 +204,12 @@ async def on_voice_state_update( if member.bot: return - vc = getattr(member.guild, 'voice_client', None) - if not isinstance(vc, discord.VoiceClient) or not vc.is_connected() or not vc.channel: + vc = getattr(member.guild, "voice_client", None) + if ( + not isinstance(vc, discord.VoiceClient) + or not vc.is_connected() + or not vc.channel + ): return # Only react to events involving the channel we're currently in. @@ -201,7 +229,7 @@ async def on_voice_state_update( except Exception: return - @commands.hybrid_command(name='about', description='Learn about Eigen Bot') + @commands.hybrid_command(name="about", description="Learn about Eigen Bot") async def about(self, ctx: commands.Context): """Show information about the bot.""" embed = discord.Embed( @@ -210,14 +238,16 @@ async def about(self, ctx: commands.Context): "Feature-rich Discord bot for community engagement, " "support tickets, and utilities. Built with discord.py." ), - color=0x000000 + color=0x000000, ) - + # Add bot stats total_guilds = len(self.bot.guilds) - total_users = sum(guild.member_count for guild in self.bot.guilds if guild.member_count) + total_users = sum( + guild.member_count for guild in self.bot.guilds if guild.member_count + ) total_commands = len(self.bot.tree.get_commands()) - + embed.add_field( name="Statistics", value=( @@ -225,9 +255,9 @@ async def about(self, ctx: commands.Context): f"Users: **{total_users:,}**\n" f"Commands: **{total_commands}**" ), - inline=True + inline=True, ) - + embed.add_field( name="Features", value=( @@ -240,9 +270,9 @@ async def about(self, ctx: commands.Context): "Fun Commands\n" "Utilities" ), - inline=True + inline=True, ) - + embed.add_field( name="Links", value=( @@ -250,38 +280,40 @@ async def about(self, ctx: commands.Context): "[Invite](https://discord.com/api/oauth2/authorize) · " "[Support](https://discord.gg/4TkQYz7qea)" ), - inline=False + inline=False, ) - - embed.set_footer( - text=f"discord.py {discord.__version__} · TheCodeVerseHub" - ) - + + embed.set_footer(text=f"discord.py {discord.__version__} · TheCodeVerseHub") + # Set bot thumbnail if self.bot.user and self.bot.user.avatar: embed.set_thumbnail(url=self.bot.user.avatar.url) - + await ctx.send(embed=embed) - @commands.hybrid_command(name='song', aliases=['sp', 'spotify'], description='Show what you are currently listening to on Spotify') + @commands.hybrid_command( + name="song", + aliases=["sp", "spotify"], + description="Show what you are currently listening to on Spotify", + ) async def song(self, ctx: commands.Context, user: Optional[discord.Member] = None): """Display the current song/music that a user is listening to on Spotify or other music apps.""" target_user = user or ctx.author - + # Ensure target_user is a Member (has activities attribute) if not isinstance(target_user, discord.Member): embed = discord.Embed( title="❌ Error", description="This command only works in servers, not in DMs.", - color=discord.Color.red() + color=discord.Color.red(), ) await ctx.send(embed=embed) return - + # Check all activities - be more comprehensive spotify_activity = None music_activity = None - + for activity in target_user.activities: # Check for Spotify specifically if isinstance(activity, discord.Spotify): @@ -290,92 +322,101 @@ async def song(self, ctx: commands.Context, user: Optional[discord.Member] = Non # Check for any listening activity (including other music apps) elif activity.type == discord.ActivityType.listening: music_activity = activity - + if spotify_activity: # Create rich embed for Spotify embed = discord.Embed( title="Now Playing · Spotify", description=f"{target_user.display_name}", - color=0x000000 + color=0x000000, ) - + # Song details embed.add_field( name="Track", value=f"**[{profanity.censor(spotify_activity.title)}]({spotify_activity.track_url})**", - inline=False + inline=False, ) - + embed.add_field( name="Artist", value=profanity.censor(", ".join(spotify_activity.artists)), - inline=True + inline=True, ) - + embed.add_field( name="Album", value=profanity.censor(spotify_activity.album), - inline=True + inline=True, ) - + # Duration duration = spotify_activity.duration current = (discord.utils.utcnow() - spotify_activity.start).total_seconds() - + duration_str = f"{int(duration.total_seconds() // 60)}:{int(duration.total_seconds() % 60):02d}" current_str = f"{int(current // 60)}:{int(current % 60):02d}" - + # Progress bar progress = min(current / duration.total_seconds(), 1.0) bar_length = 20 filled = int(bar_length * progress) bar = "━" * filled + "○" + "─" * (bar_length - filled - 1) - + embed.add_field( name="Duration", value=f"`{current_str}` {bar} `{duration_str}`", - inline=False + inline=False, ) - + # Add album art if available if spotify_activity.album_cover_url: embed.set_thumbnail(url=spotify_activity.album_cover_url) - + embed.set_footer(text=f"Requested by {ctx.author.display_name}") - + elif music_activity: # Found other music activity (not Spotify) # Generic music activity embed = discord.Embed( title="Now Listening", description=f"{target_user.display_name}", - color=0x000000 + color=0x000000, ) - + embed.add_field( name="Activity", value=f"**{profanity.censor(music_activity.name)}**", - inline=False + inline=False, ) - + # Use getattr to safely access optional attributes - details = getattr(music_activity, 'details', None) + details = getattr(music_activity, "details", None) if details: - embed.add_field(name="Details", value=profanity.censor(details), inline=False) - - state = getattr(music_activity, 'state', None) + embed.add_field( + name="Details", value=profanity.censor(details), inline=False + ) + + state = getattr(music_activity, "state", None) if state: - embed.add_field(name="State", value=profanity.censor(state), inline=False) - - embed.set_footer(text=f"Requested by {ctx.author.display_name}", icon_url=ctx.author.display_avatar.url) + embed.add_field( + name="State", value=profanity.censor(state), inline=False + ) + + embed.set_footer( + text=f"Requested by {ctx.author.display_name}", + icon_url=ctx.author.display_avatar.url, + ) else: # No music activity found - show debug info if target_user == ctx.author: # Show what activities were detected activities_list = [] for activity in target_user.activities: - activities_list.append(f"• **{activity.name}** (Type: {activity.type.name})") - + activities_list.append( + f"• **{activity.name}** (Type: {activity.type.name})" + ) + if activities_list: debug_info = "\n".join(activities_list) message = ( @@ -401,35 +442,33 @@ async def song(self, ctx: commands.Context, user: Optional[discord.Member] = Non f"❌ **{target_user.display_name} is not currently listening to any music!**\n\n" "They must be listening to Spotify or another music app with activity status enabled." ) - + embed = discord.Embed( - title="No Music Playing", - description=message, - color=0x000000 + title="No Music Playing", description=message, color=0x000000 ) embed.set_footer(text="Activity Privacy must be enabled") - + await ctx.send(embed=embed) - @commands.command(name='uptime', hidden=True) + @commands.command(name="uptime", hidden=True) async def uptime(self, ctx: commands.Context): """Show the bot's uptime.""" - start_time = getattr(self.bot, 'start_time', None) + start_time = getattr(self.bot, "start_time", None) if not start_time: await ctx.send("Start time not tracked.") return now = discord.utils.utcnow() delta = now - start_time - + days = delta.days hours, remainder = divmod(delta.seconds, 3600) minutes, seconds = divmod(remainder, 60) - + uptime_str = f"{days}d {hours}h {minutes}m {seconds}s" await ctx.send(f"⏱️ **Uptime:** {uptime_str}") - @commands.command(name='diagnose', hidden=True) + @commands.command(name="diagnose", hidden=True) @commands.has_permissions(administrator=True) async def diagnose(self, ctx: commands.Context): """Show diagnostic information (Admin only).""" @@ -443,22 +482,28 @@ async def diagnose(self, ctx: commands.Context): users = sum(g.member_count for g in self.bot.guilds if g.member_count) # Latency latency = round(self.bot.latency * 1000) - + embed = discord.Embed(title="Diagnostic Info", color=0x000000) embed.add_field(name="Slash Commands", value=str(slash_commands), inline=True) embed.add_field(name="Prefix Commands", value=str(prefix_commands), inline=True) embed.add_field(name="Guilds", value=str(guilds), inline=True) embed.add_field(name="Users", value=str(users), inline=True) embed.add_field(name="Latency", value=f"{latency}ms", inline=True) - - start_time = getattr(self.bot, 'start_time', None) + + start_time = getattr(self.bot, "start_time", None) if start_time: - embed.add_field(name="Start Time", value=discord.utils.format_dt(start_time, 'R'), inline=True) + embed.add_field( + name="Start Time", + value=discord.utils.format_dt(start_time, "R"), + inline=True, + ) await ctx.send(embed=embed) - @commands.hybrid_command(name='bug', description='Report a bug to the bot dev - Only for small bugs') - @app_commands.describe(bug='Describe the bug you encountered') + @commands.hybrid_command( + name="bug", description="Report a bug to the bot dev - Only for small bugs" + ) + @app_commands.describe(bug="Describe the bug you encountered") async def bug_report(self, ctx: commands.Context, *, bug: str): """Report a bug to the support server.""" # Support server channel ID @@ -466,38 +511,41 @@ async def bug_report(self, ctx: commands.Context, *, bug: str): user = ctx.author guild = ctx.guild - + try: # Get the support channel support_channel = self.bot.get_channel(SUPPORT_CHANNEL_ID) - - if not support_channel or not isinstance(support_channel, discord.TextChannel): + + if not support_channel or not isinstance( + support_channel, discord.TextChannel + ): response = "❌ Could not access the support channel. Please join our [support server](https://discord.gg/4TkQYz7qea) and report the bug there." if ctx.interaction: - await ctx.interaction.response.send_message(response, ephemeral=True) + await ctx.interaction.response.send_message( + response, ephemeral=True + ) else: await ctx.send(response) return - + # Create bug report embed - embed = discord.Embed( - title="Bug Report", - color=0x000000 - ) - + embed = discord.Embed(title="Bug Report", color=0x000000) + # Add bug details - embed.add_field(name="Details", value="*"+bug+"*", inline=False) - + embed.add_field(name="Details", value="*" + bug + "*", inline=False) + # Add reporter and server info inline reporter_info = f"{user.mention} · `{user.id}`" - location_info = f"{guild.name} · `{guild.id}`" if guild else "Direct Message" - + location_info = ( + f"{guild.name} · `{guild.id}`" if guild else "Direct Message" + ) + embed.add_field(name="Reported by", value=reporter_info, inline=True) embed.add_field(name="Server", value=location_info, inline=True) - + # Send to support channel await support_channel.send(embed=embed) - + # Confirm to user response = ( "✅ Your bug report has been submitted to our support team. Thank you for helping us improve!\n\n" @@ -506,12 +554,14 @@ async def bug_report(self, ctx: commands.Context, *, bug: str): ) if ctx.interaction: if not ctx.interaction.response.is_done(): - await ctx.interaction.response.send_message(response, ephemeral=True) + await ctx.interaction.response.send_message( + response, ephemeral=True + ) else: await ctx.interaction.followup.send(response, ephemeral=True) else: await ctx.send(response) - + except Exception as e: response = ( f"❌ An error occurred while submitting your bug report: {str(e)}\n\n" @@ -519,17 +569,21 @@ async def bug_report(self, ctx: commands.Context, *, bug: str): ) if ctx.interaction: if not ctx.interaction.response.is_done(): - await ctx.interaction.response.send_message(response, ephemeral=True) + await ctx.interaction.response.send_message( + response, ephemeral=True + ) else: await ctx.interaction.followup.send(response, ephemeral=True) else: await ctx.send(response) - - @commands.hybrid_command(name='support', description='Get the support server invite link') + + @commands.hybrid_command( + name="support", description="Get the support server invite link" + ) async def support(self, ctx: commands.Context): """Send the support server invite link.""" content = "Join our support server: https://discord.gg/4TkQYz7qea" - + if ctx.interaction: await ctx.interaction.response.send_message(content, ephemeral=True) else: @@ -541,110 +595,126 @@ async def support(self, ctx: commands.Context): pass except: await ctx.send(content, delete_after=10) - - @app_commands.command(name='newfeature', description='Suggest a new feature for the bot') - @app_commands.describe(feature='Describe the feature you would like to see') + + @app_commands.command( + name="newfeature", description="Suggest a new feature for the bot" + ) + @app_commands.describe(feature="Describe the feature you would like to see") async def new_feature(self, interaction: discord.Interaction, feature: str): """Submit a feature request to the support server.""" # Feature requests channel ID FEATURE_CHANNEL_ID = 1452740031419777096 - + try: # Get the feature requests channel feature_channel = self.bot.get_channel(FEATURE_CHANNEL_ID) - - if not feature_channel or not isinstance(feature_channel, discord.TextChannel): + + if not feature_channel or not isinstance( + feature_channel, discord.TextChannel + ): await interaction.response.send_message( "❌ Could not access the feature requests channel. Please join our [support server](https://discord.gg/4TkQYz7qea) and submit your request there.", - ephemeral=True + ephemeral=True, ) return - + # Create feature request embed - embed = discord.Embed( - title="Feature Request", - color=0x000000 - ) - + embed = discord.Embed(title="Feature Request", color=0x000000) + # Add feature details - embed.add_field(name="Details", value="*"+feature+"*", inline=False) - + embed.add_field(name="Details", value="*" + feature + "*", inline=False) + # Add requester and server info inline requester_info = f"{interaction.user.mention} · `{interaction.user.id}`" - location_info = f"{interaction.guild.name} · `{interaction.guild.id}`" if interaction.guild else "Direct Message" - + location_info = ( + f"{interaction.guild.name} · `{interaction.guild.id}`" + if interaction.guild + else "Direct Message" + ) + embed.add_field(name="Requested By", value=requester_info, inline=True) embed.add_field(name="Location", value=location_info, inline=True) - + # Send to feature requests channel await feature_channel.send(embed=embed) - + # Confirm to user await interaction.response.send_message( "✅ Your feature request has been submitted! Our team will review it soon.\n\n" "**Want to discuss your idea or see other requests?**\n" "Join our support server: https://discord.gg/4TkQYz7qea", - ephemeral=True + ephemeral=True, ) - + except Exception as e: await interaction.response.send_message( f"❌ An error occurred while submitting your feature request: {str(e)}\n\n" "Please submit it directly in our [support server](https://discord.gg/4TkQYz7qea).", - ephemeral=True + ephemeral=True, ) - - @app_commands.command(name='feedback', description='Share your feedback about the bot') + + @app_commands.command( + name="feedback", description="Share your feedback about the bot" + ) @app_commands.describe( - rating='Rate your experience (1-5 stars)', - feedback='Share your thoughts, suggestions, or testimonial' + rating="Rate your experience (1-5 stars)", + feedback="Share your thoughts, suggestions, or testimonial", ) - @app_commands.choices(rating=[ - app_commands.Choice(name='⭐ 1 Star - Poor', value=1), - app_commands.Choice(name='⭐⭐ 2 Stars - Fair', value=2), - app_commands.Choice(name='⭐⭐⭐ 3 Stars - Good', value=3), - app_commands.Choice(name='⭐⭐⭐⭐ 4 Stars - Very Good', value=4), - app_commands.Choice(name='⭐⭐⭐⭐⭐ 5 Stars - Excellent', value=5) - ]) - async def feedback_command(self, interaction: discord.Interaction, rating: int, feedback: str): + @app_commands.choices( + rating=[ + app_commands.Choice(name="⭐ 1 Star - Poor", value=1), + app_commands.Choice(name="⭐⭐ 2 Stars - Fair", value=2), + app_commands.Choice(name="⭐⭐⭐ 3 Stars - Good", value=3), + app_commands.Choice(name="⭐⭐⭐⭐ 4 Stars - Very Good", value=4), + app_commands.Choice(name="⭐⭐⭐⭐⭐ 5 Stars - Excellent", value=5), + ] + ) + async def feedback_command( + self, interaction: discord.Interaction, rating: int, feedback: str + ): """Submit feedback/testimonial to the support server.""" # Feedback/testimonials channel ID FEEDBACK_CHANNEL_ID = 1453356371952275527 - + try: # Get the feedback channel feedback_channel = self.bot.get_channel(FEEDBACK_CHANNEL_ID) - - if not feedback_channel or not isinstance(feedback_channel, discord.TextChannel): + + if not feedback_channel or not isinstance( + feedback_channel, discord.TextChannel + ): await interaction.response.send_message( "❌ Could not access the feedback channel. Please join our [support server](https://discord.gg/4TkQYz7qea) and share your feedback there.", - ephemeral=True + ephemeral=True, ) return - + # Create star rating display stars = "⭐" * rating rating_text = ["Poor", "Fair", "Good", "Very Good", "Excellent"][rating - 1] - + # Create feedback embed with professional black design embed = discord.Embed( - title=f"{stars} {rating}/5 · {rating_text}", - color=0x000000 + title=f"{stars} {rating}/5 · {rating_text}", color=0x000000 ) - + # Add feedback content - embed.add_field(name="Feedback", value="*"+feedback+"*", inline=False) - + embed.add_field(name="Feedback", value="*" + feedback + "*", inline=False) + # Add reviewer and server info inline reviewer_info = f"{interaction.user.mention} · `{interaction.user.id}`" - location_info = f"{interaction.guild.name} · `{interaction.guild.id}`" if interaction.guild else "Direct Message" - + location_info = ( + f"{interaction.guild.name} · `{interaction.guild.id}`" + if interaction.guild + else "Direct Message" + ) + embed.add_field(name="User", value=reviewer_info, inline=True) embed.add_field(name="Server", value=location_info, inline=True) - + # Send to feedback channel await feedback_channel.send(embed=embed) - + # Confirm to user with different messages based on rating if rating >= 4: message = ( @@ -658,34 +728,36 @@ async def feedback_command(self, interaction: discord.Interaction, rating: int, "**Want to discuss further?**\n" "Join our support server: https://discord.gg/4TkQYz7qea" ) - + await interaction.response.send_message(message, ephemeral=True) - + except Exception as e: await interaction.response.send_message( f"❌ An error occurred while submitting your feedback: {str(e)}\n\n" "Please share it directly in our [support server](https://discord.gg/4TkQYz7qea).", - ephemeral=True + ephemeral=True, ) - - @app_commands.command(name='timestamp', description='Generate Discord timestamps for any date/time') + + @app_commands.command( + name="timestamp", description="Generate Discord timestamps for any date/time" + ) @app_commands.describe( - year='Year (e.g., 2025)', - month='Month (1-12)', - day='Day of month (1-31)', - hour='Hour in 24-hour format (0-23, optional)', - minute='Minute (0-59, optional)', - utc_offset='UTC offset in hours (e.g., -5 for EST, 5.5 for IST, optional)' + year="Year (e.g., 2025)", + month="Month (1-12)", + day="Day of month (1-31)", + hour="Hour in 24-hour format (0-23, optional)", + minute="Minute (0-59, optional)", + utc_offset="UTC offset in hours (e.g., -5 for EST, 5.5 for IST, optional)", ) async def timestamp_command( - self, + self, interaction: discord.Interaction, year: int, month: app_commands.Range[int, 1, 12], day: app_commands.Range[int, 1, 31], hour: Optional[app_commands.Range[int, 0, 23]] = None, minute: Optional[app_commands.Range[int, 0, 59]] = None, - utc_offset: float = 0.0 + utc_offset: float = 0.0, ): """Generate Discord timestamps with all available formats.""" # Set defaults if None @@ -693,47 +765,46 @@ async def timestamp_command( hour = 0 if minute is None: minute = 0 - + try: # Validate UTC offset range if utc_offset < -12 or utc_offset > 14: await interaction.response.send_message( - "❌ UTC offset must be between -12 and +14 hours!", - ephemeral=True + "❌ UTC offset must be between -12 and +14 hours!", ephemeral=True ) return - + # Validate the date if day > calendar.monthrange(year, month)[1]: await interaction.response.send_message( f"❌ Invalid date: {month}/{day}/{year} does not exist!", - ephemeral=True + ephemeral=True, ) return - + # Create datetime object in UTC # User provides time in their timezone, convert to UTC offset_hours = int(utc_offset) offset_minutes = int((utc_offset - offset_hours) * 60) - + # Create the datetime in user's timezone user_dt = datetime(year, month, day, hour, minute) - + # Convert to UTC by subtracting the offset utc_dt = user_dt.replace(tzinfo=None) # Subtract offset to get UTC time utc_dt = utc_dt - timedelta(hours=offset_hours, minutes=offset_minutes) - + # Get Unix timestamp unix_timestamp = int(utc_dt.replace(tzinfo=timezone.utc).timestamp()) - + # Create embed with all timestamp formats embed = discord.Embed( title="Discord Timestamps", description=f"{month}/{day}/{year} {hour:02d}:{minute:02d} (UTC{utc_offset:+.1f})", - color=0x000000 + color=0x000000, ) - + # Add all timestamp formats formats = [ ("Short Time", "t", f""), @@ -744,18 +815,18 @@ async def timestamp_command( ("Long Date/Time", "F", f""), ("Relative Time", "R", f""), ] - + format_text = "" for name, code, timestamp_code in formats: # Show both the code and how it renders - format_text += f"**{name}** (`{code}`)\n`{timestamp_code}` → {timestamp_code}\n\n" - + format_text += ( + f"**{name}** (`{code}`)\n`{timestamp_code}` → {timestamp_code}\n\n" + ) + embed.add_field( - name="Available Formats", - value=format_text.strip(), - inline=False + name="Available Formats", value=format_text.strip(), inline=False ) - + # Add copy-paste section embed.add_field( name="Quick Copy", @@ -764,51 +835,51 @@ async def timestamp_command( f"**Default:** ``\n" f"**Relative:** ``" ), - inline=False + inline=False, ) - + embed.set_footer(text="Click 'Copy' on any code block to use it") - + await interaction.response.send_message(embed=embed, ephemeral=True) - + except ValueError as e: await interaction.response.send_message( - f"❌ Invalid date/time: {str(e)}", - ephemeral=True + f"❌ Invalid date/time: {str(e)}", ephemeral=True ) except Exception as e: await interaction.response.send_message( - f"❌ An error occurred: {str(e)}", - ephemeral=True + f"❌ An error occurred: {str(e)}", ephemeral=True ) - @app_commands.command(name='say', description='Make the bot say something (Admin only)') - @app_commands.describe(text='The text you want the bot to say') + @app_commands.command( + name="say", description="Make the bot say something (Admin only)" + ) + @app_commands.describe(text="The text you want the bot to say") @app_commands.default_permissions(administrator=True) async def say(self, interaction: discord.Interaction, text: str): """Make the bot send a message (admin only to prevent abuse).""" # Double-check permissions (extra safety) if not interaction.guild: await interaction.response.send_message( - "❌ This command can only be used in a server.", - ephemeral=True + "❌ This command can only be used in a server.", ephemeral=True ) return - if not isinstance(interaction.user, discord.Member) or not interaction.user.guild_permissions.administrator: + if ( + not isinstance(interaction.user, discord.Member) + or not interaction.user.guild_permissions.administrator + ): await interaction.response.send_message( - "❌ This command is restricted to administrators only.", - ephemeral=True + "❌ This command is restricted to administrators only.", ephemeral=True ) return - + # Check if channel is messageable if not isinstance(interaction.channel, discord.abc.Messageable): - await interaction.response.send_message( - "❌ Cannot send messages in this channel type.", - ephemeral=True + await interaction.response.send_message( + "❌ Cannot send messages in this channel type.", ephemeral=True ) - return + return # Send the text in the channel sent = await interaction.channel.send(text) @@ -823,12 +894,9 @@ async def say(self, interaction: discord.Interaction, text: str): ) except Exception: pass - + # Confirm to admin (ephemeral so only they see it) - await interaction.response.send_message( - "✅ Message sent!", - ephemeral=True - ) + await interaction.response.send_message("✅ Message sent!", ephemeral=True) @app_commands.command( name="edit", @@ -845,7 +913,10 @@ async def edit(self, interaction: discord.Interaction, message_id: str): ) return - if not isinstance(interaction.user, discord.Member) or not interaction.user.guild_permissions.administrator: + if ( + not isinstance(interaction.user, discord.Member) + or not interaction.user.guild_permissions.administrator + ): await interaction.response.send_message( "❌ This command is restricted to administrators only.", ephemeral=True, @@ -870,7 +941,9 @@ async def edit(self, interaction: discord.Interaction, message_id: str): return _guild_id, channel_id = say_row - channel = interaction.guild.get_channel(channel_id) or self.bot.get_channel(channel_id) + channel = interaction.guild.get_channel(channel_id) or self.bot.get_channel( + channel_id + ) if not isinstance(channel, (discord.TextChannel, discord.Thread)): await interaction.response.send_message( "❌ Could not find the channel for that message.", @@ -900,7 +973,9 @@ async def edit(self, interaction: discord.Interaction, message_id: str): ) return - await interaction.response.send_modal(_EditSayModal(target_message=target_message)) + await interaction.response.send_modal( + _EditSayModal(target_message=target_message) + ) @app_commands.command( name="react", @@ -924,7 +999,10 @@ async def react( ) return - if not isinstance(interaction.user, discord.Member) or not interaction.user.guild_permissions.administrator: + if ( + not isinstance(interaction.user, discord.Member) + or not interaction.user.guild_permissions.administrator + ): await interaction.response.send_message( "❌ This command is restricted to administrators only.", ephemeral=True, @@ -954,7 +1032,9 @@ async def react( ephemeral=True, ) return - ch = interaction.guild.get_channel(channel_id) or self.bot.get_channel(channel_id) + ch = interaction.guild.get_channel(channel_id) or self.bot.get_channel( + channel_id + ) if not isinstance(ch, (discord.TextChannel, discord.Thread)): await interaction.response.send_message( "❌ Could not access that channel.", @@ -970,8 +1050,12 @@ async def react( # Allow providing just a message ID for the current channel. try: message_id = int(raw) - if isinstance(interaction.channel, (discord.TextChannel, discord.Thread)): - target_message = await interaction.channel.fetch_message(message_id) + if isinstance( + interaction.channel, (discord.TextChannel, discord.Thread) + ): + target_message = await interaction.channel.fetch_message( + message_id + ) except Exception: target_message = None @@ -1020,7 +1104,9 @@ async def react( await interaction.response.send_message("✅ Reaction added!", ephemeral=True) - @commands.command(name='dm', description='Explains why you should not DM members for help') + @commands.command( + name="dm", description="Explains why you should not DM members for help" + ) async def dm_command(self, ctx: commands.Context): """Explains why questions should be asked in the server instead of DMs.""" message = ( diff --git a/utils/codebuddy_database.py b/utils/codebuddy_database.py index 0f8864f..c595635 100644 --- a/utils/codebuddy_database.py +++ b/utils/codebuddy_database.py @@ -1,14 +1,16 @@ -import aiosqlite import datetime +import aiosqlite + DB_PATH = "botdata.db" + async def init_db(): """Initialisiert die Datenbank und erstellt die Tabelle, falls sie nicht existiert.""" async with aiosqlite.connect(DB_PATH) as db: # Enable Write-Ahead Logging for better concurrency await db.execute("PRAGMA journal_mode=WAL") - + await db.execute(""" CREATE TABLE IF NOT EXISTS leaderboard ( user_id INTEGER PRIMARY KEY, @@ -18,7 +20,7 @@ async def init_db(): last_activity DATE ) """) - + # Daily quests table await db.execute(""" CREATE TABLE IF NOT EXISTS daily_quests ( @@ -32,14 +34,16 @@ async def init_db(): saves REAL NOT NULL DEFAULT 0 ) """) - + # Check for missing columns in daily_quests (lightweight migrations) cursor = await db.execute("PRAGMA table_info(daily_quests)") dq_columns = [row[1] async for row in cursor] # Very old DBs may miss the legacy `saves` column. if "saves" not in dq_columns: - await db.execute("ALTER TABLE daily_quests ADD COLUMN saves REAL NOT NULL DEFAULT 0") + await db.execute( + "ALTER TABLE daily_quests ADD COLUMN saves REAL NOT NULL DEFAULT 0" + ) dq_columns.append("saves") # Inventory balances stored as integer tenths to avoid float drift. @@ -81,46 +85,46 @@ async def init_db(): # Keep the old columns around for backward compatibility (streak_freezes/saves), # but new code reads/writes *_units. - + # Weekly leaderboard table # Note: user_id is NOT a primary key here because we might want to store history, # or at least we need (user_id, week_start) to be unique. # Since we can't easily alter PK in sqlite, we'll just create it correctly if not exists. # If it exists with wrong schema, we might need to drop it. - + # Check if table exists and has correct schema (simple check) cursor = await db.execute("PRAGMA table_info(weekly_leaderboard)") columns = await cursor.fetchall() - + # If table exists but user_id is the single PK, we should probably recreate it. # For now, let's just try to create it if not exists with a composite PK. # But since the user likely already has the wrong table, we will DROP it if it exists # to ensure the schema is correct. This is a one-time migration for this integration. - + # We will check if we need to migrate by checking if we can insert a duplicate user_id # or just by checking the PK definition. # Simplest way for this context: Drop and recreate if it's the old schema. - + # Let's just use INSERT OR REPLACE in update_weekly_score instead of relying on complex schema changes # if we want to avoid dropping data. But dropping is cleaner for "integration". - + # Let's try to create with composite primary key. # If the table was created by the previous run with `user_id INTEGER PRIMARY KEY`, # we should drop it to fix the schema. - + # Check if user_id is the only PK is_bad_schema = False if columns: # columns is list of (cid, name, type, notnull, dflt_value, pk) # pk > 0 means it is part of primary key. pk_cols = [c[1] for c in columns if c[5] > 0] - if len(pk_cols) == 1 and pk_cols[0] == 'user_id': + if len(pk_cols) == 1 and pk_cols[0] == "user_id": is_bad_schema = True - + if is_bad_schema: print("Migrating weekly_leaderboard schema...") await db.execute("DROP TABLE weekly_leaderboard") - + await db.execute(""" CREATE TABLE IF NOT EXISTS weekly_leaderboard ( user_id INTEGER, @@ -161,6 +165,17 @@ async def init_db(): ) """) + # Per-guild daily counting tracker. + # Tracks how many valid counting numbers have been posted in a guild for the current day. + # When a guild reaches every 10 counts in a single day, the guild is awarded 1.0 save (10 units). + await db.execute(""" + CREATE TABLE IF NOT EXISTS counting_guild_daily ( + guild_id INTEGER PRIMARY KEY, + date DATE NOT NULL, + count INTEGER NOT NULL DEFAULT 0 + ) + """) + # Truth or Dare table await db.execute(""" CREATE TABLE IF NOT EXISTS tod_questions ( @@ -170,22 +185,22 @@ async def init_db(): rating TEXT DEFAULT 'PG' ) """) - + # Check if TOD table is empty, if so populate it async with db.execute("SELECT COUNT(*) FROM tod_questions") as cursor: count = await cursor.fetchone() if count and count[0] == 0: await populate_tod_questions(db) - + await db.commit() await migrate_leaderboard() # Prüft und fügt fehlende Spalten hinzu MAX_STREAK_FREEZE_UNITS = 20 # 2.0 -MAX_SAVE_UNITS = 40 # 4.0 -USE_ITEM_UNITS = 10 # 1.0 +MAX_SAVE_UNITS = 40 # 4.0 +USE_ITEM_UNITS = 10 # 1.0 QUEST_REWARD_FREEZE_UNITS = 2 # 0.2 -QUEST_REWARD_SAVE_UNITS = 5 # 0.5 +QUEST_REWARD_SAVE_UNITS = 5 # 0.5 def _coerce_date(value: object) -> datetime.date: @@ -276,9 +291,9 @@ async def populate_tod_questions(db): "Have you ever peed in a pool?", "Have you ever broken a bone?", "Have you ever been to another country?", - "Have you ever met a celebrity?" + "Have you ever met a celebrity?", ] - + dares = [ "Do 10 pushups.", "Sing a song.", @@ -299,14 +314,19 @@ async def populate_tod_questions(db): "Touch your toes.", "Lick your elbow.", "Wiggle your ears.", - "Raise one eyebrow." + "Raise one eyebrow.", ] - + for t in truths: - await db.execute("INSERT INTO tod_questions (type, question) VALUES (?, ?)", ("truth", t)) - + await db.execute( + "INSERT INTO tod_questions (type, question) VALUES (?, ?)", ("truth", t) + ) + for d in dares: - await db.execute("INSERT INTO tod_questions (type, question) VALUES (?, ?)", ("dare", d)) + await db.execute( + "INSERT INTO tod_questions (type, question) VALUES (?, ?)", ("dare", d) + ) + async def migrate_leaderboard(): """Fügt fehlende Spalten hinzu, falls die Tabelle schon existierte ohne diese Spalten.""" @@ -315,13 +335,18 @@ async def migrate_leaderboard(): columns = [row[1] async for row in cursor] if "streak" not in columns: - await db.execute("ALTER TABLE leaderboard ADD COLUMN streak INTEGER NOT NULL DEFAULT 0") + await db.execute( + "ALTER TABLE leaderboard ADD COLUMN streak INTEGER NOT NULL DEFAULT 0" + ) if "best_streak" not in columns: - await db.execute("ALTER TABLE leaderboard ADD COLUMN best_streak INTEGER NOT NULL DEFAULT 0") + await db.execute( + "ALTER TABLE leaderboard ADD COLUMN best_streak INTEGER NOT NULL DEFAULT 0" + ) if "last_activity" not in columns: await db.execute("ALTER TABLE leaderboard ADD COLUMN last_activity DATE") await db.commit() + def get_current_week(): """Returns the start and end date of the current week (Monday to Sunday).""" today = datetime.date.today() @@ -330,141 +355,153 @@ def get_current_week(): week_end = week_start + datetime.timedelta(days=6) return week_start, week_end + async def update_weekly_score(user_id: int, points: int = 1): """Updates weekly score for a user.""" week_start, week_end = get_current_week() - + async with aiosqlite.connect(DB_PATH) as db: # Check if user has entry for current week cursor = await db.execute( "SELECT weekly_score FROM weekly_leaderboard WHERE user_id = ? AND week_start = ?", - (user_id, week_start) + (user_id, week_start), ) row = await cursor.fetchone() - + if row: # Update existing weekly score new_score = row[0] + points await db.execute( "UPDATE weekly_leaderboard SET weekly_score = ? WHERE user_id = ? AND week_start = ?", - (new_score, user_id, week_start) + (new_score, user_id, week_start), ) else: # Create new weekly entry await db.execute( "INSERT INTO weekly_leaderboard (user_id, weekly_score, week_start, week_end) VALUES (?, ?, ?, ?)", - (user_id, points, week_start, week_end) + (user_id, points, week_start, week_end), ) await db.commit() + async def reset_weekly_leaderboard(): """Resets weekly leaderboard for new week.""" week_start, week_end = get_current_week() - + async with aiosqlite.connect(DB_PATH) as db: # Delete old weekly entries (older than current week) await db.execute( - "DELETE FROM weekly_leaderboard WHERE week_start < ?", - (week_start,) + "DELETE FROM weekly_leaderboard WHERE week_start < ?", (week_start,) ) await db.commit() + async def get_weekly_leaderboard(limit=10): """Gets current weekly leaderboard.""" week_start, week_end = get_current_week() - + async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( "SELECT user_id, weekly_score FROM weekly_leaderboard WHERE week_start = ? ORDER BY weekly_score DESC LIMIT ?", - (week_start, limit) + (week_start, limit), ) return await cursor.fetchall() + async def get_streak_leaderboard(limit=10): """Gets leaderboard sorted by current streak.""" # await migrate_leaderboard() # Removed to prevent overhead/locking, called in init_db async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( "SELECT user_id, streak, best_streak FROM leaderboard WHERE streak > 0 ORDER BY streak DESC, best_streak DESC LIMIT ?", - (limit,) + (limit,), ) return await cursor.fetchall() + async def update_user_activity(user_id: int): """Updates last activity date for streak tracking.""" today = datetime.date.today() - + async with aiosqlite.connect(DB_PATH) as db: await db.execute( "UPDATE leaderboard SET last_activity = ? WHERE user_id = ?", - (today, user_id) + (today, user_id), ) await db.commit() -async def increment_user_score(user_id: int, points: int = 1, reset_streak: bool = False): + +async def increment_user_score( + user_id: int, points: int = 1, reset_streak: bool = False +): """Erhöht den Score eines Users und aktualisiert Streaks.""" # await migrate_leaderboard() today = datetime.date.today() - + async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( - "SELECT correct_answers, streak, best_streak, last_activity FROM leaderboard WHERE user_id = ?", - (user_id,) + "SELECT correct_answers, streak, best_streak, last_activity FROM leaderboard WHERE user_id = ?", + (user_id,), ) row = await cursor.fetchone() if row: current_score, current_streak, best_streak, last_activity = row - + # Check if streak should be reset due to missed days if last_activity: last_date = datetime.datetime.strptime(last_activity, "%Y-%m-%d").date() days_diff = (today - last_date).days if days_diff > 1: # More than 1 day gap resets streak reset_streak = True - + new_streak = 1 if reset_streak else current_streak + 1 best_streak = max(best_streak, new_streak) new_score = current_score + points await db.execute( "UPDATE leaderboard SET correct_answers = ?, streak = ?, best_streak = ?, last_activity = ? WHERE user_id = ?", - (new_score, new_streak, best_streak, today, user_id) + (new_score, new_streak, best_streak, today, user_id), ) else: streak = 1 if reset_streak else 1 best_streak = streak await db.execute( "INSERT INTO leaderboard (user_id, correct_answers, streak, best_streak, last_activity) VALUES (?, ?, ?, ?, ?)", - (user_id, points, streak, best_streak, today) + (user_id, points, streak, best_streak, today), ) await db.commit() - + # Also update weekly score await update_weekly_score(user_id, points) + async def reset_user_streak(user_id: int): """Setzt die aktuelle Streak eines Users auf 0 zurück.""" # await migrate_leaderboard() async with aiosqlite.connect(DB_PATH) as db: - await db.execute("UPDATE leaderboard SET streak = 0 WHERE user_id = ?", (user_id,)) + await db.execute( + "UPDATE leaderboard SET streak = 0 WHERE user_id = ?", (user_id,) + ) await db.commit() + async def get_leaderboard(limit=10): """Gibt die Top-N User nach korrekt beantworteten Fragen zurück.""" # await migrate_leaderboard() async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( "SELECT user_id, correct_answers, streak, best_streak FROM leaderboard ORDER BY correct_answers DESC LIMIT ?", - (limit,) + (limit,), ) return await cursor.fetchall() - + + async def get_user_stats(user_id: int): """Gibt die Stats (score, streak, best_streak) für einen bestimmten User zurück.""" # await migrate_leaderboard() async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( "SELECT correct_answers, streak, best_streak FROM leaderboard WHERE user_id = ?", - (user_id,) + (user_id,), ) row = await cursor.fetchone() if row: @@ -480,8 +517,7 @@ async def get_user_rank(user_id: int): async with aiosqlite.connect(DB_PATH) as db: # Zuerst Score holen cursor = await db.execute( - "SELECT correct_answers FROM leaderboard WHERE user_id = ?", - (user_id,) + "SELECT correct_answers FROM leaderboard WHERE user_id = ?", (user_id,) ) row = await cursor.fetchone() if not row: @@ -490,13 +526,13 @@ async def get_user_rank(user_id: int): # Rang berechnen: alle User zählen, die mehr Punkte haben cursor = await db.execute( - "SELECT COUNT(*) FROM leaderboard WHERE correct_answers > ?", - (score,) + "SELECT COUNT(*) FROM leaderboard WHERE correct_answers > ?", (score,) ) row = await cursor.fetchone() higher_count = row[0] if row is not None else 0 return higher_count + 1 + async def get_score_gap(user_id: int): """ Gibt die Punkte-Differenz und User-ID des nächsthöheren Spielers zurück. @@ -506,8 +542,7 @@ async def get_score_gap(user_id: int): async with aiosqlite.connect(DB_PATH) as db: # Eigenen Score holen cursor = await db.execute( - "SELECT correct_answers FROM leaderboard WHERE user_id = ?", - (user_id,) + "SELECT correct_answers FROM leaderboard WHERE user_id = ?", (user_id,) ) row = await cursor.fetchone() if not row: @@ -517,7 +552,7 @@ async def get_score_gap(user_id: int): # Nächsthöheren Score + User-ID finden cursor = await db.execute( "SELECT user_id, correct_answers FROM leaderboard WHERE correct_answers > ? ORDER BY correct_answers ASC LIMIT 1", - (score,) + (score,), ) higher = await cursor.fetchone() if higher: @@ -529,6 +564,7 @@ async def get_score_gap(user_id: int): # ========== Daily Quests Functions ========== + async def get_daily_quest_progress(user_id: int): """ Get the daily quest progress for a user. @@ -566,7 +602,16 @@ async def get_daily_quest_progress(user_id: int): freeze_units = int(row[5] or 0) save_units = int(row[6] or 0) - return (quest_date, quizzes, counting_numbers, quiz_done, counting_done, freeze_units, save_units) + return ( + quest_date, + quizzes, + counting_numbers, + quiz_done, + counting_done, + freeze_units, + save_units, + ) + async def increment_quest_quiz_count(user_id: int): """ @@ -600,8 +645,12 @@ async def increment_quest_quiz_count(user_id: int): quest_complete = new_quizzes >= 5 if quest_complete: - new_freeze_units = _clamp_int(freeze_units + QUEST_REWARD_FREEZE_UNITS, 0, MAX_STREAK_FREEZE_UNITS) - new_save_units = _clamp_int(save_units + QUEST_REWARD_SAVE_UNITS, 0, MAX_SAVE_UNITS) + new_freeze_units = _clamp_int( + freeze_units + QUEST_REWARD_FREEZE_UNITS, 0, MAX_STREAK_FREEZE_UNITS + ) + new_save_units = _clamp_int( + save_units + QUEST_REWARD_SAVE_UNITS, 0, MAX_SAVE_UNITS + ) await db.execute( """ UPDATE daily_quests @@ -655,8 +704,12 @@ async def increment_quest_counting_count(user_id: int): quest_complete = new_counted >= 5 if quest_complete: - new_freeze_units = _clamp_int(freeze_units + QUEST_REWARD_FREEZE_UNITS, 0, MAX_STREAK_FREEZE_UNITS) - new_save_units = _clamp_int(save_units + QUEST_REWARD_SAVE_UNITS, 0, MAX_SAVE_UNITS) + new_freeze_units = _clamp_int( + freeze_units + QUEST_REWARD_FREEZE_UNITS, 0, MAX_STREAK_FREEZE_UNITS + ) + new_save_units = _clamp_int( + save_units + QUEST_REWARD_SAVE_UNITS, 0, MAX_SAVE_UNITS + ) await db.execute( """ UPDATE daily_quests @@ -677,6 +730,7 @@ async def increment_quest_counting_count(user_id: int): await db.commit() return quest_complete + async def mark_quest_voted(user_id: int): """ Mark that the user has voted today. @@ -695,6 +749,7 @@ async def mark_quest_voted(user_id: int): await db.commit() return False + async def use_streak_freeze(user_id: int): """ Use a streak freeze to prevent streak reset. @@ -720,6 +775,7 @@ async def use_streak_freeze(user_id: int): await db.commit() return True + async def use_bonus_hint(user_id: int): """ Use a bonus hint for a quiz. @@ -727,22 +783,22 @@ async def use_bonus_hint(user_id: int): """ async with aiosqlite.connect(DB_PATH) as db: cursor = await db.execute( - "SELECT bonus_hints FROM daily_quests WHERE user_id = ?", - (user_id,) + "SELECT bonus_hints FROM daily_quests WHERE user_id = ?", (user_id,) ) row = await cursor.fetchone() - + if not row or row[0] <= 0: return False - + # Use one hint await db.execute( "UPDATE daily_quests SET bonus_hints = bonus_hints - 1 WHERE user_id = ?", - (user_id,) + (user_id,), ) await db.commit() return True + async def get_quest_rewards(user_id: int): """ Get the current inventory balances. @@ -763,7 +819,9 @@ async def get_quest_rewards(user_id: int): async def get_user_save_units(user_id: int) -> int: async with aiosqlite.connect(DB_PATH) as db: await _ensure_daily_quest_row(db, user_id) - cursor = await db.execute("SELECT save_units FROM daily_quests WHERE user_id = ?", (user_id,)) + cursor = await db.execute( + "SELECT save_units FROM daily_quests WHERE user_id = ?", (user_id,) + ) row = await cursor.fetchone() return int(row[0] or 0) if row else 0 @@ -772,7 +830,9 @@ async def try_use_user_save(user_id: int) -> bool: """Consume 1.0 personal save if available.""" async with aiosqlite.connect(DB_PATH) as db: await _ensure_daily_quest_row(db, user_id) - cursor = await db.execute("SELECT save_units FROM daily_quests WHERE user_id = ?", (user_id,)) + cursor = await db.execute( + "SELECT save_units FROM daily_quests WHERE user_id = ?", (user_id,) + ) row = await cursor.fetchone() units = int(row[0] or 0) if row else 0 if units < USE_ITEM_UNITS: @@ -827,3 +887,77 @@ async def try_use_guild_save(guild_id: int) -> bool: await db.commit() return True + +async def get_guild_daily_count(guild_id: int) -> int: + """ + Return the number of valid counting numbers recorded for the given guild for today. + If no row exists or the stored date is older than today, returns 0. + """ + async with aiosqlite.connect(DB_PATH) as db: + cursor = await db.execute( + "SELECT count, date FROM counting_guild_daily WHERE guild_id = ?", + (guild_id,), + ) + row = await cursor.fetchone() + if not row: + return 0 + row_count = int(row[0] or 0) + row_date = _coerce_date(row[1]) + if row_date < datetime.date.today(): + return 0 + return row_count + + +async def increment_guild_daily_count(guild_id: int): + """ + Increment the daily count for the given guild by 1. + + Returns a tuple (awarded_save: bool, new_count: int): + - awarded_save is True if this increment caused the guild to reach a multiple of 10 counts + for the day and a 1.0 save (10 units) was awarded to the guild. + - new_count is the updated count for today after this increment. + + Notes: + - This uses the counting_guild_daily table to track per-guild daily counts and will reset + the counter automatically when the stored date is older than today. + - When a multiple of 10 is reached (10, 20, 30, ...), the guild is awarded 10 units + (i.e., 1.0 save) via add_guild_save_units. + """ + today = datetime.date.today() + async with aiosqlite.connect(DB_PATH) as db: + cursor = await db.execute( + "SELECT count, date FROM counting_guild_daily WHERE guild_id = ?", + (guild_id,), + ) + row = await cursor.fetchone() + if not row: + new_count = 1 + await db.execute( + "INSERT INTO counting_guild_daily (guild_id, date, count) VALUES (?, ?, ?)", + (guild_id, today, new_count), + ) + else: + row_count = int(row[0] or 0) + row_date = _coerce_date(row[1]) + if row_date < today: + new_count = 1 + await db.execute( + "UPDATE counting_guild_daily SET date = ?, count = ? WHERE guild_id = ?", + (today, new_count, guild_id), + ) + else: + new_count = row_count + 1 + await db.execute( + "UPDATE counting_guild_daily SET count = ? WHERE guild_id = ?", + (new_count, guild_id), + ) + + await db.commit() + + awarded = False + if new_count % 10 == 0: + # Award 1.0 guild save (10 units). + await add_guild_save_units(guild_id, 10) + awarded = True + + return awarded, new_count From c5eb13e2e1b22820e1a3c344833dabf346d58907 Mon Sep 17 00:00:00 2001 From: youngcoder45 Date: Wed, 20 May 2026 16:33:35 +0530 Subject: [PATCH 2/2] Pat is now a normal msg not an embed --- cogs/fun.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cogs/fun.py b/cogs/fun.py index b5fef8a..141f5f8 100644 --- a/cogs/fun.py +++ b/cogs/fun.py @@ -230,9 +230,8 @@ async def fridge(self, ctx: commands.Context): async def pat(self, ctx: commands.Context): """Send a wholesome pat gif (prefix-only).""" gif_url = "https://tenor.com/bQSNq.gif" - embed = discord.Embed(color=0xFFB6C1) - embed.set_image(url=gif_url) - await ctx.reply(embed=embed, mention_author=False) + # Send as a plain message with the GIF URL (not embedded) + await ctx.reply(gif_url, mention_author=False) @commands.hybrid_command( name="compliment", help="Receive a professional programming compliment"