Compare commits
4 Commits
ccc944ef84
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b315069b1c | |||
| 4b07ca86b9 | |||
| 3e6410d112 | |||
| 5fdb37f7f8 |
+19
-1
@@ -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
|
||||||
|
._*
|
||||||
@@ -56,25 +56,37 @@ Here’s 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
@@ -42,16 +42,18 @@ class Client(commands.Bot):
|
|||||||
print("Loaded cogs")
|
print("Loaded cogs")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
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.")
|
||||||
client.run(token)
|
|
||||||
else:
|
# Print the token for debugging purposes (remove this in production)
|
||||||
print("Token is missing.")
|
print(f"DEBUG: Using token starting with {token[:5]}.")
|
||||||
|
|
||||||
|
client.run(token)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+143
-71
@@ -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 (
|
||||||
|
bank_data,
|
||||||
|
create_account,
|
||||||
|
reset_bank,
|
||||||
|
update_daily_timestamp,
|
||||||
|
update_money,
|
||||||
|
)
|
||||||
|
from utils.sql_commands import DatabaseManager
|
||||||
|
|
||||||
|
|
||||||
async def check_transfer(
|
def validate_transfer(
|
||||||
ctx: commands.Context,
|
payer_balance: int, author_id: int, receiver_id: int, amount: int
|
||||||
payer_balance: int,
|
) -> tuple[bool, str]:
|
||||||
receiver_balance: int,
|
# Validate amount is a positive number
|
||||||
member: discord.Member,
|
if not isinstance(amount, (int, float)):
|
||||||
amount: int,
|
return False, "Amount must be a number"
|
||||||
) -> bool:
|
if amount <= 0:
|
||||||
"""Check if a transfer is valid.
|
return False, "Please enter an amount greater than 0."
|
||||||
|
|
||||||
Args:
|
if payer_balance is None:
|
||||||
- ctx (commands.Context): The context of the invoked command.
|
return False, "Your account does not exist."
|
||||||
- payer_balance (int): The balance of the payer.
|
|
||||||
- receiver_balance (int): The balance of the receiver.
|
if author_id == receiver_id:
|
||||||
- member (discord.Member): The member to check against.
|
return False, "You cannot give yourself money."
|
||||||
- amount (int): The amount to transfer.
|
|
||||||
|
if payer_balance < amount:
|
||||||
Returns:
|
return False, (
|
||||||
- bool: Whether the transfer is valid.
|
f"You do not have {amount:,}<:flooney:1194943899765051473>. "
|
||||||
"""
|
f"You have {payer_balance:,}<:flooney:1194943899765051473>."
|
||||||
if payer_balance is None or receiver_balance is None:
|
)
|
||||||
await ctx.reply("Bank account doesn't exist.")
|
|
||||||
return False
|
return True, ""
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
class Economy(commands.Cog):
|
class Economy(commands.Cog):
|
||||||
@@ -213,13 +206,18 @@ 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(
|
||||||
await update_money(target, wallet=amount)
|
payer_wallet, ctx.author.id, target.id, amount
|
||||||
await update_money(ctx.author, wallet=-amount)
|
)
|
||||||
|
if not valid:
|
||||||
|
return await ctx.reply(reason, mention_author=False)
|
||||||
|
|
||||||
await ctx.reply(
|
await update_money(target, wallet=amount)
|
||||||
f"You gave {target} {amount:,} flooneys.", mention_author=False
|
await update_money(ctx.author, wallet=-amount)
|
||||||
)
|
|
||||||
|
await ctx.reply(
|
||||||
|
f"You gave {target} {amount:,} flooneys.", mention_author=False
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command(name="transfer")
|
@commands.command(name="transfer")
|
||||||
async def _transfer(self, ctx, target: discord.Member, amount: int):
|
async def _transfer(self, ctx, target: discord.Member, amount: int):
|
||||||
@@ -229,13 +227,18 @@ 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(
|
||||||
await update_money(target, bank=amount)
|
payer_bank, ctx.author.id, target.id, amount
|
||||||
await update_money(ctx.author, bank=-amount)
|
)
|
||||||
|
if not valid:
|
||||||
|
return await ctx.reply(reason, mention_author=False)
|
||||||
|
|
||||||
await ctx.reply(
|
await update_money(target, bank=amount)
|
||||||
f"Transferred {amount:,} flooneys to {target}.", mention_author=False
|
await update_money(ctx.author, bank=-amount)
|
||||||
)
|
|
||||||
|
await ctx.reply(
|
||||||
|
f"Transferred {amount:,} flooneys to {target}.", mention_author=False
|
||||||
|
)
|
||||||
|
|
||||||
@commands.command(
|
@commands.command(
|
||||||
name="deposit",
|
name="deposit",
|
||||||
@@ -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"""
|
||||||
.----------------. .----------------. .----------------. .----------------.
|
.----------------. .----------------. .----------------. .----------------.
|
||||||
| .--------------. || .--------------. || .--------------. || .--------------. |
|
| .--------------. || .--------------. || .--------------. || .--------------. |
|
||||||
|
|||||||
+86
-92
@@ -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")
|
text = build_feedback_html(feedback_rows)
|
||||||
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>
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
msg.attach(MIMEText(text, "html"))
|
||||||
|
s.send_message(msg)
|
||||||
text = f"""<!DOCTYPE html>
|
await ctx.reply("Mail sent.", delete_after=2)
|
||||||
<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"))
|
|
||||||
s.send_message(msg)
|
|
||||||
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)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
)
|
|
||||||
|
|||||||
+83
-74
@@ -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",
|
||||||
@@ -246,54 +263,55 @@ class DatabaseManager:
|
|||||||
)
|
)
|
||||||
return [col.strip() for col in match.group(1).split(",") if col.strip()]
|
return [col.strip() for col in match.group(1).split(",") if col.strip()]
|
||||||
|
|
||||||
def execute_query(self, query, params=None, retries=3, delay=1):
|
def execute_query(self, query, params=None, retries=3, delay=1):
|
||||||
connection = None
|
connection = None
|
||||||
cursor = None
|
cursor = None
|
||||||
for attempt in range(retries):
|
for attempt in range(retries):
|
||||||
try:
|
try:
|
||||||
connection = self.get_connection()
|
connection = self.get_connection()
|
||||||
cursor = connection.cursor(dictionary=True, buffered=True)
|
cursor = connection.cursor(dictionary=True, buffered=True)
|
||||||
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
|
||||||
except mysql.connector.Error as err:
|
return cursor.fetchall() if cursor.with_rows else cursor.rowcount
|
||||||
logger.warning(f"Attempt {attempt + 1} failed: {err}")
|
except mysql.connector.Error as err:
|
||||||
time.sleep(delay * (2**attempt))
|
logger.warning(f"Attempt {attempt + 1} failed: {err}")
|
||||||
finally:
|
time.sleep(delay * (2**attempt))
|
||||||
if cursor:
|
finally:
|
||||||
cursor.close()
|
if cursor:
|
||||||
if connection:
|
cursor.close()
|
||||||
connection.close()
|
if connection:
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
logger.error(f"All {retries} attempts failed for query: {query}")
|
||||||
|
return None
|
||||||
|
|
||||||
logger.error(f"All {retries} attempts failed for query: {query}")
|
def insert(self, query: str, params: tuple, overwrite: bool = True) -> None:
|
||||||
return None
|
"""
|
||||||
|
Inserts data into the database using the given query and parameters.
|
||||||
def insert(self, query: str, params: tuple, overwrite: bool = True) -> None:
|
|
||||||
"""
|
Args:
|
||||||
Inserts data into the database using the given query and parameters.
|
query (str): The SQL query to execute.
|
||||||
|
params (tuple): The parameters to pass into the query.
|
||||||
Args:
|
overwrite (bool, optional): Whether to perform an upsert operation. Defaults to True.
|
||||||
query (str): The SQL query to execute.
|
|
||||||
params (tuple): The parameters to pass into the query.
|
Raises:
|
||||||
overwrite (bool, optional): Whether to perform an upsert operation. Defaults to True.
|
ValueError: If no parameters are provided.
|
||||||
|
"""
|
||||||
Raises:
|
if not params:
|
||||||
ValueError: If no parameters are provided.
|
raise ValueError("Params must be provided for the insert operation.")
|
||||||
"""
|
|
||||||
if not params:
|
if overwrite:
|
||||||
raise ValueError("Params must be provided for the insert operation.")
|
columns = self._parse_insert_columns(query)
|
||||||
|
update_set = ", ".join(f"{col} = VALUES({col})" for col in columns)
|
||||||
if overwrite:
|
query = f"{query} ON DUPLICATE KEY UPDATE {update_set}"
|
||||||
columns = self._parse_insert_columns(query)
|
|
||||||
update_set = ", ".join(f"{col} = VALUES({col})" for col in columns)
|
rowcount = self.execute_query(query, params)
|
||||||
query = f"{query} ON DUPLICATE KEY UPDATE {update_set}"
|
if rowcount is None:
|
||||||
|
logger.error(f"Insert failed with query: {query}.")
|
||||||
rowcount = self.execute_query(query, params)
|
else:
|
||||||
if rowcount is None:
|
logger.info(f"Insert completed with query: {query}.")
|
||||||
logger.error(f"Insert failed with query: {query}.")
|
|
||||||
else:
|
|
||||||
logger.info(f"Insert completed with query: {query}.")
|
|
||||||
|
|
||||||
def bulk_insert(self, query, params=None):
|
def bulk_insert(self, query, params=None):
|
||||||
if not params:
|
if not params:
|
||||||
@@ -328,13 +346,13 @@ class DatabaseManager:
|
|||||||
if connection:
|
if connection:
|
||||||
connection.close()
|
connection.close()
|
||||||
|
|
||||||
def delete(self, table_name: str, condition: dict) -> None:
|
def delete(self, table_name: str, condition: dict) -> None:
|
||||||
"""Deletes a record from the specified table based on the condition provided."""
|
"""Deletes a record from the specified table based on the condition provided."""
|
||||||
table_name = self._sanitize_identifier(table_name)
|
table_name = self._sanitize_identifier(table_name)
|
||||||
condition_column, condition_value = next(iter(condition.items()))
|
condition_column, condition_value = next(iter(condition.items()))
|
||||||
condition_column = self._sanitize_identifier(condition_column)
|
condition_column = self._sanitize_identifier(condition_column)
|
||||||
query = f"DELETE FROM {table_name} WHERE {condition_column} = %s"
|
query = f"DELETE FROM {table_name} WHERE {condition_column} = %s"
|
||||||
self.execute_query(query, (condition_value,))
|
self.execute_query(query, (condition_value,))
|
||||||
|
|
||||||
def fetch_one(self, query, params=None):
|
def fetch_one(self, query, params=None):
|
||||||
connection = None
|
connection = None
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user