feat: enhance gitignore and bot prefix handling

- Updated .gitignore to properly exclude Python cache files and environment variables
- Modified bot.py to improve prefix case handling for better command recognition
- Refactored mail.py to streamline feedback email generation and database interaction
- Added environment variable loading in mail.py for better configuration management
This commit is contained in:
2026-06-01 14:14:52 +00:00
parent 3e6410d112
commit 4b07ca86b9
10 changed files with 166 additions and 147 deletions
+19 -1
View File
@@ -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
._*
+1 -1
View File
@@ -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)
+80 -86
View File
@@ -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"""
<li style="margin-bottom:25px; border-bottom:1px solid #edf2f7; padding-bottom:20px;">
<div class="feedback-card" style="background:#ffffff; border-radius:8px; position:relative;">
<div style="display:flex; align-items:center; margin-bottom:12px;">
<div style="background:#4361ee; width:36px; height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; color:white; font-weight:bold; flex-shrink:0;">
{i}
</div>
<div style="margin-left:15px;">
<h3 style="margin:0; font-size:16px; color:#2d3748;">{user}</h3>
<p style="margin:3px 0 0; font-size:13px; color:#718096;">{timestamp}</p>
</div>
</div>
<div style="background:#f8f9fc; padding:15px; border-radius:8px; border-left:3px solid #4361ee;">
<p style="margin:0; font-size:15px; line-height:1.5; color:#4a5568;">{content}</p>
</div>
</div>
</li>
"""
)
return f"""<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">
<title>User Feedback Report</title>
<style>
@media only screen and (max-width: 600px) {{
.container {{
width: 95% !important;
}}
.feedback-card {{
padding: 12px !important;
}}
}}
</style>
</head>
<body style=\"margin:0; padding:20px 0; background-color:#f7f9fc; font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;\">
<div class=\"container\" style=\"max-width:600px; margin:0 auto; background:#ffffff; border-radius:10px; box-shadow:0 4px 15px rgba(0,0,0,0.05);\">
<div style=\"background: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%); padding:30px 0; border-radius:10px 10px 0 0; text-align:center;\">
<h1 style=\"color:#fff; margin:0; font-weight:600;\">User Feedback Report</h1>
<p style=\"color:rgba(255,255,255,0.8); margin:8px 0 0; font-size:18px;\">New feedback submissions</p>
</div>
<div style=\"padding:20px 30px; background:#f0f7ff; border-bottom:1px solid #e3f2fd;\">
<p style=\"margin:0; font-size:16px; color:#2d3748;\">
Total feedback submissions: <strong>{len(feedback_rows)}</strong>
</p>
</div>
<div style=\"padding:10px 30px 30px;\">
<ul style=\"list-style:none; padding:0; margin:0;\">
{''.join(feedback_items)}
</ul>
</div>
<div style=\"padding:20px 30px; text-align:center; background:#f8f9fa; border-top:1px solid #eaeaea; border-radius:0 0 10px 10px; color:#718096; font-size:14px;\">
<p style=\"margin:0;\">Generated automatically by PyBot • {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
<p style=\"margin:8px 0 0;\">Do not reply to this automated message</p>
</div>
</div>
</body>
</html>
"""
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
port = int(port)
with smtplib.SMTP(host=server, port=port) as s:
s.starttls()
s.login(username, password) # type: ignore
s.login(username, password)
msg = MIMEMultipart("alternative")
msg["To"] = receiver # type: ignore
msg["From"] = username # type: ignore
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"""
<li style="margin-bottom:25px; border-bottom:1px solid #edf2f7; padding-bottom:20px;">
<div class="feedback-card" style="background:#ffffff; border-radius:8px; position:relative;">
<div style="display:flex; align-items:center; margin-bottom:12px;">
<div style="background:#4361ee; width:36px; height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; color:white; font-weight:bold; flex-shrink:0;">
{i}
</div>
<div style="margin-left:15px;">
<h3 style="margin:0; font-size:16px; color:#2d3748;">{user}</h3>
<p style="margin:3px 0 0; font-size:13px; color:#718096;">{timestamp}</p>
</div>
</div>
<div style="background:#f8f9fc; padding:15px; border-radius:8px; border-left:3px solid #4361ee;">
<p style="margin:0; font-size:15px; line-height:1.5; color:#4a5568;">{content}</p>
</div>
</div>
</li>
"""
text = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>User Feedback Report</title>
<style>
@media only screen and (max-width: 600px) {{
.container {{
width: 95% !important;
}}
.feedback-card {{
padding: 12px !important;
}}
}}
</style>
</head>
<body style="margin:0; padding:20px 0; background-color:#f7f9fc; font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
<div class="container" style="max-width:600px; margin:0 auto; background:#ffffff; border-radius:10px; box-shadow:0 4px 15px rgba(0,0,0,0.05);">
<!-- Header -->
<div style="background: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%); padding:30px 0; border-radius:10px 10px 0 0; text-align:center;">
<h1 style="color:#fff; margin:0; font-weight:600;">User Feedback Report</h1>
<p style="color:rgba(255,255,255,0.8); margin:8px 0 0; font-size:18px;">New feedback submissions</p>
</div>
<!-- Summary -->
<div style="padding:20px 30px; background:#f0f7ff; border-bottom:1px solid #e3f2fd;">
<p style="margin:0; font-size:16px; color:#2d3748;">
Total feedback submissions: <strong>{len(feedback_rows)}</strong>
</p>
</div>
<!-- Feedback Items -->
<div style="padding:10px 30px 30px;">
<ul style="list-style:none; padding:0; margin:0;">
{all_feedback}
</ul>
</div>
<!-- Footer -->
<div style="padding:20px 30px; text-align:center; background:#f8f9fa; border-top:1px solid #eaeaea; border-radius:0 0 10px 10px; color:#718096; font-size:14px;">
<p style="margin:0;">Generated automatically by PyBot • {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
<p style="margin:8px 0 0;">Do not reply to this automated message</p>
</div>
</div>
</body>
</html>
"""
feedback_rows: list[dict[str, str]] = self.db.fetch_all("SELECT * FROM feedback")
text = build_feedback_html(feedback_rows)
msg.attach(MIMEText(text, "html"))
s.send_message(msg)
await ctx.reply("Mail sent.", delete_after=2)
s.quit()
except Exception as e:
await ctx.reply(f"Failed to send mail: {e}", delete_after=5)
-17
View File
@@ -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}")
+21 -19
View File
@@ -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,
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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):
+15 -2
View File
@@ -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.")
Binary file not shown.
+21 -12
View File
@@ -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)