diff --git a/.gitignore b/.gitignore
index b3961c2..bc3276a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
# Byte-compiled / optimized / DLL files
-*__pycache__/
+__pycache__/
*.py[cod]
*$py.class
@@ -7,10 +7,12 @@
venv/
env/
ENV/
+.venv/
# Environment variables
.env
.env.local
+.env.*.local
# IDE
.vscode/
@@ -27,6 +29,7 @@ config.json
token.txt
logs/
*.log
+*.log.*
# Database
*.db
@@ -41,3 +44,18 @@ logs/
# Cache files
*.cache
__pycache__/
+
+# Python
+*.pyc
+.pytest_cache/
+.coverage
+.pytest_cache/
+.mypy_cache/
+
+# Web interface
+web/static/uploads/
+web/static/cache/
+
+# System
+.DS_Store
+._*
\ No newline at end of file
diff --git a/bot.py b/bot.py
index 2bce749..31d49c0 100755
--- a/bot.py
+++ b/bot.py
@@ -24,7 +24,7 @@ class Client(commands.Bot):
intents=discord.Intents.all(),
help_command=MyNewHelp(),
)
- def iterate_prefix(self, prefix):
+ def iterate_prefix(self, prefix): #Needed as case_insensitive doesn't work with prefixes and only commands, not the bot itself. This is a workaround to make the bot respond to both uppercase and lowercase prefixes.
prefixes = list(map(''.join, itertools.product(*zip(prefix.upper(), prefix.lower()))))
print(prefixes)
diff --git a/cogs/mail.py b/cogs/mail.py
index 3d33606..b7ba768 100755
--- a/cogs/mail.py
+++ b/cogs/mail.py
@@ -7,11 +7,83 @@ from os import getenv
import html
from datetime import datetime
+load_dotenv()
+
+
+def build_feedback_html(feedback_rows: list[dict[str, str]]) -> str:
+ feedback_items = []
+ for i, row in enumerate(feedback_rows, 1):
+ content = html.escape(row["CONTENT"])
+ user = html.escape(row["USER"])
+ timestamp = html.escape(row["TIMESTAMP"])
+
+ feedback_items.append(
+ f"""
+
+
+
+ """
+ )
+
+ return f"""
+
+
+
+
+ User Feedback Report
+
+
+
+
+
+
User Feedback Report
+
New feedback submissions
+
+
+
+ Total feedback submissions: {len(feedback_rows)}
+
+
+
+
+ {''.join(feedback_items)}
+
+
+
+
Generated automatically by PyBot • {datetime.now().strftime('%Y-%m-%d %H:%M')}
+
Do not reply to this automated message
+
+
+
+
+"""
+
class Mail(commands.Cog):
def __init__(self, client:commands.Bot):
self.client = client
- load_dotenv()
from utils.sql_commands import DatabaseManager
self.db = DatabaseManager()
@@ -33,99 +105,21 @@ class Mail(commands.Cog):
return
try:
- port = int(port) # type: ignore # Error invalid as for problem is taken care of above
- s = smtplib.SMTP(host=server, port=port) # type: ignore
- s.starttls()
- s.login(username, password) # type: ignore
- msg = MIMEMultipart("alternative")
- msg["To"] = receiver # type: ignore
- msg["From"] = username # type: ignore
- msg["Subject"] = "Py feedback"
+ port = int(port)
+ with smtplib.SMTP(host=server, port=port) as s:
+ s.starttls()
+ s.login(username, password)
+ msg = MIMEMultipart("alternative")
+ msg["To"] = receiver
+ msg["From"] = username
+ msg["Subject"] = "Py feedback"
- # Fetch feedback from the database
- feedback_rows:list[dict[str,str]] = self.db.fetch_all("SELECT * FROM feedback")
- all_feedback = ""
- for i, row in enumerate(feedback_rows, 1):
- content = html.escape(row["CONTENT"])
- user = html.escape(row["USER"])
- timestamp = html.escape(row["TIMESTAMP"])
-
- all_feedback += f"""
-
-
-
- """
+ feedback_rows: list[dict[str, str]] = self.db.fetch_all("SELECT * FROM feedback")
+ text = build_feedback_html(feedback_rows)
-
-
- text = f"""
-
-
-
-
- User Feedback Report
-
-
-
-
-
-
-
User Feedback Report
-
New feedback submissions
-
-
-
-
-
- Total feedback submissions: {len(feedback_rows)}
-
-
-
-
-
-
-
-
-
Generated automatically by PyBot • {datetime.now().strftime('%Y-%m-%d %H:%M')}
-
Do not reply to this automated message
-
-
-
-
-"""
-
-
-
- msg.attach(MIMEText(text, "html"))
- s.send_message(msg)
- await ctx.reply("Mail sent.", delete_after=2)
- s.quit()
+ msg.attach(MIMEText(text, "html"))
+ s.send_message(msg)
+ await ctx.reply("Mail sent.", delete_after=2)
except Exception as e:
await ctx.reply(f"Failed to send mail: {e}", delete_after=5)
diff --git a/main.py b/main.py
deleted file mode 100755
index 808ec02..0000000
--- a/main.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import requests
-
-url = "https://tarot-cards1.p.rapidapi.com/tarot/"
-
-querystring = {"minor":"2","major":"2"}
-
-headers = {
- "x-rapidapi-key": "915784f37bmsh01add6a88639e60p15c4aajsnbaee391c4ef7",
- "x-rapidapi-host": "tarot-cards1.p.rapidapi.com"
-}
-
-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}")
\ No newline at end of file
diff --git a/message_command_stats.json b/message_command_stats.json
index f363dda..a87a3f7 100755
--- a/message_command_stats.json
+++ b/message_command_stats.json
@@ -1,43 +1,45 @@
{
"messages": {
"601579326714019840": {
- "total": 106,
- "commands": 94,
- "non_commands": 12
+ "total": 137,
+ "commands": 118,
+ "non_commands": 19
}
},
"commands": {
- "purge": 5,
+ "purge": 6,
"mail_feedback": 2,
- "help": 11,
+ "help": 16,
"nuke": 4,
"ping": 2,
- "top": 2,
- "stats": 2,
+ "top": 3,
+ "stats": 3,
"poker": 7,
- "balance": 5,
- "daily": 4,
+ "balance": 8,
+ "daily": 8,
"withdraw": 2,
- "leaderboard": 2,
- "afk": 2,
+ "leaderboard": 3,
+ "afk": 3,
"afklist": 1,
"whois": 1,
"reset_money": 2,
- "listcommands": 1,
+ "listcommands": 3,
"talk": 27,
"npcs": 2,
"remove_money": 1,
- "guildids": 1,
- "give_money": 1,
+ "guildids": 2,
+ "give_money": 2,
"coinflip": 2,
"deposit": 1,
"gameroom": 3,
- "reload": 1
+ "reload": 1,
+ "slots": 2,
+ "addcommand": 1
},
"channels": {
- "bot": 86,
+ "bot": 91,
"membercount": 3,
- "members-3": 2,
+ "members-3": 28,
"faq": 1,
"polls": 8,
"bot-commands": 2,
@@ -45,8 +47,8 @@
"general": 3
},
"guilds": {
- "Plex": 89,
- "TEST SERVER BOT": 17
+ "Plex": 94,
+ "TEST SERVER BOT": 43
},
"total_messages": 0,
"command_messages": 0,
diff --git a/requirements.txt b/requirements.txt
index aa56b62..c606e55 100755
--- a/requirements.txt
+++ b/requirements.txt
@@ -15,7 +15,7 @@ itsdangerous==2.2.0
Jinja2==3.1.6
MarkupSafe==3.0.3
multidict==6.1.0
-mysql-connector==2.2.9
+mysql-connector-python>=8.1.0,<9
pillow==12.1.1
propcache==0.2.0
python-dateutil==2.9.0.post0
@@ -27,4 +27,4 @@ tzdata==2024.2
urllib3==2.2.3
Werkzeug==3.1.7
yarl==1.17.1
-waitress==2.2.0
+waitress==3.0.2
diff --git a/utils/bank_functions.py b/utils/bank_functions.py
index f7a10a5..242e064 100755
--- a/utils/bank_functions.py
+++ b/utils/bank_functions.py
@@ -4,7 +4,7 @@ from typing import Dict
from datetime import datetime
-db = DatabaseManager("economy")
+db = DatabaseManager()
async def create_account(user: discord.Member | discord.User):
diff --git a/utils/sql_commands.py b/utils/sql_commands.py
index 988f64d..a7d0780 100755
--- a/utils/sql_commands.py
+++ b/utils/sql_commands.py
@@ -21,8 +21,17 @@ logger = logging.getLogger(__name__)
class DatabaseManager:
_instances = {}
+ @classmethod
+ def _resolve_instance_key(cls, env: str | None) -> str:
+ key = (env or "").strip()
+ if not key:
+ return "default"
+
+ env_file = f".env.{key}"
+ return key if os.path.exists(env_file) else "default"
+
def __new__(cls, env="development"):
- instance_key = env or "default"
+ instance_key = cls._resolve_instance_key(env)
if instance_key in cls._instances:
return cls._instances[instance_key]
instance = super().__new__(cls)
@@ -49,8 +58,12 @@ class DatabaseManager:
),
}
+ instance_key = self._resolve_instance_key(env)
+ pool_name = f"mypool_{instance_key}" if instance_key else "mypool_default"
self.pool = pooling.MySQLConnectionPool(
- pool_name="mypool", pool_size=5, **self.config
+ pool_name=pool_name,
+ pool_size=5,
+ **self.config,
)
logger.info("Database connection pool created.")
diff --git a/web/__pycache__/app.cpython-312.pyc b/web/__pycache__/app.cpython-312.pyc
index 4c2f066..6064f23 100644
Binary files a/web/__pycache__/app.cpython-312.pyc and b/web/__pycache__/app.cpython-312.pyc differ
diff --git a/web/app.py b/web/app.py
index 70427b2..1ca52be 100755
--- a/web/app.py
+++ b/web/app.py
@@ -11,6 +11,7 @@ else:
import os
import requests
+from urllib.parse import urlencode
from flask import Flask, redirect, url_for, session, request, render_template, flash
from dotenv import load_dotenv
from collections import defaultdict
@@ -49,6 +50,19 @@ DISCORD_BOT_TOKEN = os.getenv("TOKEN")
http = requests.Session()
http.headers.update({"User-Agent": "DiscordBotWeb/1.0"})
+# Default request timeout (seconds) for external HTTP calls
+REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "10"))
+
+def build_discord_oauth_url():
+ params = {
+ "client_id": DISCORD_CLIENT_ID,
+ "redirect_uri": DISCORD_REDIRECT_URI,
+ "response_type": "code",
+ "scope": "identify guilds applications.commands bot",
+ }
+ return f"{DISCORD_API_BASE_URL}/oauth2/authorize?{urlencode(params)}"
+
+
def discord_request(method, endpoint, headers=None, **kwargs):
url = f"{DISCORD_API_BASE_URL}{endpoint}"
try:
@@ -59,9 +73,6 @@ def discord_request(method, endpoint, headers=None, **kwargs):
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.")
@@ -70,12 +81,7 @@ if not all([DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI]):
# OAuth2 login route
@app.route("/login")
def login():
- discord_auth_url = (
- f"{DISCORD_API_BASE_URL}/oauth2/authorize"
- f"?client_id={DISCORD_CLIENT_ID}&redirect_uri={DISCORD_REDIRECT_URI}"
- f"&response_type=code&scope=identify%20guilds%20applications.commands%20bot"
- )
- return redirect(discord_auth_url)
+ return redirect(build_discord_oauth_url())
def can_add_bot(guild):
@@ -174,7 +180,6 @@ def callback():
def logout():
session.pop("user", None)
session.pop("guilds", None)
- session.pop("access_token", None) # Ensure access token is also cleared
session.pop("guilds_with_bot_permission", None)
flash("You have been logged out.")
return redirect(url_for("home"))
@@ -253,8 +258,12 @@ def transactions():
flash("You are not logged in.")
return redirect(url_for("login"))
- sort_by = request.args.get("sort", "date")
- order = request.args.get("order", "asc")
+ sort_by = request.args.get("sort", "date").lower()
+ order = request.args.get("order", "asc").lower()
+ if sort_by not in {"date", "amount"}:
+ sort_by = "date"
+ if order not in {"asc", "desc"}:
+ order = "asc"
user_transactions = get_user_transactions(str(user["id"]), sort_by, order)
daily_totals = defaultdict(float)