Compare commits

..

4 Commits

Author SHA1 Message Date
Nobody2503 ccc944ef84 revert iteratePrefix 2026-05-31 12:35:26 +00:00
Nobody2503 55b16529b5 Refactor bot setup and improve error handling; update command prefix handling and enhance session security in Flask app. Add profile and balance card generation features with image processing capabilities. 2026-05-31 12:07:12 +00:00
Nobody2503 1b91cbcb2f Refactor bot server setup to use Waitress for production; fallback to Flask dev server for local development. Added timeout to HTTP requests in Fun and Test cogs. Improved error handling for missing environment variables. Enhanced secret key management in Flask app. Added request timeout configuration. Introduced new experimental features including user profile and balance cards, and a Tic-Tac-Toe game with Minimax AI. Addressed various database and security issues, and improved code quality across multiple files. 2026-05-31 12:01:12 +00:00
Nobody2503 be89cc3acd Refactor database management and schema initialization
- Removed the old npc_memory.db file.
- Updated time.txt with a new timestamp.
- Refactored transaction recording in bank_functions.py to use parameterized queries.
- Enhanced DatabaseManager in sql_commands.py to support singleton pattern and improved table creation logic.
- Added methods for sanitizing SQL identifiers and parsing insert columns for upsert operations.
- Improved error handling and connection management in execute_query, fetch_one, fetch_all, and fetch_as_dataframe methods.
- Introduced a new bootstrap_database.py script for initializing the database schema.
- Updated app.py to use the new initialize_database function for database management.
2026-05-31 11:16:44 +00:00
28 changed files with 2781 additions and 138 deletions
Executable → Regular
+6 -3
View File
@@ -116,9 +116,12 @@ async def create_balance_card(user, credits, tokens, bucks):
draw.line([(0, y), (width, y)], fill=(r, g, b))
# Avatar with circular border
avatar_asset = user.avatar
avatar_response = requests.get(avatar_asset)
avatar = Image.open(io.BytesIO(avatar_response.content)).resize((60, 60))
try:
avatar_bytes = await user.avatar.read()
avatar = Image.open(io.BytesIO(avatar_bytes)).resize((60, 60))
except Exception:
# Fallback: blank avatar
avatar = Image.new("RGBA", (60, 60), (128, 128, 128, 255))
mask = Image.new("L", avatar.size, 0)
ImageDraw.Draw(mask).ellipse((0, 0, 60, 60), fill=255)
avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5))
Executable → Regular
+9 -3
View File
@@ -6,8 +6,13 @@ from random import shuffle
# Step 1: Fetch JSON data from the URL
url = "https://picsum.photos/v2/list?limit=1000"
response = requests.get(url)
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
print(f"Failed to fetch image list: {e}")
data = []
# Step 2: Extract image download links
image_urls = [item["download_url"] for item in data]
@@ -21,8 +26,9 @@ desired_size = (450, 250)
# Step 4: Download and resize each image
for i, image_url in enumerate(image_urls):
img_response = requests.get(image_url)
if img_response.status_code == 200:
try:
img_response = requests.get(image_url, timeout=10)
img_response.raise_for_status()
# Open image from response content
img = Image.open(BytesIO(img_response.content))
# Resize the image
+254
View File
@@ -0,0 +1,254 @@
import discord
from discord.ext import commands
from PIL import Image, ImageDraw, ImageFont, ImageOps
import io
import os
from dotenv import load_dotenv
import requests
from random import randint
# Bot setup
load_dotenv()
TOKEN = os.getenv("TOKEN")
intents = discord.Intents.all()
bot = commands.Bot(command_prefix="!?", intents=intents)
# Simple user data structure
user_data = {}
def get_user_data(user_id):
"""Get or initialize user data."""
if user_id not in user_data:
user_data[user_id] = {"xp": 0, "level": 53}
return user_data[user_id]
import io
from PIL import Image, ImageDraw, ImageFont
async def create_profile_card(user, background_image_path):
"""
Create a profile card image for a user with a custom background and text overlay.
Args:
user (discord.Member): The user whose profile card is being generated.
background_image_path (str): Path to the background image.
Returns:
io.BytesIO: A BytesIO object containing the profile card image.
"""
# Profile card dimensions
card_width, card_height = 400, 200
text_color = (255, 255, 255)
progress_bar_color = (0, 255, 0)
outline_color = (255, 255, 255)
tint_color = (50, 50, 50)
transparency = 25
opacity = int(255 * transparency / 100)
# Load and resize the background image
background_image = Image.open(background_image_path).resize((card_width, card_height))
# Create a new image with the background
card_image = Image.new("RGBA", (card_width, card_height))
card_image.paste(background_image, (0, 0))
# Create a semi-transparent overlay
overlay = Image.new("RGBA", card_image.size, tint_color + (opacity,))
card_image = Image.alpha_composite(card_image, overlay)
draw = ImageDraw.Draw(card_image)
# Load fonts
font_path = "arial.ttf"
font = ImageFont.truetype(font_path, 20)
# Draw user info
draw.text((10, 10), f"User: {user.display_name}", fill=text_color, font=font)
user_info = get_user_data(user.id)
draw.text((10, 40), f"Level: {user_info['level']}", fill=text_color, font=font)
draw.text((10, 70), f"XP: {user_info['xp']}/{user_info['level'] * 100}", fill=text_color, font=font)
# Draw the progress bar
max_xp = user_info['level'] * 100
progress_ratio = user_info['xp'] / max_xp if max_xp > 0 else 0
progress_width = int(progress_ratio * 200)
draw.rectangle([(10, 100), (210, 120)], outline=outline_color, width=2)
draw.rectangle([(10, 100), (10 + progress_width, 120)], fill=progress_bar_color)
# Draw the user's profile picture
try:
profile_size = 96
margin = 30
profile_picture_data = await user.avatar.read()
profile_image = Image.open(io.BytesIO(profile_picture_data)).resize((profile_size, profile_size))
card_image.paste(profile_image, (card_width - profile_size - margin, margin), profile_image.convert("RGBA"))
except Exception as error:
print(f"Error fetching profile picture: {error}")
# Save image to a BytesIO object
image_bytes = io.BytesIO()
card_image.save(image_bytes, format="PNG")
image_bytes.seek(0)
return image_bytes
async def create_balance_card(user, credits, tokens, bucks):
# Card dimensions
width, height = 450, 250
card = Image.new("RGB", (width, height), (255, 255, 255))
draw = ImageDraw.Draw(card)
# Gradient Background
gradient_color_1 = (38, 0, 77) # Dark purple
gradient_color_2 = (128, 0, 128) # Purple
for y in range(height):
blend = y / height
r = int(gradient_color_1[0] * (1 - blend) + gradient_color_2[0] * blend)
g = int(gradient_color_1[1] * (1 - blend) + gradient_color_2[1] * blend)
b = int(gradient_color_1[2] * (1 - blend) + gradient_color_2[2] * blend)
draw.line([(0, y), (width, y)], fill=(r, g, b))
# Avatar with circular border
try:
avatar_bytes = await user.avatar.read()
avatar = Image.open(io.BytesIO(avatar_bytes)).resize((60, 60))
except Exception:
# Fallback: blank avatar
avatar = Image.new("RGBA", (60, 60), (128, 128, 128, 255))
mask = Image.new("L", avatar.size, 0)
ImageDraw.Draw(mask).ellipse((0, 0, 60, 60), fill=255)
avatar = ImageOps.fit(avatar, mask.size, centering=(0.5, 0.5))
avatar.putalpha(mask)
# Draw avatar circle background
avatar_bg = Image.new("RGBA", (70, 70), (255, 255, 255, 0))
draw_bg = ImageDraw.Draw(avatar_bg)
draw_bg.ellipse((0, 0, 70, 70), fill=(255, 255, 255, 100)) # Border color
card.paste(avatar_bg, (20, height - 110), avatar_bg)
card.paste(avatar, (25, height - 105), avatar)
# Fonts
title_font = ImageFont.load_default()
balance_font = ImageFont.load_default()
# Header Text
draw.text((110, 20), "tatsu.", font=title_font, fill="white")
draw.text((width - 90, 25), "Member", font=title_font, fill="white")
# Balance Background Box
balance_box = (100, 70, width - 20, 200)
draw.rounded_rectangle(balance_box, radius=15, fill=(255, 255, 255, 50))
# Balance Items
icon_x = 120
text_x = icon_x + 35
# Credits
draw.ellipse((icon_x, 90, icon_x + 25, 115), fill=(255, 215, 0)) # Coin icon color
draw.text((text_x, 90), f"{credits:,} Credits", font=balance_font, fill="black")
# Tokens
draw.ellipse(
(icon_x, 125, icon_x + 25, 150), fill=(0, 255, 127)
) # Token icon color
draw.text((text_x, 125), f"{tokens:,} Tokens", font=balance_font, fill="black")
# Bucks
draw.ellipse(
(icon_x, 160, icon_x + 25, 185), fill=(50, 205, 50)
) # Bucks icon color
draw.text((text_x, 160), f"{bucks:,} Guild Bucks", font=balance_font, fill="black")
# User tag
user_tag = f"{user.name}#{user.discriminator}"
draw.text((110, height - 35), user_tag, font=title_font, fill="white")
# Add Chip (Credit Card Style)
chip_width, chip_height = 50, 35
chip_x = width - 80
chip_y = 100
draw.rectangle(
[chip_x, chip_y, chip_x + chip_width, chip_y + chip_height],
fill=(192, 192, 192), # Light gray color for the chip
outline="white",
width=2,
)
# Chip lines for texture
line_spacing = 7
for i in range(1, chip_height // line_spacing):
y = chip_y + i * line_spacing
draw.line(
[(chip_x + 5, y), (chip_x + chip_width - 5, y)], fill="white", width=1
)
# Save to a BytesIO object to send on Discord
with io.BytesIO() as image_binary:
card.save(image_binary, "PNG")
image_binary.seek(0)
return discord.File(fp=image_binary, filename="balance_card.png")
@bot.event
async def on_ready():
if bot.user is not None:
print(f"Logged in as {bot.user.name}")
else:
print(f"Logged in")
@bot.command(name="profile")
async def profile(ctx):
"""Command to display user profile."""
user = ctx.author
user_info = get_user_data(user.id)
# Create profile card image
profile_image = create_profile_card(user, f"images/image_{randint(1,100)}.jpg")
# Send the image in the channel
await ctx.send(file=discord.File(await profile_image, "profile.png"))
@bot.command()
async def balance(ctx):
user = ctx.author
# Example balance data; replace with actual data retrieval
credits = 9493006
tokens = 58
bucks = 234328
file = await create_balance_card(user, credits, tokens, bucks)
await ctx.send(file=file)
# Example command to add XP for testing
@bot.command(name="addxp")
async def add_xp(ctx, amount: int):
"""Command to add XP for testing."""
user_info = get_user_data(ctx.author.id)
user_info["xp"] += amount
# Level up logic (example: every 100 XP earns a level)
if user_info["xp"] >= user_info["level"] * 100:
user_info["level"] += 1
user_info["xp"] = 0 # Reset XP after leveling up
await ctx.send(
f"Added {amount} XP to {ctx.author.mention}. Total XP: {user_info['xp']} | Level: {user_info['level']}"
)
if TOKEN is not None:
bot.run(TOKEN)
else:
print(f"{TOKEN=}")
+42
View File
@@ -0,0 +1,42 @@
import requests
import os
from PIL import Image
from io import BytesIO
from random import shuffle
# Step 1: Fetch JSON data from the URL
url = "https://picsum.photos/v2/list?limit=1000"
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
print(f"Failed to fetch image list: {e}")
data = []
# Step 2: Extract image download links
image_urls = [item["download_url"] for item in data]
shuffle(image_urls)
# Step 3: Create a directory to save images
os.makedirs("images", exist_ok=True)
# Desired size for resizing
desired_size = (450, 250)
# Step 4: Download and resize each image
for i, image_url in enumerate(image_urls):
try:
img_response = requests.get(image_url, timeout=10)
img_response.raise_for_status()
# Open image from response content
img = Image.open(BytesIO(img_response.content))
# Resize the image
img = img.resize(desired_size, Image.Resampling.LANCZOS)
# Save the resized image
img.save(f"images/image_{17}.jpg")
print(f"Downloaded and resized: image_{17}.jpg")
else:
print(f"Failed to download image from {image_url}")
if input() !="":
break
+93
View File
@@ -0,0 +1,93 @@
## Major problems found
### 1. Database / SQL issues
- sql_commands.py
- `execute_query()` returns `copy(cursor)` after closing the cursor/connection, which is invalid and likely breaks fetch operations.
- `insert()` parses SQL text to build `ON DUPLICATE KEY UPDATE`; this is brittle and unsafe for many query forms.
- `delete()` builds SQL using f-strings with `table_name` and column names from caller input, so it can be SQL-injection vulnerable if those values are not controlled.
- `create_table_if_not_exists()` also uses unescaped table/schema names via f-string.
- `bulk_insert()` assumes params are list of dicts and rebuilds SQL based on keys; this is fragile and can break on mixed column orders.
- `DatabaseManager` is instantiated in many modules/cogs, creating multiple separate connection pools instead of reusing one singleton.
- bank_functions.py
- `db = DatabaseManager("economy")` is created at import time and may use incorrect/duplicate env loading.
- `bank_data()` treats `fetch_one()` result inconsistently; if it returns `{}` instead of `None`, account creation may not happen.
- `update_money()` uses `insert(..., overwrite=True)` to update balances, which is a weird and brittle upsert pattern.
- `record_transaction()` calls `db.insert("transactions", ...)` without a proper query skeleton.
- npc_memory.py
- Uses `sqlite3.connect('npc_memory.db')` with a hardcoded path and no thread-safety handling.
- Single SQLite connection is reused without controlling concurrency or isolation.
### 2. Secret/configuration handling
- app.py
- `app.secret_key = os.getenv("SECRET_KEY")` can be `None`, breaking Flask session security.
- Required env-vars are checked, but `DISCORD_BOT_TOKEN` is loaded and never used; config is inconsistent with actual bot startup.
- mail.py
- Loads email credentials from env and may fail silently if missing.
- bot.py
- Prints the bot token with `print(token)`; this is a secret leak risk.
- Multiple files call `load_dotenv()` repeatedly instead of once at startup.
### 3. Bot startup / architecture
- bot.py
- `iterate_prefix("Vamoc")` and `iterate_prefix("!V")` generate all uppercase/lowercase permutations, which is extremely inefficient and unnecessary.
- `self.iterate_prefix("Vamoc")+self.iterate_prefix("!V")` creates huge prefixes list and prints it.
- A Flask dev server is started in a daemon thread with `app.run(...)`, which is not suitable for production and may cause shutdown issues.
- `token` missing path handling is minimal and should use explicit error handling.
- app.py
- Uses Flask dev server, OAuth flows, and session storage without clear production readiness.
- Raw `requests` calls to Discord API are used without timeouts in some cases.
### 4. Code quality / maintainability
- economy.py
- `from utils.bank_functions import *` is bad practice and obscures imports.
- Duplicate `from discord.ext import commands` import.
- Many methods have weak validation and inconsistent return semantics.
- `check_transfer()` has odd parameter names and does not cleanly separate logic from reply behavior.
- `daily` command uses inconsistent datetime handling and stores timestamp as float in DB without schema enforcement.
- sql_commands.py
- Uses `pandas` only for `fetch_as_dataframe`, which is heavy for a bot and likely unnecessary.
- `pool_reset_session` config uses `bool(os.getenv(...))`, which returns `True` for any non-empty string, not only `"True"`.
- README.md
- The documented repo structure does not match actual workspace structure.
- It claims SQLite usage while sql_commands.py uses MySQL.
- It references files/paths that are not present (e.g. `extras/`, `LICENSE`).
### 5. Dependency / packaging
- requirements.txt
- Includes large packages like `numpy`, `pandas`, and `mysql-connector` even though the bot only needs lightweight DB access and small utilities.
- `mysql-connector==2.2.9` is very old; upgrade or use `mysql-connector-python` / `mysqlclient`.
### 6. Security / robustness
- app.py
- OAuth redirect URL and query parameters are concatenated without URL encoding.
- Session data stores access tokens directly, which is sensitive.
- `get_user_transactions()` builds ORDER BY using string interpolation; it is validated but could be improved with safer mapping.
- mail.py
- HTML email is generated via large f-strings; this is hard to maintain and could be moved to a template.
- Many files mix business logic and I/O without error boundaries, so failures can cascade.
### 7. Other issues / enhancements
- Several experimental files under Experimantal and implementing use `input()` and are not production-ready.
- npc_memory.py and sql_commands.py should expose a shared data access layer rather than ad-hoc DB wrappers in each module.
- Missing tests, linting, formatting, and structured error logging across the repo.
- app.py has duplicated code patterns for Discord API requests and could benefit from helper functions.
- bot.py and bot_development.py are almost duplicates; consolidate startup logic.
## Recommended enhancements
- Centralize configuration and database initialization.
- Use a shared singleton `DatabaseManager` or dependency injection.
- Add strong type hints and remove wildcard imports.
- Replace Flask dev thread with proper web hosting or decouple the web interface.
- Validate all environment variables at startup.
- Add command-level checks and better error messages.
- Remove or replace heavy dependencies if they are not needed.
- Add tests for DB operations, command behavior, and web routes.
- Clean up README to match actual repository state.
> Note: this is a high-level audit, not a full code rewrite. If you want, I can now create a prioritized issue list or fix the worst bugs first.
+14 -7
View File
@@ -28,9 +28,17 @@ class Client(commands.Bot):
help_command=MyNewHelp(),
)
def iterate_prefix(self, prefix):
prefixes = list(map(''.join, itertools.product(*zip(prefix.upper(), prefix.lower()))))
print(prefixes)
# Avoid generating all case permutations for long prefixes.
# Provide a small set of common-case variants and rely on
# `case_insensitive=True` for command matching.
variants = [prefix, prefix.lower(), prefix.upper()]
# Preserve order but remove duplicates
seen = set()
prefixes = []
for p in variants:
if p not in seen:
seen.add(p)
prefixes.append(p)
return prefixes
async def setup_hook(self): # overwriting a handler
@@ -48,11 +56,10 @@ class Client(commands.Bot):
def main():
load_dotenv()
client = Client()
token = "ODA2Mjg0OTY2NzQ0NTU1NjAw.GFQoZn.Jh0OJ7KczDOfRxFFESnAPOiodUAkjSyjQ-ClGg"
if token is not None:
token = os.getenv("DISCORD_TOKEN") or os.getenv("TOKEN")
if not token:
raise SystemExit("ERROR: Discord token not found. Set DISCORD_TOKEN (or TOKEN) in environment.")
client.run(token)
else:
print("Token is missing.")
if __name__ == "__main__":
+41
View File
@@ -145,3 +145,44 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
For support, please contact [yourname@example.com](mailto:yourname@example.com).
Happy Coding!
---
## Deployment (Web UI)
This project includes a small Flask-based web UI used for OAuth flows and guild management. The development server is fine for local testing, but for production use you should run the app under a WSGI server such as `waitress` or `gunicorn`.
Recommended steps to serve the web UI with `waitress`:
1. Install dependencies (includes `waitress`):
```bash
pip install -r requirements.txt
```
2. Set the required environment variables (example `.env`):
```env
# Bot and web settings
TOKEN=your_bot_token_here
DISCORD_CLIENT_ID=your_client_id
DISCORD_CLIENT_SECRET=your_client_secret
DISCORD_REDIRECT_URI=https://yourdomain.com/callback
# Flask session secret (must be set in production)
SECRET_KEY=replace_with_a_secure_random_value
# Optional runtime
START_WEB=1 # set to 1 if you want the bot process to spawn the web UI
REQUEST_TIMEOUT=10 # default HTTP timeout in seconds for external requests
```
3. Run the web app using `waitress` (example):
```bash
python -m waitress --host=0.0.0.0 --port=5000 web.app:app
```
Notes:
- If you set `START_WEB=1`, `bot_development.py` will spawn the web UI when the bot starts. This is convenient for small deployments but consider running the web UI in its own process or container for reliability and easier scaling.
- Never run the Flask development server (app.run) in production.
- Ensure `SECRET_KEY` is kept secret and not committed to source control. Use a secure random value like `openssl rand -hex 32`.
If you want, I can add a small `Procfile` and Dockerfile snippet for deployment — tell me which target (Heroku, Docker Compose, or Kubernetes) you prefer.
+6
View File
@@ -0,0 +1,6 @@
from utils.sql_commands import initialize_database
if __name__ == "__main__":
initialize_database()
print("Database initialization complete.")
+3 -9
View File
@@ -2,15 +2,9 @@
import discord
from discord.ext import commands
import os
import sys
from dotenv import load_dotenv
from web.app import app
import threading
import itertools
def run_web():
app.run(debug=False, host="0.0.0.0", port=8080)
return
from utils.sql_commands import initialize_database
class MyNewHelp(commands.MinimalHelpCommand):
@@ -50,9 +44,9 @@ class Client(commands.Bot):
def main():
load_dotenv()
initialize_database()
client = Client()
token = os.getenv("TOKEN")
print(token)
if token is not None:
threading.Thread(target=run_web, daemon=True).start()
client.run(token)
+21 -5
View File
@@ -4,11 +4,22 @@ from discord.ext import commands
import os
from dotenv import load_dotenv
from web.app import app
from utils.sql_commands import initialize_database
import threading
def run_web():
app.run(debug=False, host="0.0.0.0", port=5000)
try:
# Prefer a production-ready WSGI server if available
from waitress import serve
serve(app, host="0.0.0.0", port=5000)
except Exception:
# Fall back to Flask dev server for local development only.
print(
"Warning: Running Flask development server. For production use a WSGI server (gunicorn, waitress, etc.)."
)
app.run(debug=False, host="0.0.0.0", port=5000, use_reloader=False)
class MyNewHelp(commands.MinimalHelpCommand):
@@ -58,13 +69,18 @@ class Client(commands.Bot):
def main():
load_dotenv()
initialize_database()
client = Client()
token = os.getenv("TOKEN")
if token is not None:
threading.Thread(target=run_web, daemon=True).start()
if not token:
raise SystemExit("ERROR: TOKEN environment variable not set.")
# Only start the web interface if explicitly enabled to avoid
# running a dev server inside the bot process by default.
if os.getenv("START_WEB") == "1":
threading.Thread(target=run_web, daemon=False).start()
client.run(token)
else:
print("Token is missing.")
if __name__ == "__main__":
+1 -1
View File
@@ -63,7 +63,7 @@ class CustomCommandsCog(commands.Cog):
)
self.invalidate_cache(guild_id) # Invalidate cache after change
if deleted_rows is not None and len(deleted_rows) > 0:
if deleted_rows and deleted_rows > 0:
await ctx.send(f"Custom command `{command_name}` has been deleted!")
else:
await ctx.send(f"Custom command `{command_name}` not found.")
+1974
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -242,7 +242,7 @@ class Fun(commands.Cog):
api_url = f"https://tenor.googleapis.com/v2/search?q={search_query}&key={self.tenor_api_key}&limit=10"
try:
response = requests.get(api_url)
response = requests.get(api_url, timeout=10)
response.raise_for_status()
gif_data = response.json()
@@ -263,7 +263,7 @@ class Fun(commands.Cog):
api_url = "https://meme-api.com/memes/random"
try:
response = requests.get(api_url)
response = requests.get(api_url, timeout=10)
response.raise_for_status()
meme_data = response.json()
+10 -1
View File
@@ -38,7 +38,16 @@ class Test(commands.Cog):
@commands.is_owner()
@commands.command(name="quiz")
async def quiz(self, ctx):
response = requests.get("https://opentdb.com/api.php?amount=1&category=18&difficulty=medium&type=multiple").json()["results"][0]
try:
resp = requests.get(
"https://opentdb.com/api.php?amount=1&category=18&difficulty=medium&type=multiple",
timeout=10,
)
resp.raise_for_status()
response = resp.json()["results"][0]
except requests.RequestException:
await ctx.send("Failed to fetch quiz question.")
return
question = response["question"]
view = MyView(response["correct_answer"], response["incorrect_answers"])
+5 -2
View File
@@ -9,6 +9,9 @@ headers = {
"x-rapidapi-host": "tarot-cards1.p.rapidapi.com"
}
response = requests.get(url, headers=headers, params=querystring)
try:
response = requests.get(url, headers=headers, params=querystring, timeout=10)
response.raise_for_status()
print(response.json())
except requests.RequestException as e:
print(f"Request failed: {e}")
BIN
View File
Binary file not shown.
+1
View File
@@ -29,3 +29,4 @@ tzdata==2024.2
urllib3==2.2.3
Werkzeug==3.1.7
yarl==1.17.1
waitress==2.2.0
+1 -1
View File
@@ -1 +1 @@
1759391917.148779
1775047922.8093011
+4 -1
View File
@@ -40,7 +40,10 @@ async def record_transaction(
transaction_type: The type of transaction performed.
amount: The amount of the transaction.
"""
db.insert("transactions", (user_id, transaction_type, amount))
db.execute_query(
"INSERT INTO transactions (USERID, TYPE, AMOUNT) VALUES (%s, %s, %s)",
(user_id, transaction_type, amount),
)
async def update_money(
+171 -35
View File
@@ -3,11 +3,11 @@ from mysql.connector import pooling
from dotenv import load_dotenv
import os
import random
import re
import time
from datetime import datetime, timedelta
import logging
import pandas as pd
from copy import copy
# Configure logging
@@ -20,16 +20,33 @@ logger = logging.getLogger(__name__)
class DatabaseManager:
_instances = {}
def __new__(cls, env="development"):
instance_key = env or "default"
if instance_key in cls._instances:
return cls._instances[instance_key]
instance = super().__new__(cls)
cls._instances[instance_key] = instance
return instance
def __init__(self, env="development"):
# Load environment variables based on environment
self.load_env(env)
if getattr(self, "_initialized", False):
return
self._initialized = True
env_file = f".env.{env}" if env else ".env"
if not os.path.exists(env_file):
env_file = ".env"
self.load_env(env_file)
self.config = {
"host": os.getenv("SQLHOST", "localhost"),
"user": os.getenv("SQLUSER", "root"),
"password": os.getenv("SQLPASS", ""),
"database": os.getenv("SQLDB", "testdb"),
"pool_reset_session": bool(os.getenv("POOL_RESET_SESSION", False)),
"pool_reset_session": os.getenv("POOL_RESET_SESSION", "false").lower()
in ("true", "1", "yes"),
}
self.pool = pooling.MySQLConnectionPool(
@@ -124,26 +141,122 @@ class DatabaseManager:
inactivity INT NOT NULL
""",
)
self.create_table_if_not_exists(
"economy",
"""
ID BIGINT PRIMARY KEY,
WALLET BIGINT NOT NULL DEFAULT 0,
BANK BIGINT NOT NULL DEFAULT 0,
DAILY DOUBLE DEFAULT 0
""",
)
self.create_table_if_not_exists(
"transactions",
"""
ID INT AUTO_INCREMENT PRIMARY KEY,
USERID BIGINT NOT NULL,
TYPE VARCHAR(50),
AMOUNT DECIMAL(18,2),
TIME DATETIME DEFAULT CURRENT_TIMESTAMP
""",
)
self.create_table_if_not_exists(
"custom_commands",
"""
ID INT AUTO_INCREMENT PRIMARY KEY,
GUILDID VARCHAR(32) NOT NULL,
COMMANDNAME VARCHAR(100) NOT NULL,
RESPONSE TEXT NOT NULL,
MATCHTYPE VARCHAR(20) NOT NULL DEFAULT 'exact'
""",
)
self.create_table_if_not_exists(
"guilds",
"""
GUILD BIGINT PRIMARY KEY,
WELCOME BIGINT DEFAULT NULL,
RULES BIGINT DEFAULT NULL,
GUIDE BIGINT DEFAULT NULL,
INTRODUCTIONS BIGINT DEFAULT NULL,
EVENTS BIGINT DEFAULT NULL,
MEMBERCOUNT BIGINT DEFAULT NULL,
LOGGING BIGINT DEFAULT NULL,
TICKETING BIGINT DEFAULT NULL
""",
)
self.create_table_if_not_exists(
"rewards",
"""
ID INT AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(50) NOT NULL,
amount INT NOT NULL DEFAULT 0,
description TEXT DEFAULT NULL
""",
)
self.create_table_if_not_exists(
"logs",
"""
ID INT AUTO_INCREMENT PRIMARY KEY,
guild_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
type VARCHAR(50) NOT NULL,
message TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
""",
)
self.create_table_if_not_exists(
"gamble_limits",
"""
USERID BIGINT PRIMARY KEY,
DAILY_LIMIT BIGINT DEFAULT NULL,
EXCLUDED_UNTIL DATETIME DEFAULT NULL
""",
)
self.create_table_if_not_exists(
"users",
"""
ID BIGINT PRIMARY KEY,
XP INT DEFAULT 0,
LEVEL INT DEFAULT 0,
birthday VARCHAR(10) DEFAULT NULL
""",
)
def load_env(self, env):
env_file = f".env"
def load_env(self, env_file):
load_dotenv(env_file)
logger.info(f"Loaded environment variables from {env_file}")
def get_connection(self):
return self.pool.get_connection()
def _sanitize_identifier(self, identifier: str) -> str:
if not re.match(r"^[A-Za-z0-9_]+$", identifier):
raise ValueError(f"Invalid SQL identifier: {identifier}")
return identifier
def _parse_insert_columns(self, query: str) -> list[str]:
match = re.search(
r"INSERT\s+INTO\s+\S+\s*\(([^)]+)\)\s*VALUES",
query,
re.IGNORECASE,
)
if not match:
raise ValueError(
"Insert query must contain a column list for overwrite upsert support."
)
return [col.strip() for col in match.group(1).split(",") if col.strip()]
def execute_query(self, query, params=None, retries=3, delay=1):
cursor = None
connection = None
cursor = None
for attempt in range(retries):
try:
connection = self.get_connection()
cursor = connection.cursor(dictionary=True, buffered=True)
cursor.execute(query, params or ())
logger.info(f"Executed query: {query} with params: {params}")
connection.commit()
return copy(cursor)
logger.info(f"Executed query: {query} with params: {params}")
return cursor.rowcount
except mysql.connector.Error as err:
logger.warning(f"Attempt {attempt + 1} failed: {err}")
time.sleep(delay * (2**attempt))
@@ -163,7 +276,7 @@ class DatabaseManager:
Args:
query (str): The SQL query to execute.
params (tuple): The parameters to pass into the query.
overwrite (bool, optional): Whether to perform an upsert operation. Defaults to False.
overwrite (bool, optional): Whether to perform an upsert operation. Defaults to True.
Raises:
ValueError: If no parameters are provided.
@@ -171,37 +284,29 @@ class DatabaseManager:
if not params:
raise ValueError("Params must be provided for the insert operation.")
try:
if overwrite:
columns = [
col.split("=")[0].strip()
for col in query.split("VALUES")[0]
.split("(")[1]
.split(")")[0]
.split(",")
]
columns = self._parse_insert_columns(query)
update_set = ", ".join(f"{col} = VALUES({col})" for col in columns)
query = f"{query} ON DUPLICATE KEY UPDATE {update_set}"
cursor = self.execute_query(query, params)
if cursor:
rowcount = self.execute_query(query, params)
if rowcount is None:
logger.error(f"Insert failed with query: {query}.")
else:
logger.info(f"Insert completed with query: {query}.")
except mysql.connector.Error as err:
logger.error(f"Insert failed with query: {query}. Error: {err}")
def bulk_insert(self, query, params=None):
if not params:
logger.warning("No data provided for bulk insert.")
return
# Assuming params is a list of dictionaries
if not isinstance(params, list) or not all(isinstance(d, dict) for d in params):
raise ValueError("Params must be a list of dictionaries for bulk insert.")
keys = params[0].keys()
keys = list(params[0].keys())
placeholders = ", ".join(["%s"] * len(keys))
query = f"{query} ({', '.join(keys)}) VALUES ({placeholders})"
values = [tuple(data.values()) for data in params]
values = [tuple(data[key] for key in keys) for data in params]
connection = None
cursor = None
@@ -216,7 +321,7 @@ class DatabaseManager:
except mysql.connector.Error as err:
logger.error(f"Bulk insert failed: {err}")
if connection:
connection.rollback() # Roll back on error
connection.rollback()
finally:
if cursor:
cursor.close()
@@ -225,34 +330,60 @@ class DatabaseManager:
def delete(self, table_name: str, condition: dict) -> None:
"""Deletes a record from the specified table based on the condition provided."""
table_name = self._sanitize_identifier(table_name)
condition_column, condition_value = next(iter(condition.items()))
condition_column = self._sanitize_identifier(condition_column)
query = f"DELETE FROM {table_name} WHERE {condition_column} = %s"
self.execute_query(query, (condition_value,))
def fetch_one(self, query, params=None):
cursor = self.execute_query(query, params)
return cursor.fetchone() if cursor else {}
connection = None
cursor = None
try:
connection = self.get_connection()
cursor = connection.cursor(dictionary=True, buffered=True)
cursor.execute(query, params or ())
return cursor.fetchone()
finally:
if cursor:
cursor.close()
if connection:
connection.close()
def fetch_all(self, query, params=None):
cursor = self.execute_query(query, params)
return cursor.fetchall() if cursor else []
connection = None
cursor = None
try:
connection = self.get_connection()
cursor = connection.cursor(dictionary=True, buffered=True)
cursor.execute(query, params or ())
return cursor.fetchall()
finally:
if cursor:
cursor.close()
if connection:
connection.close()
def fetch_as_dataframe(self, query, params=None):
cursor = self.execute_query(query, params)
if cursor:
connection = None
cursor = None
try:
# Ensure cursor has a result to fetch
connection = self.get_connection()
cursor = connection.cursor(dictionary=True, buffered=True)
cursor.execute(query, params or ())
if cursor.with_rows:
results = cursor.fetchall()
return pd.DataFrame(results) if results else pd.DataFrame()
else:
logger.warning("No result set to fetch from.")
return pd.DataFrame()
finally:
if cursor:
cursor.close()
return pd.DataFrame()
if connection:
connection.close()
def create_table_if_not_exists(self, table_name, schema):
table_name = self._sanitize_identifier(table_name)
query = f"CREATE TABLE IF NOT EXISTS {table_name} ({schema})"
self.execute_query(query)
logger.info(f"Ensured table {table_name} exists with schema: {schema}")
@@ -275,6 +406,11 @@ class DatabaseManager:
return f"{base_query} WHERE {conditions}", list(filters.values())
def initialize_database(env="development"):
"""Initialize the database schema and return a shared DatabaseManager."""
return DatabaseManager(env)
# SQL scripts to create tables
create_feedback_table = """
CREATE TABLE IF NOT EXISTS feedback (
Binary file not shown.
+98 -43
View File
@@ -4,10 +4,10 @@ import os
if __package__ is None or __package__ == "":
# Running as __main__ (e.g. python web/app.py)
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from utils.sql_commands import DatabaseManager
from utils.sql_commands import DatabaseManager, initialize_database
else:
# Imported as a module (e.g. from bot.py)
from utils.sql_commands import DatabaseManager
from utils.sql_commands import DatabaseManager, initialize_database
import os
import requests
@@ -20,8 +20,24 @@ import json
load_dotenv()
app = Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY")
db = DatabaseManager()
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SECURE=os.getenv("FLASK_ENV") == "production",
SESSION_COOKIE_SAMESITE="Lax",
PERMANENT_SESSION_LIFETIME=datetime.timedelta(hours=1),
)
# Ensure a secret key is set for session security. In production this MUST be
# provided via the SECRET_KEY environment variable. In development we generate
# a temporary one (not suitable for production).
secret = os.getenv("SECRET_KEY")
if not secret:
if os.getenv("FLASK_ENV") == "production":
raise EnvironmentError("SECRET_KEY must be set in production environment")
print("Warning: SECRET_KEY not set; generating a temporary key for development.")
secret = os.urandom(32)
app.secret_key = secret
db = initialize_database()
# Ensure required environment variables are loaded
DISCORD_CLIENT_ID = os.getenv("DISCORD_CLIENT_ID")
@@ -30,6 +46,22 @@ DISCORD_REDIRECT_URI = os.getenv("DISCORD_REDIRECT_URI")
DISCORD_API_BASE_URL = "https://discord.com/api"
DISCORD_BOT_TOKEN = os.getenv("TOKEN")
http = requests.Session()
http.headers.update({"User-Agent": "DiscordBotWeb/1.0"})
def discord_request(method, endpoint, headers=None, **kwargs):
url = f"{DISCORD_API_BASE_URL}{endpoint}"
try:
response = http.request(method, url, headers=headers, timeout=REQUEST_TIMEOUT, **kwargs)
response.raise_for_status()
return response
except requests.RequestException as exc:
app.logger.warning("Discord API request failed: %s %s %s", method, url, exc)
raise
# Default request timeout (seconds) for external HTTP calls
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "10"))
# Check for missing environment variables
if not all([DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI]):
raise EnvironmentError("One or more required environment variables are missing.")
@@ -71,11 +103,14 @@ def callback():
"redirect_uri": DISCORD_REDIRECT_URI,
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(
f"{DISCORD_API_BASE_URL}/oauth2/token", data=data, headers=headers
try:
response = discord_request(
"POST",
"/oauth2/token",
data=data,
headers=headers,
)
if response.status_code != 200:
except requests.RequestException:
flash("Failed to retrieve access token from Discord.")
return redirect(url_for("home"))
@@ -84,49 +119,52 @@ def callback():
flash("Access token missing in the response.")
return redirect(url_for("home"))
# Store access token in session for later use
session["access_token"] = access_token
session.permanent = True
# Fetch user info
user_data = requests.get(
f"{DISCORD_API_BASE_URL}/users/@me",
try:
user_resp = discord_request(
"GET",
"/users/@me",
headers={"Authorization": f"Bearer {access_token}"},
).json()
)
user_data = user_resp.json()
except requests.RequestException:
flash("Failed to fetch user info from Discord.")
return redirect(url_for("home"))
# Store user information in session
session["user"] = user_data
# Fetch guilds the user is in
guilds_response = requests.get(
f"{DISCORD_API_BASE_URL}/users/@me/guilds",
try:
guilds_response = discord_request(
"GET",
"/users/@me/guilds",
headers={"Authorization": f"Bearer {access_token}"},
)
if guilds_response.status_code == 200:
guilds = guilds_response.json() # Store guilds in a variable
session["guilds"] = guilds # Store guilds in session
guilds = guilds_response.json()
session["guilds"] = guilds
# Fetch member count and permissions for each guild
for guild in guilds:
guild_id = guild["id"]
guild_info_response = requests.get(
f"{DISCORD_API_BASE_URL}/guilds/{guild_id}",
try:
guild_info_response = discord_request(
"GET",
f"/guilds/{guild_id}",
headers={"Authorization": f"Bearer {access_token}"},
)
if guild_info_response.status_code == 200:
guild_info = guild_info_response.json()
guild["approx_member_count"] = guild_info.get("member_count", "N/A")
guild["permissions"] = guild_info.get(
"permissions", 0
) # Store permissions
else:
guild["approx_member_count"] = "N/A" # Fallback if unable to fetch
guild["permissions"] = guild_info.get("permissions", 0)
except requests.RequestException:
guild["approx_member_count"] = "N/A"
# Filter guilds where user can add bots
guilds_with_bot_permission = [guild for guild in guilds if can_add_bot(guild)]
session["guilds_with_bot_permission"] = guilds_with_bot_permission
else:
except requests.RequestException:
flash("Failed to retrieve guilds from Discord.")
return redirect(url_for("home"))
return redirect(url_for("home"))
@@ -309,31 +347,36 @@ def guild_settings(guild_id):
flash("You are not logged in.")
return redirect(url_for("login"))
bot_headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
guild_info_response = requests.get(
f"{DISCORD_API_BASE_URL}/guilds/{guild_id}?with_counts=true",
headers=bot_headers,
)
if guild_info_response.status_code != 200:
flash("Failed to retrieve guild information (bot may not be in this guild).")
if not DISCORD_BOT_TOKEN:
flash("Bot token not configured on server; cannot fetch guild info.")
return redirect(url_for("home"))
bot_headers = {"Authorization": f"Bot {DISCORD_BOT_TOKEN}"}
try:
guild_info_response = discord_request(
"GET",
f"/guilds/{guild_id}?with_counts=true",
headers=bot_headers,
)
guild_info = guild_info_response.json()
except requests.RequestException:
flash("Failed to retrieve guild information (bot may not be in this guild or network error).")
return redirect(url_for("home"))
owner_id = guild_info.get("owner_id", "N/A")
member_count = guild_info.get("approximate_member_count", "N/A")
# Fetch owner's username using the bot token
owner_name = "Unknown"
if owner_id != "N/A":
owner_response = requests.get(
f"{DISCORD_API_BASE_URL}/users/{owner_id}",
try:
owner_response = discord_request(
"GET",
f"/users/{owner_id}",
headers=bot_headers,
)
if owner_response.status_code == 200:
owner_data = owner_response.json()
owner_name = f"{owner_data.get('username', 'Unknown')}#{owner_data.get('discriminator', '0000')}"
else:
except requests.RequestException:
owner_name = owner_id # fallback to ID if fetch fails
json_formatted_str = json.dumps(guild_info, indent=2)
@@ -360,4 +403,16 @@ def profile():
if __name__ == "__main__":
app.run(debug=True, port=5000, host="0.0.0.0")
# Running via the Flask development server for local testing.
# For production, run under a WSGI server (gunicorn, waitress, etc.).
if os.getenv("FLASK_ENV") == "production":
raise RuntimeError(
"Do not use the Flask development server in production. "
"Please serve this app with a WSGI server like gunicorn or waitress."
)
app.run(
debug=True,
port=int(os.getenv("PORT", "5000")),
host="0.0.0.0",
use_reloader=False,
)