First Commit
This commit is contained in:
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
+224
@@ -0,0 +1,224 @@
|
||||
import datetime
|
||||
import time
|
||||
import os
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
|
||||
async def reload_all(client, ctx):
|
||||
"""
|
||||
Reloads all cogs (modules) in the 'cogs' folder and syncs the command tree.
|
||||
|
||||
Args:
|
||||
client (discord.Client): The bot's main client object.
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
"""
|
||||
cogs_folder = os.path.abspath(os.path.dirname(__file__)) # Get the cogs folder path
|
||||
for filename in os.listdir(cogs_folder):
|
||||
if filename.endswith(".py"):
|
||||
try:
|
||||
await client.reload_extension(f"cogs.{filename[:-3]}")
|
||||
except Exception as e:
|
||||
print(f"Error reloading {filename}: {e}")
|
||||
await client.load_extension(f"cogs.{filename[:-3]}")
|
||||
await client.tree.sync() # Synchronize command tree
|
||||
await ctx.reply("Reloaded all cogs.")
|
||||
|
||||
|
||||
class Admin(commands.Cog):
|
||||
"""Cog that provides administrative commands for bot control and information."""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@commands.command(
|
||||
name="ping",
|
||||
brief="Bot Latency",
|
||||
description="Displays the bot's latency in milliseconds.",
|
||||
)
|
||||
async def _ping(self, ctx):
|
||||
"""
|
||||
Responds with the bot's latency (ping time).
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
"""
|
||||
latency = round(self.client.latency * 1000, 2) # Convert to ms
|
||||
await ctx.reply(f"Pong! 🏓\nLatency: {latency}ms")
|
||||
|
||||
@commands.command(
|
||||
name="info",
|
||||
brief="Bot Info",
|
||||
description="Provides information about the bot's owner and team (if applicable).",
|
||||
)
|
||||
async def _info(self, ctx):
|
||||
"""
|
||||
Displays information about the bot's owner or team members.
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
"""
|
||||
app_info = await self.client.application_info()
|
||||
if app_info.team:
|
||||
team_members = "\n".join(
|
||||
f"{member.name}#{member.discriminator} ({'Owner' if member.id == app_info.team.owner_id else 'Member'})"
|
||||
for member in app_info.team.members
|
||||
)
|
||||
await ctx.reply(
|
||||
f"Bot is part of the team: {app_info.team.name}\nMembers:\n{team_members}"
|
||||
)
|
||||
else:
|
||||
await ctx.reply(
|
||||
f"Bot is owned by {app_info.owner} and is not part of a team."
|
||||
)
|
||||
|
||||
@commands.command(
|
||||
name="uptime",
|
||||
brief="Bot Runtime",
|
||||
description="Displays how long the bot has been running since its last restart.",
|
||||
)
|
||||
async def _uptime(self, ctx):
|
||||
"""
|
||||
Calculates and sends the bot's uptime based on a start timestamp.
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
"""
|
||||
with open("time.txt", "r") as file:
|
||||
start_time = float(file.read())
|
||||
uptime = str(
|
||||
datetime.timedelta(seconds=int(time.time() - start_time))
|
||||
) # Calculate uptime
|
||||
await ctx.send(f"Uptime: {uptime}")
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="reload",
|
||||
brief="Reload Cog",
|
||||
description="Reloads a specific cog or all cogs.",
|
||||
aliases=["rl"],
|
||||
)
|
||||
@commands.is_owner()
|
||||
async def _reload(self, ctx, extension: str):
|
||||
"""
|
||||
Reloads a specified cog/module or all modules if 'all' is passed.
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
extension (str): Name of the cog to reload, or 'all' to reload all cogs.
|
||||
"""
|
||||
if extension.lower() == "all":
|
||||
await reload_all(self.client, ctx)
|
||||
return
|
||||
try:
|
||||
await self.client.unload_extension(f"cogs.{extension}")
|
||||
await self.client.load_extension(f"cogs.{extension}")
|
||||
await ctx.send(f"Successfully reloaded {extension}.")
|
||||
except commands.ExtensionNotLoaded:
|
||||
await ctx.send(f"Extension {extension} was not loaded, loading it now.")
|
||||
try:
|
||||
await self.client.load_extension(f"cogs.{extension}")
|
||||
await ctx.send(f"Successfully loaded {extension}.")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Failed to load {extension}: {e}")
|
||||
except commands.ExtensionNotFound:
|
||||
await ctx.send(f"Extension {extension} was not found.")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Failed to reload {extension}: {e}")
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="purge",
|
||||
brief="Delete messages",
|
||||
description="Deletes a specified number of messages from the current channel (default: 100).",
|
||||
)
|
||||
@commands.has_permissions(manage_channels=True)
|
||||
@commands.cooldown(1, 10, commands.BucketType.user)
|
||||
async def _purge(self, ctx, n: int = 100):
|
||||
"""
|
||||
Deletes a specified number of messages from the current channel.
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
n (int, optional): The number of messages to delete. Defaults to 100.
|
||||
"""
|
||||
n += 1
|
||||
if n <= 0:
|
||||
return
|
||||
if n < 100:
|
||||
await ctx.channel.purge(limit=n)
|
||||
await ctx.channel.send(
|
||||
f"Successfully purged last {n-1} messages", delete_after=2
|
||||
)
|
||||
elif n == 100:
|
||||
await ctx.channel.purge()
|
||||
await ctx.channel.send(
|
||||
"Successfully purged last 100 messages", delete_after=2
|
||||
)
|
||||
else: # n > 100
|
||||
for i in range(n // 100):
|
||||
await ctx.channel.purge()
|
||||
await ctx.channel.purge(limit=n % 100)
|
||||
await ctx.channel.send(
|
||||
f"Successfully purged last {n-1} messages", delete_after=2
|
||||
)
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="nuke",
|
||||
brief="Clear channel",
|
||||
description="Deletes all messages by duplicating and deleting the channel.",
|
||||
)
|
||||
@commands.has_permissions(manage_channels=True)
|
||||
@commands.cooldown(1, 30, commands.BucketType.user)
|
||||
async def _nuke(self, ctx, channel_name: str = "Current"):
|
||||
"""
|
||||
Duplicates and deletes a specified channel (or the current channel) to clear messages.
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
channel_name (str, optional): Name of the channel to nuke. Defaults to 'Current' (the invoking channel).
|
||||
"""
|
||||
# Determine the channel ID based on the channel_name parameter
|
||||
if channel_name.lower() == "current":
|
||||
channel_id = ctx.channel.id
|
||||
else:
|
||||
channel_id = discord.utils.get(ctx.guild.channels, name=channel_name)
|
||||
if channel_id is None:
|
||||
return await ctx.send(f"Channel **{channel_name}** was not found.")
|
||||
|
||||
existing_channel = self.client.get_channel(channel_id)
|
||||
if existing_channel:
|
||||
await existing_channel.clone(reason="Channel has been nuked")
|
||||
await existing_channel.delete()
|
||||
await self._send_embed(
|
||||
ctx,
|
||||
f"Channel **{existing_channel.name}** has been nuked.",
|
||||
discord.Color.red(),
|
||||
)
|
||||
else:
|
||||
await ctx.send(f"No channel with ID {channel_id} was found.")
|
||||
|
||||
@commands.command(
|
||||
name="guildids",
|
||||
brief="List all guild IDs",
|
||||
description="Lists all guild IDs the bot is currently in. Owner only.",
|
||||
)
|
||||
@commands.is_owner()
|
||||
async def _guildids(self, ctx):
|
||||
"""
|
||||
Sends a list of all guild IDs the bot is in to the owner.
|
||||
|
||||
Args:
|
||||
ctx (commands.Context): The context in which the command was invoked.
|
||||
"""
|
||||
guild_ids = [str(guild.id) for guild in self.client.guilds]
|
||||
guilds_str = "\n".join(guild_ids)
|
||||
await ctx.author.send(f"Guild IDs:\n{guilds_str}")
|
||||
await ctx.reply("Sent you a DM with all guild IDs.")
|
||||
|
||||
async def _send_embed(self, ctx, description, color=discord.Color.blurple()):
|
||||
embed = discord.Embed(description=description, color=color)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(client):
|
||||
"""Sets up the Admin cog for the bot."""
|
||||
await client.add_cog(Admin(client))
|
||||
Executable
+224
@@ -0,0 +1,224 @@
|
||||
import discord, re
|
||||
from discord.ext import commands
|
||||
from utils.sql_commands import DatabaseManager
|
||||
|
||||
|
||||
class CustomCommandsCog(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.db = DatabaseManager("")
|
||||
self.command_cache = {} # {guild_id: [commands_dicts]}
|
||||
|
||||
def get_guild_commands(self, guild_id):
|
||||
if guild_id not in self.command_cache:
|
||||
self.command_cache[guild_id] = self.db.fetch_all(
|
||||
"SELECT * FROM custom_commands WHERE GUILDID = %s", (guild_id,)
|
||||
)
|
||||
return self.command_cache[guild_id]
|
||||
|
||||
def invalidate_cache(self, guild_id):
|
||||
if guild_id in self.command_cache:
|
||||
del self.command_cache[guild_id]
|
||||
|
||||
@commands.hybrid_command(name="addcommand")
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.cooldown(1, 10, commands.BucketType.user)
|
||||
async def add_command(self, ctx, command_name: str, *, response: str):
|
||||
"""Add a new custom command"""
|
||||
guild_id = str(ctx.guild.id)
|
||||
command_name = command_name.lower()
|
||||
|
||||
existing_command = self.db.fetch_one(
|
||||
"SELECT 1 FROM custom_commands WHERE GUILDID = %s AND COMMANDNAME = %s",
|
||||
(guild_id, command_name),
|
||||
)
|
||||
if existing_command:
|
||||
await ctx.send(f"A command with the name `{command_name}` already exists.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Insert the new command into the database
|
||||
self.db.execute_query(
|
||||
"INSERT INTO custom_commands (GUILDID, COMMANDNAME, RESPONSE) VALUES (%s, %s, %s)",
|
||||
(guild_id, command_name, response),
|
||||
)
|
||||
self.invalidate_cache(guild_id) # Invalidate cache after change
|
||||
await ctx.send(f"Custom command `{command_name}` has been added!")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Failed to add command `{command_name}`. Error: {e}")
|
||||
|
||||
@commands.hybrid_command(name="delcommand")
|
||||
@commands.has_permissions(manage_guild=True)
|
||||
@commands.cooldown(1, 10, commands.BucketType.user)
|
||||
async def delete_command(self, ctx, command_name: str):
|
||||
"""Delete a custom command"""
|
||||
guild_id = str(ctx.guild.id)
|
||||
command_name = command_name.lower()
|
||||
|
||||
try:
|
||||
# Delete the command from the database
|
||||
deleted_rows = self.db.execute_query(
|
||||
"DELETE FROM custom_commands WHERE GUILDID = %s AND COMMANDNAME = %s",
|
||||
(guild_id, command_name),
|
||||
)
|
||||
self.invalidate_cache(guild_id) # Invalidate cache after change
|
||||
|
||||
if deleted_rows is not None and len(deleted_rows) > 0:
|
||||
await ctx.send(f"Custom command `{command_name}` has been deleted!")
|
||||
else:
|
||||
await ctx.send(f"Custom command `{command_name}` not found.")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Failed to delete command `{command_name}`. Error: {e}")
|
||||
|
||||
@commands.hybrid_command(name="listcommands")
|
||||
async def list_commands(self, ctx):
|
||||
"""List all custom commands for this guild"""
|
||||
guild_id = str(ctx.guild.id)
|
||||
|
||||
try:
|
||||
# Retrieve all commands for the guild
|
||||
commands = self.db.fetch_all(
|
||||
"SELECT COMMANDNAME, RESPONSE FROM custom_commands WHERE GUILDID = %s",
|
||||
(guild_id,),
|
||||
)
|
||||
|
||||
if commands:
|
||||
commands_list = "\n".join(
|
||||
[f"`{cmd['COMMANDNAME']}`: {cmd['RESPONSE']}" for cmd in commands]
|
||||
)
|
||||
await ctx.send(f"**Custom Commands:**\n{commands_list}")
|
||||
else:
|
||||
await ctx.send("No custom commands found for this server.")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Failed to retrieve commands. Error: {e}")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message):
|
||||
"""Listen for custom command invocations"""
|
||||
if message.author.bot or not message.guild:
|
||||
return
|
||||
|
||||
guild_id = str(message.guild.id)
|
||||
command_name = message.content.strip().lower()
|
||||
|
||||
# Use cache instead of DB call
|
||||
data: list = self.get_guild_commands(guild_id)
|
||||
if not data:
|
||||
return
|
||||
|
||||
result = None
|
||||
|
||||
for cmd in data:
|
||||
if cmd["MATCHTYPE"] == "exact" and command_name == cmd["COMMANDNAME"]:
|
||||
result = cmd
|
||||
break
|
||||
elif cmd["MATCHTYPE"] == "contains" and cmd["COMMANDNAME"] in command_name:
|
||||
result = cmd
|
||||
break
|
||||
elif cmd["MATCHTYPE"] == "startswith" and command_name.startswith(
|
||||
cmd["COMMANDNAME"]
|
||||
):
|
||||
result = cmd
|
||||
break
|
||||
elif cmd["MATCHTYPE"] == "endswith" and command_name.endswith(
|
||||
cmd["COMMANDNAME"]
|
||||
):
|
||||
result = cmd
|
||||
break
|
||||
elif cmd["MATCHTYPE"] == "regex" and re.match(
|
||||
re.compile(cmd["REGEX"]), command_name
|
||||
):
|
||||
result = cmd
|
||||
break
|
||||
else:
|
||||
return
|
||||
|
||||
if result:
|
||||
response = result["RESPONSE"]
|
||||
|
||||
# Build a dictionary of variables to replace
|
||||
variables = {
|
||||
"{USER}": message.author.name,
|
||||
"{USER_MENTION}": message.author.mention,
|
||||
"{USER_ID}": str(message.author.id),
|
||||
"{USER_TAG}": str(message.author),
|
||||
"{USER_AVATAR}": str(message.author.display_avatar.url),
|
||||
"{USER_TOP_ROLE}": getattr(message.author.top_role, "name", ""),
|
||||
"{USER_CREATED}": message.author.created_at.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
),
|
||||
"{USER_JOINED}": (
|
||||
message.author.joined_at.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if message.author.joined_at
|
||||
else ""
|
||||
),
|
||||
"{USER_NICK}": message.author.nick or message.author.name,
|
||||
"{USER_COLOR}": str(
|
||||
getattr(message.author.color, "to_rgb", lambda: "")()
|
||||
),
|
||||
"{USER_STATUS}": str(message.author.status),
|
||||
"{USER_IS_BOT}": str(message.author.bot),
|
||||
"{USER_DISCRIMINATOR}": message.author.discriminator,
|
||||
"{CHANNEL}": message.channel.name,
|
||||
"{CHANNEL_MENTION}": message.channel.mention,
|
||||
"{CHANNEL_ID}": str(message.channel.id),
|
||||
"{CHANNEL_TOPIC}": getattr(message.channel, "topic", ""),
|
||||
"{CHANNEL_TYPE}": str(message.channel.type),
|
||||
"{CHANNEL_CREATED}": message.channel.created_at.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
),
|
||||
"{GUILD}": message.guild.name,
|
||||
"{GUILD_ID}": str(message.guild.id),
|
||||
"{GUILD_OWNER}": str(message.guild.owner),
|
||||
"{GUILD_OWNER_MENTION}": (
|
||||
message.guild.owner.mention if message.guild.owner else ""
|
||||
),
|
||||
"{GUILD_MEMBERCOUNT}": str(message.guild.member_count),
|
||||
"{GUILD_ICON}": (
|
||||
str(message.guild.icon.url) if message.guild.icon else ""
|
||||
),
|
||||
"{GUILD_CREATED}": message.guild.created_at.strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
),
|
||||
"{GUILD_BOOSTS}": str(
|
||||
getattr(message.guild, "premium_subscription_count", "")
|
||||
),
|
||||
"{GUILD_BOOST_LEVEL}": str(getattr(message.guild, "premium_tier", "")),
|
||||
"{MESSAGE}": message.content,
|
||||
"{MESSAGE_ID}": str(message.id),
|
||||
"{MESSAGE_LINK}": message.jump_url,
|
||||
"{MESSAGE_TIMESTAMP}": message.created_at.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"{TIME}": discord.utils.utcnow().strftime("%H:%M:%S UTC"),
|
||||
"{DATE}": discord.utils.utcnow().strftime("%Y-%m-%d"),
|
||||
"{DAY}": discord.utils.utcnow().strftime("%A"),
|
||||
"{MONTH}": discord.utils.utcnow().strftime("%B"),
|
||||
"{YEAR}": discord.utils.utcnow().strftime("%Y"),
|
||||
"{BOT}": (
|
||||
message.guild.me.name if message.guild else self.client.user.name
|
||||
),
|
||||
"{BOT_MENTION}": (
|
||||
message.guild.me.mention
|
||||
if message.guild
|
||||
else self.client.user.mention
|
||||
),
|
||||
"{BOT_ID}": str(
|
||||
message.guild.me.id if message.guild else self.client.user.id
|
||||
),
|
||||
"{BOT_AVATAR}": str(
|
||||
message.guild.me.display_avatar.url
|
||||
if message.guild
|
||||
else self.client.user.display_avatar.url
|
||||
),
|
||||
}
|
||||
|
||||
# Replace all variables in the response
|
||||
for var, value in variables.items():
|
||||
if value is None:
|
||||
continue
|
||||
response = response.replace(var, value)
|
||||
|
||||
await message.channel.send(response)
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(CustomCommandsCog(client))
|
||||
Executable
+423
@@ -0,0 +1,423 @@
|
||||
import logging
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from utils.bank_functions import *
|
||||
from discord.ext import commands
|
||||
from datetime import datetime, timedelta
|
||||
from random import randint
|
||||
from typing import Literal
|
||||
import datetime
|
||||
|
||||
|
||||
async def check_transfer(
|
||||
ctx: commands.Context,
|
||||
payer_balance: int,
|
||||
receiver_balance: int,
|
||||
member: discord.Member,
|
||||
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
|
||||
|
||||
|
||||
class Economy(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.db = DatabaseManager("")
|
||||
|
||||
r"""
|
||||
.----------------. .----------------. .----------------. .----------------. .-----------------.
|
||||
| .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |
|
||||
| | __ | || | ________ | || | ____ ____ | || | _____ | || | ____ _____ | |
|
||||
| | / \ | || | |_ ___ `. | || ||_ \ / _|| || | |_ _| | || ||_ \|_ _| | |
|
||||
| | / /\ \ | || | | | `. \ | || | | \/ | | || | | | | || | | \ | | | |
|
||||
| | / ____ \ | || | | | | | | || | | |\ /| | | || | | | | || | | |\ \| | | |
|
||||
| | _/ / \ \_ | || | _| |___.' / | || | _| |_\/_| |_ | || | _| |_ | || | _| |_\ |_ | |
|
||||
| ||____| |____|| || | |________.' | || ||_____||_____|| || | |_____| | || ||_____|\____| | |
|
||||
| | | || | | || | | || | | || | | |
|
||||
| '--------------' || '--------------' || '--------------' || '--------------' || '--------------' |
|
||||
'----------------' '----------------' '----------------' '----------------' '----------------'
|
||||
"""
|
||||
|
||||
@commands.command(
|
||||
name="give_money",
|
||||
aliases=["addmoney"],
|
||||
usage="<member*: @member> <amount*: integer>",
|
||||
)
|
||||
@commands.is_owner()
|
||||
@commands.cooldown(3, 2 * 60, commands.BucketType.user)
|
||||
async def add_money(
|
||||
self, ctx: commands.Context, member: discord.Member, amount_str: str
|
||||
):
|
||||
"""Add money to a user's bank."""
|
||||
if member.bot:
|
||||
return await ctx.reply("You can't add money to a bot", mention_author=False)
|
||||
if not member:
|
||||
member = ctx.author
|
||||
|
||||
try:
|
||||
amount = int(amount_str)
|
||||
except ValueError:
|
||||
return await ctx.reply("Please enter a valid amount")
|
||||
|
||||
if amount <= 0:
|
||||
return await ctx.reply("Please enter an amount greater than 0")
|
||||
|
||||
limit = 1_000_000
|
||||
if amount > limit:
|
||||
return await ctx.reply(f"You cannot add money more than {limit:,}")
|
||||
|
||||
await update_money(member, amount)
|
||||
await ctx.reply(
|
||||
f"You added {amount:,} in {member.mention}'s bank.", mention_author=False
|
||||
)
|
||||
|
||||
@commands.command(
|
||||
aliases=["remoney"],
|
||||
usage="<member*: @member> <amount*: integer> <mode: wallet or bank>",
|
||||
)
|
||||
@commands.is_owner()
|
||||
@commands.cooldown(3, 2 * 60, commands.BucketType.user)
|
||||
async def remove_money(
|
||||
self, ctx, member: discord.Member, amount_str: str, mode: str = "bank"
|
||||
):
|
||||
mode = mode.lower()
|
||||
if member.bot:
|
||||
return await ctx.reply(
|
||||
"You can't remove money from a bot", mention_author=False
|
||||
)
|
||||
|
||||
try:
|
||||
amount = int(amount_str)
|
||||
if amount <= 0:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.reply("Please enter a valid amount")
|
||||
|
||||
if mode not in ["wallet", "bank"]:
|
||||
return await ctx.reply("Please enter either wallet or bank only")
|
||||
|
||||
user_balance = await bank_data(member)
|
||||
current_balance = user_balance["BANK" if mode == "bank" else "WALLET"]
|
||||
|
||||
if current_balance < amount:
|
||||
return await ctx.reply(
|
||||
f"You can only remove {current_balance:,} from {member.mention}'s {mode}"
|
||||
)
|
||||
|
||||
if mode == "bank":
|
||||
await update_money(member, bank=-amount)
|
||||
else:
|
||||
await update_money(member, wallet=-amount)
|
||||
|
||||
await ctx.reply(
|
||||
f"You removed {amount:,} from {member.mention}'s {mode}",
|
||||
mention_author=False,
|
||||
)
|
||||
|
||||
@commands.command(usage="<member*: @member>")
|
||||
@commands.is_owner()
|
||||
@commands.cooldown(2, 3 * 60, commands.BucketType.user)
|
||||
async def reset_money(self, ctx, member: discord.Member):
|
||||
if member.bot:
|
||||
return await ctx.reply(
|
||||
"Bots don't have bank accounts", mention_author=False
|
||||
)
|
||||
|
||||
await reset_bank(member)
|
||||
|
||||
return await ctx.reply(
|
||||
f"{member.mention}'s account has been reset", mention_author=False
|
||||
)
|
||||
|
||||
r"""
|
||||
.----------------. .----------------. .-----------------. .----------------.
|
||||
| .--------------. || .--------------. || .--------------. || .--------------. |
|
||||
| | ______ | || | __ | || | ____ _____ | || | ___ ____ | |
|
||||
| | |_ _ \ | || | / \ | || ||_ \|_ _| | || | |_ ||_ _| | |
|
||||
| | | |_) | | || | / /\ \ | || | | \ | | | || | | |_/ / | |
|
||||
| | | __'. | || | / ____ \ | || | | |\ \| | | || | | __'. | |
|
||||
| | _| |__) | | || | _/ / \ \_ | || | _| |_\ |_ | || | _| | \ \_ | |
|
||||
| | |_______/ | || ||____| |____|| || ||_____|\____| | || | |____||____| | |
|
||||
| | | || | | || | | || | | |
|
||||
| '--------------' || '--------------' || '--------------' || '--------------' |
|
||||
'----------------' '----------------' '----------------' '----------------'
|
||||
"""
|
||||
|
||||
@commands.command(
|
||||
name="balance",
|
||||
aliases=["bal"],
|
||||
brief="Check your money",
|
||||
description="Check how much money you have.",
|
||||
)
|
||||
async def _balance(
|
||||
self, ctx: commands.Context, member: discord.Member | None = None
|
||||
):
|
||||
"""
|
||||
Check how much money you have.
|
||||
"""
|
||||
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 balance
|
||||
await ctx.reply(
|
||||
f"{target.mention} has {bank_balance:,}<:flooney:1194943899765051473> in their bank and "
|
||||
f"{wallet_balance:,}<:flooney:1194943899765051473> in their wallet."
|
||||
)
|
||||
|
||||
@commands.command(name="give")
|
||||
async def _give(self, ctx, target: discord.Member, amount: int):
|
||||
"""Give money to another user."""
|
||||
|
||||
payer_wallet = int((await bank_data(ctx.author)).get("WALLET", 0))
|
||||
receiver_wallet = int((await bank_data(target)).get("WALLET", 0))
|
||||
|
||||
if await check_transfer(ctx, payer_wallet, receiver_wallet, target, amount):
|
||||
await update_money(target, wallet=amount)
|
||||
await update_money(ctx.author, wallet=-amount)
|
||||
|
||||
await ctx.reply(
|
||||
f"You gave {target} {amount:,} flooneys.", mention_author=False
|
||||
)
|
||||
|
||||
@commands.command(name="transfer")
|
||||
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))
|
||||
|
||||
if await check_transfer(ctx, payer_bank, receiver_bank, target, amount):
|
||||
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
|
||||
)
|
||||
|
||||
@commands.command(
|
||||
name="deposit",
|
||||
aliases=["dep"],
|
||||
brief="Deposit money",
|
||||
description="Deposit money from your wallet to the bank",
|
||||
)
|
||||
async def _deposit(self, ctx, amount: int | Literal["all"] = 0) -> None:
|
||||
"""Deposit money from your wallet to your bank account. Amount can be an integer or 'all' to deposit all money from wallet."""
|
||||
|
||||
user_data = await bank_data(ctx.author)
|
||||
if user_data is None:
|
||||
await create_account(ctx)
|
||||
user_data = await bank_data(ctx.author)
|
||||
|
||||
wallet_balance = int(user_data.get("WALLET", 0))
|
||||
|
||||
if amount == "all":
|
||||
amount = wallet_balance
|
||||
|
||||
if 0 <= amount <= wallet_balance:
|
||||
await update_money(ctx.author, bank=amount, wallet=-amount)
|
||||
await ctx.reply("Transaction successful.")
|
||||
else:
|
||||
await ctx.reply("Insufficient funds in wallet")
|
||||
|
||||
@commands.command(name="withdraw", brief="Withdraw money from bank to wallet")
|
||||
async def _withdraw(self, ctx, amount: int | Literal["all"] = 0) -> None:
|
||||
"""Withdraw money from your bank account to your wallet. Amount can be an integer or 'all' to withdraw all money from bank."""
|
||||
|
||||
user_data = await bank_data(ctx.author)
|
||||
if user_data is None:
|
||||
await create_account(ctx)
|
||||
user_data = await bank_data(ctx.author)
|
||||
|
||||
bank_balance = int(user_data.get("BANK", 0))
|
||||
|
||||
if amount == "all":
|
||||
amount = bank_balance
|
||||
|
||||
if 0 <= amount <= bank_balance:
|
||||
await update_money(ctx.author, bank=-amount, wallet=amount)
|
||||
await ctx.reply("Transaction successful.")
|
||||
else:
|
||||
await ctx.reply("Insufficient funds")
|
||||
|
||||
@commands.command(aliases=["lb"])
|
||||
@commands.guild_only()
|
||||
async def leaderboard(self, ctx):
|
||||
users_data = self.db.fetch_all(
|
||||
"SELECT * FROM economy ORDER BY BANK + WALLET DESC"
|
||||
)
|
||||
|
||||
leaderboard_entries = []
|
||||
position = 1
|
||||
for user_data in users_data:
|
||||
user_id = int(user_data["ID"])
|
||||
user = ctx.guild.get_member(user_id) or self.client.get_user(user_id)
|
||||
if user is None:
|
||||
try:
|
||||
user = await self.client.fetch_user(user_id)
|
||||
user_display = (
|
||||
user.display_name
|
||||
if hasattr(user, "display_name")
|
||||
else str(user)
|
||||
)
|
||||
except Exception:
|
||||
user_display = f"User {user_id}"
|
||||
else:
|
||||
user_display = (
|
||||
user.display_name if hasattr(user, "display_name") else str(user)
|
||||
)
|
||||
|
||||
total_balance = int(user_data["BANK"]) + int(user_data["WALLET"])
|
||||
flooney_icon = "<:flooney:1194943899765051473>"
|
||||
|
||||
if position <= 3:
|
||||
entry = f"{['🥇', '🥈', '🥉'][position-1]}**{position} {user_display}** -- {total_balance:,}{flooney_icon} "
|
||||
else:
|
||||
entry = (
|
||||
f"**{position} {user_display}** -- {total_balance:,}{flooney_icon}"
|
||||
)
|
||||
|
||||
leaderboard_entries.append(entry)
|
||||
position += 1
|
||||
if position > 10:
|
||||
break
|
||||
|
||||
embed = discord.Embed(
|
||||
title=f"Top {len(leaderboard_entries)} Richest Users - Leaderboard",
|
||||
description="\n".join(leaderboard_entries),
|
||||
color=discord.Color(0x00FF00),
|
||||
timestamp=datetime.datetime.utcnow(),
|
||||
)
|
||||
embed.set_footer(text=f"GLOBAL - {ctx.guild.name}")
|
||||
await ctx.reply(embed=embed, mention_author=False)
|
||||
|
||||
r"""
|
||||
.----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------. .----------------.
|
||||
| .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |
|
||||
| | ______ | || | _________ | || | _______ | || | _____ | || | ____ | || | ________ | || | _____ | || | ______ | || | __ | || | _____ | || | _____ | || | ____ ____ | |
|
||||
| | |_ __ \ | || | |_ ___ | | || | |_ __ \ | || | |_ _| | || | .' `. | || | |_ ___ `. | || | |_ _| | || | .' ___ | | || | / \ | || | |_ _| | || | |_ _| | || | |_ _||_ _| | |
|
||||
| | | |__) | | || | | |_ \_| | || | | |__) | | || | | | | || | / .--. \ | || | | | `. \ | || | | | | || | / .' \_| | || | / /\ \ | || | | | | || | | | | || | \ \ / / | |
|
||||
| | | ___/ | || | | _| _ | || | | __ / | || | | | | || | | | | | | || | | | | | | || | | | | || | | | | || | / ____ \ | || | | | _ | || | | | _ | || | \ \/ / | |
|
||||
| | _| |_ | || | _| |___/ | | || | _| | \ \_ | || | _| |_ | || | \ `--' / | || | _| |___.' / | || | _| |_ | || | \ `.___.'\ | || | _/ / \ \_ | || | _| |__/ | | || | _| |__/ | | || | _| |_ | |
|
||||
| | |_____| | || | |_________| | || | |____| |___| | || | |_____| | || | `.____.' | || | |________.' | || | |_____| | || | `._____.' | || ||____| |____|| || | |________| | || | |________| | || | |______| | |
|
||||
| | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | || | | |
|
||||
| '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' || '--------------' |
|
||||
'----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------' '----------------'
|
||||
"""
|
||||
|
||||
@commands.command(name="daily", help="Claim your daily pocket money")
|
||||
async def claim_daily_money(self, ctx: commands.Context) -> None:
|
||||
"""Claim your daily pocket money. You can claim once every 24 hours."""
|
||||
try:
|
||||
user_data = await bank_data(ctx.author)
|
||||
last_claim = user_data.get("DAILY")
|
||||
now = discord.utils.utcnow() # Modern, timezone-aware
|
||||
|
||||
# Convert last_claim to datetime if it's a timestamp (int or float)
|
||||
if last_claim:
|
||||
if isinstance(last_claim, (int, float)):
|
||||
last_claim_dt = datetime.datetime.fromtimestamp(
|
||||
last_claim, tz=datetime.timezone.utc
|
||||
)
|
||||
elif isinstance(last_claim, str):
|
||||
try:
|
||||
last_claim_dt = datetime.datetime.fromtimestamp(
|
||||
float(last_claim), tz=datetime.timezone.utc
|
||||
)
|
||||
except Exception:
|
||||
last_claim_dt = None
|
||||
elif isinstance(last_claim, datetime.datetime):
|
||||
# If it's naive, make it aware
|
||||
if last_claim.tzinfo is None:
|
||||
last_claim_dt = last_claim.replace(tzinfo=datetime.timezone.utc)
|
||||
else:
|
||||
last_claim_dt = last_claim
|
||||
else:
|
||||
last_claim_dt = None
|
||||
else:
|
||||
last_claim_dt = None
|
||||
|
||||
if not last_claim_dt or (now - last_claim_dt).total_seconds() >= 86400:
|
||||
daily_reward = randint(200, 1000)
|
||||
await update_money(ctx.author, daily_reward)
|
||||
# Save the new timestamp as a float (UNIX time)
|
||||
await update_daily_timestamp(ctx.author, now)
|
||||
await ctx.reply(
|
||||
f"Your daily pocket money is {daily_reward:,}<:flooney:1194943899765051473>",
|
||||
mention_author=False,
|
||||
)
|
||||
else:
|
||||
next_claim = last_claim_dt + timedelta(days=1)
|
||||
time_left = next_claim - now
|
||||
hours, remainder = divmod(int(time_left.total_seconds()), 3600)
|
||||
minutes, seconds = divmod(remainder, 60)
|
||||
await ctx.reply(
|
||||
f"Already claimed today. You need to wait **{hours}H {minutes}M {seconds}S**.",
|
||||
mention_author=False,
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in daily claim: {e}")
|
||||
await ctx.reply(
|
||||
f"An error occurred while claiming your daily reward. {e}",
|
||||
mention_author=False,
|
||||
)
|
||||
|
||||
r"""
|
||||
.----------------. .----------------. .----------------. .----------------.
|
||||
| .--------------. || .--------------. || .--------------. || .--------------. |
|
||||
| | _____ | || | ____ | || | ______ | || | _______ | |
|
||||
| | |_ _| | || | .' `. | || | |_ _ \ | || | / ___ | | |
|
||||
| | | | | || | / .--. \ | || | | |_) | | || | | (__ \_| | |
|
||||
| | _ | | | || | | | | | | || | | __'. | || | '.___`-. | |
|
||||
| | | |_' | | || | \ `--' / | || | _| |__) | | || | |`\____) | | |
|
||||
| | `.___.' | || | `.____.' | || | |_______/ | || | |_______.' | |
|
||||
| | | || | | || | | || | | |
|
||||
| '--------------' || '--------------' || '--------------' || '--------------' |
|
||||
'----------------' '----------------' '----------------' '----------------'
|
||||
"""
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(Economy(client))
|
||||
Executable
+979
@@ -0,0 +1,979 @@
|
||||
import discord
|
||||
from random import shuffle, choices, choice
|
||||
from discord.ext import commands
|
||||
from utils.sql_commands import DatabaseManager
|
||||
from utils.bank_functions import bank_data, update_money
|
||||
from datetime import datetime, timedelta
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
# --- Constants and Configs ---
|
||||
|
||||
lottery_list = ["🎁", "🎮", "🎷", "🔫", "📸", "🎃", "🏅"]
|
||||
weights = [0.15, 0.1, 0.02, 0.2, 0.1, 0.28, 0.2]
|
||||
|
||||
lottery_win = {
|
||||
"🎁": 2,
|
||||
"🎁🎁": 5,
|
||||
"🎁🎁🎁": 50,
|
||||
"🔫🔫🔫": 3,
|
||||
"📸📸📸": 5,
|
||||
"🎃🎃🎃": 7,
|
||||
"🏅🏅🏅": 10,
|
||||
"🎮🎮🎮": 25,
|
||||
"🎷🎷🎷": 1000,
|
||||
}
|
||||
|
||||
bj_values = {
|
||||
"2♦️": 2,
|
||||
"2♥️": 2,
|
||||
"2♣️": 2,
|
||||
"2♠️": 2,
|
||||
"3♦️": 3,
|
||||
"3♥️": 3,
|
||||
"3♣️": 3,
|
||||
"3♠️": 3,
|
||||
"4♦️": 4,
|
||||
"4♥️": 4,
|
||||
"4♣️": 4,
|
||||
"4♠️": 4,
|
||||
"5♦️": 5,
|
||||
"5♥️": 5,
|
||||
"5♣️": 5,
|
||||
"5♠️": 5,
|
||||
"6♦️": 6,
|
||||
"6♥️": 6,
|
||||
"6♣️": 6,
|
||||
"6♠️": 6,
|
||||
"7♦️": 7,
|
||||
"7♥️": 7,
|
||||
"7♣️": 7,
|
||||
"7♠️": 7,
|
||||
"8♦️": 8,
|
||||
"8♥️": 8,
|
||||
"8♣️": 8,
|
||||
"8♠️": 8,
|
||||
"9♦️": 9,
|
||||
"9♥️": 9,
|
||||
"9♣️": 9,
|
||||
"9♠️": 9,
|
||||
"10♦️": 10,
|
||||
"10♥️": 10,
|
||||
"10♣️": 10,
|
||||
"10♠️": 10,
|
||||
"J♦️": 10,
|
||||
"J♥️": 10,
|
||||
"J♣️": 10,
|
||||
"J♠️": 10,
|
||||
"Q♦️": 10,
|
||||
"Q♥️": 10,
|
||||
"Q♣️": 10,
|
||||
"Q♠️": 10,
|
||||
"K♦️": 10,
|
||||
"K♥️": 10,
|
||||
"K♣️": 10,
|
||||
"K♠️": 10,
|
||||
"A♦️": 11,
|
||||
"A♥️": 11,
|
||||
"A♣️": 11,
|
||||
"A♠️": 11,
|
||||
}
|
||||
|
||||
ROOMS_FILE = "active_rooms.json"
|
||||
|
||||
# --- Blackjack Classes and Helpers ---
|
||||
|
||||
|
||||
class Deck:
|
||||
def __init__(self):
|
||||
self.deck = list(bj_values.keys())
|
||||
shuffle(self.deck)
|
||||
|
||||
async def deal(self):
|
||||
return self.deck.pop()
|
||||
|
||||
|
||||
class Hand:
|
||||
def __init__(self, name, bet):
|
||||
self.cards = []
|
||||
self.value = 0
|
||||
self.aces = 0
|
||||
self.bust = False
|
||||
self.name = name
|
||||
self.bet = bet
|
||||
|
||||
async def add_card(self, card):
|
||||
self.cards.append(card)
|
||||
self.aces = ("".join(self.cards)).count("A")
|
||||
value = 0
|
||||
for card in self.cards:
|
||||
value += bj_values[card]
|
||||
self.value = value
|
||||
while self.value > 21 and self.aces:
|
||||
self.value -= 10
|
||||
self.aces -= 1
|
||||
|
||||
|
||||
async def hit(deck, hand):
|
||||
await hand.add_card(await deck.deal())
|
||||
|
||||
|
||||
async def payout(ctx, play, amount, multiplier, db):
|
||||
player_balance = int(
|
||||
db.fetch_one("SELECT WALLET FROM economy WHERE ID = %s", (ctx.author.id,)).get(
|
||||
"WALLET", 0
|
||||
)
|
||||
)
|
||||
db.execute_query(
|
||||
"UPDATE economy SET WALLET = %s WHERE ID = %s",
|
||||
(player_balance + amount * (multiplier - 1), ctx.author.id),
|
||||
)
|
||||
await ctx.reply(
|
||||
f"{play}\nBet: {amount:,}\nYou won {amount * multiplier:,} <:flooney:1194943899765051473>"
|
||||
)
|
||||
|
||||
|
||||
async def check_winner(msg: discord.Interaction, player_hand, dealer_hand, db):
|
||||
p_value, d_value = player_hand.value, dealer_hand.value
|
||||
embed = discord.Embed(description="Blackjack")
|
||||
embed.add_field(
|
||||
name="Bet: ",
|
||||
value=f"{player_hand.bet:,}<:flooney:1194943899765051473>",
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(
|
||||
name=player_hand.name,
|
||||
value=f"{', '.join(player_hand.cards)}: {player_hand.value}",
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
name=dealer_hand.name,
|
||||
value=f"{', '.join(dealer_hand.cards)}: {dealer_hand.value}",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
# Check for Blackjack
|
||||
if p_value == 21 and len(player_hand.cards) == 2:
|
||||
if d_value == 21 and len(dealer_hand.cards) == 2:
|
||||
embed.add_field(
|
||||
name="Winner:", value="Draw (Both got Blackjack!)", inline=False
|
||||
)
|
||||
player_balance = int(
|
||||
db.fetch_one(
|
||||
"SELECT WALLET FROM economy WHERE ID = %s", (player_hand.name.id,)
|
||||
).get("WALLET", 0)
|
||||
)
|
||||
db.execute_query(
|
||||
"UPDATE economy SET WALLET = %s WHERE ID = %s",
|
||||
(player_balance + player_hand.bet, player_hand.name.id),
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name="Winner:", value=f"{player_hand.name} (Blackjack!)", inline=False
|
||||
)
|
||||
player_balance = int(
|
||||
db.fetch_one(
|
||||
"SELECT WALLET FROM economy WHERE ID = %s", (player_hand.name.id,)
|
||||
).get("WALLET", 0)
|
||||
)
|
||||
db.execute_query(
|
||||
"UPDATE economy SET WALLET = %s WHERE ID = %s",
|
||||
(player_balance + int(player_hand.bet * 3), player_hand.name.id),
|
||||
)
|
||||
elif d_value == 21 and len(dealer_hand.cards) == 2:
|
||||
embed.add_field(
|
||||
name="Winner:", value=f"{dealer_hand.name} (Blackjack!)", inline=False
|
||||
)
|
||||
else:
|
||||
if (p_value > d_value and not player_hand.bust) or dealer_hand.bust:
|
||||
embed.add_field(name="Winner:", value=f"{player_hand.name}", inline=False)
|
||||
player_balance = int(
|
||||
db.fetch_one(
|
||||
"SELECT WALLET FROM economy WHERE ID = %s", (player_hand.name.id,)
|
||||
).get("WALLET", 0)
|
||||
)
|
||||
db.execute_query(
|
||||
"UPDATE economy SET WALLET = %s WHERE ID = %s",
|
||||
(player_balance + player_hand.bet * 2, player_hand.name.id),
|
||||
)
|
||||
elif (p_value < d_value and not dealer_hand.bust) or player_hand.bust:
|
||||
embed.add_field(name="Winner:", value=f"{dealer_hand.name}", inline=False)
|
||||
else:
|
||||
embed.add_field(name="Winner:", value="Draw", inline=False)
|
||||
player_balance = int(
|
||||
db.fetch_one(
|
||||
"SELECT WALLET FROM economy WHERE ID = %s", (player_hand.name.id,)
|
||||
).get("WALLET", 0)
|
||||
)
|
||||
db.execute_query(
|
||||
"UPDATE economy SET WALLET = %s WHERE ID = %s",
|
||||
(player_balance + player_hand.bet, player_hand.name.id),
|
||||
)
|
||||
|
||||
await msg.edit_original_response(embed=embed)
|
||||
|
||||
|
||||
async def dealer(msg: discord.Interaction, player_hand, dealer_hand, cards, db):
|
||||
while dealer_hand.value < 17 and not player_hand.bust and not dealer_hand.bust:
|
||||
await hit(cards, dealer_hand)
|
||||
if dealer_hand.value > 21:
|
||||
dealer_hand.bust = True
|
||||
embed = discord.Embed(description="Blackjack")
|
||||
embed.add_field(
|
||||
name="Bet: ",
|
||||
value=f"{player_hand.bet:,}<:flooney:1194943899765051473>",
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(
|
||||
name=player_hand.name,
|
||||
value=f"{', '.join(player_hand.cards)}: {player_hand.value}",
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
name=dealer_hand.name,
|
||||
value=f"{', '.join(dealer_hand.cards)}: {dealer_hand.value}",
|
||||
inline=False,
|
||||
)
|
||||
await msg.edit_original_response(embed=embed)
|
||||
await check_winner(msg, player_hand, dealer_hand, db)
|
||||
|
||||
|
||||
class MyView(discord.ui.View):
|
||||
def __init__(self, player_hand, dealer_hand, cards, db):
|
||||
super().__init__()
|
||||
self.player_hand = player_hand
|
||||
self.dealer_hand = dealer_hand
|
||||
self.cards = cards
|
||||
self.db = db
|
||||
|
||||
@discord.ui.button(label="Hit", style=discord.ButtonStyle.primary, emoji="✅")
|
||||
async def hit(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user.id == self.player_hand.name.id:
|
||||
await hit(self.cards, self.player_hand)
|
||||
embed = discord.Embed(description="Blackjack")
|
||||
embed.add_field(
|
||||
name="Bet: ",
|
||||
value=f"{self.player_hand.bet:,}<:flooney:1194943899765051473>",
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(
|
||||
name=self.player_hand.name,
|
||||
value=f"{', '.join(self.player_hand.cards)}: {self.player_hand.value}",
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
name=self.dealer_hand.name,
|
||||
value=f"{(self.dealer_hand.cards)[0]}",
|
||||
inline=False,
|
||||
)
|
||||
msg = interaction
|
||||
if self.player_hand.value < 21:
|
||||
await msg.response.edit_message(embed=embed)
|
||||
else:
|
||||
await msg.response.edit_message(embed=embed, view=None)
|
||||
if self.player_hand.value > 21:
|
||||
self.player_hand.bust = True
|
||||
await dealer(
|
||||
msg, self.player_hand, self.dealer_hand, self.cards, self.db
|
||||
)
|
||||
|
||||
@discord.ui.button(label="Stand", style=discord.ButtonStyle.secondary, emoji="🛑")
|
||||
async def stand(self, interaction: discord.Interaction, button: discord.ui.Button):
|
||||
if interaction.user.id == self.player_hand.name.id:
|
||||
embed = discord.Embed(description="Blackjack")
|
||||
embed.add_field(
|
||||
name="Bet: ",
|
||||
value=f"{self.player_hand.bet:,}<:flooney:1194943899765051473>",
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(
|
||||
name=self.player_hand.name,
|
||||
value=f"{', '.join(self.player_hand.cards)}: {self.player_hand.value}",
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
name=self.dealer_hand.name,
|
||||
value=f"{(self.dealer_hand.cards)[0]}",
|
||||
inline=False,
|
||||
)
|
||||
msg = interaction
|
||||
await msg.response.edit_message(embed=embed, view=None)
|
||||
await dealer(msg, self.player_hand, self.dealer_hand, self.cards, self.db)
|
||||
|
||||
|
||||
# --- Main Gamble Cog ---
|
||||
|
||||
|
||||
class Gamble(commands.Cog):
|
||||
"""Cog for all gambling-related commands."""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.db = DatabaseManager()
|
||||
self.active_rooms = {}
|
||||
self.room_timers = {}
|
||||
|
||||
def save_rooms(self):
|
||||
"""Save all active rooms to the database."""
|
||||
try:
|
||||
for channel_id, room in self.active_rooms.items():
|
||||
invited_str = ",".join(str(uid) for uid in room["invited"])
|
||||
self.db.execute_query(
|
||||
"REPLACE INTO gamble_rooms (channel_id, host_id, invited, inactivity) VALUES (%s, %s, %s, %s)",
|
||||
(channel_id, room["host"], invited_str, room["inactivity"]),
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[DB ERROR] Failed to save rooms: {e}")
|
||||
|
||||
def load_rooms(self):
|
||||
"""Load all rooms from the database."""
|
||||
try:
|
||||
self.active_rooms = {}
|
||||
rows = self.db.fetch_all("SELECT * FROM gamble_rooms")
|
||||
for row in rows:
|
||||
invited = set(int(uid) for uid in row["invited"].split(",") if uid)
|
||||
self.active_rooms[int(row["channel_id"])] = {
|
||||
"host": int(row["host_id"]),
|
||||
"invited": invited,
|
||||
"inactivity": int(row["inactivity"]),
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"[DB ERROR] Failed to load rooms: {e}")
|
||||
|
||||
def delete_room(self, channel_id):
|
||||
"""Delete a room from the database and memory."""
|
||||
try:
|
||||
self.db.execute_query(
|
||||
"DELETE FROM gamble_rooms WHERE channel_id = %s", (channel_id,)
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[DB ERROR] Failed to delete room: {e}")
|
||||
self.active_rooms.pop(channel_id, None)
|
||||
self.save_rooms()
|
||||
|
||||
async def cog_load(self):
|
||||
"""Load rooms on cog load."""
|
||||
self.load_rooms()
|
||||
|
||||
async def cog_unload(self):
|
||||
"""Save rooms on cog unload."""
|
||||
self.save_rooms()
|
||||
|
||||
async def _check_limit_and_exclusion(self, ctx, amount: int) -> bool:
|
||||
"""Returns True if user is allowed to gamble, else sends a message and returns False."""
|
||||
user = ctx.author
|
||||
if amount <= 0:
|
||||
await ctx.reply("⚠️ Bet amount must be positive.", mention_author=False)
|
||||
return False
|
||||
try:
|
||||
limit_row = self.db.fetch_one(
|
||||
"SELECT * FROM gamble_limits WHERE USERID = %s", (user.id,)
|
||||
)
|
||||
except Exception as e:
|
||||
await ctx.reply(
|
||||
"⚠️ Database error. Please try again later.", mention_author=False
|
||||
)
|
||||
print(f"[DB ERROR] Limit check: {e}")
|
||||
return False
|
||||
if limit_row:
|
||||
if (
|
||||
limit_row.get("EXCLUDED_UNTIL")
|
||||
and datetime.utcnow() < limit_row["EXCLUDED_UNTIL"]
|
||||
):
|
||||
await ctx.reply(
|
||||
"🚫 You are currently self-excluded from gambling.",
|
||||
mention_author=False,
|
||||
)
|
||||
return False
|
||||
if limit_row.get("DAILY_LIMIT") and amount > limit_row["DAILY_LIMIT"]:
|
||||
await ctx.reply(
|
||||
f"🚫 Your personal bet limit is {limit_row['DAILY_LIMIT']:,}.",
|
||||
mention_author=False,
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def _send_embed(
|
||||
self, ctx, description: str, color: discord.Color = discord.Color.blurple()
|
||||
):
|
||||
"""Helper to send an embed message."""
|
||||
embed = discord.Embed(description=description, color=color)
|
||||
await ctx.reply(embed=embed, mention_author=False)
|
||||
|
||||
# --- Coinflip ---
|
||||
@commands.cooldown(1, 2, commands.BucketType.user)
|
||||
@commands.command(
|
||||
name="coinflip",
|
||||
aliases=["cf", "coin_flip"],
|
||||
usage="<bet_on: heads(H) or tails(T)> <amount: integer>",
|
||||
description="Flip a coin and bet on heads or tails.",
|
||||
brief="Bet on heads or tails.",
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def coin_flip(self, ctx, bet_on: str, amount: int):
|
||||
if not await self._check_limit_and_exclusion(ctx, amount):
|
||||
return
|
||||
user = ctx.author
|
||||
bet_on = "heads" if "h" in bet_on.lower() else "tails"
|
||||
reward = round(amount / 2)
|
||||
users = await bank_data(user)
|
||||
if int(users["WALLET"]) < amount:
|
||||
return await self._send_embed(
|
||||
ctx, "❌ You don't have enough money.", discord.Color.red()
|
||||
)
|
||||
coin = ["heads", "tails"]
|
||||
result = choice(coin)
|
||||
if result != bet_on:
|
||||
await update_money(user, wallet=-abs(amount))
|
||||
return await self._send_embed(
|
||||
ctx,
|
||||
f"🪙 Got **{result}**, you lost **{amount:,}**.",
|
||||
discord.Color.red(),
|
||||
)
|
||||
await update_money(user, wallet=+abs(amount))
|
||||
return await self._send_embed(
|
||||
ctx,
|
||||
f"🪙 Got **{result}**, you won **{amount + reward:,}**!",
|
||||
discord.Color.green(),
|
||||
)
|
||||
|
||||
# --- Dice ---
|
||||
@commands.cooldown(1, 2, commands.BucketType.user)
|
||||
@commands.command(
|
||||
name="dice",
|
||||
usage="<amount: integer> <bet_on: integer (1-6)>",
|
||||
description="Bet on a dice roll (choose a number 1-6).",
|
||||
brief="Bet on a dice roll.",
|
||||
)
|
||||
async def dice(self, ctx, amount: int, bet_on: int = 6):
|
||||
if not await self._check_limit_and_exclusion(ctx, amount):
|
||||
return
|
||||
user = ctx.author
|
||||
rdice = [1, 2, 3, 4, 5, 6]
|
||||
if bet_on not in rdice:
|
||||
return await self._send_embed(
|
||||
ctx, "🎲 Enter a number of dice (1 - 6).", discord.Color.orange()
|
||||
)
|
||||
users = await bank_data(user)
|
||||
if int(users["WALLET"]) < amount:
|
||||
return await self._send_embed(
|
||||
ctx, "❌ You don't have enough money.", discord.Color.red()
|
||||
)
|
||||
rand_num = choice(rdice)
|
||||
if rand_num != bet_on:
|
||||
await update_money(user, wallet=-abs(amount))
|
||||
return await self._send_embed(
|
||||
ctx,
|
||||
f"🎲 Got **{rand_num}**, you lost **{amount:,}**.",
|
||||
discord.Color.red(),
|
||||
)
|
||||
reward = round(amount / 2)
|
||||
await update_money(user, wallet=+abs(reward))
|
||||
await self._send_embed(
|
||||
ctx,
|
||||
f"🎲 Got **{rand_num}**, you won **{amount + reward:,}**!",
|
||||
discord.Color.green(),
|
||||
)
|
||||
|
||||
# --- Slots ---
|
||||
@commands.cooldown(1, 1, commands.BucketType.user)
|
||||
@commands.command(
|
||||
name="slots",
|
||||
usage="<bet: integer or 'all'>",
|
||||
description="Spin the slot machine for a chance to win big!",
|
||||
brief="Spin the slot machine.",
|
||||
)
|
||||
async def slots(self, ctx, bet):
|
||||
user = ctx.author
|
||||
player_balance = int(
|
||||
self.db.fetch_one(
|
||||
"SELECT WALLET FROM economy WHERE ID = %s", (user.id,)
|
||||
).get("WALLET", 0)
|
||||
)
|
||||
bet = int(bet.replace("all", str(player_balance)))
|
||||
if not await self._check_limit_and_exclusion(ctx, bet):
|
||||
return
|
||||
if player_balance < bet:
|
||||
await self._send_embed(
|
||||
ctx, "❌ You have insufficient funds.", discord.Color.red()
|
||||
)
|
||||
return
|
||||
|
||||
# Animation: edit the embed to show spinning, then result
|
||||
spin_emoji = "<a:slots:1381060458999713843>"
|
||||
embed = discord.Embed(
|
||||
description=f"{spin_emoji} {spin_emoji} {spin_emoji}\nSpinning...",
|
||||
color=discord.Color.blurple(),
|
||||
)
|
||||
msg = await ctx.send(embed=embed)
|
||||
await asyncio.sleep(1.2)
|
||||
|
||||
play = "".join(choices(lottery_list, weights=weights, k=3))
|
||||
result_embed = discord.Embed(
|
||||
description=f"{play[0]} {play[1]} {play[2]}",
|
||||
color=(
|
||||
discord.Color.green()
|
||||
if play in lottery_win or play.count("🎁") > 0
|
||||
else discord.Color.red()
|
||||
),
|
||||
)
|
||||
# Ensure description is always a string
|
||||
if result_embed.description is None:
|
||||
result_embed.description = ""
|
||||
|
||||
if play in lottery_win:
|
||||
mult = lottery_win[play]
|
||||
result_embed.description = (result_embed.description or "") + (
|
||||
f"\nYou won **{bet * mult:,}** <:flooney:1194943899765051473>"
|
||||
)
|
||||
await update_money(user, wallet=bet * (mult - 1))
|
||||
elif play.count("🎁") == 2:
|
||||
mult = lottery_win["🎁🎁"]
|
||||
result_embed.description = (result_embed.description or "") + (
|
||||
f"\nYou won **{bet * mult:,}** <:flooney:1194943899765051473>"
|
||||
)
|
||||
await update_money(user, wallet=bet * (mult - 1))
|
||||
elif play.count("🎁") == 1:
|
||||
mult = lottery_win["🎁"]
|
||||
result_embed.description = (result_embed.description or "") + (
|
||||
f"\nYou won **{bet * mult:,}** <:flooney:1194943899765051473>"
|
||||
)
|
||||
await update_money(user, wallet=bet * (mult - 1))
|
||||
else:
|
||||
await update_money(user, wallet=-bet)
|
||||
result_embed.description = (result_embed.description or "") + (
|
||||
f"\nYou lost **{int(bet):,}** <:flooney:1194943899765051473>"
|
||||
)
|
||||
|
||||
await msg.edit(embed=result_embed)
|
||||
|
||||
# --- Roulette ---
|
||||
@commands.cooldown(1, 2, commands.BucketType.user)
|
||||
@commands.command(
|
||||
name="roulette",
|
||||
usage="<bet_type> <amount: integer>",
|
||||
description=(
|
||||
"Bet on roulette! Options: "
|
||||
"`red`, `black`, `green`, `even`, `odd`, "
|
||||
"`1st12`, `2nd12`, `3rd12` (dozens), "
|
||||
"`col1`, `col2`, `col3` (columns), or a number (0-36)."
|
||||
),
|
||||
brief="Bet on roulette.",
|
||||
)
|
||||
async def roulette(self, ctx, bet_type: str, amount: int):
|
||||
if not await self._check_limit_and_exclusion(ctx, amount):
|
||||
return
|
||||
user = ctx.author
|
||||
colors = {
|
||||
"red": [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36],
|
||||
"black": [
|
||||
2,
|
||||
4,
|
||||
6,
|
||||
8,
|
||||
10,
|
||||
11,
|
||||
13,
|
||||
15,
|
||||
17,
|
||||
20,
|
||||
22,
|
||||
24,
|
||||
26,
|
||||
28,
|
||||
29,
|
||||
31,
|
||||
33,
|
||||
35,
|
||||
],
|
||||
"green": [0],
|
||||
}
|
||||
bet_type = bet_type.lower()
|
||||
valid_bets = [
|
||||
"red",
|
||||
"black",
|
||||
"green",
|
||||
"even",
|
||||
"odd",
|
||||
"1st12",
|
||||
"2nd12",
|
||||
"3rd12",
|
||||
"col1",
|
||||
"col2",
|
||||
"col3",
|
||||
] + [str(i) for i in range(37)]
|
||||
if bet_type not in valid_bets:
|
||||
return await self._send_embed(
|
||||
ctx,
|
||||
"Bet on 'red', 'black', 'green', 'even', 'odd', '1st12', '2nd12', '3rd12', 'col1', 'col2', 'col3', or a number (0-36).",
|
||||
discord.Color.orange(),
|
||||
)
|
||||
result = choice(range(37))
|
||||
color = (
|
||||
"green" if result == 0 else "red" if result in colors["red"] else "black"
|
||||
)
|
||||
payout = 0
|
||||
# Number bet
|
||||
if bet_type.isdigit() and int(bet_type) == result:
|
||||
payout = amount * 35
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
# Color bet
|
||||
elif bet_type == color:
|
||||
payout = amount * (14 if color == "green" else 2)
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
# Even/Odd
|
||||
elif bet_type == "even" and result != 0 and result % 2 == 0:
|
||||
payout = amount * 2
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
elif bet_type == "odd" and result % 2 == 1:
|
||||
payout = amount * 2
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
# Dozens
|
||||
elif bet_type == "1st12" and 1 <= result <= 12:
|
||||
payout = amount * 3
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
elif bet_type == "2nd12" and 13 <= result <= 24:
|
||||
payout = amount * 3
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
elif bet_type == "3rd12" and 25 <= result <= 36:
|
||||
payout = amount * 3
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
# Columns
|
||||
elif bet_type == "col1" and result in [i for i in range(1, 37, 3)]:
|
||||
payout = amount * 3
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
elif bet_type == "col2" and result in [i for i in range(2, 37, 3)]:
|
||||
payout = amount * 3
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
elif bet_type == "col3" and result in [i for i in range(3, 37, 3)]:
|
||||
payout = amount * 3
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You won {payout:,}!"
|
||||
else:
|
||||
payout = -amount
|
||||
msg = f"🎲 The ball landed on {result} ({color}). You lost {amount:,}."
|
||||
await update_money(user, wallet=payout)
|
||||
await self._send_embed(
|
||||
ctx, msg, discord.Color.green() if payout > 0 else discord.Color.red()
|
||||
)
|
||||
|
||||
# --- Poker ---
|
||||
@commands.cooldown(1, 2, commands.BucketType.user)
|
||||
@commands.is_owner()
|
||||
@commands.command(
|
||||
name="poker",
|
||||
usage="<bet: integer>",
|
||||
description="Play a simple 5-card draw poker game against the bot.",
|
||||
brief="Play poker against the bot.",
|
||||
)
|
||||
async def poker(self, ctx, bet: int):
|
||||
if not await self._check_limit_and_exclusion(ctx, bet):
|
||||
return
|
||||
user = ctx.author
|
||||
user_data = await bank_data(user)
|
||||
if int(user_data["WALLET"]) < bet:
|
||||
await self._send_embed(
|
||||
ctx, "❌ You don't have enough money.", discord.Color.red()
|
||||
)
|
||||
return
|
||||
# Poker deck and hands
|
||||
suits = ["♠️", "♥️", "♦️", "♣️"]
|
||||
ranks = ["2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"]
|
||||
deck = [f"{r}{s}" for r in ranks for s in suits]
|
||||
random.shuffle(deck)
|
||||
player_hand = [deck.pop() for _ in range(5)]
|
||||
bot_hand = [deck.pop() for _ in range(5)]
|
||||
|
||||
# Simple hand value: count pairs, triples, etc. (not full poker logic)
|
||||
def hand_value(hand):
|
||||
values = [card[:-2] if card[:-2] != "10" else "10" for card in hand]
|
||||
counts = {v: values.count(v) for v in set(values)}
|
||||
if 4 in counts.values():
|
||||
return 7 # Four of a kind
|
||||
if sorted(counts.values()) == [2, 3]:
|
||||
return 6 # Full house
|
||||
if 3 in counts.values():
|
||||
return 3 # Three of a kind
|
||||
if list(counts.values()).count(2) == 2:
|
||||
return 2 # Two pair
|
||||
if 2 in counts.values():
|
||||
return 1 # One pair
|
||||
return 0 # High card
|
||||
|
||||
player_score = hand_value(player_hand)
|
||||
bot_score = hand_value(bot_hand)
|
||||
|
||||
if player_score > bot_score:
|
||||
await update_money(user, wallet=bet)
|
||||
msg = f"🃏 Your hand: {', '.join(player_hand)}\n🤖 Bot's hand: {', '.join(bot_hand)}\nYou win {bet:,} coins!"
|
||||
elif player_score < bot_score:
|
||||
await update_money(user, wallet=-bet)
|
||||
msg = f"🃏 Your hand: {', '.join(player_hand)}\n🤖 Bot's hand: {', '.join(bot_hand)}\nYou lose {bet:,} coins!"
|
||||
else:
|
||||
msg = f"🃏 Your hand: {', '.join(player_hand)}\n🤖 Bot's hand: {', '.join(bot_hand)}\nIt's a draw! No coins won or lost."
|
||||
await self._send_embed(
|
||||
ctx,
|
||||
msg,
|
||||
(
|
||||
discord.Color.green()
|
||||
if "win" in msg
|
||||
else discord.Color.red() if "lose" in msg else discord.Color.blurple()
|
||||
),
|
||||
)
|
||||
|
||||
# --- Higher/Lower ---
|
||||
@commands.cooldown(1, 2, commands.BucketType.user)
|
||||
@commands.command(
|
||||
name="higherlower",
|
||||
usage="<bet: integer>",
|
||||
description="Guess if the next number will be higher or lower.",
|
||||
brief="Guess higher or lower.",
|
||||
)
|
||||
async def higherlower(self, ctx, bet: int):
|
||||
if not await self._check_limit_and_exclusion(ctx, bet):
|
||||
return
|
||||
user = ctx.author
|
||||
number = random.randint(1, 100)
|
||||
await self._send_embed(
|
||||
ctx,
|
||||
f"The number is **{number}**. Will the next number be higher or lower? Type `higher` or `lower`.",
|
||||
discord.Color.blurple(),
|
||||
)
|
||||
|
||||
def check(m):
|
||||
return (
|
||||
m.author == ctx.author
|
||||
and m.channel == ctx.channel
|
||||
and m.content.lower() in ["higher", "lower"]
|
||||
)
|
||||
|
||||
try:
|
||||
guess_msg = await self.client.wait_for("message", check=check, timeout=15)
|
||||
except Exception:
|
||||
await self._send_embed(
|
||||
ctx, "Timed out. Please try again.", discord.Color.orange()
|
||||
)
|
||||
return
|
||||
next_number = random.randint(1, 100)
|
||||
win = (guess_msg.content.lower() == "higher" and next_number > number) or (
|
||||
guess_msg.content.lower() == "lower" and next_number < number
|
||||
)
|
||||
if next_number == number:
|
||||
msg = f"The next number was also **{next_number}**. It's a tie! No coins won or lost."
|
||||
elif win:
|
||||
await update_money(user, wallet=bet)
|
||||
msg = f"The next number was **{next_number}**. You win {bet:,} coins!"
|
||||
else:
|
||||
await update_money(user, wallet=-bet)
|
||||
msg = f"The next number was **{next_number}**. You lose {bet:,} coins!"
|
||||
await self._send_embed(
|
||||
ctx, msg, discord.Color.green() if win else discord.Color.red()
|
||||
)
|
||||
|
||||
# --- Scratch ---
|
||||
@commands.cooldown(1, 2, commands.BucketType.user)
|
||||
@commands.command(
|
||||
name="scratch",
|
||||
usage="<bet: integer>",
|
||||
description="Buy a scratch card for a chance to win up to 10x your bet.",
|
||||
brief="Buy a scratch card.",
|
||||
)
|
||||
async def scratch(self, ctx, bet: int):
|
||||
if not await self._check_limit_and_exclusion(ctx, bet):
|
||||
return
|
||||
user = ctx.author
|
||||
symbols = ["🍀", "💎", "⭐", "🍒", "7️⃣"]
|
||||
card = [random.choice(symbols) for _ in range(3)]
|
||||
payout = 0
|
||||
if card.count(card[0]) == 3:
|
||||
payout = bet * 10
|
||||
msg = f"Scratch Card: {' '.join(card)}\nJackpot! You win {payout:,} coins!"
|
||||
elif len(set(card)) == 2:
|
||||
payout = bet * 2
|
||||
msg = f"Scratch Card: {' '.join(card)}\nTwo of a kind! You win {payout:,} coins!"
|
||||
else:
|
||||
payout = -bet
|
||||
msg = f"Scratch Card: {' '.join(card)}\nNo match. You lose {bet:,} coins."
|
||||
await update_money(user, wallet=payout)
|
||||
await self._send_embed(
|
||||
ctx, msg, discord.Color.green() if payout > 0 else discord.Color.red()
|
||||
)
|
||||
|
||||
# --- All other user-facing responses (invite, kick, end_gameroom, etc.) ---
|
||||
# Replace ctx.reply(...) or ctx.send(...) with self._send_embed(...) for consistency.
|
||||
|
||||
@commands.command(
|
||||
name="gameroom",
|
||||
usage="[game: str] [inactivity: int (minutes)]",
|
||||
description="Create a private game room that auto-deletes after inactivity.",
|
||||
brief="Create a private game room.",
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def gameroom(self, ctx, game: str = "game", inactivity: int = 10):
|
||||
"""Create a private text channel for a game session."""
|
||||
guild = ctx.guild
|
||||
overwrites = {
|
||||
guild.default_role: discord.PermissionOverwrite(read_messages=False),
|
||||
ctx.author: discord.PermissionOverwrite(
|
||||
read_messages=True, send_messages=True
|
||||
),
|
||||
}
|
||||
category = ctx.channel.category
|
||||
channel_name = f"{game}-room-{ctx.author.display_name}".replace(
|
||||
" ", "-"
|
||||
).lower()
|
||||
channel = await guild.create_text_channel(
|
||||
name=channel_name,
|
||||
overwrites=overwrites,
|
||||
category=category,
|
||||
reason="Private game room",
|
||||
)
|
||||
await channel.send(
|
||||
f"{ctx.author.mention} Your private **{game}** session has started!\n"
|
||||
f"Use `py invite <user>` and `py kick <user>` in this channel to manage access.\n"
|
||||
f"This channel will be deleted after **{inactivity} minutes of inactivity**."
|
||||
)
|
||||
|
||||
self.active_rooms[channel.id] = {
|
||||
"host": ctx.author.id,
|
||||
"invited": set([ctx.author.id]),
|
||||
"inactivity": inactivity,
|
||||
}
|
||||
self.save_rooms()
|
||||
self.reset_inactivity_timer(channel.id, inactivity)
|
||||
|
||||
def reset_inactivity_timer(self, channel_id, inactivity):
|
||||
"""Reset the inactivity timer for a game room."""
|
||||
if channel_id in self.room_timers:
|
||||
self.room_timers[channel_id].cancel()
|
||||
task = asyncio.create_task(self.inactivity_task(channel_id, inactivity))
|
||||
self.room_timers[channel_id] = task
|
||||
|
||||
async def inactivity_task(self, channel_id, inactivity):
|
||||
"""Delete the channel after inactivity period."""
|
||||
try:
|
||||
await asyncio.sleep(inactivity * 60)
|
||||
channel = self.client.get_channel(channel_id)
|
||||
if channel:
|
||||
await channel.send("Room deleted due to inactivity.")
|
||||
await channel.delete(reason="Game room inactive")
|
||||
self.delete_room(channel_id)
|
||||
self.room_timers.pop(channel_id, None)
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message):
|
||||
"""Reset inactivity timer on message in a game room."""
|
||||
if (
|
||||
hasattr(self, "active_rooms")
|
||||
and message.channel.id in self.active_rooms
|
||||
and not message.author.bot
|
||||
):
|
||||
inactivity = self.active_rooms[message.channel.id].get("inactivity", 10)
|
||||
self.reset_inactivity_timer(message.channel.id, inactivity)
|
||||
|
||||
@commands.command(
|
||||
name="invite",
|
||||
usage="<user: id|name>",
|
||||
description="Invite a user to your private game room.",
|
||||
brief="Invite a user to your game room.",
|
||||
hidden=True,
|
||||
)
|
||||
async def invite(
|
||||
self,
|
||||
ctx,
|
||||
member: discord.Member | None = None,
|
||||
*,
|
||||
identifier: str | None = None,
|
||||
):
|
||||
"""Invite a user to the game room by mention, ID, or username."""
|
||||
room = self.active_rooms.get(ctx.channel.id)
|
||||
if not room or ctx.author.id != room["host"]:
|
||||
await ctx.reply(
|
||||
"Only the host can invite others, and only in a private game room channel.",
|
||||
mention_author=False,
|
||||
)
|
||||
return
|
||||
|
||||
# If member is not provided, try to resolve by ID or name
|
||||
if member is None and identifier:
|
||||
try:
|
||||
member = ctx.guild.get_member(int(identifier))
|
||||
except ValueError:
|
||||
member = discord.utils.find(
|
||||
lambda m: m.name.lower() == identifier.lower()
|
||||
or m.display_name.lower() == identifier.lower(),
|
||||
ctx.guild.members,
|
||||
)
|
||||
if member is None:
|
||||
await ctx.reply(
|
||||
"User not found. Please use a mention, user ID, or username.",
|
||||
mention_author=False,
|
||||
)
|
||||
return
|
||||
|
||||
await ctx.channel.set_permissions(
|
||||
member, read_messages=True, send_messages=True
|
||||
)
|
||||
room["invited"].add(member.id)
|
||||
self.save_rooms()
|
||||
await ctx.send(
|
||||
f"{member.mention} has been invited to the game room!",
|
||||
allowed_mentions=discord.AllowedMentions(users=True),
|
||||
)
|
||||
|
||||
@commands.command(
|
||||
name="kick",
|
||||
usage="<user: mention>",
|
||||
description="Kick a user from your private game room.",
|
||||
brief="Kick a user from your game room.",
|
||||
hidden=True,
|
||||
)
|
||||
async def kick(self, ctx, member: discord.Member):
|
||||
"""Kick a user from the game room."""
|
||||
room = self.active_rooms.get(ctx.channel.id)
|
||||
if not room or ctx.author.id != room["host"]:
|
||||
await ctx.reply(
|
||||
"Only the host can kick others, and only in a private game room channel.",
|
||||
mention_author=False,
|
||||
)
|
||||
return
|
||||
if member.id == ctx.author.id:
|
||||
await ctx.reply("You cannot kick yourself (host).", mention_author=False)
|
||||
return
|
||||
if member.id not in room["invited"]:
|
||||
await ctx.reply(
|
||||
f"{member.mention} is not in this game room.", mention_author=False
|
||||
)
|
||||
return
|
||||
await ctx.channel.set_permissions(member, overwrite=None)
|
||||
room["invited"].discard(member.id)
|
||||
self.save_rooms()
|
||||
await ctx.reply(
|
||||
f"{member.mention} has been kicked from the game room!",
|
||||
mention_author=False,
|
||||
)
|
||||
|
||||
@commands.command(
|
||||
name="end",
|
||||
aliases=["delete"],
|
||||
description="End and delete your private game room.",
|
||||
brief="Delete your game room.",
|
||||
hidden=True,
|
||||
)
|
||||
async def end_gameroom(self, ctx):
|
||||
"""Allows the host to manually delete the game room channel."""
|
||||
room = self.active_rooms.get(ctx.channel.id)
|
||||
if not room or ctx.author.id != room["host"]:
|
||||
await ctx.reply(
|
||||
"Only the host can end this game room, and only in a private game room channel.",
|
||||
mention_author=False,
|
||||
)
|
||||
return
|
||||
await ctx.send("This game room will now be deleted by the host.")
|
||||
self.delete_room(ctx.channel.id)
|
||||
await ctx.channel.delete(reason="Game room ended by host")
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(Gamble(client))
|
||||
Executable
+216
@@ -0,0 +1,216 @@
|
||||
import discord
|
||||
import time
|
||||
from discord.ext import commands
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from utils.sql_commands import DatabaseManager
|
||||
|
||||
# --- Localization dictionary (expand as needed) ---
|
||||
LOCALE = {
|
||||
"en": {
|
||||
"afk_set": "{user} has gone afk.{reason}",
|
||||
"afk_reason": " Reason: {reason}",
|
||||
"afk_notify": "{user} is currently unavailable.{reason}",
|
||||
"feedback_received": "Feedback received. Thank you!",
|
||||
"missing_perms": "⚠️ The bot is missing the following permissions in **{guild}**: {perms}",
|
||||
}
|
||||
}
|
||||
LANG = "en" # Set your language code here
|
||||
|
||||
|
||||
def _(key, **kwargs):
|
||||
return LOCALE[LANG][key].format(**kwargs)
|
||||
|
||||
|
||||
class Informational(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.db = DatabaseManager()
|
||||
|
||||
async def cog_load(self):
|
||||
# Bot Permissions Check at startup
|
||||
for guild in self.client.guilds:
|
||||
me = guild.me
|
||||
missing = []
|
||||
if not me.guild_permissions.manage_roles:
|
||||
missing.append("Manage Roles")
|
||||
if not me.guild_permissions.manage_nicknames:
|
||||
missing.append("Manage Nicknames")
|
||||
if missing:
|
||||
owner = guild.owner
|
||||
try:
|
||||
await owner.send(
|
||||
_("missing_perms", guild=guild.name, perms=", ".join(missing))
|
||||
)
|
||||
except Exception:
|
||||
logging.warning(
|
||||
f"Could not DM owner of {guild.name} about missing permissions."
|
||||
)
|
||||
|
||||
@commands.command(name="whois")
|
||||
async def _userinfo(self, ctx, member: discord.Member = None): # type: ignore
|
||||
if member is None:
|
||||
member = ctx.message.author
|
||||
roles = [role for role in member.roles][1:]
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.purple(),
|
||||
timestamp=ctx.message.created_at,
|
||||
title=f"User Info - {member}",
|
||||
)
|
||||
embed.set_thumbnail(url=member.display_avatar)
|
||||
embed.set_footer(text=f"Requested by {ctx.author}")
|
||||
|
||||
embed.add_field(name="ID:", value=member.id)
|
||||
embed.add_field(name="Display Name:", value=member.display_name)
|
||||
embed.add_field(
|
||||
name="Created Account On:",
|
||||
value=member.created_at.strftime("%a, %#d %B %Y, %I:%M %p UTC"),
|
||||
)
|
||||
embed.add_field(
|
||||
name="Joined Server On:",
|
||||
value=member.joined_at.strftime("%a, %#d %B %Y, %I:%M %p UTC"), # type: ignore
|
||||
)
|
||||
embed.add_field(name="Roles:", value="".join([role.mention for role in roles]))
|
||||
embed.add_field(name="Highest Role:", value=member.top_role.mention)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="serverInfo")
|
||||
@commands.has_permissions(view_audit_log=True) # Example permission
|
||||
async def _server(self, ctx):
|
||||
embed = discord.Embed(
|
||||
title=f"{ctx.guild.name} Info",
|
||||
description="Information of this Server",
|
||||
color=discord.Colour.blue(),
|
||||
)
|
||||
embed.add_field(name="🆔Server ID", value=f"{ctx.guild.id}", inline=True)
|
||||
embed.add_field(
|
||||
name="📆Created On",
|
||||
value=ctx.guild.created_at.strftime("%b %d %Y"),
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(name="👑Owner", value=f"{ctx.guild.owner.mention}", inline=True)
|
||||
embed.add_field(
|
||||
name="👥Members", value=f"{ctx.guild.member_count} Members", inline=True
|
||||
)
|
||||
embed.add_field(
|
||||
name="💬Channels",
|
||||
value=f"{len(ctx.guild.text_channels)} Text | {len(ctx.guild.voice_channels)} Voice",
|
||||
inline=True,
|
||||
)
|
||||
embed.set_thumbnail(url=ctx.guild.icon)
|
||||
embed.set_footer(text="⭐ • Duo")
|
||||
embed.set_author(name=f"{ctx.author.name}", icon_url=ctx.message.author.avatar)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
@commands.command(name="afk")
|
||||
@commands.cooldown(1, 10, commands.BucketType.user)
|
||||
async def _afk(self, ctx, *reason):
|
||||
afk_reason = " ".join(reason)
|
||||
timestamp = datetime.utcnow().isoformat()
|
||||
# Store AFK reason and timestamp in DB
|
||||
self.db.execute_query(
|
||||
"REPLACE INTO afk_status (USERID, GUILDID, REASON, TIMESTAMP) VALUES (%s, %s, %s, %s)",
|
||||
(ctx.author.id, ctx.guild.id, afk_reason, timestamp),
|
||||
)
|
||||
msg = _(
|
||||
"afk_set",
|
||||
user=ctx.author.mention,
|
||||
reason=_("afk_reason", reason=afk_reason) if afk_reason else "",
|
||||
)
|
||||
await ctx.send(msg)
|
||||
|
||||
# Prevent stacking [AFK]
|
||||
try:
|
||||
current_nick = ctx.author.nick or ctx.author.name
|
||||
if "[AFK]" not in current_nick:
|
||||
await ctx.author.edit(nick=f"{current_nick} [AFK]")
|
||||
except discord.Forbidden:
|
||||
logging.warning(f"Missing permissions to edit nickname for {ctx.author}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error editing nickname: {e}")
|
||||
|
||||
guild = ctx.guild
|
||||
try:
|
||||
role = discord.utils.get(ctx.guild.roles, name="AFK")
|
||||
if not role:
|
||||
role = await guild.create_role(name="AFK", hoist=True)
|
||||
all_roles = await guild.fetch_roles()
|
||||
num_roles = len(all_roles)
|
||||
await role.edit(reason=None, position=num_roles - 2)
|
||||
await ctx.author.add_roles(role)
|
||||
except discord.Forbidden:
|
||||
logging.warning("Missing permissions to create/add AFK role.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error handling AFK role: {e}")
|
||||
|
||||
try:
|
||||
await ctx.message.delete()
|
||||
except discord.Forbidden:
|
||||
pass
|
||||
|
||||
@commands.command(name="afklist")
|
||||
async def afk_list(self, ctx):
|
||||
afks = self.db.fetch_all(
|
||||
"SELECT USERID, REASON, TIMESTAMP FROM afk_status WHERE GUILDID = %s",
|
||||
(ctx.guild.id,),
|
||||
)
|
||||
if not afks:
|
||||
await ctx.reply("No one is AFK right now.")
|
||||
return
|
||||
lines = []
|
||||
for afk in afks:
|
||||
member = ctx.guild.get_member(int(afk["USERID"]))
|
||||
reason = afk["REASON"] or "No reason given."
|
||||
timestamp = afk.get("TIMESTAMP")
|
||||
# Convert timestamp string to datetime
|
||||
if isinstance(timestamp, str):
|
||||
try:
|
||||
timestamp = datetime.fromisoformat(timestamp)
|
||||
except Exception:
|
||||
timestamp = None
|
||||
if timestamp:
|
||||
duration = datetime.utcnow() - timestamp
|
||||
duration_str = str(duration).split(".")[0]
|
||||
else:
|
||||
duration_str = "unknown duration"
|
||||
lines.append(
|
||||
f"{member.mention if member else afk['USERID']} - {reason} (AFK for {duration_str})"
|
||||
)
|
||||
await ctx.send("\n".join(lines))
|
||||
|
||||
@commands.command(name="feedback")
|
||||
@commands.cooldown(1, 30, commands.BucketType.user)
|
||||
async def _feedback(self, ctx, *info):
|
||||
feedback_text = " ".join(info)
|
||||
timestamp = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
user = f"{ctx.author} ({ctx.author.id})"
|
||||
# Store feedback in DB
|
||||
self.db.execute_query(
|
||||
"INSERT INTO feedback (USER, GUILDID, TIMESTAMP, CONTENT) VALUES (%s, %s, %s, %s)",
|
||||
(user, ctx.guild.id if ctx.guild else None, timestamp, feedback_text),
|
||||
)
|
||||
await ctx.reply(_("feedback_received"))
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_command_error(self, ctx, error):
|
||||
# Notify admin if permissions are missing
|
||||
if isinstance(
|
||||
error, (commands.MissingPermissions, commands.BotMissingPermissions)
|
||||
):
|
||||
perms = getattr(error, "missing_perms", None)
|
||||
perms_str = ", ".join(perms) if perms else "Unknown"
|
||||
owner = ctx.guild.owner if ctx.guild else None
|
||||
if owner:
|
||||
try:
|
||||
await owner.send(
|
||||
_("missing_perms", guild=ctx.guild.name, perms=perms_str)
|
||||
)
|
||||
except Exception:
|
||||
logging.warning(
|
||||
f"Could not DM owner of {ctx.guild.name} about missing permissions."
|
||||
)
|
||||
raise error # Let default handler run too
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(Informational(client))
|
||||
Executable
+308
@@ -0,0 +1,308 @@
|
||||
import discord
|
||||
import time
|
||||
from datetime import timedelta
|
||||
from discord.ext import commands
|
||||
from utils.sql_commands import DatabaseManager
|
||||
import logging
|
||||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
async def remove_afk_status(member, db=None):
|
||||
"""Removes AFK status for a member by resetting their nickname and removing the AFK role and DB entry."""
|
||||
afk_role = discord.utils.get(member.guild.roles, name="AFK")
|
||||
bot_member = member.guild.me
|
||||
changed = False
|
||||
|
||||
# Remove AFK role if present
|
||||
if afk_role and afk_role in member.roles:
|
||||
try:
|
||||
await member.remove_roles(afk_role)
|
||||
changed = True
|
||||
except discord.Forbidden:
|
||||
logging.warning(f"Missing permissions to remove AFK role for {member.name}")
|
||||
except discord.HTTPException as e:
|
||||
logging.error(f"HTTP error removing AFK role for {member.name}: {e}")
|
||||
|
||||
# Remove [AFK] from nickname if possible, not owner, and bot has higher role
|
||||
try:
|
||||
if (
|
||||
member.guild.owner_id != member.id
|
||||
and member.nick
|
||||
and "[AFK]" in member.nick
|
||||
and bot_member.top_role > member.top_role
|
||||
):
|
||||
await member.edit(
|
||||
nick=member.nick.replace("[AFK]", "").replace(" ", " ").strip()
|
||||
)
|
||||
changed = True
|
||||
except discord.Forbidden:
|
||||
logging.warning(f"Missing permissions to edit nickname for {member.name}")
|
||||
except Exception as e:
|
||||
logging.error(f"Error editing nickname: {e}")
|
||||
|
||||
# Remove AFK entry from DB if provided
|
||||
if db:
|
||||
db.execute_query(
|
||||
"DELETE FROM afk_status WHERE USERID = %s AND GUILDID = %s",
|
||||
(member.id, member.guild.id),
|
||||
)
|
||||
changed = True
|
||||
|
||||
return changed if changed else False
|
||||
|
||||
|
||||
class Listeners(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.db = DatabaseManager()
|
||||
# Load or initialize stats
|
||||
self.stats_file = "message_command_stats.json"
|
||||
self.stats = self.load_stats()
|
||||
# Add new counters if not present
|
||||
if "total_messages" not in self.stats:
|
||||
self.stats["total_messages"] = 0
|
||||
if "command_messages" not in self.stats:
|
||||
self.stats["command_messages"] = 0
|
||||
if "non_command_messages" not in self.stats:
|
||||
self.stats["non_command_messages"] = 0
|
||||
|
||||
def load_stats(self):
|
||||
try:
|
||||
with open(self.stats_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return {
|
||||
"messages": defaultdict(int),
|
||||
"commands": defaultdict(int),
|
||||
"total_messages": 0,
|
||||
"command_messages": 0,
|
||||
"non_command_messages": 0,
|
||||
}
|
||||
|
||||
def save_stats(self):
|
||||
# Convert defaultdicts to dicts for JSON serialization
|
||||
stats_to_save = {
|
||||
"messages": dict(self.stats["messages"]),
|
||||
"commands": dict(self.stats["commands"]),
|
||||
"channels": dict(self.stats.get("channels", {})),
|
||||
"guilds": dict(self.stats.get("guilds", {})),
|
||||
"total_messages": self.stats.get("total_messages", 0),
|
||||
"command_messages": self.stats.get("command_messages", 0),
|
||||
"non_command_messages": self.stats.get("non_command_messages", 0),
|
||||
}
|
||||
with open(self.stats_file, "w", encoding="utf-8") as f:
|
||||
json.dump(stats_to_save, f, indent=2)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self):
|
||||
print(f"{self.client.user} has been loaded")
|
||||
activity = discord.Activity(type=discord.ActivityType.watching, name="You :)")
|
||||
|
||||
await self.client.change_presence(
|
||||
status=discord.Status.online, activity=activity
|
||||
)
|
||||
# Record the start time in a more readable format
|
||||
with open("time.txt", "w") as file:
|
||||
file.write(str(time.time()))
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_command_error(self, ctx: commands.Context, error):
|
||||
user = ctx.author
|
||||
if isinstance(error, commands.CommandNotFound):
|
||||
return
|
||||
|
||||
if isinstance(error, (commands.MissingPermissions, commands.NotOwner)):
|
||||
return await ctx.reply("You cannot use this command.", mention_author=False)
|
||||
|
||||
if isinstance(error, commands.MemberNotFound):
|
||||
return await ctx.reply(
|
||||
"The member you provided is incorrect or not found.",
|
||||
mention_author=False,
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(error, commands.MissingRequiredArgument)
|
||||
and ctx.command is not None
|
||||
):
|
||||
cmd_parent = ctx.command.parent
|
||||
cmd_name = (
|
||||
f"{cmd_parent} {ctx.command.name}" if cmd_parent else ctx.command.name
|
||||
)
|
||||
cmd_usage = ctx.command.usage or ""
|
||||
|
||||
cmd_params = list(ctx.command.params.values())
|
||||
params = []
|
||||
|
||||
for param in cmd_params[1:]: # Skip the 'self' parameter
|
||||
log = (
|
||||
f"<{param.name}>"
|
||||
if param.default is not param.empty
|
||||
else f"<{param.name}*>"
|
||||
) # Required args
|
||||
params.append(log)
|
||||
|
||||
usage = (
|
||||
f"Usage: `{cmd_name} {' '.join(params)}`"
|
||||
if not cmd_usage
|
||||
else f"Usage: `{cmd_usage}`"
|
||||
)
|
||||
|
||||
em = discord.Embed(description=f"**Correct usage**\n{usage}")
|
||||
if ctx.command.aliases:
|
||||
em.add_field(name="Aliases", value=", ".join(ctx.command.aliases))
|
||||
em.set_footer(text="' * ' means that argument is required")
|
||||
return await ctx.reply(embed=em, mention_author=False)
|
||||
|
||||
if isinstance(error, commands.CommandOnCooldown):
|
||||
time_left = timedelta(seconds=error.retry_after)
|
||||
return await ctx.reply(
|
||||
f"You are on cooldown. Try after `{time_left}`.", mention_author=False
|
||||
)
|
||||
|
||||
# For any other error, we will raise it to be handled by the default error handler
|
||||
raise error
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message):
|
||||
if message.author.bot or not message.guild:
|
||||
return
|
||||
|
||||
user_id = str(message.author.id)
|
||||
if user_id not in self.stats["messages"]:
|
||||
self.stats["messages"][user_id] = {
|
||||
"total": 0,
|
||||
"commands": 0,
|
||||
"non_commands": 0,
|
||||
}
|
||||
|
||||
self.stats["messages"][user_id]["total"] += 1
|
||||
|
||||
# --- Per-channel stats by channel name ---
|
||||
channel_name = message.channel.name
|
||||
if "channels" not in self.stats:
|
||||
self.stats["channels"] = {}
|
||||
if channel_name not in self.stats["channels"]:
|
||||
self.stats["channels"][channel_name] = 0
|
||||
self.stats["channels"][channel_name] += 1
|
||||
|
||||
# --- Per-guild stats by guild name ---
|
||||
guild_name = message.guild.name
|
||||
if "guilds" not in self.stats:
|
||||
self.stats["guilds"] = {}
|
||||
if guild_name not in self.stats["guilds"]:
|
||||
self.stats["guilds"][guild_name] = 0
|
||||
self.stats["guilds"][guild_name] += 1
|
||||
|
||||
ctx = await self.client.get_context(message)
|
||||
if ctx.valid and ctx.command:
|
||||
self.stats["messages"][user_id]["commands"] += 1
|
||||
else:
|
||||
self.stats["messages"][user_id]["non_commands"] += 1
|
||||
|
||||
self.save_stats()
|
||||
|
||||
# Prevent AFK removal on the afk command itself
|
||||
invoked_afk = False
|
||||
ctx = await self.client.get_context(message)
|
||||
if ctx.valid and ctx.command and ctx.command.name == "afk":
|
||||
invoked_afk = True
|
||||
|
||||
if not invoked_afk:
|
||||
afk_role = discord.utils.get(message.guild.roles, name="AFK")
|
||||
was_afk = False
|
||||
|
||||
# Check DB for AFK status (covers owner and anyone else)
|
||||
afk_db_entry = self.db.fetch_one(
|
||||
"SELECT REASON FROM afk_status WHERE USERID = %s AND GUILDID = %s",
|
||||
(message.author.id, message.guild.id),
|
||||
)
|
||||
if afk_db_entry:
|
||||
was_afk = True
|
||||
# Remove AFK role if present
|
||||
if afk_role and afk_role in message.author.roles:
|
||||
try:
|
||||
await message.author.remove_roles(afk_role)
|
||||
except discord.Forbidden:
|
||||
logging.warning("Missing permissions to remove AFK role.")
|
||||
except Exception as e:
|
||||
logging.error(f"Error removing AFK role: {e}")
|
||||
# Remove [AFK] from nickname if possible and not owner/higher role
|
||||
try:
|
||||
bot_member = message.guild.me
|
||||
if (
|
||||
message.guild.owner_id != message.author.id
|
||||
and message.author.nick
|
||||
and "[AFK]" in message.author.nick
|
||||
and bot_member.top_role > message.author.top_role
|
||||
):
|
||||
await message.author.edit(
|
||||
nick=message.author.nick.replace("[AFK]", "")
|
||||
.replace(" ", " ")
|
||||
.strip()
|
||||
)
|
||||
except discord.Forbidden:
|
||||
logging.warning(
|
||||
f"Missing permissions to edit nickname for {message.author}"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error editing nickname: {e}")
|
||||
# Remove AFK entry from DB
|
||||
self.db.execute_query(
|
||||
"DELETE FROM afk_status WHERE USERID = %s AND GUILDID = %s",
|
||||
(message.author.id, message.guild.id),
|
||||
)
|
||||
# Send a welcome back message if user was AFK (even owner)
|
||||
if was_afk:
|
||||
await message.channel.send(
|
||||
f"Welcome back, {message.author.mention}! You are no longer AFK."
|
||||
)
|
||||
|
||||
# Notify if mentioned user is AFK (check DB, not just role)
|
||||
for member in message.mentions:
|
||||
afk_entry = self.db.fetch_one(
|
||||
"SELECT REASON FROM afk_status WHERE USERID = %s AND GUILDID = %s",
|
||||
(member.id, message.guild.id),
|
||||
)
|
||||
if afk_entry:
|
||||
reason = afk_entry["REASON"] if afk_entry["REASON"] else ""
|
||||
msg = f"{member.mention} is currently unavailable." + (
|
||||
f" Reason: {reason}" if reason else ""
|
||||
)
|
||||
await message.channel.send(msg)
|
||||
break
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_command(self, ctx):
|
||||
# Track command usage
|
||||
cmd_name = ctx.command.qualified_name if ctx.command else "unknown"
|
||||
self.stats["commands"][cmd_name] = self.stats["commands"].get(cmd_name, 0) + 1
|
||||
self.save_stats()
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_guild_join(self, guild):
|
||||
# Log to console
|
||||
print(f"Joined new guild: {guild.name} (ID: {guild.id})")
|
||||
|
||||
# Optionally, send a message to the first text channel the bot can send messages in
|
||||
for channel in guild.text_channels:
|
||||
if channel.permissions_for(guild.me).send_messages:
|
||||
await channel.send(
|
||||
f"Hello! Thanks for inviting me to **{guild.name}**.\n"
|
||||
"Use `py help` to see my commands!"
|
||||
)
|
||||
break
|
||||
|
||||
# Optionally, update your stats file
|
||||
if hasattr(self, "stats"):
|
||||
guild_name = guild.name
|
||||
if "guilds" not in self.stats:
|
||||
self.stats["guilds"] = {}
|
||||
self.stats["guilds"][guild_name] = (
|
||||
self.stats["guilds"].get(guild_name, 0) + 1
|
||||
)
|
||||
self.save_stats()
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(Listeners(client))
|
||||
Executable
+135
@@ -0,0 +1,135 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
import smtplib
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from dotenv import load_dotenv
|
||||
from os import getenv
|
||||
import html
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class Mail(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
load_dotenv()
|
||||
from utils.sql_commands import DatabaseManager
|
||||
|
||||
self.db = DatabaseManager()
|
||||
|
||||
@commands.is_owner()
|
||||
@commands.command(name="mail_feedback")
|
||||
async def mail(self, ctx):
|
||||
password = getenv("EMAILPASS")
|
||||
username = getenv("EMAILUSER")
|
||||
server = getenv("EMAILSERVER")
|
||||
port = getenv("EMAILPORT")
|
||||
receiver = getenv("FEEDBACKRECEIVER")
|
||||
|
||||
if None in [password, username, server, port, receiver]:
|
||||
await ctx.reply(
|
||||
"Email configuration is missing. Please check your environment variables.",
|
||||
delete_after=5,
|
||||
)
|
||||
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"
|
||||
|
||||
# Fetch feedback from the database
|
||||
feedback_rows = self.db.fetch_all("SELECT * FROM feedback")
|
||||
all_feedback = ""
|
||||
for i, row in enumerate(feedback_rows, 1):
|
||||
content = html.escape(row["CONTENT"])
|
||||
user = html.escape(row["USER"])
|
||||
timestamp = html.escape(row["TIMESTAMP"])
|
||||
|
||||
all_feedback += f"""
|
||||
<li style="margin-bottom:25px; border-bottom:1px solid #edf2f7; padding-bottom:20px;">
|
||||
<div class="feedback-card" style="background:#ffffff; border-radius:8px; position:relative;">
|
||||
<div style="display:flex; align-items:center; margin-bottom:12px;">
|
||||
<div style="background:#4361ee; width:36px; height:36px; border-radius:50%; display:flex; align-items:center; justify-content:center; color:white; font-weight:bold; flex-shrink:0;">
|
||||
{i}
|
||||
</div>
|
||||
<div style="margin-left:15px;">
|
||||
<h3 style="margin:0; font-size:16px; color:#2d3748;">{user}</h3>
|
||||
<p style="margin:3px 0 0; font-size:13px; color:#718096;">{timestamp}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background:#f8f9fc; padding:15px; border-radius:8px; border-left:3px solid #4361ee;">
|
||||
<p style="margin:0; font-size:15px; line-height:1.5; color:#4a5568;">{content}</p>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
"""
|
||||
|
||||
|
||||
|
||||
text = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>User Feedback Report</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 600px) {{
|
||||
.container {{
|
||||
width: 95% !important;
|
||||
}}
|
||||
.feedback-card {{
|
||||
padding: 12px !important;
|
||||
}}
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body style="margin:0; padding:20px 0; background-color:#f7f9fc; font-family:'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;">
|
||||
<div class="container" style="max-width:600px; margin:0 auto; background:#ffffff; border-radius:10px; box-shadow:0 4px 15px rgba(0,0,0,0.05);">
|
||||
<!-- Header -->
|
||||
<div style="background: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%); padding:30px 0; border-radius:10px 10px 0 0; text-align:center;">
|
||||
<h1 style="color:#fff; margin:0; font-weight:600;">User Feedback Report</h1>
|
||||
<p style="color:rgba(255,255,255,0.8); margin:8px 0 0; font-size:18px;">New feedback submissions</p>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div style="padding:20px 30px; background:#f0f7ff; border-bottom:1px solid #e3f2fd;">
|
||||
<p style="margin:0; font-size:16px; color:#2d3748;">
|
||||
Total feedback submissions: <strong>{len(feedback_rows)}</strong>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Feedback Items -->
|
||||
<div style="padding:10px 30px 30px;">
|
||||
<ul style="list-style:none; padding:0; margin:0;">
|
||||
{all_feedback}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div style="padding:20px 30px; text-align:center; background:#f8f9fa; border-top:1px solid #eaeaea; border-radius:0 0 10px 10px; color:#718096; font-size:14px;">
|
||||
<p style="margin:0;">Generated automatically by PyBot • {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
|
||||
<p style="margin:8px 0 0;">Do not reply to this automated message</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
|
||||
msg.attach(MIMEText(text, "html"))
|
||||
s.send_message(msg)
|
||||
await ctx.reply("Mail sent.", delete_after=2)
|
||||
s.quit()
|
||||
except Exception as e:
|
||||
await ctx.reply(f"Failed to send mail: {e}", delete_after=5)
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(Mail(client))
|
||||
Executable
+245
@@ -0,0 +1,245 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from utils.sql_commands import DatabaseManager
|
||||
from utils.bank_functions import *
|
||||
from random import randint
|
||||
from math import log2
|
||||
from datetime import datetime
|
||||
import io
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
import logging
|
||||
import os
|
||||
|
||||
|
||||
def calculate_xp_needed_for_next_level(level: int) -> int:
|
||||
"""Calculates the XP needed for the next level."""
|
||||
return int(35 * (level**2) + 35 * level)
|
||||
|
||||
|
||||
class XP(commands.Cog):
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.db = DatabaseManager("")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, ctx: discord.Message) -> None:
|
||||
if ctx.author.bot:
|
||||
return
|
||||
|
||||
try:
|
||||
data = self.db.fetch_one(
|
||||
"SELECT XP, LEVEL FROM users WHERE ID = %s", (ctx.author.id,)
|
||||
)
|
||||
if data is None:
|
||||
# Insert a new row for this user
|
||||
current_level = 0
|
||||
current_xp = 0
|
||||
self.db.execute_query(
|
||||
"INSERT INTO users (ID, XP, LEVEL) VALUES (%s, %s, %s)",
|
||||
(ctx.author.id, current_xp, current_level),
|
||||
)
|
||||
else:
|
||||
current_level = data.get("LEVEL", 0)
|
||||
current_xp = data.get("XP", 0)
|
||||
|
||||
logging.info(f"XP: {current_xp}, Level: {current_level}")
|
||||
|
||||
# Balanced XP calculation
|
||||
base = randint(5, 10)
|
||||
length_bonus = (
|
||||
min(len(ctx.content), 100) // 10
|
||||
) # +1 XP per 10 chars, max +10
|
||||
extra_xp = base + length_bonus
|
||||
extra_xp = min(extra_xp, 20) # cap at 20 XP per message
|
||||
current_xp += extra_xp
|
||||
|
||||
if current_xp > calculate_xp_needed_for_next_level(current_level):
|
||||
current_level += 1
|
||||
player_balance = self.db.fetch_one(
|
||||
"SELECT BANK FROM economy WHERE ID = %s", (ctx.author.id,)
|
||||
)
|
||||
player_balance = player_balance.get("BANK", 0) if player_balance else 0
|
||||
levelup_bonus = randint(500, 2000)
|
||||
new_balance = player_balance + levelup_bonus
|
||||
await update_money(ctx.author, bank=new_balance)
|
||||
|
||||
await ctx.channel.send(
|
||||
f"Congrats!! You achieved a new level.\nYou are now level {current_level}. You received {levelup_bonus}<:flooney:1194943899765051473>"
|
||||
)
|
||||
current_xp = 0
|
||||
|
||||
# Use INSERT ... ON DUPLICATE KEY UPDATE for XP/level
|
||||
self.db.execute_query(
|
||||
"INSERT INTO users (ID, XP, LEVEL) VALUES (%s, %s, %s) "
|
||||
"ON DUPLICATE KEY UPDATE XP = %s, LEVEL = %s",
|
||||
(ctx.author.id, current_xp, current_level, current_xp, current_level),
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Error in XP on_message: {e}")
|
||||
|
||||
@commands.command()
|
||||
async def top(self, ctx: commands.Context) -> None:
|
||||
embed = discord.Embed(
|
||||
colour=discord.Colour.purple(),
|
||||
timestamp=ctx.message.created_at,
|
||||
title="Top Messengers",
|
||||
)
|
||||
users = self.db.fetch_all("SELECT ID, XP, LEVEL FROM users")
|
||||
if not users:
|
||||
return
|
||||
|
||||
sorted_users = sorted(users, key=lambda x: int(x["XP"]), reverse=True)[:10]
|
||||
data = []
|
||||
|
||||
for index, member in enumerate(sorted_users, start=1):
|
||||
try:
|
||||
member_obj = await self.client.fetch_user(int(member["ID"]))
|
||||
member_name = (
|
||||
member_obj.display_name
|
||||
if hasattr(member_obj, "display_name")
|
||||
else str(member_obj)
|
||||
)
|
||||
except Exception:
|
||||
member_name = f"User {member['ID']}"
|
||||
member_xp, member_lvl = int(member["XP"]), int(member["LEVEL"])
|
||||
medals = ["🥇", "🥈", "🥉"]
|
||||
msg = f"**{medals[index - 1] if index <= 3 else index} `{member_name}` -- {member_xp:,}xp, {member_lvl:,}level**"
|
||||
data.append(msg)
|
||||
|
||||
msg = "\n".join(data)
|
||||
embed.description = f"It's Based on XP of Global Users\n\n{msg}"
|
||||
guild_name = ctx.guild.name if ctx.guild is not None else "Direct Message"
|
||||
embed.set_footer(text=f"GLOBAL - {guild_name}")
|
||||
embed.color = discord.Color(0x00FF00)
|
||||
await ctx.reply(embed=embed, mention_author=False)
|
||||
|
||||
@commands.command()
|
||||
async def stats(self, ctx: commands.Context) -> None:
|
||||
"""Shows the user's current XP and level."""
|
||||
try:
|
||||
# Use a fallback background if the random one doesn't exist
|
||||
bg_path = f"images/image_{randint(1,100)}.jpg"
|
||||
if not os.path.exists(bg_path):
|
||||
bg_path = "images/default.jpg"
|
||||
profile_image = await self.create_profile_card(ctx.author, bg_path)
|
||||
|
||||
await ctx.send(file=discord.File(profile_image, "profile.png"))
|
||||
|
||||
if profile_image is not None:
|
||||
profile_image.close()
|
||||
|
||||
except ZeroDivisionError:
|
||||
await ctx.send(f"{ctx.author.mention}, you haven't earned any XP yet.")
|
||||
except Exception as e:
|
||||
await ctx.send(f"Error creating profile card: {e}")
|
||||
logging.error(f"Error creating profile card: {e}")
|
||||
|
||||
async def create_profile_card(
|
||||
self, user: discord.Member | discord.User, background_image_path: str
|
||||
) -> io.BytesIO:
|
||||
"""Create a profile card image for a user with a custom background and text overlay."""
|
||||
card_width, card_height = 400, 200
|
||||
text_color = (255, 255, 255)
|
||||
progress_bar_color = (0, 255, 0)
|
||||
outline_color = (255, 255, 255)
|
||||
tint_color = (50, 50, 50)
|
||||
transparency = 25
|
||||
opacity = int(255 * transparency / 100)
|
||||
|
||||
# Load and resize the background image
|
||||
try:
|
||||
background_image = Image.open(background_image_path).resize(
|
||||
(card_width, card_height)
|
||||
)
|
||||
except Exception:
|
||||
# Fallback to a plain background if image fails
|
||||
# type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
background_image = Image.new(
|
||||
"RGBA", (card_width, card_height), (30, 30, 30, 255) # type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
)
|
||||
|
||||
card_image = Image.new("RGBA", (card_width, card_height))
|
||||
card_image.paste(background_image, (0, 0))
|
||||
|
||||
overlay = Image.new(
|
||||
"RGBA",
|
||||
card_image.size,
|
||||
tint_color + (opacity,), # type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
)
|
||||
card_image = Image.alpha_composite(card_image, overlay)
|
||||
|
||||
draw = ImageDraw.Draw(card_image)
|
||||
|
||||
# Load fonts with fallback
|
||||
font_path = "arial.ttf"
|
||||
try:
|
||||
font = ImageFont.truetype(font_path, 20)
|
||||
except Exception:
|
||||
font = ImageFont.load_default()
|
||||
|
||||
draw.text((10, 10), f"User: {user.display_name}", fill=text_color, font=font)
|
||||
user_info = self.db.fetch_one(
|
||||
"SELECT XP, LEVEL FROM users WHERE ID = %s", (user.id,)
|
||||
)
|
||||
draw.text(
|
||||
(10, 40), f"Level: {user_info.get('LEVEL', 0)}", fill=text_color, font=font
|
||||
)
|
||||
draw.text(
|
||||
(10, 70),
|
||||
f"XP: {user_info.get('XP', 0)}/{calculate_xp_needed_for_next_level(user_info.get('LEVEL', 0))}",
|
||||
fill=text_color,
|
||||
font=font,
|
||||
)
|
||||
draw.text(
|
||||
(10, 100),
|
||||
f"Next level in: {calculate_xp_needed_for_next_level(user_info.get('LEVEL', 0))-user_info.get('XP', 0)}XP",
|
||||
fill=text_color,
|
||||
font=font,
|
||||
)
|
||||
|
||||
max_xp = calculate_xp_needed_for_next_level(user_info.get("LEVEL", 0))
|
||||
progress_ratio = user_info.get("XP", 0) / max_xp if max_xp > 0 else 0
|
||||
progress_width = progress_ratio * 200
|
||||
|
||||
draw.rectangle([(10, 130), (210, 150)], outline=outline_color, width=2)
|
||||
draw.rectangle([(10, 130), (10 + progress_width, 150)], fill=progress_bar_color)
|
||||
|
||||
try:
|
||||
profile_size = 96
|
||||
margin = 30
|
||||
if user.avatar is not None:
|
||||
profile_picture_data = await user.avatar.read()
|
||||
profile_image = Image.open(io.BytesIO(profile_picture_data)).resize(
|
||||
(profile_size, profile_size)
|
||||
)
|
||||
else:
|
||||
# Use a default avatar image if user has no avatar
|
||||
default_avatar_path = "images/default_avatar.png"
|
||||
if os.path.exists(default_avatar_path):
|
||||
profile_image = Image.open(default_avatar_path).resize(
|
||||
(profile_size, profile_size)
|
||||
)
|
||||
else:
|
||||
# Create a blank image if default not found
|
||||
# type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
profile_image = Image.new(
|
||||
"RGBA", (profile_size, profile_size), (100, 100, 100, 255) # type: ignore[reportArgumentType] - Pillow expects a tuple for RGBA color, but Pylance incorrectly warns here
|
||||
)
|
||||
card_image.paste(
|
||||
profile_image,
|
||||
(card_width - profile_size - margin, margin),
|
||||
profile_image.convert("RGBA"),
|
||||
)
|
||||
except Exception as error:
|
||||
logging.error(f"Error fetching profile picture: {error}")
|
||||
|
||||
image_bytes = io.BytesIO()
|
||||
card_image.save(image_bytes, format="PNG")
|
||||
image_bytes.seek(0)
|
||||
|
||||
return image_bytes
|
||||
|
||||
|
||||
async def setup(client):
|
||||
await client.add_cog(XP(client))
|
||||
Reference in New Issue
Block a user