Compare commits

..

4 Commits

Author SHA1 Message Date
Nobody2503 b315069b1c feat: Add bank and wallet balance commands with improved transfer validation
- Added /bank and /wallet commands to check user balances
- Enhanced transfer validation to check for valid number input
- Included debug token logging in main function (to be removed in production)
- Added account creation logic for new users in balance commands
- Improved error handling for invalid transfer amounts
2026-06-03 11:56:09 +00:00
Nobody2503 4b07ca86b9 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
2026-06-01 14:14:52 +00:00
Nobody2503 3e6410d112 Refactor project structure and update README; remove unused dependencies from requirements.txt and enhance database management in sql_commands.py. 2026-05-31 12:56:55 +00:00
Nobody2503 5fdb37f7f8 Refactor token retrieval and error handling in main function; streamline environment variable checks for Discord token. 2026-05-31 12:37:57 +00:00
12 changed files with 426 additions and 324 deletions
+19 -1
View File
@@ -1,5 +1,5 @@
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
*__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
@@ -7,10 +7,12 @@
venv/ venv/
env/ env/
ENV/ ENV/
.venv/
# Environment variables # Environment variables
.env .env
.env.local .env.local
.env.*.local
# IDE # IDE
.vscode/ .vscode/
@@ -27,6 +29,7 @@ config.json
token.txt token.txt
logs/ logs/
*.log *.log
*.log.*
# Database # Database
*.db *.db
@@ -41,3 +44,18 @@ logs/
# Cache files # Cache files
*.cache *.cache
__pycache__/ __pycache__/
# Python
*.pyc
.pytest_cache/
.coverage
.pytest_cache/
.mypy_cache/
# Web interface
web/static/uploads/
web/static/cache/
# System
.DS_Store
._*
+30 -18
View File
@@ -56,25 +56,37 @@ Heres an outline of the directory and file structure for this project:
``` ```
discord-bot/ discord-bot/
├── bot.py # Main bot file for initializing the bot and loading cogs ├── bot.py
├── requirements.txt # List of required Python packages ├── bot_development.py
├── .env # Environment file containing sensitive data (e.g., bot token) ├── main.py
├── dockerfile
├── requirements.txt
├── README.md
├── Fixes.md
├── cogs/ # Folder containing individual cogs for separate functionalities ├── cogs/
│ ├── admin.py # Admin commands such as ban, kick, etc. │ ├── admin.py
│ ├── economy.py # Economy-related commands │ ├── customCommands.py
│ ├── fun.py # Fun commands for user engagement │ ├── economy.py
── roles.py # Commands for managing server roles ── gamble.py
│ ├── informational.py
│ ├── listeners.py
│ ├── mail.py
│ ├── npc.py
│ ├── xp.py
├── utils/ # Folder containing utility files for common functionality ├── utils/
│ ├── bank_functions.py # Economy and balance management functions │ ├── bank_functions.py
── sql_Commands.py # Database helper functions for handling SQL commands ── npc_data.py
│ ├── npc_handler.py
│ ├── npc_memory.py
│ ├── sql_commands.py
├── extras/ # Experimental or additional features (optional) ├── web/
── example.py ── __init__.py
├── app.py
└── docs/ # Documentation files │ ├── static/
── CheckList.txt # Planning and improvement list (optional) ── templates/
``` ```
--- ---
@@ -117,7 +129,7 @@ Make sure your bot has the appropriate permissions for each command. You can man
Store sensitive information like your bot token in the `.env` file. Avoid committing this file to version control. Store sensitive information like your bot token in the `.env` file. Avoid committing this file to version control.
### Database ### Database
The bot currently uses SQLite for user and balance data. You can update `sql_Commands.py` and `bank_functions.py` in the `utils` folder to manage other data or switch to another database if necessary. The bot currently uses MySQL for user and balance data through `utils/sql_commands.py` and `utils/bank_functions.py`. You can update these modules if you want to change the database engine or schema.
--- ---
@@ -136,7 +148,7 @@ Make sure to include docstrings for any new functions and comments for complex l
## License ## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. No license file is included in this repository. Add a `LICENSE` file if you want to make the license explicit.
--- ---
+9 -7
View File
@@ -1,8 +1,8 @@
# main.py # main.py
import discord import discord
from discord.ext import commands from discord.ext import commands
import itertools
import os import os
import sys
from dotenv import load_dotenv from dotenv import load_dotenv
from utils.sql_commands import initialize_database from utils.sql_commands import initialize_database
@@ -24,7 +24,7 @@ class Client(commands.Bot):
intents=discord.Intents.all(), intents=discord.Intents.all(),
help_command=MyNewHelp(), 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())))) prefixes = list(map(''.join, itertools.product(*zip(prefix.upper(), prefix.lower()))))
print(prefixes) print(prefixes)
@@ -46,12 +46,14 @@ def main():
load_dotenv() load_dotenv()
initialize_database() initialize_database()
client = Client() client = Client()
token = os.getenv("TOKEN") token = os.getenv("DISCORD_TOKEN") or os.getenv("TOKEN")
if token is not None: if not token:
threading.Thread(target=run_web, daemon=True).start() raise SystemExit("ERROR: Discord token not found. Set DISCORD_TOKEN or TOKEN in environment.")
# Print the token for debugging purposes (remove this in production)
print(f"DEBUG: Using token starting with {token[:5]}.")
client.run(token) client.run(token)
else:
print("Token is missing.")
if __name__ == "__main__": if __name__ == "__main__":
+133 -61
View File
@@ -1,49 +1,42 @@
import logging import logging
import discord import discord
from discord.ext import commands from discord.ext import commands
from utils.bank_functions import * from datetime import datetime, timedelta, timezone
from discord.ext import commands
from datetime import datetime, timedelta
from random import randint from random import randint
from typing import Literal from typing import Literal
import datetime
from utils.bank_functions import (
async def check_transfer( bank_data,
ctx: commands.Context, create_account,
payer_balance: int, reset_bank,
receiver_balance: int, update_daily_timestamp,
member: discord.Member, update_money,
amount: int,
) -> bool:
"""Check if a transfer is valid.
Args:
- ctx (commands.Context): The context of the invoked command.
- payer_balance (int): The balance of the payer.
- receiver_balance (int): The balance of the receiver.
- member (discord.Member): The member to check against.
- amount (int): The amount to transfer.
Returns:
- bool: Whether the transfer is valid.
"""
if payer_balance is None or receiver_balance is None:
await ctx.reply("Bank account doesn't exist.")
return False
if payer_balance >= amount > 0:
payer_balance = payer_balance - amount
receiver_balance = receiver_balance + amount
if ctx.author.id != member.id:
return True
else:
await ctx.reply("You cannot give yourself money.")
return False
else:
await ctx.reply(
f"You do not have {int(amount):,}<:flooney:1194943899765051473>.\nYou have {int(payer_balance):,}<:flooney:1194943899765051473>."
) )
return False from utils.sql_commands import DatabaseManager
def validate_transfer(
payer_balance: int, author_id: int, receiver_id: int, amount: int
) -> tuple[bool, str]:
# Validate amount is a positive number
if not isinstance(amount, (int, float)):
return False, "Amount must be a number"
if amount <= 0:
return False, "Please enter an amount greater than 0."
if payer_balance is None:
return False, "Your account does not exist."
if author_id == receiver_id:
return False, "You cannot give yourself money."
if payer_balance < amount:
return False, (
f"You do not have {amount:,}<:flooney:1194943899765051473>. "
f"You have {payer_balance:,}<:flooney:1194943899765051473>."
)
return True, ""
class Economy(commands.Cog): class Economy(commands.Cog):
@@ -213,7 +206,12 @@ class Economy(commands.Cog):
payer_wallet = int((await bank_data(ctx.author)).get("WALLET", 0)) payer_wallet = int((await bank_data(ctx.author)).get("WALLET", 0))
receiver_wallet = int((await bank_data(target)).get("WALLET", 0)) receiver_wallet = int((await bank_data(target)).get("WALLET", 0))
if await check_transfer(ctx, payer_wallet, receiver_wallet, target, amount): valid, reason = validate_transfer(
payer_wallet, ctx.author.id, target.id, amount
)
if not valid:
return await ctx.reply(reason, mention_author=False)
await update_money(target, wallet=amount) await update_money(target, wallet=amount)
await update_money(ctx.author, wallet=-amount) await update_money(ctx.author, wallet=-amount)
@@ -229,7 +227,12 @@ class Economy(commands.Cog):
payer_bank = int((await bank_data(ctx.author)).get("BANK", 0)) payer_bank = int((await bank_data(ctx.author)).get("BANK", 0))
receiver_bank = int((await bank_data(target)).get("BANK", 0)) receiver_bank = int((await bank_data(target)).get("BANK", 0))
if await check_transfer(ctx, payer_bank, receiver_bank, target, amount): valid, reason = validate_transfer(
payer_bank, ctx.author.id, target.id, amount
)
if not valid:
return await ctx.reply(reason, mention_author=False)
await update_money(target, bank=amount) await update_money(target, bank=amount)
await update_money(ctx.author, bank=-amount) await update_money(ctx.author, bank=-amount)
@@ -248,7 +251,7 @@ class Economy(commands.Cog):
user_data = await bank_data(ctx.author) user_data = await bank_data(ctx.author)
if user_data is None: if user_data is None:
await create_account(ctx) await create_account(ctx.author)
user_data = await bank_data(ctx.author) user_data = await bank_data(ctx.author)
wallet_balance = int(user_data.get("WALLET", 0)) wallet_balance = int(user_data.get("WALLET", 0))
@@ -268,7 +271,7 @@ class Economy(commands.Cog):
user_data = await bank_data(ctx.author) user_data = await bank_data(ctx.author)
if user_data is None: if user_data is None:
await create_account(ctx) await create_account(ctx.author)
user_data = await bank_data(ctx.author) user_data = await bank_data(ctx.author)
bank_balance = int(user_data.get("BANK", 0)) bank_balance = int(user_data.get("BANK", 0))
@@ -328,7 +331,7 @@ class Economy(commands.Cog):
title=f"Top {len(leaderboard_entries)} Richest Users - Leaderboard", title=f"Top {len(leaderboard_entries)} Richest Users - Leaderboard",
description="\n".join(leaderboard_entries), description="\n".join(leaderboard_entries),
color=discord.Color(0x00FF00), color=discord.Color(0x00FF00),
timestamp=datetime.datetime.utcnow(), timestamp=datetime.utcnow(),
) )
embed.set_footer(text=f"GLOBAL - {ctx.guild.name}") embed.set_footer(text=f"GLOBAL - {ctx.guild.name}")
await ctx.reply(embed=embed, mention_author=False) await ctx.reply(embed=embed, mention_author=False)
@@ -353,36 +356,32 @@ class Economy(commands.Cog):
try: try:
user_data = await bank_data(ctx.author) user_data = await bank_data(ctx.author)
last_claim = user_data.get("DAILY") last_claim = user_data.get("DAILY")
now = discord.utils.utcnow() # Modern, timezone-aware now = discord.utils.utcnow()
# Convert last_claim to datetime if it's a timestamp (int or float)
if last_claim: if last_claim:
if isinstance(last_claim, (int, float)): if isinstance(last_claim, (int, float)):
last_claim_dt = datetime.datetime.fromtimestamp( last_claim_dt = datetime.fromtimestamp(last_claim, tz=timezone.utc)
last_claim, tz=datetime.timezone.utc
)
elif isinstance(last_claim, str): elif isinstance(last_claim, str):
try: try:
last_claim_dt = datetime.datetime.fromtimestamp( last_claim_dt = datetime.fromtimestamp(
float(last_claim), tz=datetime.timezone.utc float(last_claim), tz=timezone.utc
) )
except Exception: except ValueError:
last_claim_dt = None last_claim_dt = None
elif isinstance(last_claim, datetime.datetime): elif isinstance(last_claim, datetime):
# If it's naive, make it aware last_claim_dt = (
if last_claim.tzinfo is None: last_claim.replace(tzinfo=timezone.utc)
last_claim_dt = last_claim.replace(tzinfo=datetime.timezone.utc) if last_claim.tzinfo is None
else: else last_claim
last_claim_dt = last_claim )
else: else:
last_claim_dt = None last_claim_dt = None
else: else:
last_claim_dt = None last_claim_dt = None
if not last_claim_dt or (now - last_claim_dt).total_seconds() >= 86400: if not last_claim_dt or (now - last_claim_dt) >= timedelta(days=1):
daily_reward = randint(200, 1000) daily_reward = randint(200, 1000)
await update_money(ctx.author, daily_reward) await update_money(ctx.author, wallet=daily_reward)
# Save the new timestamp as a float (UNIX time)
await update_daily_timestamp(ctx.author, now) await update_daily_timestamp(ctx.author, now)
await ctx.reply( await ctx.reply(
f"Your daily pocket money is {daily_reward:,}<:flooney:1194943899765051473>", f"Your daily pocket money is {daily_reward:,}<:flooney:1194943899765051473>",
@@ -404,6 +403,79 @@ class Economy(commands.Cog):
mention_author=False, mention_author=False,
) )
@commands.command(name="bank", brief="Check your bank balance", description="Check your bank balance.")
async def _bank(self, ctx: commands.Context, member: discord.Member | None = None):
"""Check your bank balance."""
target: discord.Member | discord.User = member or ctx.author
# Ensure the target is not a bot
if target.bot:
return
# Get the user's data
user_data = await bank_data(target)
wallet_balance = user_data.get("WALLET", 0)
bank_balance = user_data.get("BANK", 0)
# Create an account if one does not exist
if bank_balance is None or wallet_balance is None:
await create_account(target)
user_data = await bank_data(target)
wallet_balance = user_data.get("WALLET", 0)
bank_balance = user_data.get("BANK", 0)
# Reply with the user's bank balance
await ctx.reply(
f"{target.mention} has {bank_balance:,}<:flooney:1194943899765051473> in their bank."
)
@commands.command(name="wallet", brief="Check your wallet balance", description="Check your wallet balance.")
async def _wallet(self, ctx: commands.Context, member: discord.Member | None = None):
"""Check your wallet balance."""
target: discord.Member | discord.User = member or ctx.author
# Ensure the target is not a bot
if target.bot:
return
# Get the user's data
user_data = await bank_data(target)
wallet_balance = user_data.get("WALLET", 0)
bank_balance = user_data.get("BANK", 0)
# Create an account if one does not exist
if bank_balance is None or wallet_balance is None:
await create_account(target)
user_data = await bank_data(target)
wallet_balance = user_data.get("WALLET", 0)
bank_balance = user_data.get("BANK", 0)
# Reply with the user's wallet balance
await ctx.reply(
f"{target.mention} has {wallet_balance:,}<:flooney:1194943899765051473> in their wallet."
)
@commands.command(name="transfer", brief="Transfer money from bank to another user", description="Transfer money from your bank to another user's bank.")
async def _transfer(self, ctx, target: discord.Member, amount: int):
"""
Transfer money from your bank account to another user's bank account.
"""
payer_bank = int((await bank_data(ctx.author)).get("BANK", 0))
receiver_bank = int((await bank_data(target)).get("BANK", 0))
valid, reason = validate_transfer(
payer_bank, ctx.author.id, target.id, amount
)
if not valid:
return await ctx.reply(reason, mention_author=False)
await update_money(target, bank=amount)
await update_money(ctx.author, bank=-amount)
await ctx.reply(
f"Transferred {amount:,} flooneys to {target}.", mention_author=False
)
r""" r"""
.----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------.
| .--------------. || .--------------. || .--------------. || .--------------. | | .--------------. || .--------------. || .--------------. || .--------------. |
+79 -85
View File
@@ -7,11 +7,83 @@ from os import getenv
import html import html
from datetime import datetime 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): class Mail(commands.Cog):
def __init__(self, client:commands.Bot): def __init__(self, client:commands.Bot):
self.client = client self.client = client
load_dotenv()
from utils.sql_commands import DatabaseManager from utils.sql_commands import DatabaseManager
self.db = DatabaseManager() self.db = DatabaseManager()
@@ -33,99 +105,21 @@ class Mail(commands.Cog):
return return
try: try:
port = int(port) # type: ignore # Error invalid as for problem is taken care of above port = int(port)
s = smtplib.SMTP(host=server, port=port) # type: ignore with smtplib.SMTP(host=server, port=port) as s:
s.starttls() s.starttls()
s.login(username, password) # type: ignore s.login(username, password)
msg = MIMEMultipart("alternative") msg = MIMEMultipart("alternative")
msg["To"] = receiver # type: ignore msg["To"] = receiver
msg["From"] = username # type: ignore msg["From"] = username
msg["Subject"] = "Py feedback" msg["Subject"] = "Py feedback"
# Fetch feedback from the database
feedback_rows: list[dict[str, str]] = self.db.fetch_all("SELECT * FROM feedback") feedback_rows: list[dict[str, str]] = self.db.fetch_all("SELECT * FROM feedback")
all_feedback = "" text = build_feedback_html(feedback_rows)
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>
"""
msg.attach(MIMEText(text, "html")) msg.attach(MIMEText(text, "html"))
s.send_message(msg) s.send_message(msg)
await ctx.reply("Mail sent.", delete_after=2) await ctx.reply("Mail sent.", delete_after=2)
s.quit()
except Exception as e: except Exception as e:
await ctx.reply(f"Failed to send mail: {e}", delete_after=5) 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": { "messages": {
"601579326714019840": { "601579326714019840": {
"total": 106, "total": 137,
"commands": 94, "commands": 118,
"non_commands": 12 "non_commands": 19
} }
}, },
"commands": { "commands": {
"purge": 5, "purge": 6,
"mail_feedback": 2, "mail_feedback": 2,
"help": 11, "help": 16,
"nuke": 4, "nuke": 4,
"ping": 2, "ping": 2,
"top": 2, "top": 3,
"stats": 2, "stats": 3,
"poker": 7, "poker": 7,
"balance": 5, "balance": 8,
"daily": 4, "daily": 8,
"withdraw": 2, "withdraw": 2,
"leaderboard": 2, "leaderboard": 3,
"afk": 2, "afk": 3,
"afklist": 1, "afklist": 1,
"whois": 1, "whois": 1,
"reset_money": 2, "reset_money": 2,
"listcommands": 1, "listcommands": 3,
"talk": 27, "talk": 27,
"npcs": 2, "npcs": 2,
"remove_money": 1, "remove_money": 1,
"guildids": 1, "guildids": 2,
"give_money": 1, "give_money": 2,
"coinflip": 2, "coinflip": 2,
"deposit": 1, "deposit": 1,
"gameroom": 3, "gameroom": 3,
"reload": 1 "reload": 1,
"slots": 2,
"addcommand": 1
}, },
"channels": { "channels": {
"bot": 86, "bot": 91,
"membercount": 3, "membercount": 3,
"members-3": 2, "members-3": 28,
"faq": 1, "faq": 1,
"polls": 8, "polls": 8,
"bot-commands": 2, "bot-commands": 2,
@@ -45,8 +47,8 @@
"general": 3 "general": 3
}, },
"guilds": { "guilds": {
"Plex": 89, "Plex": 94,
"TEST SERVER BOT": 17 "TEST SERVER BOT": 43
}, },
"total_messages": 0, "total_messages": 0,
"command_messages": 0, "command_messages": 0,
+2 -4
View File
@@ -15,9 +15,7 @@ itsdangerous==2.2.0
Jinja2==3.1.6 Jinja2==3.1.6
MarkupSafe==3.0.3 MarkupSafe==3.0.3
multidict==6.1.0 multidict==6.1.0
mysql-connector==2.2.9 mysql-connector-python>=8.1.0,<9
numpy==2.1.3
pandas==2.2.3
pillow==12.1.1 pillow==12.1.1
propcache==0.2.0 propcache==0.2.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
@@ -29,4 +27,4 @@ tzdata==2024.2
urllib3==2.2.3 urllib3==2.2.3
Werkzeug==3.1.7 Werkzeug==3.1.7
yarl==1.17.1 yarl==1.17.1
waitress==2.2.0 waitress==3.0.2
+7 -4
View File
@@ -4,7 +4,7 @@ from typing import Dict
from datetime import datetime from datetime import datetime
db = DatabaseManager("economy") db = DatabaseManager()
async def create_account(user: discord.Member | discord.User): async def create_account(user: discord.Member | discord.User):
@@ -27,7 +27,11 @@ async def bank_data(user: discord.Member | discord.User) -> Dict[str, int]:
if balance is None: if balance is None:
await create_account(user) await create_account(user)
return await bank_data(user) return await bank_data(user)
return balance # Ensure we return a dictionary with WALLET and BANK keys
return {
"WALLET": balance.get("WALLET", 0),
"BANK": balance.get("BANK", 0)
}
async def record_transaction( async def record_transaction(
@@ -88,5 +92,4 @@ async def update_daily_timestamp(user: discord.User | discord.Member, timestamp:
Updates the DAILY_TIMESTAMP field for the user in the economy table. Updates the DAILY_TIMESTAMP field for the user in the economy table.
Stores the timestamp as a float (UNIX time). Stores the timestamp as a float (UNIX time).
""" """
db.execute_query("UPDATE economy SET DAILY = %s WHERE ID = %s",(timestamp.timestamp(), user.id), db.execute_query("UPDATE economy SET DAILY = %s WHERE ID = %s",(timestamp.timestamp(), user.id))
)
+30 -21
View File
@@ -7,7 +7,6 @@ import re
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging import logging
import pandas as pd
# Configure logging # Configure logging
@@ -22,8 +21,17 @@ logger = logging.getLogger(__name__)
class DatabaseManager: class DatabaseManager:
_instances = {} _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"): def __new__(cls, env="development"):
instance_key = env or "default" instance_key = cls._resolve_instance_key(env)
if instance_key in cls._instances: if instance_key in cls._instances:
return cls._instances[instance_key] return cls._instances[instance_key]
instance = super().__new__(cls) instance = super().__new__(cls)
@@ -45,12 +53,17 @@ class DatabaseManager:
"user": os.getenv("SQLUSER", "root"), "user": os.getenv("SQLUSER", "root"),
"password": os.getenv("SQLPASS", ""), "password": os.getenv("SQLPASS", ""),
"database": os.getenv("SQLDB", "testdb"), "database": os.getenv("SQLDB", "testdb"),
"pool_reset_session": os.getenv("POOL_RESET_SESSION", "false").lower() "pool_reset_session": self._parse_bool(
in ("true", "1", "yes"), os.getenv("POOL_RESET_SESSION", "false")
),
} }
instance_key = self._resolve_instance_key(env)
pool_name = f"mypool_{instance_key}" if instance_key else "mypool_default"
self.pool = pooling.MySQLConnectionPool( 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.") logger.info("Database connection pool created.")
@@ -234,6 +247,10 @@ class DatabaseManager:
raise ValueError(f"Invalid SQL identifier: {identifier}") raise ValueError(f"Invalid SQL identifier: {identifier}")
return identifier return identifier
@staticmethod
def _parse_bool(value: str) -> bool:
return str(value).strip().lower() in {"true", "1", "yes", "y"}
def _parse_insert_columns(self, query: str) -> list[str]: def _parse_insert_columns(self, query: str) -> list[str]:
match = re.search( match = re.search(
r"INSERT\s+INTO\s+\S+\s*\(([^)]+)\)\s*VALUES", r"INSERT\s+INTO\s+\S+\s*\(([^)]+)\)\s*VALUES",
@@ -256,7 +273,8 @@ class DatabaseManager:
cursor.execute(query, params or ()) cursor.execute(query, params or ())
connection.commit() connection.commit()
logger.info(f"Executed query: {query} with params: {params}") logger.info(f"Executed query: {query} with params: {params}")
return cursor.rowcount # Return the actual cursor results instead of closing it
return cursor.fetchall() if cursor.with_rows else cursor.rowcount
except mysql.connector.Error as err: except mysql.connector.Error as err:
logger.warning(f"Attempt {attempt + 1} failed: {err}") logger.warning(f"Attempt {attempt + 1} failed: {err}")
time.sleep(delay * (2**attempt)) time.sleep(delay * (2**attempt))
@@ -365,22 +383,13 @@ class DatabaseManager:
connection.close() connection.close()
def fetch_as_dataframe(self, query, params=None): def fetch_as_dataframe(self, query, params=None):
connection = None results = self.fetch_all(query, params)
cursor = None
try: try:
connection = self.get_connection() import pandas as pd
cursor = connection.cursor(dictionary=True, buffered=True) except ImportError as err:
cursor.execute(query, params or ()) logger.error("Pandas is not installed; fetch_as_dataframe cannot return a DataFrame.")
if cursor.with_rows: raise
results = cursor.fetchall() return pd.DataFrame(results)
return pd.DataFrame(results) if results else pd.DataFrame()
logger.warning("No result set to fetch from.")
return pd.DataFrame()
finally:
if cursor:
cursor.close()
if connection:
connection.close()
def create_table_if_not_exists(self, table_name, schema): def create_table_if_not_exists(self, table_name, schema):
table_name = self._sanitize_identifier(table_name) table_name = self._sanitize_identifier(table_name)
Binary file not shown.
+21 -12
View File
@@ -11,6 +11,7 @@ else:
import os import os
import requests import requests
from urllib.parse import urlencode
from flask import Flask, redirect, url_for, session, request, render_template, flash from flask import Flask, redirect, url_for, session, request, render_template, flash
from dotenv import load_dotenv from dotenv import load_dotenv
from collections import defaultdict from collections import defaultdict
@@ -49,6 +50,19 @@ DISCORD_BOT_TOKEN = os.getenv("TOKEN")
http = requests.Session() http = requests.Session()
http.headers.update({"User-Agent": "DiscordBotWeb/1.0"}) 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): def discord_request(method, endpoint, headers=None, **kwargs):
url = f"{DISCORD_API_BASE_URL}{endpoint}" url = f"{DISCORD_API_BASE_URL}{endpoint}"
try: 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) app.logger.warning("Discord API request failed: %s %s %s", method, url, exc)
raise raise
# Default request timeout (seconds) for external HTTP calls
REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", "10"))
# Check for missing environment variables # Check for missing environment variables
if not all([DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI]): if not all([DISCORD_CLIENT_ID, DISCORD_CLIENT_SECRET, DISCORD_REDIRECT_URI]):
raise EnvironmentError("One or more required environment variables are missing.") 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 # OAuth2 login route
@app.route("/login") @app.route("/login")
def login(): def login():
discord_auth_url = ( return redirect(build_discord_oauth_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)
def can_add_bot(guild): def can_add_bot(guild):
@@ -174,7 +180,6 @@ def callback():
def logout(): def logout():
session.pop("user", None) session.pop("user", None)
session.pop("guilds", None) session.pop("guilds", None)
session.pop("access_token", None) # Ensure access token is also cleared
session.pop("guilds_with_bot_permission", None) session.pop("guilds_with_bot_permission", None)
flash("You have been logged out.") flash("You have been logged out.")
return redirect(url_for("home")) return redirect(url_for("home"))
@@ -253,8 +258,12 @@ def transactions():
flash("You are not logged in.") flash("You are not logged in.")
return redirect(url_for("login")) return redirect(url_for("login"))
sort_by = request.args.get("sort", "date") sort_by = request.args.get("sort", "date").lower()
order = request.args.get("order", "asc") 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) user_transactions = get_user_transactions(str(user["id"]), sort_by, order)
daily_totals = defaultdict(float) daily_totals = defaultdict(float)