diff --git a/bronxbot.py b/bronxbot.py index 69bb2d3..22615a9 100644 --- a/bronxbot.py +++ b/bronxbot.py @@ -145,7 +145,7 @@ async def before_update_guilds(self): bot = BronxBot( command_prefix='.', intents=intents, - shard_count=2, + shard_count=round(config['GUILD_COUNT']/20), # a shard for every 20 servers case_insensitive=True, application_id=config["CLIENT_ID"] # <-- Add this line ) diff --git a/cogs/LastFm.py b/cogs/LastFm.py index 50b23a2..bfd6a9c 100644 --- a/cogs/LastFm.py +++ b/cogs/LastFm.py @@ -1,4 +1,4 @@ -import discord +import discord from discord.ext import commands import aiohttp import os @@ -36,9 +36,12 @@ def get_lastfm_api_secret() -> Optional[str]: except Exception: continue return None +with open("data/config.json", "r", encoding="utf-8") as f: + config = json.load(f) LASTFM_API_KEY = get_lastfm_api_key() LASTFM_API_SECRET = get_lastfm_api_secret() +#TODO: Migrate this to mongo def load_links() -> dict: if not os.path.exists(DATA_PATH): @@ -106,7 +109,7 @@ def __init__(self, bot): def get_auth_url(self, discord_id: int) -> str: params = { "api_key": LASTFM_API_KEY, - "cb": f"https://your-production-domain.com/api/lastfm/callback?discord_id={discord_id}" + "cb": f"https://bronxbot.onrender.com/api/lastfm/callback?discord_id={discord_id}" } return f"https://www.last.fm/api/auth/?{urlencode(params)}" diff --git a/cogs/economy/Bazaar.py b/cogs/economy/Bazaar.py index 94a2acb..aae74d6 100644 --- a/cogs/economy/Bazaar.py +++ b/cogs/economy/Bazaar.py @@ -88,21 +88,17 @@ async def buy_items(self, interaction: discord.Interaction, button: discord.ui.B await interaction.response.send_message("❌ No items available in the bazaar right now.", ephemeral=True) return - # First send the select menu modal = ItemSelectModal(self.cog, self.cog.current_items) - await interaction.response.send_message( - "Select an item to purchase:", - view=modal.select_view, - ephemeral=True - ) - - # Then send the modal for amount - await interaction.followup.send_modal(modal) + await interaction.response.send_modal(modal) @discord.ui.button(label="📈 Buy Stock", style=discord.ButtonStyle.secondary) async def buy_stock(self, interaction: discord.Interaction, button: discord.ui.Button): await self.cog.handle_stock_purchase(interaction) + @discord.ui.button(label="📉 Sell Stock", style=discord.ButtonStyle.secondary) + async def sell_stock(self, interaction: discord.Interaction, button: discord.ui.Button): + await self.cog.handle_stock_sale(interaction) + @discord.ui.button(label="🗑️ Close", style=discord.ButtonStyle.danger) async def close(self, interaction: discord.Interaction, button: discord.ui.Button): await interaction.response.defer() @@ -225,7 +221,7 @@ async def reset_bazaar(self): num_items = random.randint(3, 5) self.current_items = random.sample(category_items, min(num_items, len(category_items))) - # Apply bazaar-specific modifications + # Apply bazaar specific modifications for item in self.current_items: # Apply random discount (10-30%) discount = random.uniform(0.1, 0.3) @@ -340,8 +336,9 @@ async def bazaar(self, ctx): embed = discord.Embed( title="🛒 The Wandering Bazaar", description=f"*Exotic goods from distant lands*\n\n" - f"Your Bazaar Stock: **{user_stock}** (Discount: **{user_discount*100:.0f}%**)\n" - f"Current Stock Price: **{self.calculate_stock_price()}** {self.currency}", + f"Your Bazaar Stock: **{user_stock}** (Discount: **{user_discount*100:.0f}%**)\n" + f"Current Stock Price: **{self.calculate_stock_price()}** {self.currency}\n" + f"Sell Price: **{int(self.calculate_stock_price() * 0.8)}** {self.currency} (80%)", color=0x9b59b6 ) @@ -381,12 +378,142 @@ async def bazaar(self, ctx): message = await ctx.reply(embed=embed, view=view) view.message = message - @commands.command(name="bazaar-buy", aliases=["bbuy"]) + @commands.command(name="bazaarbuy", aliases=["bbuy"]) @commands.cooldown(1, 5, commands.BucketType.user) async def bazaar_buy(self, ctx, item_id: str, amount: int = 1): """Buy an item from the bazaar""" await self.handle_bazaar_purchase(ctx, item_id, amount) + @commands.command(name="bazaarsell", aliases=["bsell"]) + @commands.cooldown(1, 10, commands.BucketType.user) + async def bazaar_sell(self, ctx, amount: int = 1): + """Sell your bazaar stock""" + await self.handle_stock_sale(ctx, amount) + + async def handle_stock_sale(self, interaction_or_ctx, amount: int = 1): + """Handle stock sale from either interaction or command""" + is_interaction = isinstance(interaction_or_ctx, discord.Interaction) + + if is_interaction: + interaction = interaction_or_ctx + user = interaction.user + guild = interaction.guild + respond = interaction.response.send_message + else: + ctx = interaction_or_ctx + user = ctx.author + guild = ctx.guild + respond = ctx.reply + + if amount <= 0: + msg = "❌ Amount must be positive." + if is_interaction: + await respond(msg, ephemeral=True) + else: + await respond(msg) + return + + # Get user's current stock + current_stock = await self.get_user_stock(user.id) + + if current_stock < amount: + msg = f"❌ You only have {current_stock} stock to sell!" + if is_interaction: + await respond(msg, ephemeral=True) + else: + await respond(msg) + return + + # Calculate sale price (80% of current stock price) + stock_price = int(self.calculate_stock_price() * 0.8) + total_gain = stock_price * amount + + # For interactions, defer first + if is_interaction: + await interaction.response.defer() + + # Add money to wallet + if not await db.update_wallet(user.id, total_gain, guild.id if guild else None): + msg = "❌ Failed to process sale. Please try again." + if is_interaction: + await interaction.followup.send(msg, ephemeral=True) + else: + await respond(msg) + return + + # Remove stock + result = await db.db.users.update_one( + {"_id": str(user.id)}, + {"$set": {"bazaar_stock": current_stock - amount}} + ) + + if result.modified_count == 0: + # Refund if failed + await db.update_wallet(user.id, -total_gain, guild.id if guild else None) + msg = "❌ Failed to remove stock. Transaction cancelled." + if is_interaction: + await interaction.followup.send(msg, ephemeral=True) + else: + await respond(msg) + return + + # Get updated user data + new_stock = current_stock - amount + new_discount = self.calculate_discount(new_stock) + new_balance = await db.get_wallet_balance(user.id, guild.id if guild else None) + + # Create embed + embed = discord.Embed( + title="✅ Stock Sale Complete", + description=f"You sold **{amount}** bazaar stock!", + color=0x00ff00 + ) + + embed.add_field( + name="Sale Details", + value=f"Price per Stock: **{stock_price}** {self.currency}\n" + f"Total Gain: **{total_gain}** {self.currency}", + inline=False + ) + + embed.add_field( + name="Your New Holdings", + value=f"Remaining Stock: **{new_stock}**\n" + f"Current Discount: **{new_discount*100:.0f}%**\n" + f"New Balance: **{new_balance}** {self.currency}", + inline=False + ) + + # Check if they lost secret shop access + if new_stock < self.stock_threshold and current_stock >= self.stock_threshold: + embed.add_field( + name="🔒 Secret Shop Lost", + value=f"You no longer meet the {self.stock_threshold} stock requirement for secret shop access!", + inline=False + ) + + try: + if hasattr(self, 'last_stock_message') and self.last_stock_message: + # Edit the existing message + if is_interaction: + await self.last_stock_message.edit(embed=embed) + else: + await self.last_stock_message.edit(embed=embed) + else: + # Send new message and store reference + if is_interaction: + msg = await interaction.followup.send(embed=embed) + else: + msg = await respond(embed=embed) + self.last_stock_message = msg + except Exception as e: + self.logger.error(f"Error updating stock sale message: {e}") + # Fallback to sending new message if edit fails + if is_interaction: + await interaction.followup.send(embed=embed) + else: + await respond(embed=embed) + async def handle_bazaar_purchase(self, interaction_or_ctx, item_id: str = None, amount: int = 1): """Handle bazaar purchase from either interaction or command""" is_interaction = isinstance(interaction_or_ctx, discord.Interaction) @@ -554,7 +681,7 @@ async def handle_bazaar_purchase(self, interaction_or_ctx, item_id: str = None, else: await respond(embed=embed) - @commands.command(name="bazaar-stock", aliases=["bstock"]) + @commands.command(name="bazaarstock", aliases=["bstock"]) @commands.cooldown(1, 10, commands.BucketType.user) async def bazaar_stock(self, ctx, amount: int = 1): """Buy bazaar stock""" diff --git a/cogs/economy/Economy.py b/cogs/economy/Economy.py index 9232797..41d5ce0 100644 --- a/cogs/economy/Economy.py +++ b/cogs/economy/Economy.py @@ -248,6 +248,7 @@ async def rob(self, ctx, victim: discord.Member): fine = int((random.random() * 0.3 + 0.1) * victim_bal) await db.update_wallet(ctx.author.id, -fine, ctx.guild.id) + await db.update_wallet(victim.id, fine, ctx.guild.id) return await ctx.reply(f"You got caught and paid **{fine}** {self.currency} in fines!") stolen = int(victim_bal * random.uniform(0.1, 0.5)) diff --git a/cogs/economy/Gambling.py b/cogs/economy/Gambling.py index 051144b..dcbb7fa 100644 --- a/cogs/economy/Gambling.py +++ b/cogs/economy/Gambling.py @@ -429,9 +429,9 @@ async def _run_crash_game(self, ctx, view, bet: int, current_balance: int): increment = 0.1 crash_point = random.uniform(1.1, 2.0) # Determine crash point first - # 1 in 1000 chance for big multiplier + # 1 in 1000 chance for a big multiplier if random.random() < 0.001: - crash_point = random.uniform(100.0, 1000.0) + crash_point = random.uniform(10.0, 1000000.0) while True: # First check if we've reached crash point @@ -480,7 +480,7 @@ async def _run_crash_game(self, ctx, view, bet: int, current_balance: int): self.active_games.remove(ctx.author.id) return - await asyncio.sleep(0.5) + await asyncio.sleep(0.75) def _crash_view(self, user_id: int, bet: int, current_balance: int): """Create the crash game view with cashout button""" @@ -951,7 +951,7 @@ async def roulette(self, ctx, bet: str = None, choice: str = None): message = await ctx.reply(embed=embed) # Animation sequence - spin_duration = 3 # seconds + spin_duration = 5 # seconds spin_steps = 10 delay = spin_duration / spin_steps diff --git a/cogs/economy/Work.py b/cogs/economy/Work.py index 2df2014..53cbe5d 100644 --- a/cogs/economy/Work.py +++ b/cogs/economy/Work.py @@ -20,9 +20,36 @@ def __init__(self, bot): @commands.cooldown(1, 60, commands.BucketType.user) async def work(self, ctx): """Work for some money""" - amount = random.randint(50, 200) + amount = random.randint(100, 1800) await db.update_wallet(ctx.author.id, amount, ctx.guild.id) - await ctx.reply(f"You worked and earned **{amount}** {self.currency}") + responses = [ + f"You worked hard and earned **{amount}** {self.currency}!", + f"Great effort! You just made **{amount}** {self.currency}.", + f"Your dedication paid off! You received **{amount}** {self.currency}.", + f"Well done! You've earned **{amount}** {self.currency}.", + f"Awesome work! You've collected **{amount}** {self.currency}.", + f"Success! You gained **{amount}** {self.currency} from your work.", + f"Keep it up! You earned **{amount}** {self.currency}.", + f"Fantastic job! You made **{amount}** {self.currency}.", + f"Your hard work has been rewarded with **{amount}** {self.currency}.", + f"You put in the effort and earned **{amount}** {self.currency}.", + f"Your labor was fruitful! You received **{amount}** {self.currency}.", + f"You've been productive! You earned **{amount}** {self.currency}.", + f"Your work ethic is impressive! You made **{amount}** {self.currency}.", + f"You've shown great diligence! You earned **{amount}** {self.currency}.", + f"Your commitment to work has paid off! You received **{amount}** {self.currency}.", + f"You've been industrious! You earned **{amount}** {self.currency}.", + f"Your efforts have been fruitful! You made **{amount}** {self.currency}.", + f"You've been a hard worker! You earned **{amount}** {self.currency}.", + f"You've shown great perseverance! You received **{amount}** {self.currency}.", + f"You've been diligent! You earned **{amount}** {self.currency}.", + f"You've been a model employee! You made **{amount}** {self.currency}.", + f"You've been a star worker! You earned **{amount}** {self.currency}.", + f"You've been a top performer! You received **{amount}** {self.currency}.", + f"You've been a valuable asset! You earned **{amount}** {self.currency}.", + f"You've been a key contributor! You made **{amount}** {self.currency}.", + ] + await ctx.reply(random.choice(responses)) async def setup(bot): diff --git a/dashboard/api/index.py b/dashboard/api/index.py deleted file mode 100644 index 95a58c6..0000000 --- a/dashboard/api/index.py +++ /dev/null @@ -1,21 +0,0 @@ -from flask import Flask, Request -import sys -import os - -# Add the root directory to Python path -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -# Import the blueprint -from lastfm_callback import lastfm_callback - -app = Flask(__name__) - -# Register the blueprint -app.register_blueprint(lastfm_callback) - -def handler(request: Request): - """Handle requests in Vercel serverless function""" - return app.wsgi_app(request.environ, lambda x, y: y) - -if __name__ == "__main__": - app.run(port=5000) diff --git a/dashboard/api/lastfm_callback.py b/dashboard/api/lastfm_callback.py deleted file mode 100644 index 324aea1..0000000 --- a/dashboard/api/lastfm_callback.py +++ /dev/null @@ -1,51 +0,0 @@ -from flask import Blueprint, request -import os -import json -import requests -import hashlib - -lastfm_callback = Blueprint("lastfm_callback", __name__) - -LASTFM_API_KEY = os.getenv("LASTFM_API_KEY") -LASTFM_API_SECRET = os.getenv("LASTFM_API_SECRET") -DATA_PATH = os.path.join("data", "lastfm_links.json") - -def generate_api_sig(params, secret): - items = sorted((k, v) for k, v in params.items() if k != "format") - sig = "".join(f"{k}{v}" for k, v in items) - sig += secret - return hashlib.md5(sig.encode("utf-8")).hexdigest() - -def save_link(discord_id, session_key, username): - if os.path.exists(DATA_PATH): - with open(DATA_PATH, "r", encoding="utf-8") as f: - links = json.load(f) - else: - links = {} - links[str(discord_id)] = {"session": session_key, "username": username} - os.makedirs(os.path.dirname(DATA_PATH), exist_ok=True) - with open(DATA_PATH, "w", encoding="utf-8") as f: - json.dump(links, f, indent=2) - -@lastfm_callback.route("/api/lastfm/callback") -def lastfm_callback_handler(): - token = request.args.get("token") - discord_id = request.args.get("discord_id") - if not token or not discord_id: - return "Missing token or discord_id", 400 - - params = { - "method": "auth.getSession", - "api_key": LASTFM_API_KEY, - "token": token, - "format": "json" - } - api_sig = generate_api_sig(params, LASTFM_API_SECRET) - params["api_sig"] = api_sig - resp = requests.get("http://ws.audioscrobbler.com/2.0/", params=params) - data = resp.json() - if "session" in data: - save_link(discord_id, data["session"]["key"], data["session"]["name"]) - return f"✅ Linked Discord ID {discord_id} to Last.fm user {data['session']['name']}! You can now use the bot." - else: - return f"❌ Error: {data.get('message', 'Unknown error')}", 400 \ No newline at end of file diff --git a/dashboard/app.py b/dashboard/app.py deleted file mode 100644 index c140767..0000000 --- a/dashboard/app.py +++ /dev/null @@ -1,735 +0,0 @@ -from flask import Flask, render_template, redirect, request, make_response, url_for, jsonify, Response -import requests -import json -from functools import wraps -import time -import os -from pymongo import MongoClient -import pymongo.errors -from dotenv import load_dotenv - -# Load environment variables from .env file -load_dotenv() - -app = Flask(__name__) # Initialize Flask app at module level - -# Configure for production -app.config['SERVER_NAME'] = None - -# Initialize MongoDB with better error handling -MONGODB_URI = os.environ.get("MONGO_URI") -try: - mongo_client = MongoClient(MONGODB_URI, serverSelectionTimeoutMS=5000) # 5 second timeout - # Test the connection - mongo_client.admin.command('ping') - db = mongo_client.bronxbot - MONGODB_AVAILABLE = True - print("MongoDB connection successful") -except pymongo.errors.ServerSelectionTimeoutError as e: - print(f"MongoDB connection failed: {e}") - print("Running without database functionality") - MONGODB_AVAILABLE = False - db = None -except Exception as e: - print(f"Unexpected MongoDB error: {e}") - MONGODB_AVAILABLE = False - db = None -# Initialize MongoDB with better error handling -MONGODB_URI = os.environ.get("MONGO_URI") -try: - mongo_client = MongoClient(MONGODB_URI, serverSelectionTimeoutMS=5000) # 5 second timeout - # Test the connection - mongo_client.admin.command('ping') - db = mongo_client.bronxbot - MONGODB_AVAILABLE = True - print("MongoDB connection successful") -except pymongo.errors.ServerSelectionTimeoutError as e: - print(f"MongoDB connection failed: {e}") - print("Running without database functionality") - MONGODB_AVAILABLE = False - db = None -except Exception as e: - print(f"Unexpected MongoDB error: {e}") - MONGODB_AVAILABLE = False - db = None - -def get_guild_settings(guild_id: str): - """Get guild settings synchronously with error handling""" - if not MONGODB_AVAILABLE or db is None: - print("MongoDB not available, returning default settings") - return { - 'prefixes': ['!'], - 'welcome': { - 'enabled': False, - 'channel_id': None, - 'message': 'Welcome to the server!' - }, - 'moderation': { - 'log_channel': None, - 'mute_role': None, - 'jail_role': None - } - } - - try: - settings = db.guild_settings.find_one({"_id": str(guild_id)}) - return settings if settings else {} - except Exception as e: - print(f"Error getting guild settings: {e}") - return {} - -def get_user_balance(user_id: str): - """Get user balance from database""" - if not MONGODB_AVAILABLE or not db: - return {'balance': 0, 'bank': 0} - - try: - user_data = db.users.find_one({"_id": str(user_id)}) - if user_data: - return { - 'balance': user_data.get('balance', 0), - 'bank': user_data.get('bank', 0) - } - return {'balance': 0, 'bank': 0} - except Exception as e: - print(f"Error getting user balance: {e}") - return {'balance': 0, 'bank': 0} - -def get_guild_stats(guild_id: str): - """Get guild statistics from database""" - if not MONGODB_AVAILABLE or not db: - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - - try: - stats = db.guild_stats.find_one({"_id": str(guild_id)}) - if stats: - return { - 'member_count': stats.get('member_count', 0), - 'message_count': stats.get('message_count', 0), - 'active_users': stats.get('active_users', 0) - } - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - except Exception as e: - print(f"Error getting guild stats: {e}") - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - -def get_guild_settings(guild_id: str): - """Get guild settings synchronously with error handling""" - if not MONGODB_AVAILABLE or db is None: - print("MongoDB not available, returning default settings") - return { - 'prefixes': ['!'], - 'welcome': { - 'enabled': False, - 'channel_id': None, - 'message': 'Welcome to the server!' - }, - 'moderation': { - 'log_channel': None, - 'mute_role': None, - 'jail_role': None - } - } - - try: - settings = db.guild_settings.find_one({"_id": str(guild_id)}) - return settings if settings else {} - except Exception as e: - print(f"Error getting guild settings: {e}") - return {} - -def get_user_balance(user_id: str): - """Get user balance from database""" - if not MONGODB_AVAILABLE or not db: - return {'balance': 0, 'bank': 0} - - try: - user_data = db.users.find_one({"_id": str(user_id)}) - if user_data: - return { - 'balance': user_data.get('balance', 0), - 'bank': user_data.get('bank', 0) - } - return {'balance': 0, 'bank': 0} - except Exception as e: - print(f"Error getting user balance: {e}") - return {'balance': 0, 'bank': 0} - -def get_guild_stats(guild_id: str): - """Get guild statistics from database""" - if not MONGODB_AVAILABLE or not db: - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - - try: - stats = db.guild_stats.find_one({"_id": str(guild_id)}) - if stats: - return { - 'member_count': stats.get('member_count', 0), - 'message_count': stats.get('message_count', 0), - 'active_users': stats.get('active_users', 0) - } - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - except Exception as e: - print(f"Error getting guild stats: {e}") - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - -def get_guild_settings(guild_id: str): - """Get guild settings synchronously with error handling""" - if not MONGODB_AVAILABLE or db is None: - print("MongoDB not available, returning default settings") - return { - 'prefixes': ['!'], - 'welcome': { - 'enabled': False, - 'channel_id': None, - 'message': 'Welcome to the server!' - }, - 'moderation': { - 'log_channel': None, - 'mute_role': None, - 'jail_role': None - } - } - - try: - settings = db.guild_settings.find_one({"_id": str(guild_id)}) - return settings if settings else {} - except Exception as e: - print(f"Error getting guild settings: {e}") - return {} - -def get_user_balance(user_id: str): - """Get user balance from database""" - if not MONGODB_AVAILABLE or not db: - return {'balance': 0, 'bank': 0} - - try: - user_data = db.users.find_one({"_id": str(user_id)}) - if user_data: - return { - 'balance': user_data.get('balance', 0), - 'bank': user_data.get('bank', 0) - } - return {'balance': 0, 'bank': 0} - except Exception as e: - print(f"Error getting user balance: {e}") - return {'balance': 0, 'bank': 0} - -def get_guild_stats(guild_id: str): - """Get guild statistics from database""" - if not MONGODB_AVAILABLE or not db: - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - - try: - stats = db.guild_stats.find_one({"_id": str(guild_id)}) - if stats: - return { - 'member_count': stats.get('member_count', 0), - 'message_count': stats.get('message_count', 0), - 'active_users': stats.get('active_users', 0) - } - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - except Exception as e: - print(f"Error getting guild stats: {e}") - return {'member_count': 0, 'message_count': 0, 'active_users': 0} - -# Add thousands filter -@app.template_filter('thousands') -def thousands_filter(value): - """Format a number with thousands separator""" - try: - return "{:,}".format(int(value)) - except (ValueError, TypeError): - return "0" - -# Initialize configuration with default empty values -DISCORD_CLIENT_ID = None -DISCORD_CLIENT_SECRET = None -DISCORD_BOT_OWNER_ID = None -DISCORD_BOT_TOKEN = None -config = {} - -def load_config(): - """Load configuration from environment variables or config file""" - global DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_OWNER_ID, DISCORD_BOT_TOKEN, config - - # First try environment variables - DISCORD_CLIENT_ID = os.environ.get('DISCORD_CLIENT_ID') - DISCORD_CLIENT_SECRET = os.environ.get('DISCORD_CLIENT_SECRET') - DISCORD_BOT_OWNER_ID = os.environ.get('DISCORD_BOT_OWNER_ID') - DISCORD_BOT_TOKEN = os.environ.get('DISCORD_BOT_TOKEN') - - # If env vars not set, try config file - if not all([DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_OWNER_ID]): - try: - with open("data/config.json", "r") as f: - config = json.load(f) - DISCORD_CLIENT_ID = DISCORD_CLIENT_ID or config.get('CLIENT_ID') - DISCORD_CLIENT_SECRET = DISCORD_CLIENT_SECRET or config.get('CLIENT_SECRET') - DISCORD_BOT_OWNER_ID = DISCORD_BOT_OWNER_ID or config.get('OWNER_ID') - DISCORD_BOT_TOKEN = DISCORD_BOT_TOKEN or config.get('TOKEN') - except FileNotFoundError: - print("Config file not found, using environment variables only") - except json.JSONDecodeError: - print("Invalid JSON in config file, using environment variables only") - - return all([DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_BOT_OWNER_ID]) - -def require_discord_config(f): - """Decorator to ensure Discord configuration is available""" - @wraps(f) - def decorated_function(*args, **kwargs): - if not load_config(): - return jsonify({ - "error": "Discord configuration is not set up. Please configure DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, and DISCORD_BOT_OWNER_ID." - }), 503 - return f(*args, **kwargs) - return decorated_function - -# Try initial config load but don't fail if unsuccessful -try: - load_config() -except Exception as e: - print(f"Warning: Error loading initial configuration: {e}") - -# Set callback URI based on environment -if os.environ.get('RENDER_EXTERNAL_URL'): - DISCORD_REDIRECT_URI = f"{os.environ['RENDER_EXTERNAL_URL']}/callback" -else: - DISCORD_REDIRECT_URI = 'http://localhost:5000/callback' - -# Global stats dictionary -bot_stats = { - 'server_count': 0, - 'user_count': 0, - 'uptime': 0, - 'latency': 0, - 'guilds': [] # List of guild IDs where bot is present -} - -def login_required(f): - @wraps(f) - def decorated_function(*args, **kwargs): - user_id = request.cookies.get('user_id') - if not user_id: - return redirect(url_for('login')) - return f(*args, **kwargs) - return decorated_function - -@app.route('/api/stats', methods=['GET', 'POST']) -def api_stats(): - global bot_stats - if request.method == 'POST': - bot_stats.update(request.json) - return jsonify({"status": "success"}) - return jsonify(bot_stats) - -@app.route('/') -def home(): - user_id = request.cookies.get('user_id') - # Only check bot owner ID if Discord config is loaded - if DISCORD_BOT_OWNER_ID and user_id and user_id == DISCORD_BOT_OWNER_ID and request.host == 'localhost:5000': - username = request.cookies.get('username', 'User') - return render_template('DEVindex.html', username=username, stats=bot_stats) - elif user_id: - username = request.cookies.get('username', 'User') - return render_template('index.html', username=username, stats=bot_stats) - return render_template('home.html', stats=bot_stats) - -@app.route('/login') -def login(): - if not DISCORD_CLIENT_ID: - return "Discord configuration not set up", 503 - return redirect(f'https://discord.com/api/oauth2/authorize?client_id={DISCORD_CLIENT_ID}&redirect_uri={DISCORD_REDIRECT_URI}&response_type=code&scope=identify+guilds') - -@app.route('/callback') -def callback(): - code = request.args.get('code') - if not code: - return 'No authorization code provided', 400 - - data = { - 'client_id': DISCORD_CLIENT_ID, - 'client_secret': DISCORD_CLIENT_SECRET, - 'grant_type': 'authorization_code', - 'code': code, - 'redirect_uri': DISCORD_REDIRECT_URI, - 'scope': 'identify' - } - headers = { - 'Content-Type': 'application/x-www-form-urlencoded' - } - response = requests.post('https://discord.com/api/oauth2/token', data=data, headers=headers) - if response.status_code == 200: - credentials = response.json() - access_token = credentials['access_token'] - - # Get user info - user_response = requests.get('https://discord.com/api/users/@me', headers={ - 'Authorization': f'Bearer {access_token}' - }) - user = user_response.json() - - resp = make_response(redirect('/')) - resp.set_cookie('user_id', user['id']) - resp.set_cookie('username', user['username']) - resp.set_cookie('access_token', access_token) - return resp - return 'Authentication failed', 400 - -@app.route('/logout') -def logout(): - resp = make_response(redirect('/')) - resp.delete_cookie('user_id') - resp.delete_cookie('username') - resp.delete_cookie('access_token') - return resp - -def get_user_guilds(access_token): - """Fetch user's Discord servers""" - response = requests.get('https://discord.com/api/users/@me/guilds', headers={ - 'Authorization': f'Bearer {access_token}' - }) - if response.status_code == 200: - return response.json() - return [] - -def get_bot_guilds(): - """Fetch bot's server list from stats""" - global bot_stats - return bot_stats.get('guilds', []) - -@app.route('/health') -def health(): - """Health check endpoint""" - return jsonify({ - 'status': 'ok', - 'mongodb_available': MONGODB_AVAILABLE, - 'discord_configured': load_config() - }) - -@app.route('/servers') -@login_required -def servers(): - """Show list of servers the user has access to""" - access_token = request.cookies.get('access_token') - if not access_token: - return redirect('/login') - - user_guilds = get_user_guilds(access_token) - bot_guilds = get_bot_guilds() - - # Filter guilds where user has manage server permission - manage_guilds = [ - guild for guild in user_guilds - if (int(guild['permissions']) & 0x20) == 0x20 # Check for MANAGE_GUILD permission - ] - - # Mark guilds where bot is present and add icon URLs - for guild in manage_guilds: - guild['bot_present'] = str(guild['id']) in bot_guilds - guild['icon_url'] = f"https://cdn.discordapp.com/icons/{guild['id']}/{guild['icon']}.png" if guild['icon'] else None - - # Sort guilds to show bot-present servers first - manage_guilds.sort(key=lambda g: (not g['bot_present'], g['name'].lower())) - - return render_template('servers.html', - guilds=manage_guilds, - username=request.cookies.get('username', 'User'), - config={'CLIENT_ID': DISCORD_CLIENT_ID} - ) - -@app.route('/servers//settings') -@login_required -def server_settings(guild_id): - """Show settings for a specific server""" - access_token = request.cookies.get('access_token') - if not access_token: - return redirect('/login') - - # Verify user has access to this server - user_guilds = get_user_guilds(access_token) - if not any(g['id'] == guild_id and (int(g['permissions']) & 0x20) == 0x20 for g in user_guilds): - return "Unauthorized", 403 - - # Get server settings from database - settings = get_guild_settings(guild_id) - - # Initialize default values if settings is empty - guild_info = None - channels = [] - roles = [] - - # Only try to get Discord API data if bot token is available - if DISCORD_BOT_TOKEN: - headers = {'Authorization': f'Bot {DISCORD_BOT_TOKEN}'} - try: - guild_response = requests.get(f'https://discord.com/api/v10/guilds/{guild_id}', headers=headers) - channels_response = requests.get(f'https://discord.com/api/v10/guilds/{guild_id}/channels', headers=headers) - roles_response = requests.get(f'https://discord.com/api/v10/guilds/{guild_id}/roles', headers=headers) - - guild_info = guild_response.json() if guild_response.ok else None - channels = channels_response.json() if channels_response.ok else [] - roles = roles_response.json() if roles_response.ok else [] - except Exception as e: - print(f"Error fetching Discord API data: {e}") - else: - print("No bot token available, using minimal guild info") - # Use basic guild info from user's guild list - user_guild = next((g for g in user_guilds if g['id'] == guild_id), None) - if user_guild: - guild_info = { - 'id': user_guild['id'], - 'name': user_guild['name'], - 'icon': user_guild.get('icon') - } - - # Filter text channels only and sort by position - text_channels = sorted( - [c for c in channels if c.get('type') == 0], # 0 is text channel - key=lambda c: c.get('position', 0) - ) - - # Sort roles by position - roles = sorted(roles, key=lambda r: r.get('position', 0), reverse=True) - - return render_template('settings.html', - guild=guild_info, - settings=settings, - channels=text_channels, - roles=roles, - username=request.cookies.get('username', 'User'), - mongodb_available=MONGODB_AVAILABLE - ) - -@app.route('/servers//settings/update', methods=['POST']) -@login_required -def update_settings(guild_id): - """Update settings for a specific server""" - access_token = request.cookies.get('access_token') - if not access_token: - return redirect('/login') - - # Verify user has access to this server - user_guilds = get_user_guilds(access_token) - if not any(g['id'] == guild_id and (int(g['permissions']) & 0x20) == 0x20 for g in user_guilds): - return "Unauthorized", 403 - - if not MONGODB_AVAILABLE: - return redirect(f'/servers/{guild_id}/settings?error=database_unavailable') - - # Get settings from form - settings = { - 'prefixes': [p.strip() for p in request.form.get('prefixes', '').split(',') if p.strip()], - 'welcome': { - 'enabled': bool(request.form.get('welcome_enabled')), - 'channel_id': request.form.get('welcome_channel'), - 'message': request.form.get('welcome_message') - }, - 'moderation': { - 'log_channel': request.form.get('log_channel'), - 'mute_role': request.form.get('mute_role'), - 'jail_role': request.form.get('jail_role') - } - } - - try: - # Update database - result = db.guild_settings.update_one( - {"_id": str(guild_id)}, - {"$set": settings}, - upsert=True - ) - - if result.acknowledged: - return redirect(f'/servers/{guild_id}/settings?success=1') - else: - return redirect(f'/servers/{guild_id}/settings?error=1') - except Exception as e: - print(f"Error updating guild settings: {e}") - return redirect(f'/servers/{guild_id}/settings?error=1') - -@app.route('/settings') -@login_required -def settings_select(): - """Show server selection for settings""" - access_token = request.cookies.get('access_token') - if not access_token: - return redirect('/login') - - user_guilds = get_user_guilds(access_token) - bot_guilds = get_bot_guilds() - - # Filter guilds where user has manage server permission - manage_guilds = [ - guild for guild in user_guilds - if (int(guild['permissions']) & 0x20) == 0x20 - ] - - # Add bot presence info - for guild in manage_guilds: - guild['bot_present'] = str(guild['id']) in bot_guilds - guild['icon_url'] = f"https://cdn.discordapp.com/icons/{guild['id']}/{guild['icon']}.png" if guild['icon'] else None - - # Sort guilds to show bot-present servers first - manage_guilds.sort(key=lambda g: (not g['bot_present'], g['name'].lower())) - - return render_template('settings_select.html', - guilds=manage_guilds, - username=request.cookies.get('username', 'User'), - config={'CLIENT_ID': DISCORD_CLIENT_ID} - ) - -@app.route('/api/user//balance') -@login_required -def get_user_balance_api(user_id): - """API endpoint to get user balance""" - # Only allow the user to see their own balance or allow bot owner to see any balance - requester_id = request.cookies.get('user_id') - if requester_id != user_id and requester_id != DISCORD_BOT_OWNER_ID: - return jsonify({"error": "Unauthorized"}), 403 - - balance = get_user_balance(user_id) - return jsonify(balance) - -@app.route('/api/guild//stats') -@login_required -def get_guild_stats_api(guild_id): - """API endpoint to get guild stats""" - access_token = request.cookies.get('access_token') - if not access_token: - return jsonify({"error": "No access token"}), 401 - - # Verify user has access to this server - user_guilds = get_user_guilds(access_token) - if not any(g['id'] == guild_id and (int(g['permissions']) & 0x20) == 0x20 for g in user_guilds): - return jsonify({"error": "Unauthorized"}), 403 - - stats = get_guild_stats(guild_id) - return jsonify(stats) - -@app.route('/debug') -def debug(): - """Debug endpoint to check application status""" - return jsonify({ - 'status': 'ok', - 'env': os.environ.get('FLASK_ENV'), - 'discord_configured': load_config(), - 'mongodb_available': MONGODB_AVAILABLE, - 'bot_token_available': bool(DISCORD_BOT_TOKEN), - 'redirect_uri': DISCORD_REDIRECT_URI, - 'config_source': 'env' if any([os.environ.get('DISCORD_CLIENT_ID'), - os.environ.get('DISCORD_CLIENT_SECRET'), - os.environ.get('DISCORD_BOT_OWNER_ID')]) else 'config_file', - 'missing_config': [k for k in ['DISCORD_CLIENT_ID', 'DISCORD_CLIENT_SECRET', 'DISCORD_BOT_OWNER_ID'] - if not os.environ.get(k) and not config.get(k.split('_', 1)[1])] - }) - -# Error handlers -@app.errorhandler(500) -def server_error(e): - return jsonify({ - 'error': 'Internal Server Error', - 'message': str(e), - 'type': '500' - }), 500 - -@app.errorhandler(404) -def not_found(e): - return jsonify({ - 'error': 'Not Found', - 'message': str(e), - 'type': '404' - }), 404 - -@app.errorhandler(404) -def not_found_error(error): - """Handle 404 errors""" - if request.headers.get('Accept', '').startswith('application/json'): - return jsonify({"error": "Not found"}), 404 - return render_template('home.html', stats=bot_stats), 404 - -@app.errorhandler(500) -def internal_error(error): - """Handle 500 errors""" - if request.headers.get('Accept', '').startswith('application/json'): - return jsonify({"error": "Internal server error"}), 500 - return render_template('home.html', stats=bot_stats, error="Internal server error"), 500 - -def get_available_port(start_port, max_port=65535): - """Find first available port in range""" - import socket - for port in range(start_port, max_port + 1): - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('127.0.0.1', port)) - s.close() - return port - except OSError: - continue - return None - -def run(as_thread=False): - """Run the Flask application server - - Args: - as_thread (bool): If True, run in a separate thread for the Discord bot - """ - default_port = int(os.environ.get('PORT', 5000)) - port = get_available_port(default_port) - if not port: - port = get_available_port(8000) # Try alternate port range - if not port: - raise RuntimeError("No available ports found") - - host = '0.0.0.0' if os.environ.get('FLASK_ENV') == 'production' else '127.0.0.1' - - if as_thread: - import threading - from werkzeug.serving import make_server - - server = make_server(host, port, app, threaded=True) - print(f"Web server starting on http://{host}:{port}") - - def run_server(): - server.serve_forever() - - server_thread = threading.Thread(target=run_server, daemon=True) - server_thread.start() - else: - # Only use debug mode when running directly (not in thread) - debug = os.environ.get('FLASK_ENV') != 'production' - app.run(host=host, port=port, debug=debug) - -def shutdown_server(): - """Shutdown the Flask server (placeholder for now)""" - pass - -@app.route('/api/stats/live') -def live_stats(): - def generate(): - while True: - # Read the latest stats from the file - try: - with open('data/stats.json', 'r') as f: - stats = json.load(f) - data = f"data: {json.dumps(stats)}\n\n" - yield data - except Exception as e: - print(f"Error reading stats: {e}") - yield "data: {}\n\n" - time.sleep(1) # Update every second - - return Response(generate(), mimetype='text/event-stream') - -@app.route("/invite") -def invite(): - return redirect("https://discord.com/oauth2/authorize?client_id=828380019406929962&permissions=8&response_type=code&redirect_uri=https%3A%2F%2Fbronxbot.onrender.com%2Fcallback&integration_type=0&scope=identify+guilds+bot") - -if __name__ == "__main__": - try: - run() - except Exception as e: - print(f"Error starting server: {e}") - import sys - sys.exit(1) \ No newline at end of file diff --git a/dashboard/config.py b/dashboard/config.py deleted file mode 100644 index bd1f096..0000000 --- a/dashboard/config.py +++ /dev/null @@ -1,27 +0,0 @@ -import os - -class Config: - # Flask - SECRET_KEY = os.environ.get('SECRET_KEY', 'dev') - FLASK_ENV = os.environ.get('FLASK_ENV', 'production') - - # Database - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL', 'sqlite:///data/database.sqlite') - SQLALCHEMY_TRACK_MODIFICATIONS = False - - # Discord - DISCORD_CLIENT_ID = os.environ.get('DISCORD_CLIENT_ID') - DISCORD_CLIENT_SECRET = os.environ.get('DISCORD_CLIENT_SECRET') - DISCORD_BOT_TOKEN = os.environ.get('DISCORD_TOKEN') - - # MongoDB (if used) - MONGO_URI = os.environ.get('MONGO_URI') - - # Session config - SESSION_TYPE = 'filesystem' - PERMANENT_SESSION_LIFETIME = 86400 # 24 hours - - # Security - WTF_CSRF_ENABLED = True - REMEMBER_COOKIE_HTTPONLY = True - SESSION_COOKIE_HTTPONLY = True diff --git a/dashboard/gunicorn_config.py b/dashboard/gunicorn_config.py deleted file mode 100644 index 2d5144d..0000000 --- a/dashboard/gunicorn_config.py +++ /dev/null @@ -1,72 +0,0 @@ -import os -import socket -import multiprocessing - -def get_available_port(start_port, max_port=65535): - """Find first available port in range""" - for port in range(start_port, max_port + 1): - try: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.bind(('0.0.0.0', port)) - s.close() - return port - except OSError: - continue - return None - -# Binding -default_port = int(os.environ.get("PORT", 5001)) # Changed default to 5001 -port = get_available_port(default_port) -if port != default_port: # If we couldn't get 5001, try other ports - port = get_available_port(5000) # Try 5000 next - if not port: - port = get_available_port(8000) # Try alternate port range as last resort -if not port: - raise RuntimeError("No available ports found") -bind = f"0.0.0.0:{port}" - -# Worker processes -workers = int(os.environ.get('WEB_CONCURRENCY', 2)) -worker_class = 'gthread' # Use threads -threads = int(os.environ.get('PYTHON_MAX_THREADS', 4)) -worker_connections = 1000 - -# Timeout -timeout = 120 -graceful_timeout = 30 -keepalive = 65 - -# Logging -accesslog = "-" -errorlog = "-" -loglevel = os.environ.get('LOG_LEVEL', 'info') -access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' -capture_output = True - -# SSL and security -forwarded_allow_ips = '*' -secure_scheme_headers = { - 'X-FORWARDED-PROTOCOL': 'ssl', - 'X-FORWARDED-PROTO': 'https', - 'X-FORWARDED-SSL': 'on' -} - -# Performance tuning -max_requests = 1000 -max_requests_jitter = 50 -preload_app = True -reuse_port = True - -# Process naming -proc_name = 'bronxbot-web' - -# Flask specific -wsgi_app = 'wsgi:app' -pythonpath = '.' - -# Error pages -errorlog = '-' -loglevel = 'debug' - -# Reload in development -reload = os.environ.get('FLASK_ENV') == 'development' diff --git a/dashboard/render.yaml b/dashboard/render.yaml deleted file mode 100644 index 9dbc23f..0000000 --- a/dashboard/render.yaml +++ /dev/null @@ -1,32 +0,0 @@ -services: - - type: web - name: bronxbot-web - env: python - buildCommand: pip install -r requirements.web.txt - startCommand: gunicorn -c gunicorn_config.py wsgi:app - envVars: - - key: GUNICORN_CMD_ARGS - value: "--log-level=debug --timeout=120 --keep-alive=65 --workers=2 --threads=4 --worker-class=gthread" - - key: PYTHONUNBUFFERED - value: "true" - envVars: - - key: PYTHON_VERSION - value: 3.9.0 - - key: FLASK_ENV - value: production - - key: PYTHONPATH - value: . - - key: MONGO_URI - sync: false - - key: DISCORD_TOKEN - sync: false - - key: DISCORD_CLIENT_ID - sync: false - - key: DISCORD_CLIENT_SECRET - sync: false - - key: DISCORD_BOT_OWNER_ID - sync: false - - key: SQLITE_DATABASE_PATH - value: /data/database.sqlite - healthCheckPath: /debug - autoDeploy: true diff --git a/dashboard/requirements.txt b/dashboard/requirements.txt deleted file mode 100644 index add3b5a..0000000 --- a/dashboard/requirements.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Web Framework -flask==3.0.0 -werkzeug==3.0.6 -click==8.1.7 -itsdangerous==2.1.2 -jinja2==3.1.6 -markupsafe==2.1.5 -gunicorn==23.0.0 - -# Discord Bot -discord.py==2.3.2 -aiohttp==3.11.0b0 - -# Database -pymongo==4.6.3 -motor==3.3.2 -sqlalchemy==2.0.25 -aiosqlite==0.19.0 - -# Utils -python-dotenv==1.0.0 -requests==2.32.2 - -asgiref==3.7.2 diff --git a/dashboard/static/css/style.css b/dashboard/static/css/style.css deleted file mode 100644 index 3d77944..0000000 --- a/dashboard/static/css/style.css +++ /dev/null @@ -1,613 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Orbitron:wght@400;500;700&display=swap'); - -:root { - --primary: #5865F2; - --secondary: #2C2F33; - --background: #1a1b1e; - --text: #FFFFFF; - --text-secondary: #99AAB5; - --success: #57F287; - --warning: #FEE75C; - --danger: #ED4245; -} - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Inter', sans-serif; - background-color: var(--background); - color: var(--text); - line-height: 1.6; -} - -.navbar { - background-color: var(--secondary); - padding: 1rem 2rem; - position: sticky; - top: 0; - z-index: 1000; -} - -.nav-content { - max-width: 1200px; - margin: 0 auto; - display: flex; - justify-content: space-between; - align-items: center; -} - -.nav-brand { - font-size: 1.5rem; - font-weight: 700; - color: var(--text); - text-decoration: none; -} - -.nav-links { - display: flex; - gap: 2rem; -} - -.nav-link { - color: var(--text); - text-decoration: none; - font-weight: 500; - position: relative; - transition: color 0.2s; - padding: 5px 0; -} - -.nav-link::after { - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 2px; - background: var(--primary); - transform: scaleX(0); - transform-origin: right; - transition: transform 0.4s cubic-bezier(0.86, 0, 0.07, 1); -} - -.nav-link:hover::after { - transform: scaleX(1); - transform-origin: left; -} - -.hero { - padding: 4rem 2rem; - text-align: center; - background: linear-gradient(to bottom right, var(--primary), #7289DA); -} - -.hero a { - color: var(--text); - text-decoration: none; - font-weight: 600; - transition: color ease-in-out.2s; -} -.hero a:hover { - color: var(--text-secondary); -} - -.hero h1 { - font-family: 'Orbitron', sans-serif; - font-size: 3rem; - margin-bottom: 1rem; - letter-spacing: 2px; - text-shadow: 0 0 15px rgba(88, 101, 242, 0.5); -} - -.features { - text-align:center; -} -.stats-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 2rem; - padding: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -.stat-card { - background-color: var(--secondary); - padding: 1.5rem; - border-radius: 10px; - text-align: center; -} - -.stat-card h3 { - color: var(--text-secondary); - font-size: 0.9rem; - text-transform: uppercase; - letter-spacing: 0.1em; - margin-bottom: 0.5rem; -} - -.stat-card p { - font-size: 1.5rem; - font-weight: 700; - color: var(--primary); -} - -.btn { - display: inline-block; - padding: 0.8rem 1.5rem; - border-radius: 5px; - font-weight: 600; - text-decoration: none; - transition: transform 0.2s; -} - -.btn:hover { - transform: translateY(-2px); -} - -.btn-primary { - background-color: var(--primary); - color: var(--text); -} - -.container { - display: flex; - min-height: calc(100vh - 60px); -} - -.sidebar { - width: 250px; - background: var(--secondary); - padding: 1rem; - border-right: 1px solid #40444b; - transition: transform 0.3s ease; -} - -.sidebar.fixed { - position: fixed; - height: 100vh; -} - -.sidebar.collapsible { - position: fixed; - height: calc(100vh - 60px); - top: 60px; - transform: translateX(-100%); -} - -.sidebar.collapsible.active { - transform: translateX(0); -} - -.with-fixed-sidebar .main-content { - margin-left: 250px; -} - -.sidebar-header { - padding: 1rem; - text-align: center; - border-bottom: 1px solid #40444b; - margin-bottom: 1rem; -} - -.sidebar-menu { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.sidebar-item { - display: flex; - align-items: center; - gap: 1rem; - padding: 0.75rem 1rem; - color: #99aab5; - text-decoration: none; - border-radius: 4px; - transition: all 0.3s ease; -} - -.sidebar-item:hover { - background: #40444b; - color: #fff; -} - -.sidebar-item.active { - background: #7289da; - color: #fff; -} - -.sidebar-item i { - width: 20px; - text-align: center; -} - -.main-content { - flex: 1; - padding: 2rem; -} - -.nav-toggle { - display: block; - background: none; - border: none; - color: var(--text); - font-size: 1.5rem; - cursor: pointer; - margin-right: 1rem; -} - -.nav-toggle i { - transition: transform 0.3s ease; -} - -.nav-toggle.active i { - transform: rotate(180deg); -} - -@media (min-width: 769px) { - .nav-toggle { - display: none; - } - - .nav-links.collapsible { - display: flex; - position: static; - padding: 0; - flex-direction: row; - } -} - -@media (max-width: 768px) { - .nav-links { - display: none; - } - - .hero h1 { - font-size: 2rem; - } - - .stats-grid { - grid-template-columns: 1fr; - } - - .sidebar { - display: none; - } - - .sidebar.active { - display: block; - position: fixed; - left: 0; - top: 60px; - height: calc(100vh - 60px); - z-index: 100; - } - - .with-fixed-sidebar .main-content { - margin-left: 0; - } - - .sidebar.fixed { - display: none; - } - - .nav-toggle { - display: block; - } - - .nav-content { - position: relative; - } - - .nav-links.collapsible { - position: absolute; - top: 100%; - left: 0; - right: 0; - background: var(--secondary); - padding: 1rem; - flex-direction: column; - align-items: center; - display: none; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - } - - .nav-links.collapsible.active { - display: flex; - } -} - -/* Settings Form Styles */ -.form-group { - margin-bottom: 1.5rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - color: var(--text); -} - -.form-group input[type="text"], -.form-group select, -.form-group textarea { - width: 100%; - padding: 0.5rem; - border: 1px solid #40444b; - background: var(--secondary); - color: var(--text); - border-radius: 4px; -} - -.form-group textarea { - min-height: 100px; - resize: vertical; -} - -.form-group small { - display: block; - color: var(--text-secondary); - margin-top: 0.25rem; -} - -section { - margin-bottom: 2rem; - padding: 1rem; - background: var(--secondary); - border-radius: 8px; -} - -section h2 { - margin-bottom: 1rem; - color: var(--text); -} - -/* Prefix Management Styles */ -.prefix-input-container { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.prefix-buttons { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - min-height: 38px; - padding: 0.5rem; - border: 1px solid #40444b; - border-radius: 4px; - background: rgba(0,0,0,0.2); -} - -.prefix-tag { - display: inline-flex; - align-items: center; - gap: 0.5rem; - padding: 0.25rem 0.75rem; - background: var(--primary); - color: white; - border: none; - border-radius: 15px; - font-size: 0.9rem; - cursor: pointer; - transition: all 0.3s ease; - animation: slideIn 0.3s ease; -} - -.prefix-tag:hover { - background: var(--danger); - transform: translateY(-2px); -} - -.remove-prefix { - font-size: 1.2rem; - font-weight: bold; -} - -.prefix-counter { - color: var(--text-secondary); - font-size: 0.8rem; - text-align: right; -} - -@keyframes slideIn { - from { - transform: translateX(-10px); - opacity: 0; - } - to { - transform: translateX(0); - opacity: 1; - } -} - -/* Server Selection Styles */ -.server-select { - max-width: 1000px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.server-select h1 { - margin-bottom: 1rem; -} - -.server-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 1.5rem; - margin-top: 2rem; -} - -.server-card { - display: flex; - align-items: center; - gap: 1rem; - padding: 1rem; - background: var(--secondary); - border-radius: 8px; - text-decoration: none; - color: var(--text); - transition: all 0.3s ease; -} - -.server-card:not(.disabled):hover { - transform: translateY(-2px); - background: #40444b; -} - -.server-card.disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.server-icon { - width: 48px; - height: 48px; - border-radius: 50%; - overflow: hidden; - background: #40444b; - display: flex; - align-items: center; - justify-content: center; -} - -.server-icon img { - width: 100%; - height: 100%; - object-fit: cover; -} - -.server-icon i { - font-size: 1.5rem; - color: var(--text-secondary); -} - -.server-info { - flex: 1; - text-align: left; -} - -.server-info h3 { - margin: 0; - font-size: 1.1rem; -} - -.server-status { - font-size: 0.8rem; - color: var(--text-secondary); -} - -.server-status.active { - color: var(--success); -} - -.server-status.inactive { - color: var(--warning); -} - -/* Server Action Styles */ -.server-actions { - display: flex; - flex-direction: column; - gap: 0.5rem; - margin-top: 0.5rem; -} - -.server-card.active { - background: linear-gradient(135deg, var(--secondary), #40444b); - border: 1px solid #40444b; -} - -.server-card.active:hover { - transform: translateY(-2px); - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); -} - -.settings-hint { - font-size: 0.8rem; - color: var(--primary); - display: flex; - align-items: center; - gap: 0.5rem; -} - -.server-card.active:hover .settings-hint { - color: var(--text); -} - -.add-bot-link { - color: var(--primary); - text-decoration: none; - font-size: 0.8rem; - display: flex; - align-items: center; - gap: 0.5rem; - transition: color 0.3s ease; -} - -.add-bot-link:hover { - color: var(--text); -} - -.shard-status { - margin: 2rem 0; - padding: 1rem; - background: rgba(0, 0, 0, 0.2); - border-radius: 10px; -} - -.shard-status h2 { - text-align: center; - margin-bottom: 1rem; -} - -.shard-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1rem; - margin-top: 2rem; -} - -.shard-card { - background: rgba(0, 0, 0, 0.3); - border-radius: 8px; - padding: 1rem; - transition: transform 0.2s; -} - -.shard-card:hover { - transform: translateY(-2px); -} - -.shard-card.online { - border-left: 4px solid #43b581; -} - -.shard-card.offline { - border-left: 4px solid #f04747; -} - -.shard-card h4 { - margin: 0 0 0.5rem 0; - font-size: 1.1rem; -} - -.shard-card p { - margin: 0.25rem 0; - font-size: 0.9rem; - opacity: 0.9; -} - -.shard-card .uptime { - margin-top: 0.5rem; - font-size: 0.8rem; - opacity: 0.7; -} \ No newline at end of file diff --git a/dashboard/static/images/southbronx.png b/dashboard/static/images/southbronx.png deleted file mode 100644 index 45064b0..0000000 Binary files a/dashboard/static/images/southbronx.png and /dev/null differ diff --git a/dashboard/static/js/main.js b/dashboard/static/js/main.js deleted file mode 100644 index 7688fe6..0000000 --- a/dashboard/static/js/main.js +++ /dev/null @@ -1,138 +0,0 @@ -document.addEventListener('DOMContentLoaded', function() { - // Update uptime counter - const initialServerTime = Math.floor(Date.now() / 1000); - let uptimeOffset = 0; - - function updateUptime() { - const uptimeElement = document.getElementById('uptime'); - if (!uptimeElement) return; - - const baseUptime = parseInt(uptimeElement.dataset.uptime); - // Add the time elapsed since page load - const currentUptime = baseUptime + uptimeOffset; - - const days = Math.floor(currentUptime / 86400); - const hours = Math.floor((currentUptime % 86400) / 3600); - const minutes = Math.floor((currentUptime % 3600) / 60); - const seconds = currentUptime % 60; - - uptimeElement.textContent = `${days}d ${hours}h ${minutes}m ${seconds}s`; - uptimeOffset++; - } - - // Link hover effect - document.querySelectorAll('.nav-link').forEach(link => { - link.addEventListener('mouseover', () => { - link.style.transform = 'translateY(-2px)'; - }); - - link.addEventListener('mouseout', () => { - link.style.transform = 'translateY(0)'; - }); - }); - - // Mobile navigation toggle - const navToggle = document.getElementById('nav-toggle'); - const navLinks = document.querySelector('.nav-links'); - - if (navToggle) { - navToggle.addEventListener('click', () => { - navLinks.classList.toggle('active'); - navToggle.classList.toggle('active'); - }); - - // Close navbar when clicking outside - document.addEventListener('click', (e) => { - if (!navLinks.contains(e.target) && - !navToggle.contains(e.target) && - navLinks.classList.contains('active')) { - navLinks.classList.remove('active'); - navToggle.classList.remove('active'); - } - }); - } - - // Collapsible sidebar toggle - const sidebarToggle = document.getElementById('sidebar-toggle'); - const sidebar = document.querySelector('.sidebar.collapsible'); - - if (sidebarToggle && sidebar) { - sidebarToggle.addEventListener('click', () => { - sidebar.classList.toggle('active'); - }); - - // Close sidebar when clicking outside - document.addEventListener('click', (e) => { - if (!sidebar.contains(e.target) && - !sidebarToggle.contains(e.target) && - sidebar.classList.contains('active')) { - sidebar.classList.remove('active'); - } - }); - } - - // Initialize uptime - setInterval(updateUptime, 1000); - updateUptime(); - - // Prefix Management - const prefixInput = document.getElementById('newPrefix'); - const prefixContainer = document.getElementById('prefixContainer'); - const prefixesHiddenInput = document.getElementById('prefixesInput'); - - if (prefixInput && prefixContainer) { - // Add new prefix - prefixInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const prefix = prefixInput.value.trim(); - if (prefix && !getPrefixes().includes(prefix)) { - addPrefix(prefix); - prefixInput.value = ''; - updatePrefixCounter(); - } - } - }); - - // Remove prefix - prefixContainer.addEventListener('click', (e) => { - if (e.target.classList.contains('prefix-tag') || e.target.classList.contains('remove-prefix')) { - const button = e.target.closest('.prefix-tag'); - if (button && getPrefixes().length > 1) { // Prevent removing last prefix - button.style.animation = 'slideIn 0.3s ease reverse'; - setTimeout(() => { - button.remove(); - updatePrefixCounter(); - updateHiddenInput(); - }, 300); - } - } - }); - - function addPrefix(prefix) { - const button = document.createElement('button'); - button.type = 'button'; - button.className = 'prefix-tag'; - button.dataset.prefix = prefix; - button.innerHTML = `${prefix}×`; - prefixContainer.appendChild(button); - updateHiddenInput(); - } - - function getPrefixes() { - return Array.from(prefixContainer.children).map(btn => btn.dataset.prefix); - } - - function updateHiddenInput() { - prefixesHiddenInput.value = getPrefixes().join(','); - } - - function updatePrefixCounter() { - const counter = document.querySelector('.prefix-counter'); - if (counter) { - const count = getPrefixes().length; - counter.textContent = `${count} total ${count === 1 ? 'prefix' : 'prefixes'}`; - } - } - } -}); \ No newline at end of file diff --git a/dashboard/static/js/prefix-handler.js b/dashboard/static/js/prefix-handler.js deleted file mode 100644 index b854fb3..0000000 --- a/dashboard/static/js/prefix-handler.js +++ /dev/null @@ -1,59 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const prefixInput = document.getElementById('newPrefix'); - const prefixContainer = document.getElementById('prefixContainer'); - const prefixesInput = document.getElementById('prefixesInput'); - - // Add new prefix - prefixInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - const prefix = prefixInput.value.trim(); - if (prefix && prefix.length <= 5) { - addPrefix(prefix); - prefixInput.value = ''; - updateHiddenInput(); - } - } - }); - - // Remove prefix - prefixContainer.addEventListener('click', (e) => { - if (e.target.classList.contains('remove-prefix')) { - const tag = e.target.parentElement; - // Don't remove if it's the last prefix - const remainingPrefixes = prefixContainer.querySelectorAll('.prefix-tag').length; - if (remainingPrefixes > 1) { - tag.remove(); - updateHiddenInput(); - } - } - }); - - // Add prefix helper - function addPrefix(prefix) { - const existingPrefixes = Array.from(prefixContainer.querySelectorAll('.prefix-tag')) - .map(tag => tag.getAttribute('data-prefix')); - - if (!existingPrefixes.includes(prefix)) { - const tag = document.createElement('button'); - tag.type = 'button'; - tag.className = 'prefix-tag'; - tag.setAttribute('data-prefix', prefix); - tag.innerHTML = `${prefix}×`; - prefixContainer.appendChild(tag); - } - } - - // Update hidden input - function updateHiddenInput() { - const prefixes = Array.from(prefixContainer.querySelectorAll('.prefix-tag')) - .map(tag => tag.getAttribute('data-prefix')); - prefixesInput.value = prefixes.join(','); - - // Update counter - const counter = document.querySelector('.prefix-counter'); - if (counter) { - counter.textContent = `${prefixes.length} total prefixes`; - } - } -}); diff --git a/dashboard/templates/DEVindex.html b/dashboard/templates/DEVindex.html deleted file mode 100644 index bd15df1..0000000 --- a/dashboard/templates/DEVindex.html +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - BronxBot Dashboard - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-

Welcome back, {{ username }}! 👋

-

Here's your bot's current status and statistics

-
- -
-
-

Servers

-

{{ stats.server_count }}

-
-
-

Users

-

{{ stats.user_count }}

-
-
-

Uptime

-

Calculating...

-
-
-

Latency

-

{{ stats.latency }}ms

-
-
- - -
-

🔋 Shard Status

-
-
-

Total Servers

-

{{ stats.server_count|default(0)|thousands }}

-
-
-

Total Users

-

{{ stats.user_count|default(0)|thousands }}

-
-
-

Latency

-

{{ "%.1f"|format(stats.latency|default(0)) }}ms

-
-
-

Shards

-

{{ stats.shards|default(2) }}

-
-
- - {% if stats.shard_stats %} -
- {% for shard_id, shard in stats.shard_stats.items() %} -
-

- {% if shard.status == 'online' %}🟢{% else %}🔴{% endif %} - Shard {{ shard_id }} -

-

Servers: {{ shard.guild_count }}

-

Users: {{ shard.user_count }}

-

Latency: {{ "%.1f"|format(shard.latency) }}ms

-

- Online for: {{ shard.uptime }} -

-
- {% endfor %} -
- {% endif %} -
-
-
- - - - - \ No newline at end of file diff --git a/dashboard/templates/error.html b/dashboard/templates/error.html deleted file mode 100644 index 3b22794..0000000 --- a/dashboard/templates/error.html +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Error - BronxBot - - - - - - - - - - - - - - - - - - - - - - - - - - -
-

Oops! Something went wrong

- {% if error %} -

{{ error }}

- {% else %} -

An unexpected error occurred. Please try again later.

- {% endif %} - Return Home -
- - diff --git a/dashboard/templates/home.html b/dashboard/templates/home.html deleted file mode 100644 index ba7b4ce..0000000 --- a/dashboard/templates/home.html +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - BronxBot - Home - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-

Welcome to BronxBot

-

The ultimate Discord bot for your community

-
- -
-
-
-

Easy to Use

-

-
-
-

Customizable

-

-
-
-

24/7 Online

-

-
-
-
- -
-
- - - - diff --git a/dashboard/templates/index.html b/dashboard/templates/index.html deleted file mode 100644 index 931e125..0000000 --- a/dashboard/templates/index.html +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - BronxBot Dashboard - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-

Welcome back, {{ username }}! 👋

-

Are you ready to take control of your Discord server?

-
- -
-
-

Servers

-

{{ stats.server_count }}

-
-
-

Users

-

{{ stats.user_count }}

-
-
-

Uptime

-

Calculating...

-
-
-

Latency

-

{{ stats.latency }}ms

-
-
- - -
-

🔋 Shard Status

-
-
-

Total Servers

-

{{ stats.server_count|default(0)|thousands }}

-
-
-

Total Users

-

{{ stats.user_count|default(0)|thousands }}

-
-
-

Latency

-

{{ "%.1f"|format(stats.latency|default(0)) }}ms

-
-
-

Shards

-

{{ stats.shard_count }}

-
-
- - {% if stats.shard_stats %} -
- {% for shard_id, shard in stats.shard_stats.items() %} -
-

- {% if shard.status == 'online' %}🟢{% else %}🔴{% endif %} - Shard {{ shard_id }} -

-

Servers: {{ shard.guild_count }}

-

Users: {{ shard.user_count }}

-

Latency: {{ "%.1f"|format(shard.latency) }}ms

-

- Online for: {{ shard.uptime }} -

-
- {% endfor %} -
- {% endif %} -
-
-
- - - - diff --git a/dashboard/templates/intro.html b/dashboard/templates/intro.html deleted file mode 100644 index b2a43ec..0000000 --- a/dashboard/templates/intro.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - - - BronxBot - Your Discord Server's Best Friend - - - - - - - - - - - - - - - - - - - - - - - - - - -
- -

BronxBot

-

Level up your Discord with games, economy, and smart moderation - where fun meets function!

- -
-
-
76
-
Servers
-
-
-
33823
-
Users
-
-
-
43ms
-
ms Latency
-
-
- -
-
-

🛡️ Moderation

-

Advanced moderation tools to keep your server safe and organized

-
-
-

👋 Welcome System

-

Customize welcome messages and roles for new members

-
-
-

⚙️ Easy Configuration

-

Simple web dashboard for all your bot settings

-
-
- - - Add to Discord - - - - - -
- - diff --git a/dashboard/templates/servers.html b/dashboard/templates/servers.html deleted file mode 100644 index 466d207..0000000 --- a/dashboard/templates/servers.html +++ /dev/null @@ -1,108 +0,0 @@ - - - - - - Servers - BronxBot - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

Your Servers

-

Manage bot settings for your Discord servers

- -
- {% for guild in guilds %} - {% if guild.bot_present %} - -
- {% if guild.icon_url %} - {{ guild.name }} - {% else %} - - {% endif %} -
-
-

{{ guild.name }}

-
- - Bot Active - - - Configure Settings - -
-
-
- {% else %} -
-
- {% if guild.icon_url %} - {{ guild.name }} - {% else %} - - {% endif %} -
-
-

{{ guild.name }}

-
- - Bot Not Added - - - Add Bot - -
-
-
- {% endif %} - {% endfor %} -
-
-
-
- - - - diff --git a/dashboard/templates/settings.html b/dashboard/templates/settings.html deleted file mode 100644 index 881a04d..0000000 --- a/dashboard/templates/settings.html +++ /dev/null @@ -1,221 +0,0 @@ - - - - - - Server Settings - BronxBot - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- {% if not guild %} -
-
-

Select a Server

-

Choose a server to configure BronxBot settings

- -
- {% for guild in guilds %} - {% if guild.bot_present %} - -
- {% if guild.icon_url %} - {{ guild.name }} - {% else %} - - {% endif %} -
-
-

{{ guild.name }}

-
- - Bot Active - - - Configure Settings - -
-
-
- {% else %} -
-
- {% if guild.icon_url %} - {{ guild.name }} - {% else %} - - {% endif %} -
-
-

{{ guild.name }}

-
- - Bot Not Added - - - Add Bot - -
-
-
- {% endif %} - {% endfor %} -
-
-
- {% else %} - - -
-
-
-

General Settings

-
- -
- -
- {% for prefix in settings.get('prefixes', ['.']) %} - - {% endfor %} -
- - {{ settings.get('prefixes', ['.'])|length }} total prefixes -
-
-
- -
-

Welcome Settings

-
- -
-
- - -
-
- - -
-
- -
-

Moderation Settings

-
- - -
-
- - -
-
- - -
-
- - -
-
- {% endif %} -
- - - - - diff --git a/dashboard/templates/settings_select.html b/dashboard/templates/settings_select.html deleted file mode 100644 index 23451bf..0000000 --- a/dashboard/templates/settings_select.html +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - Select Server - BronxBot - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

Select a Server

-

Choose a server to configure BronxBot settings

- - -
-
-
- - - - diff --git a/dashboard/utils/betting.py b/dashboard/utils/betting.py deleted file mode 100644 index 9f63012..0000000 --- a/dashboard/utils/betting.py +++ /dev/null @@ -1,55 +0,0 @@ -from typing import Union, Tuple - -def parse_bet(amount_str: str, balance: int) -> Tuple[Union[int, None], str]: - """ - Parse bet amount from various formats - Returns (amount, error_message) - Supports: - - Regular numbers: 100, 400, 1000 - - Percentages: 50%, 100%, 5.5% - - K/M notation: 1k, 1.5k, 100k, 1m, 2.5m - - Scientific: 1e3, 1.5e3, 1e6 - """ - try: - # Clean the input - amount_str = amount_str.lower().strip() - - # Handle 'all' or 'max' - if amount_str in ['all', 'max']: - return balance, None - - # Handle percentage - if amount_str.endswith('%'): - try: - percentage = float(amount_str[:-1]) - if not 0 < percentage <= 100: - return None, "Percentage must be between 0 and 100!" - return round((percentage / 100) * balance), None - except ValueError: - return None, "Invalid percentage format!" - - # Handle k/m notation - multiplier = 1 - if amount_str.endswith('k'): - multiplier = 1000 - amount_str = amount_str[:-1] - elif amount_str.endswith('m'): - multiplier = 1000000 - amount_str = amount_str[:-1] - - # Convert scientific notation and decimals - if 'e' in amount_str: - amount = float(amount_str) - else: - amount = float(amount_str) - - # Apply multiplier and round - final_amount = round(amount * multiplier) - - if final_amount <= 0: - return None, "Bet must be positive!" - - return final_amount, None - - except ValueError: - return None, "Invalid bet amount!" diff --git a/dashboard/utils/db.py b/dashboard/utils/db.py deleted file mode 100644 index c1b9902..0000000 --- a/dashboard/utils/db.py +++ /dev/null @@ -1,433 +0,0 @@ -import motor.motor_asyncio -import pymongo -import json -import datetime -import os -import asyncio -import logging -from typing import Dict, Any, Optional -import threading - -# Initialize default config -config = { - "MONGO_URI": os.getenv("MONGO_URI"), - "TOKEN": os.getenv("DISCORD_TOKEN"), - "CLIENT_ID": os.getenv("DISCORD_CLIENT_ID"), - "CLIENT_SECRET": os.getenv("DISCORD_CLIENT_SECRET"), - "OWNER_ID": os.getenv("DISCORD_BOT_OWNER_ID") -} - -# Try to load from config file if environment variables are not set -if not all([config["MONGO_URI"], config["TOKEN"], config["CLIENT_ID"]]): - try: - with open('data/config.json') as f: - file_config = json.load(f) - # Update config with file values only if env vars are not set - for key in config: - if not config[key] and key in file_config: - config[key] = file_config[key] - except (FileNotFoundError, json.JSONDecodeError) as e: - logging.warning(f"Could not load config.json: {e}. Using environment variables only.") - -class AsyncDatabase: - """Async database class for use with Discord bot""" - _instance = None - _client = None - _db = None - - @classmethod - def get_instance(cls): - if cls._instance is None: - cls._instance = cls() - return cls._instance - - def __init__(self): - self.logger = logging.getLogger('AsyncDatabase') - self._connected = False - - @property - def client(self): - if self._client is None: - MONGO_URI = os.getenv('MONGO_URI', config['MONGO_URI']) - self._client = motor.motor_asyncio.AsyncIOMotorClient(MONGO_URI) - return self._client - - @property - def db(self): - if self._db is None: - self._db = self.client.bronxbot - return self._db - - async def ensure_connected(self) -> bool: - """Ensure database connection is active""" - if not self._connected: - try: - await self.client.admin.command('ping') - self._connected = True - self.logger.info("Async database connection established") - except Exception as e: - self.logger.error(f"Async database connection failed: {e}") - return False - return True - - async def get_wallet_balance(self, user_id: int, guild_id: int = None) -> int: - """Get user's wallet balance""" - if not await self.ensure_connected(): - return 0 - user = await self.db.users.find_one({"_id": str(user_id)}) - return user.get("wallet", 0) if user else 0 - - async def get_bank_balance(self, user_id: int, guild_id: int = None) -> int: - """Get user's bank balance""" - if not await self.ensure_connected(): - return 0 - user = await self.db.users.find_one({"_id": str(user_id)}) - return user.get("bank", 0) if user else 0 - - async def get_bank_limit(self, user_id: int, guild_id: int = None) -> int: - """Get user's bank limit""" - if not await self.ensure_connected(): - return 10000 - user = await self.db.users.find_one({"_id": str(user_id)}) - return user.get("bank_limit", 10000) if user else 10000 - - async def update_wallet(self, user_id: int, amount: int, guild_id: int = None) -> bool: - """Update user's wallet balance""" - if not await self.ensure_connected(): - return False - result = await self.db.users.update_one( - {"_id": str(user_id)}, - {"$inc": {"wallet": amount}}, - upsert=True - ) - return result.modified_count > 0 or result.upserted_id is not None - - async def update_bank(self, user_id: int, amount: int, guild_id: int = None) -> bool: - """Update user's bank balance""" - if not await self.ensure_connected(): - return False - result = await self.db.users.update_one( - {"_id": str(user_id)}, - {"$inc": {"bank": amount}}, - upsert=True - ) - return result.modified_count > 0 or result.upserted_id is not None - - async def get_guild_settings(self, guild_id: int) -> Dict[str, Any]: - """Get guild settings""" - if not await self.ensure_connected(): - return {} - settings = await self.db.guild_settings.find_one({"_id": str(guild_id)}) - return settings if settings else {} - - async def update_guild_settings(self, guild_id: int, settings: Dict[str, Any]) -> bool: - """Update guild settings""" - if not await self.ensure_connected(): - return False - result = await self.db.guild_settings.update_one( - {"_id": str(guild_id)}, - {"$set": settings}, - upsert=True - ) - return result.modified_count > 0 or result.upserted_id is not None - - async def store_stats(self, guild_id: int, stat_type: str) -> None: - """Store guild stats""" - if not await self.ensure_connected(): - return - await self.db.stats.update_one( - {"_id": str(guild_id)}, - {"$inc": {stat_type: 1}}, - upsert=True - ) - - async def get_stats(self, guild_id: int) -> Dict[str, int]: - """Get guild stats""" - if not await self.ensure_connected(): - return {} - stats = await self.db.stats.find_one({"_id": str(guild_id)}) - return stats if stats else {} - - async def reset_stats(self, guild_id: int) -> bool: - """Reset guild stats""" - if not await self.ensure_connected(): - return False - result = await self.db.stats.delete_one({"_id": str(guild_id)}) - return result.deleted_count > 0 - - async def add_global_buff(self, buff_data: Dict[str, Any]) -> bool: - """Add global buff""" - if not await self.ensure_connected(): - return False - result = await self.db.global_buffs.insert_one(buff_data) - return result.inserted_id is not None - - async def get_user_balance(self, user_id: int, guild_id: int = None) -> int: - """Get user's total balance""" - wallet = await self.get_wallet_balance(user_id, guild_id) - bank = await self.get_bank_balance(user_id, guild_id) - return wallet + bank - - async def transfer_money(self, from_id: int, to_id: int, amount: int, guild_id: int = None) -> bool: - """Transfer money between users""" - if not await self.ensure_connected(): - return False - - from_balance = await self.get_wallet_balance(from_id, guild_id) - if from_balance < amount: - return False - - async with await self.client.start_session() as session: - async with session.start_transaction(): - if not await self.update_wallet(from_id, -amount, guild_id): - return False - if not await self.update_wallet(to_id, amount, guild_id): - await self.update_wallet(from_id, amount, guild_id) # Rollback - return False - return True - - async def update_balance(self, user_id: int, amount: int, guild_id: int = None) -> bool: - """Update user's wallet balance, handling both positive and negative amounts""" - current = await self.get_wallet_balance(user_id, guild_id) - if amount < 0 and abs(amount) > current: # Check if user has enough for deduction - return False - return await self.update_wallet(user_id, amount, guild_id) - - async def increase_bank_limit(self, user_id: int, amount: int, guild_id: int = None) -> bool: - """Increase user's bank storage limit""" - if not await self.ensure_connected(): - return False - result = await self.db.users.update_one( - {"_id": str(user_id)}, - {"$inc": {"bank_limit": amount}}, - upsert=True - ) - return result.modified_count > 0 or result.upserted_id is not None - - async def get_global_net_worth(self, user_id: int, excluded_guilds: list = None) -> int: - """Get user's total net worth across all guilds""" - if not await self.ensure_connected(): - return 0 - excluded_guilds = excluded_guilds or [] - pipeline = [ - {"$match": {"_id": str(user_id)}}, - {"$project": { - "total": {"$add": ["$wallet", "$bank"]} - }} - ] - result = await self.db.users.aggregate(pipeline).to_list(1) - return result[0]["total"] if result else 0 - - async def get_inventory(self, user_id: int, guild_id: int = None) -> list: - """Get user's inventory""" - if not await self.ensure_connected(): - return [] - user = await self.db.users.find_one({"_id": str(user_id)}) - return user.get("inventory", []) if user else [] - - async def add_potion(self, user_id: int, potion: dict) -> bool: - """Add active potion effect to user""" - if not await self.ensure_connected(): - return False - expiry = datetime.datetime.utcnow() + datetime.timedelta(minutes=potion['duration']) - result = await self.db.active_potions.insert_one({ - "user_id": str(user_id), - "type": potion['buff_type'], - "multiplier": potion['multiplier'], - "expires_at": expiry - }) - return result.inserted_id is not None - - async def remove_from_inventory(self, user_id: int, guild_id: int, item_id: str) -> bool: - """Remove item from user's inventory""" - if not await self.ensure_connected(): - return False - result = await self.db.users.update_one( - {"_id": str(user_id)}, - {"$pull": {"inventory": {"id": item_id}}} - ) - return result.modified_count > 0 - - -class SyncDatabase: - """Synchronous database class for use with Flask web interface""" - _instance = None - _client = None - _db = None - _lock = threading.Lock() - - def __new__(cls): - if cls._instance is None: - with cls._lock: - if cls._instance is None: - cls._instance = super(SyncDatabase, cls).__new__(cls) - cls._instance._connected = False - cls._instance.logger = logging.getLogger('SyncDatabase') - return cls._instance - - def __init__(self): - if hasattr(self, '_initialized'): - return - - self._initialized = True - self._connected = False - self.logger = logging.getLogger('SyncDatabase') - - # Ensure data directory exists - data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data') - os.makedirs(data_dir, exist_ok=True) - - # Get database path from environment or use default - db_path = os.getenv('SQLITE_DATABASE_PATH', os.path.join(data_dir, 'database.sqlite')) - self.logger.info(f"Using SQLite database at {db_path}") - - try: - # Initialize SQLite connection - import sqlite3 - self.conn = sqlite3.connect(db_path, check_same_thread=False) - self.cursor = self.conn.cursor() - - # Create tables if they don't exist - self._create_tables() - self.conn.commit() - - self.logger.info("SQLite database initialized successfully") - except Exception as e: - self.logger.error(f"Failed to initialize SQLite database: {e}") - raise - - def _create_tables(self): - """Create database tables if they don't exist""" - try: - # Economy table - self.cursor.execute(""" - CREATE TABLE IF NOT EXISTS economy ( - user_id INTEGER, - guild_id INTEGER DEFAULT 0, - wallet INTEGER DEFAULT 0, - bank INTEGER DEFAULT 0, - PRIMARY KEY (user_id, guild_id) - ) - """) - - # Guild stats table - self.cursor.execute(""" - CREATE TABLE IF NOT EXISTS guild_stats ( - guild_id INTEGER, - stat_type TEXT, - count INTEGER DEFAULT 0, - PRIMARY KEY (guild_id, stat_type) - ) - """) - except Exception as e: - self.logger.error(f"Error creating tables: {e}") - raise - - @property - def client(self): - if self._client is None: - MONGO_URI = os.getenv('MONGO_URI', config['MONGO_URI']) - # Use pymongo for synchronous operations - self._client = pymongo.MongoClient(MONGO_URI) - return self._client - - @property - def db(self): - if self._db is None: - self._db = self.client.bronxbot - return self._db - - def ensure_connected(self) -> bool: - """Ensure database connection is active""" - if not self._connected: - try: - self.client.admin.command('ping') - self._connected = True - self.logger.info("Sync database connection established") - except Exception as e: - self.logger.error(f"Sync database connection failed: {e}") - return False - return True - - def get_guild_settings(self, guild_id: str) -> Dict[str, Any]: - """Get guild settings synchronously""" - if not self.ensure_connected(): - return {} - try: - settings = self.db.guild_settings.find_one({"_id": str(guild_id)}) - return settings if settings else {} - except Exception as e: - self.logger.error(f"Error getting guild settings: {e}") - return {} - - def update_guild_settings(self, guild_id: str, settings: Dict[str, Any]) -> bool: - """Update guild settings synchronously""" - if not self.ensure_connected(): - return False - try: - result = self.db.guild_settings.update_one( - {"_id": str(guild_id)}, - {"$set": settings}, - upsert=True - ) - return result.modified_count > 0 or result.upserted_id is not None - except Exception as e: - self.logger.error(f"Error updating guild settings: {e}") - return False - - def get_user_balance(self, user_id: int, guild_id: int = None): - """Get user's wallet and bank balance""" - try: - self.cursor.execute(""" - SELECT wallet, bank FROM economy - WHERE user_id = ? AND guild_id = ? - """, (user_id, guild_id or 0)) - result = self.cursor.fetchone() - if result: - return {"wallet": result[0], "bank": result[1]} - return {"wallet": 0, "bank": 0} - except Exception as e: - self.logger.error(f"Error getting balance: {e}") - return {"wallet": 0, "bank": 0} - - # Change store_stats to be async-compatible - async def store_stats(self, guild_id: int, stat_type: str): - """Store guild statistics asynchronously""" - return self.store_stats_sync(guild_id, stat_type) - - def store_stats_sync(self, guild_id: int, stat_type: str): - """Store guild statistics synchronously""" - try: - valid_types = ["messages", "gained", "lost"] - if stat_type not in valid_types: - return False - - self.cursor.execute(""" - INSERT INTO guild_stats (guild_id, stat_type, count) - VALUES (?, ?, 1) - ON CONFLICT(guild_id, stat_type) DO UPDATE - SET count = count + 1 - """, (guild_id, stat_type)) - self.conn.commit() - return True - except Exception as e: - self.logger.error(f"Error storing stats: {e}") - return False - - def get_stats(self, guild_id: int): - """Get guild statistics""" - try: - self.cursor.execute(""" - SELECT stat_type, count FROM guild_stats - WHERE guild_id = ? - """, (guild_id,)) - results = self.cursor.fetchall() - return {stat[0]: stat[1] for stat in results} - except Exception as e: - self.logger.error(f"Error getting stats: {e}") - return {} - - -# Create global database instances -async_db = AsyncDatabase.get_instance() # For Discord bot -db = SyncDatabase() # For Flask web interface \ No newline at end of file diff --git a/dashboard/utils/error_handler.py b/dashboard/utils/error_handler.py deleted file mode 100644 index d843dc1..0000000 --- a/dashboard/utils/error_handler.py +++ /dev/null @@ -1,64 +0,0 @@ -import discord -from discord.ext import commands -import traceback -from cogs.logging.logger import CogLogger - -class ErrorHandler: - """Base class for error handling in cogs""" - - def __init__(self): - self.logger = CogLogger(self.__class__.__name__) - - async def handle_error(self, ctx, error, command_name=None): - """Common error handling logic for all cogs""" - # Get error context - command = command_name or ctx.command.qualified_name if ctx.command else "Unknown" - author = f"{ctx.author} ({ctx.author.id})" - guild = f"{ctx.guild} ({ctx.guild.id})" if ctx.guild else "DM" - - # Generate error trace - error_trace = ''.join(traceback.format_exception(type(error), error, error.__traceback__)) - error_id = hex(abs(hash(str(error))))[-6:] - - # Log the error - self.logger.error( - f"Command error in {command} (ID: {error_id}):\n" - f"User: {author}\n" - f"Guild: {guild}\n" - f"Error: {str(error)}\n" - f"Traceback:\n{error_trace}" - ) - - # Handle common error types - try: - if isinstance(error, commands.MissingPermissions): - await ctx.reply(f"❌ You need {', '.join(error.missing_permissions)} permission(s) to use this command!") - - elif isinstance(error, commands.BotMissingPermissions): - await ctx.reply("❌ I don't have the required permissions for this command!") - - elif isinstance(error, commands.MissingRequiredArgument): - await ctx.reply(f"❌ Missing required argument: `{error.param.name}`") - - elif isinstance(error, commands.BadArgument): - await ctx.reply(f"❌ Invalid argument provided: {str(error)}") - - elif isinstance(error, commands.DisabledCommand): - await ctx.reply("❌ This command is currently disabled") - - elif isinstance(error, commands.NoPrivateMessage): - await ctx.reply("❌ This command cannot be used in DMs") - - elif isinstance(error, commands.CheckFailure): - await ctx.reply("❌ You don't meet the requirements to use this command") - - else: - await ctx.reply( - f"❌ An unexpected error occurred! Error ID: `{error_id}`\n" - "This has been logged and will be investigated." - ) - - except Exception as e: - self.logger.error(f"Error handling error: {e}") - - return error_id diff --git a/dashboard/wsgi.py b/dashboard/wsgi.py deleted file mode 100644 index 48d3672..0000000 --- a/dashboard/wsgi.py +++ /dev/null @@ -1,14 +0,0 @@ -import os -import sys - -# Add the current directory to Python path -current_dir = os.path.dirname(os.path.abspath(__file__)) -if current_dir not in sys.path: - sys.path.insert(0, current_dir) - -from dashboard import app - -# For local development -if __name__ == "__main__": - port = int(os.environ.get('PORT', 5000)) - app.run(host='0.0.0.0', port=port)