From 4b07ca86b9c264b10ee71cb3e2b0104f76c33d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20L=27abb=C3=A9?= Date: Mon, 1 Jun 2026 14:14:52 +0000 Subject: [PATCH] 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 --- .gitignore | 20 +++- bot.py | 2 +- cogs/mail.py | 178 ++++++++++++++-------------- main.py | 17 --- message_command_stats.json | 40 ++++--- requirements.txt | 4 +- utils/bank_functions.py | 2 +- utils/sql_commands.py | 17 ++- web/__pycache__/app.cpython-312.pyc | Bin 14798 -> 17622 bytes web/app.py | 33 ++++-- 10 files changed, 166 insertions(+), 147 deletions(-) delete mode 100755 main.py 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)} -

    -
    - - -
    -
      - {all_feedback} -
    -
    - - -
    -

    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 4c2f0661b37edecc1370c533e385852f11c9ae21..6064f239f2ba0a9b54fd08bd9f13761c9cc0deee 100644 GIT binary patch delta 8483 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 U1GJX^qO@AwW*MdNO4syE@xC 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