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""" +
  • +
    +
    +
    + {i} +
    +
    +

    {user}

    +

    {timestamp}

    +
    +
    +
    +

    {content}

    +
    +
    +
  • + """ + ) + + return 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

    +
    +
    + + +""" + 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""" -
  • -
    -
    -
    - {i} -
    -
    -

    {user}

    -

    {timestamp}

    -
    -
    -
    -

    {content}

    -
    -
    -
  • - """ + 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)