mirror of
https://github.com/wlinator/luminara.git
synced 2024-10-02 18:03:12 +00:00
Clean up dir tree in preparation for v3
This commit is contained in:
parent
5645a78174
commit
1c9e89cb0e
92 changed files with 1 additions and 9393 deletions
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
55
.github/workflows/docker-image.yml
vendored
55
.github/workflows/docker-image.yml
vendored
|
@ -1,55 +0,0 @@
|
|||
name: Create and Publish Docker Image CI
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "v*.*.*" ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
docker:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
wlinator/luminara
|
||||
ghcr.io/wlinator/luminara
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
||||
|
43
Client.py
43
Client.py
|
@ -1,43 +0,0 @@
|
|||
import os
|
||||
import platform
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
|
||||
|
||||
class LumiBot(bridge.Bot):
|
||||
async def on_ready(self):
|
||||
"""
|
||||
Called when the bot is ready.
|
||||
|
||||
Logs various information about the bot and the environment it is running on.
|
||||
Note: This function isn't guaranteed to only be called once. The event is called when a RESUME request fails.
|
||||
"""
|
||||
logger.info(f"{CONST.TITLE} v{CONST.VERSION}")
|
||||
logger.info(f"Logged in with ID {self.user.id if self.user else 'Unknown'}")
|
||||
logger.info(f"discord.py API version: {discord.__version__}")
|
||||
logger.info(f"Python version: {platform.python_version()}")
|
||||
logger.info(f"Running on: {platform.system()} {platform.release()} ({os.name})")
|
||||
|
||||
if self.owner_ids:
|
||||
for owner_id in self.owner_ids:
|
||||
logger.info(f"Added bot admin: {owner_id}")
|
||||
|
||||
async def process_commands(self, message: discord.Message):
|
||||
"""
|
||||
Processes commands sent by users.
|
||||
|
||||
Args:
|
||||
message (discord.Message): The message object containing the command.
|
||||
"""
|
||||
if message.author.bot:
|
||||
return
|
||||
|
||||
ctx = await self.get_context(message)
|
||||
|
||||
if ctx.command:
|
||||
# await ctx.trigger_typing()
|
||||
await self.invoke(ctx)
|
101
Luminara.py
101
Luminara.py
|
@ -1,100 +1 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
import Client
|
||||
import services.config_service
|
||||
import services.help_service
|
||||
from db.database import run_migrations
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import Blacklisted
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
|
||||
# Remove the default logger configuration
|
||||
logger.remove()
|
||||
|
||||
# Add a new logger configuration with colors and a short datetime format
|
||||
log_format = (
|
||||
"<green>{time:YY-MM-DD HH:mm:ss}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
# "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
||||
"<level>{message}</level>"
|
||||
)
|
||||
logger.add(sys.stdout, format=log_format, colorize=True, level="DEBUG")
|
||||
|
||||
|
||||
async def get_prefix(bot, message):
|
||||
extras = services.config_service.GuildConfig.get_prefix(message)
|
||||
return commands.when_mentioned_or(*extras)(bot, message)
|
||||
|
||||
|
||||
client = Client.LumiBot(
|
||||
owner_ids=CONST.OWNER_IDS,
|
||||
command_prefix=get_prefix,
|
||||
intents=discord.Intents.all(),
|
||||
status=discord.Status.online,
|
||||
help_command=services.help_service.LumiHelp(),
|
||||
)
|
||||
|
||||
|
||||
@client.check
|
||||
async def blacklist_check(ctx):
|
||||
if BlacklistUserService.is_user_blacklisted(ctx.author.id):
|
||||
raise Blacklisted
|
||||
return True
|
||||
|
||||
|
||||
def load_modules():
|
||||
loaded = set()
|
||||
|
||||
# Load event listeners (handlers) and command cogs (modules)
|
||||
for directory in ["handlers", "modules"]:
|
||||
directory_path = os.path.join(os.getcwd(), directory)
|
||||
if not os.path.isdir(directory_path):
|
||||
continue
|
||||
|
||||
items = (
|
||||
[
|
||||
d
|
||||
for d in os.listdir(directory_path)
|
||||
if os.path.isdir(os.path.join(directory_path, d))
|
||||
]
|
||||
if directory == "modules"
|
||||
else [f[:-3] for f in os.listdir(directory_path) if f.endswith(".py")]
|
||||
)
|
||||
|
||||
for item in items:
|
||||
if item in loaded:
|
||||
continue
|
||||
|
||||
try:
|
||||
client.load_extension(f"{directory}.{item}")
|
||||
loaded.add(item)
|
||||
logger.debug(f"{item.upper()} loaded.")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to load {item.upper()}: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
This code is only ran when Lumi.py is the primary module,
|
||||
so NOT when main is imported from a cog. (sys.modules)
|
||||
"""
|
||||
|
||||
logger.info("LUMI IS BOOTING")
|
||||
|
||||
# Run database migrations
|
||||
run_migrations()
|
||||
|
||||
# load command and listener cogs
|
||||
load_modules()
|
||||
|
||||
if not CONST.TOKEN:
|
||||
logger.error("token is not set in .env")
|
||||
exit(1)
|
||||
|
||||
client.run(CONST.TOKEN)
|
||||
# TODO: switch to DPY
|
||||
|
|
115
db/database.py
115
db/database.py
|
@ -1,115 +0,0 @@
|
|||
import os
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
import mysql.connector
|
||||
from loguru import logger
|
||||
from mysql.connector import pooling
|
||||
|
||||
from lib.constants import CONST
|
||||
|
||||
|
||||
def create_connection_pool(name: str, size: int) -> pooling.MySQLConnectionPool:
|
||||
return pooling.MySQLConnectionPool(
|
||||
pool_name=name,
|
||||
pool_size=size,
|
||||
host="db",
|
||||
port=3306,
|
||||
database=CONST.MARIADB_DATABASE,
|
||||
user=CONST.MARIADB_USER,
|
||||
password=CONST.MARIADB_PASSWORD,
|
||||
charset="utf8mb4",
|
||||
collation="utf8mb4_unicode_ci",
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
_cnxpool = create_connection_pool("core-pool", 25)
|
||||
except mysql.connector.Error as e:
|
||||
logger.critical(f"Couldn't create the MySQL connection pool: {e}")
|
||||
raise e
|
||||
|
||||
|
||||
def execute_query(query, values=None):
|
||||
with _cnxpool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(query, values)
|
||||
conn.commit()
|
||||
return cursor
|
||||
|
||||
|
||||
def select_query(query, values=None):
|
||||
with _cnxpool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(query, values)
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
def select_query_one(query, values=None):
|
||||
with _cnxpool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
cursor.execute(query, values)
|
||||
output = cursor.fetchone()
|
||||
return output[0] if output else None
|
||||
|
||||
|
||||
def select_query_dict(query, values=None):
|
||||
with _cnxpool.get_connection() as conn:
|
||||
with conn.cursor(dictionary=True) as cursor:
|
||||
cursor.execute(query, values)
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
def run_migrations():
|
||||
migrations_dir = "db/migrations"
|
||||
migration_files = sorted(
|
||||
[f for f in os.listdir(migrations_dir) if f.endswith(".sql")],
|
||||
)
|
||||
|
||||
with _cnxpool.get_connection() as conn:
|
||||
with conn.cursor() as cursor:
|
||||
# Create migrations table if it doesn't exist
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS migrations (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
""")
|
||||
|
||||
for migration_file in migration_files:
|
||||
# Check if migration has already been applied
|
||||
cursor.execute(
|
||||
"SELECT COUNT(*) FROM migrations WHERE filename = %s",
|
||||
(migration_file,),
|
||||
)
|
||||
if cursor.fetchone()[0] > 0:
|
||||
logger.debug(
|
||||
f"Migration {migration_file} already applied, skipping.",
|
||||
)
|
||||
continue
|
||||
|
||||
# Read and execute migration file
|
||||
migration_sql = pathlib.Path(
|
||||
os.path.join(migrations_dir, migration_file),
|
||||
).read_text()
|
||||
try:
|
||||
# Split the migration file into individual statements
|
||||
statements = re.split(r";\s*$", migration_sql, flags=re.MULTILINE)
|
||||
for statement in statements:
|
||||
if statement.strip():
|
||||
cursor.execute(statement)
|
||||
|
||||
# Record successful migration
|
||||
cursor.execute(
|
||||
"INSERT INTO migrations (filename) VALUES (%s)",
|
||||
(migration_file,),
|
||||
)
|
||||
conn.commit()
|
||||
logger.debug(f"Successfully applied migration: {migration_file}")
|
||||
except mysql.connector.Error as e:
|
||||
conn.rollback()
|
||||
logger.error(f"Error applying migration {migration_file}: {e}")
|
||||
raise
|
||||
|
||||
logger.debug("All migrations completed.")
|
|
@ -1,109 +0,0 @@
|
|||
SET FOREIGN_KEY_CHECKS=0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS xp (
|
||||
user_id BIGINT NOT NULL,
|
||||
guild_id BIGINT NOT NULL,
|
||||
user_xp INT NOT NULL,
|
||||
user_level INT NOT NULL,
|
||||
cooldown DECIMAL(15,2),
|
||||
PRIMARY KEY (user_id, guild_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS currency (
|
||||
user_id BIGINT NOT NULL,
|
||||
balance BIGINT NOT NULL,
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS blackjack (
|
||||
id INT AUTO_INCREMENT,
|
||||
user_id BIGINT,
|
||||
is_won BOOLEAN,
|
||||
bet BIGINT,
|
||||
payout BIGINT,
|
||||
hand_player TEXT,
|
||||
hand_dealer TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS slots (
|
||||
id INT AUTO_INCREMENT,
|
||||
user_id BIGINT,
|
||||
is_won BOOLEAN,
|
||||
bet BIGINT,
|
||||
payout BIGINT,
|
||||
spin_type TEXT,
|
||||
icons TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS dailies (
|
||||
id INT AUTO_INCREMENT,
|
||||
user_id BIGINT,
|
||||
amount BIGINT,
|
||||
claimed_at TINYTEXT,
|
||||
streak INT,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS item (
|
||||
id INT AUTO_INCREMENT,
|
||||
name TEXT,
|
||||
display_name TEXT,
|
||||
description TEXT,
|
||||
image_url TEXT,
|
||||
emote_id BIGINT,
|
||||
quote TEXT,
|
||||
type TEXT,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS inventory (
|
||||
user_id BIGINT,
|
||||
item_id INT,
|
||||
quantity INT,
|
||||
|
||||
PRIMARY KEY (user_id, item_id),
|
||||
FOREIGN KEY (item_id) REFERENCES item (id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS birthdays (
|
||||
user_id BIGINT NOT NULL,
|
||||
guild_id BIGINT NOT NULL,
|
||||
birthday DATETIME DEFAULT NULL,
|
||||
PRIMARY KEY (user_id, guild_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS guild_config (
|
||||
guild_id BIGINT NOT NULL,
|
||||
prefix TINYTEXT,
|
||||
birthday_channel_id BIGINT,
|
||||
command_channel_id BIGINT, /* NULL: users can do XP & Currency commands everywhere. */
|
||||
intro_channel_id BIGINT,
|
||||
welcome_channel_id BIGINT,
|
||||
welcome_message TEXT,
|
||||
boost_channel_id BIGINT,
|
||||
boost_message TEXT,
|
||||
boost_image_url TEXT,
|
||||
level_channel_id BIGINT, /* level-up messages, if NULL the level-up message will be shown in current msg channel*/
|
||||
level_message TEXT, /* if NOT NULL and LEVEL_TYPE = 2, this can be a custom level up message. */
|
||||
level_message_type TINYINT(1) NOT NULL DEFAULT 1, /* 0: no level up messages, 1: levels.en-US.json, 2: generic message */
|
||||
PRIMARY KEY (guild_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS level_rewards (
|
||||
guild_id BIGINT NOT NULL,
|
||||
level INT NOT NULL,
|
||||
role_id BIGINT,
|
||||
persistent BOOLEAN,
|
||||
|
||||
PRIMARY KEY (guild_id, level)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS blacklist_user (
|
||||
user_id BIGINT NOT NULL,
|
||||
reason TEXT,
|
||||
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
PRIMARY KEY (user_id)
|
||||
);
|
|
@ -1,21 +0,0 @@
|
|||
-- Create a table to store custom reactions
|
||||
CREATE TABLE IF NOT EXISTS custom_reactions (
|
||||
id SERIAL PRIMARY KEY, -- Unique identifier for each custom reaction
|
||||
trigger_text TEXT NOT NULL, -- The text that triggers the custom reaction
|
||||
response TEXT, -- The response text for the custom reaction (nullable for emoji reactions)
|
||||
emoji_id BIGINT UNSIGNED, -- The emoji for the custom reaction (nullable for text responses)
|
||||
is_emoji BOOLEAN DEFAULT FALSE, -- Indicates if the reaction is a discord emoji reaction
|
||||
is_full_match BOOLEAN DEFAULT FALSE, -- Indicates if the trigger matches the full content of the message
|
||||
is_global BOOLEAN DEFAULT TRUE, -- Indicates if the reaction is global or specific to a guild
|
||||
guild_id BIGINT UNSIGNED, -- The ID of the guild where the custom reaction is used (nullable for global reactions)
|
||||
creator_id BIGINT UNSIGNED NOT NULL, -- The ID of the user who created the custom reaction
|
||||
usage_count INT DEFAULT 0, -- The number of times a custom reaction has been used
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the custom reaction was created
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Timestamp when the custom reaction was last updated
|
||||
CONSTRAINT unique_trigger_guild UNIQUE (trigger_text, guild_id) -- Ensure that the combination of trigger_text, guild_id, and is_full_match is unique
|
||||
);
|
||||
|
||||
-- Create indexes to speed up lookups
|
||||
CREATE OR REPLACE INDEX idx_custom_reactions_guild_id ON custom_reactions(guild_id);
|
||||
CREATE OR REPLACE INDEX idx_custom_reactions_creator_id ON custom_reactions(creator_id);
|
||||
CREATE OR REPLACE INDEX idx_custom_reactions_trigger_text ON custom_reactions(trigger_text);
|
|
@ -1,44 +0,0 @@
|
|||
CREATE TABLE IF NOT EXISTS mod_log (
|
||||
guild_id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
|
||||
channel_id BIGINT UNSIGNED NOT NULL,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS cases (
|
||||
id SERIAL PRIMARY KEY,
|
||||
guild_id BIGINT UNSIGNED NOT NULL,
|
||||
case_number INT UNSIGNED NOT NULL,
|
||||
target_id BIGINT UNSIGNED NOT NULL,
|
||||
moderator_id BIGINT UNSIGNED NOT NULL,
|
||||
action_type ENUM(
|
||||
'WARN',
|
||||
'TIMEOUT',
|
||||
'UNTIMEOUT',
|
||||
'KICK',
|
||||
'BAN',
|
||||
'UNBAN',
|
||||
'SOFTBAN',
|
||||
'TEMPBAN',
|
||||
'NOTE',
|
||||
'MUTE',
|
||||
'UNMUTE',
|
||||
'DEAFEN',
|
||||
'UNDEAFEN'
|
||||
) NOT NULL,
|
||||
reason TEXT,
|
||||
duration INT UNSIGNED, -- for timeouts
|
||||
expires_at TIMESTAMP, -- for tempbans & mutes
|
||||
modlog_message_id BIGINT UNSIGNED,
|
||||
is_closed BOOLEAN NOT NULL DEFAULT FALSE, -- to indicate if the case is closed
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_case (guild_id, case_number)
|
||||
);
|
||||
|
||||
|
||||
CREATE OR REPLACE INDEX idx_cases_guild_id ON cases(guild_id);
|
||||
CREATE OR REPLACE INDEX idx_cases_target_id ON cases(target_id);
|
||||
CREATE OR REPLACE INDEX idx_cases_moderator_id ON cases(moderator_id);
|
||||
CREATE OR REPLACE INDEX idx_cases_action_type ON cases(action_type);
|
|
@ -1,28 +0,0 @@
|
|||
services:
|
||||
core:
|
||||
image: ghcr.io/wlinator/luminara:2 # Remove "ghcr.io/" if you want to use the Docker Hub image.
|
||||
container_name: lumi-core
|
||||
restart: always
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: true
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
db:
|
||||
image: mariadb
|
||||
container_name: lumi-db
|
||||
restart: always
|
||||
environment:
|
||||
MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
|
||||
MARIADB_USER: ${MARIADB_USER}
|
||||
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
|
||||
MARIADB_DATABASE: ${MARIADB_DATABASE}
|
||||
volumes:
|
||||
- ./data:/var/lib/mysql/
|
||||
healthcheck:
|
||||
test: [ "CMD", "mariadb", "-h", "localhost", "-u", "${MARIADB_USER}", "-p${MARIADB_PASSWORD}", "-e", "SELECT 1" ]
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
|
@ -1,121 +0,0 @@
|
|||
import sys
|
||||
import traceback
|
||||
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import Cog
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions import LumiExceptions
|
||||
|
||||
|
||||
async def on_command_error(ctx, error):
|
||||
if isinstance(error, (commands.CommandNotFound, LumiExceptions.Blacklisted)):
|
||||
return
|
||||
|
||||
author_text = None
|
||||
description = None
|
||||
footer_text = None
|
||||
ephemeral = False
|
||||
|
||||
if isinstance(error, commands.MissingRequiredArgument):
|
||||
author_text = CONST.STRINGS["error_bad_argument_author"]
|
||||
description = CONST.STRINGS["error_bad_argument_description"].format(str(error))
|
||||
|
||||
elif isinstance(error, commands.BadArgument):
|
||||
author_text = CONST.STRINGS["error_bad_argument_author"]
|
||||
description = CONST.STRINGS["error_bad_argument_description"].format(str(error))
|
||||
|
||||
elif isinstance(error, commands.BotMissingPermissions):
|
||||
author_text = CONST.STRINGS["error_bot_missing_permissions_author"]
|
||||
description = CONST.STRINGS["error_bot_missing_permissions_description"]
|
||||
|
||||
elif isinstance(error, commands.CommandOnCooldown):
|
||||
author_text = CONST.STRINGS["error_command_cooldown_author"]
|
||||
description = CONST.STRINGS["error_command_cooldown_description"].format(
|
||||
int(error.retry_after // 60),
|
||||
int(error.retry_after % 60),
|
||||
)
|
||||
ephemeral = True
|
||||
|
||||
elif isinstance(error, commands.MissingPermissions):
|
||||
author_text = CONST.STRINGS["error_missing_permissions_author"]
|
||||
description = CONST.STRINGS["error_missing_permissions_description"]
|
||||
|
||||
elif isinstance(error, commands.NoPrivateMessage):
|
||||
author_text = CONST.STRINGS["error_no_private_message_author"]
|
||||
description = CONST.STRINGS["error_no_private_message_description"]
|
||||
|
||||
elif isinstance(error, commands.NotOwner):
|
||||
author_text = CONST.STRINGS["error_not_owner_author"]
|
||||
description = CONST.STRINGS["error_not_owner_description"]
|
||||
|
||||
elif isinstance(error, commands.PrivateMessageOnly):
|
||||
author_text = CONST.STRINGS["error_private_message_only_author"]
|
||||
description = CONST.STRINGS["error_private_message_only_description"]
|
||||
|
||||
elif isinstance(error, LumiExceptions.BirthdaysDisabled):
|
||||
author_text = CONST.STRINGS["error_birthdays_disabled_author"]
|
||||
description = CONST.STRINGS["error_birthdays_disabled_description"]
|
||||
footer_text = CONST.STRINGS["error_birthdays_disabled_footer"]
|
||||
|
||||
elif isinstance(error, LumiExceptions.LumiException):
|
||||
author_text = CONST.STRINGS["error_lumi_exception_author"]
|
||||
description = CONST.STRINGS["error_lumi_exception_description"].format(
|
||||
str(error),
|
||||
)
|
||||
|
||||
else:
|
||||
author_text = CONST.STRINGS["error_unknown_error_author"]
|
||||
description = CONST.STRINGS["error_unknown_error_description"]
|
||||
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=author_text,
|
||||
description=description,
|
||||
footer_text=footer_text,
|
||||
),
|
||||
ephemeral=ephemeral,
|
||||
)
|
||||
|
||||
|
||||
async def on_error(event: str, *args, **kwargs) -> None:
|
||||
logger.exception(
|
||||
f"on_error INFO: errors.event.{event} | '*args': {args} | '**kwargs': {kwargs}",
|
||||
)
|
||||
logger.exception(f"on_error EXCEPTION: {sys.exc_info()}")
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
class ErrorListener(Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@staticmethod
|
||||
async def log_command_error(ctx, error, command_type):
|
||||
log_msg = (
|
||||
f"{ctx.author.name} executed {command_type}{ctx.command.qualified_name}"
|
||||
)
|
||||
|
||||
log_msg += " in DMs" if ctx.guild is None else f" | guild: {ctx.guild.name} "
|
||||
logger.warning(f"{log_msg} | FAILED: {error}")
|
||||
|
||||
@Cog.listener()
|
||||
async def on_command_error(self, ctx, error) -> None:
|
||||
await on_command_error(ctx, error)
|
||||
await self.log_command_error(ctx, error, ".")
|
||||
|
||||
@Cog.listener()
|
||||
async def on_application_command_error(self, ctx, error) -> None:
|
||||
await on_command_error(ctx, error)
|
||||
await self.log_command_error(ctx, error, "/")
|
||||
|
||||
@Cog.listener()
|
||||
async def on_error(self, event: str, *args, **kwargs) -> None:
|
||||
await on_error(event, *args, **kwargs)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(ErrorListener(client))
|
|
@ -1,86 +0,0 @@
|
|||
from discord.ext.commands import Cog
|
||||
from loguru import logger
|
||||
|
||||
from modules.config import c_boost, c_greet
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
class EventHandler(Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@Cog.listener()
|
||||
async def on_member_join(self, member):
|
||||
if BlacklistUserService.is_user_blacklisted(member.id):
|
||||
return
|
||||
|
||||
config = GuildConfig(member.guild.id)
|
||||
|
||||
if not config.welcome_channel_id:
|
||||
return
|
||||
|
||||
embed = c_greet.create_greet_embed(member, config.welcome_message)
|
||||
|
||||
try:
|
||||
await member.guild.get_channel(config.welcome_channel_id).send(
|
||||
embed=embed,
|
||||
content=member.mention,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Greet message not sent in '{member.guild.name}'. Channel ID may be invalid. {e}",
|
||||
)
|
||||
|
||||
@Cog.listener()
|
||||
async def on_member_update(self, before, after):
|
||||
if BlacklistUserService.is_user_blacklisted(after.id):
|
||||
return
|
||||
|
||||
if before.premium_since is None and after.premium_since is not None:
|
||||
await self.on_nitro_boost(after)
|
||||
|
||||
@staticmethod
|
||||
async def on_nitro_boost(member):
|
||||
config = GuildConfig(member.guild.id)
|
||||
|
||||
if not config.boost_channel_id:
|
||||
return
|
||||
|
||||
embed = c_boost.create_boost_embed(
|
||||
member,
|
||||
config.boost_message,
|
||||
config.boost_image_url,
|
||||
)
|
||||
|
||||
try:
|
||||
await member.guild.get_channel(config.boost_channel_id).send(
|
||||
embed=embed,
|
||||
content=member.mention,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Boost message not sent in '{member.guild.name}'. Channel ID may be invalid. {e}",
|
||||
)
|
||||
|
||||
@Cog.listener()
|
||||
async def on_command_completion(self, ctx) -> None:
|
||||
log_msg = f"{ctx.author.name} executed .{ctx.command.qualified_name}"
|
||||
|
||||
if ctx.guild is not None:
|
||||
logger.debug(f"{log_msg} | guild: {ctx.guild.name} ")
|
||||
else:
|
||||
logger.debug(f"{log_msg} in DMs")
|
||||
|
||||
@Cog.listener()
|
||||
async def on_application_command_completion(self, ctx) -> None:
|
||||
log_msg = f"{ctx.author.name} executed /{ctx.command.qualified_name}"
|
||||
|
||||
if ctx.guild is not None:
|
||||
logger.debug(f"{log_msg} | guild: {ctx.guild.name} ")
|
||||
else:
|
||||
logger.debug(f"{log_msg} in DMs")
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(EventHandler(client))
|
|
@ -1,86 +0,0 @@
|
|||
import contextlib
|
||||
|
||||
from discord import Message
|
||||
from discord.ext.commands import Cog
|
||||
from loguru import logger
|
||||
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
||||
class ReactionHandler:
|
||||
"""
|
||||
Handles reactions to messages based on predefined triggers and responses.
|
||||
"""
|
||||
|
||||
def __init__(self, client, message: Message) -> None:
|
||||
self.client = client
|
||||
self.message: Message = message
|
||||
self.content: str = self.message.content.lower()
|
||||
self.reaction_service = CustomReactionsService()
|
||||
|
||||
async def run_checks(self) -> None:
|
||||
"""
|
||||
Runs checks for reactions and responses.
|
||||
Guild triggers are prioritized over global triggers if they are identical.
|
||||
"""
|
||||
guild_id = self.message.guild.id if self.message.guild else None
|
||||
|
||||
if guild_id:
|
||||
data = await self.reaction_service.find_trigger(guild_id, self.content)
|
||||
if data:
|
||||
processed = False
|
||||
try:
|
||||
if data["type"] == "text":
|
||||
processed = await self.try_respond(data)
|
||||
elif data["type"] == "emoji":
|
||||
processed = await self.try_react(data)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to process reaction: {e}")
|
||||
|
||||
if processed:
|
||||
await self.reaction_service.increment_reaction_usage(
|
||||
int(data["id"]),
|
||||
)
|
||||
|
||||
async def try_respond(self, data) -> bool:
|
||||
"""
|
||||
Tries to respond to the message.
|
||||
"""
|
||||
if response := data.get("response"):
|
||||
with contextlib.suppress(Exception):
|
||||
await self.message.reply(response)
|
||||
return True
|
||||
return False
|
||||
|
||||
async def try_react(self, data) -> bool:
|
||||
"""
|
||||
Tries to react to the message.
|
||||
"""
|
||||
if emoji_id := data.get("emoji_id"):
|
||||
with contextlib.suppress(Exception):
|
||||
if emoji := self.client.get_emoji(emoji_id):
|
||||
await self.message.add_reaction(emoji)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class ReactionListener(Cog):
|
||||
def __init__(self, client) -> None:
|
||||
self.client = client
|
||||
|
||||
@Cog.listener("on_message")
|
||||
async def reaction_listener(self, message: Message) -> None:
|
||||
"""
|
||||
Listens for new messages and processes them if the author is not a bot and not blacklisted.
|
||||
|
||||
:param message: The message to process.
|
||||
"""
|
||||
if not message.author.bot and not BlacklistUserService.is_user_blacklisted(
|
||||
message.author.id,
|
||||
):
|
||||
await ReactionHandler(self.client, message).run_checks()
|
||||
|
||||
|
||||
def setup(client) -> None:
|
||||
client.add_cog(ReactionListener(client))
|
|
@ -1,263 +0,0 @@
|
|||
import asyncio
|
||||
import contextlib
|
||||
import random
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import TextChannelConverter
|
||||
|
||||
from Client import LumiBot
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
from services.config_service import GuildConfig
|
||||
from services.xp_service import XpRewardService, XpService
|
||||
|
||||
|
||||
class XPHandler:
|
||||
def __init__(self, client: LumiBot, message: discord.Message) -> None:
|
||||
"""
|
||||
Initializes the XPHandler with the given client and message.
|
||||
|
||||
Args:
|
||||
client (LumiBot): The bot client.
|
||||
message (discord.Message): The message object.
|
||||
"""
|
||||
self.client = client
|
||||
self.message: discord.Message = message
|
||||
self.author: discord.Member | discord.User = message.author
|
||||
self.guild: discord.Guild | None = message.guild
|
||||
self.xp_conf: XpService = XpService(
|
||||
self.author.id,
|
||||
self.guild.id if self.guild else 0,
|
||||
)
|
||||
self.guild_conf: Optional[GuildConfig] = None
|
||||
|
||||
def process(self) -> bool:
|
||||
"""
|
||||
Processes the XP gain and level up for the user.
|
||||
|
||||
Returns:
|
||||
bool: True if the user leveled up, False otherwise.
|
||||
"""
|
||||
_xp: XpService = self.xp_conf
|
||||
_now: float = time.time()
|
||||
leveled_up: bool = False
|
||||
|
||||
if _xp.cooldown_time and _now < _xp.cooldown_time:
|
||||
return False
|
||||
|
||||
# Award the amount of XP specified in .env
|
||||
_xp.xp += _xp.xp_gain
|
||||
|
||||
# Check if total XP now exceeds the XP required to level up
|
||||
if _xp.xp >= XpService.xp_needed_for_next_level(_xp.level):
|
||||
_xp.level += 1
|
||||
_xp.xp = 0
|
||||
leveled_up = True
|
||||
|
||||
_xp.cooldown_time = _now + _xp.new_cooldown
|
||||
_xp.push()
|
||||
return leveled_up
|
||||
|
||||
async def notify(self) -> None:
|
||||
"""
|
||||
Notifies the user and the guild about the level up.
|
||||
"""
|
||||
if self.guild is None:
|
||||
return
|
||||
|
||||
_xp: XpService = self.xp_conf
|
||||
_gd: GuildConfig = GuildConfig(self.guild.id)
|
||||
|
||||
level_message: Optional[str] = None # Initialize level_message
|
||||
|
||||
if isinstance(self.author, discord.Member):
|
||||
level_message = await self.get_level_message(_gd, _xp, self.author)
|
||||
|
||||
if level_message:
|
||||
level_channel: Optional[discord.TextChannel] = await self.get_level_channel(
|
||||
self.message,
|
||||
_gd,
|
||||
)
|
||||
|
||||
if level_channel:
|
||||
await level_channel.send(content=level_message)
|
||||
else:
|
||||
await self.message.reply(content=level_message)
|
||||
|
||||
async def reward(self) -> None:
|
||||
"""
|
||||
Rewards the user with a role for leveling up.
|
||||
"""
|
||||
if self.guild is None:
|
||||
return
|
||||
|
||||
_xp: XpService = self.xp_conf
|
||||
_rew: XpRewardService = XpRewardService(self.guild.id)
|
||||
|
||||
if role_id := _rew.get_role(_xp.level):
|
||||
reason: str = "Automated Level Reward"
|
||||
|
||||
if role := self.guild.get_role(role_id):
|
||||
with contextlib.suppress(
|
||||
discord.Forbidden,
|
||||
discord.NotFound,
|
||||
discord.HTTPException,
|
||||
):
|
||||
if isinstance(self.author, discord.Member):
|
||||
await self.author.add_roles(role, reason=reason)
|
||||
previous, replace = _rew.should_replace_previous_reward(_xp.level)
|
||||
|
||||
if replace and isinstance(self.author, discord.Member):
|
||||
if role := self.guild.get_role(previous or role_id):
|
||||
with contextlib.suppress(
|
||||
discord.Forbidden,
|
||||
discord.NotFound,
|
||||
discord.HTTPException,
|
||||
):
|
||||
await self.author.remove_roles(role, reason=reason)
|
||||
|
||||
async def get_level_channel(
|
||||
self,
|
||||
message: discord.Message,
|
||||
guild_config: GuildConfig,
|
||||
) -> Optional[discord.TextChannel]:
|
||||
"""
|
||||
Retrieves the level up notification channel for the guild.
|
||||
|
||||
Args:
|
||||
message (discord.Message): The message object.
|
||||
guild_config (GuildConfig): The guild configuration.
|
||||
|
||||
Returns:
|
||||
Optional[discord.TextChannel]: The level up notification channel, or None if not found.
|
||||
"""
|
||||
if guild_config.level_channel_id and message.guild:
|
||||
context = await self.client.get_context(message)
|
||||
|
||||
with contextlib.suppress(commands.BadArgument, commands.CommandError):
|
||||
return await TextChannelConverter().convert(
|
||||
context,
|
||||
str(guild_config.level_channel_id),
|
||||
)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def get_level_message(
|
||||
guild_config: GuildConfig,
|
||||
level_config: XpService,
|
||||
author: discord.Member,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Retrieves the level up message for the user.
|
||||
|
||||
Args:
|
||||
guild_config (GuildConfig): The guild configuration.
|
||||
level_config (XpService): The XP service configuration.
|
||||
author (discord.Member): The user who leveled up.
|
||||
|
||||
Returns:
|
||||
Optional[str]: The level up message, or None if not found.
|
||||
"""
|
||||
match guild_config.level_message_type:
|
||||
case 0:
|
||||
level_message = None
|
||||
case 1:
|
||||
level_message = XPHandler.messages_whimsical(level_config.level, author)
|
||||
case 2:
|
||||
if not guild_config.level_message:
|
||||
level_message = XPHandler.level_message_generic(
|
||||
level_config.level,
|
||||
author,
|
||||
)
|
||||
else:
|
||||
level_message = formatter.template(
|
||||
guild_config.level_message,
|
||||
author.name,
|
||||
level_config.level,
|
||||
)
|
||||
case _:
|
||||
raise ValueError("Invalid level message type")
|
||||
|
||||
return level_message
|
||||
|
||||
@staticmethod
|
||||
def level_message_generic(level: int, author: discord.Member) -> str:
|
||||
"""
|
||||
Generates a generic level up message.
|
||||
|
||||
Args:
|
||||
level (int): The new level of the user.
|
||||
author (discord.Member): The user who leveled up.
|
||||
|
||||
Returns:
|
||||
str: The generic level up message.
|
||||
"""
|
||||
return CONST.STRINGS["level_up"].format(author.name, level)
|
||||
|
||||
@staticmethod
|
||||
def messages_whimsical(level: int, author: discord.Member) -> str:
|
||||
"""
|
||||
Generates a whimsical level up message.
|
||||
|
||||
Args:
|
||||
level (int): The new level of the user.
|
||||
author (discord.Member): The user who leveled up.
|
||||
|
||||
Returns:
|
||||
str: The whimsical level up message.
|
||||
"""
|
||||
level_range: Optional[str] = None
|
||||
for key in CONST.LEVEL_MESSAGES.keys():
|
||||
start, end = map(int, key.split("-"))
|
||||
if start <= level <= end:
|
||||
level_range = key
|
||||
break
|
||||
|
||||
if level_range is None:
|
||||
# Generic fallback
|
||||
return XPHandler.level_message_generic(level, author)
|
||||
|
||||
message_list = CONST.LEVEL_MESSAGES[level_range]
|
||||
random_message = random.choice(message_list)
|
||||
start_string = CONST.STRINGS["level_up_prefix"].format(author.name)
|
||||
return start_string + random_message.format(level)
|
||||
|
||||
|
||||
class XpListener(commands.Cog):
|
||||
def __init__(self, client: LumiBot) -> None:
|
||||
"""
|
||||
Initializes the XpListener with the given client.
|
||||
|
||||
Args:
|
||||
client (LumiBot): The bot client.
|
||||
"""
|
||||
self.client: LumiBot = client
|
||||
|
||||
@commands.Cog.listener("on_message")
|
||||
async def xp_listener(self, message: discord.Message) -> None:
|
||||
"""
|
||||
Listens for messages and processes XP gain and level up.
|
||||
|
||||
Args:
|
||||
message (discord.Message): The message object.
|
||||
"""
|
||||
if BlacklistUserService.is_user_blacklisted(message.author.id):
|
||||
return
|
||||
|
||||
if message.author.bot or message.guild is None:
|
||||
return
|
||||
|
||||
_xp: XPHandler = XPHandler(self.client, message)
|
||||
if _xp.process():
|
||||
await asyncio.gather(
|
||||
_xp.notify(),
|
||||
_xp.reward(),
|
||||
)
|
||||
|
||||
|
||||
def setup(client: LumiBot) -> None:
|
||||
client.add_cog(XpListener(client))
|
|
@ -1,19 +0,0 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from lib.exceptions import LumiExceptions
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
def birthdays_enabled():
|
||||
async def predicate(ctx):
|
||||
if ctx.guild is None:
|
||||
return True
|
||||
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.birthday_channel_id:
|
||||
raise LumiExceptions.BirthdaysDisabled
|
||||
|
||||
return True
|
||||
|
||||
return commands.check(predicate)
|
120
lib/constants.py
120
lib/constants.py
|
@ -1,120 +0,0 @@
|
|||
import os
|
||||
from typing import Optional, Set, List, Dict
|
||||
import yaml
|
||||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class _parser:
|
||||
"""Internal parser class. Not intended for direct use outside this module."""
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def read_yaml(self, path):
|
||||
return self._read_file(f"settings/{path}.yaml", yaml.safe_load)
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def read_json(self, path):
|
||||
return self._read_file(f"settings/{path}.json", json.load)
|
||||
|
||||
def _read_file(self, file_path, load_func):
|
||||
with open(file_path) as file:
|
||||
return load_func(file)
|
||||
|
||||
|
||||
class Constants:
|
||||
_p = _parser()
|
||||
_settings = _p.read_yaml("settings")
|
||||
|
||||
# bot credentials (.env file)
|
||||
TOKEN: Optional[str] = os.environ.get("TOKEN")
|
||||
INSTANCE: Optional[str] = os.environ.get("INSTANCE")
|
||||
XP_GAIN_PER_MESSAGE: int = int(os.environ.get("XP_GAIN_PER_MESSAGE", 1))
|
||||
XP_GAIN_COOLDOWN: int = int(os.environ.get("XP_GAIN_COOLDOWN", 8))
|
||||
DBX_TOKEN: Optional[str] = os.environ.get("DBX_OAUTH2_REFRESH_TOKEN")
|
||||
DBX_APP_KEY: Optional[str] = os.environ.get("DBX_APP_KEY")
|
||||
DBX_APP_SECRET: Optional[str] = os.environ.get("DBX_APP_SECRET")
|
||||
MARIADB_USER: Optional[str] = os.environ.get("MARIADB_USER")
|
||||
MARIADB_PASSWORD: Optional[str] = os.environ.get("MARIADB_PASSWORD")
|
||||
MARIADB_ROOT_PASSWORD: Optional[str] = os.environ.get("MARIADB_ROOT_PASSWORD")
|
||||
MARIADB_DATABASE: Optional[str] = os.environ.get("MARIADB_DATABASE")
|
||||
|
||||
OWNER_IDS: Optional[Set[int]] = (
|
||||
{int(id.strip()) for id in os.environ.get("OWNER_IDS", "").split(",") if id}
|
||||
if "OWNER_IDS" in os.environ
|
||||
else None
|
||||
)
|
||||
|
||||
# metadata
|
||||
TITLE: str = _settings["info"]["title"]
|
||||
AUTHOR: str = _settings["info"]["author"]
|
||||
LICENSE: str = _settings["info"]["license"]
|
||||
VERSION: str = _settings["info"]["version"]
|
||||
REPO_URL: str = _settings["info"]["repository_url"]
|
||||
|
||||
# images
|
||||
ALLOWED_IMAGE_EXTENSIONS: List[str] = _settings["images"][
|
||||
"allowed_image_extensions"
|
||||
]
|
||||
BIRTHDAY_GIF_URL: str = _settings["images"]["birthday_gif_url"]
|
||||
|
||||
# colors
|
||||
COLOR_DEFAULT: int = _settings["colors"]["color_default"]
|
||||
COLOR_WARNING: int = _settings["colors"]["color_warning"]
|
||||
COLOR_ERROR: int = _settings["colors"]["color_error"]
|
||||
|
||||
# economy
|
||||
DAILY_REWARD: int = _settings["economy"]["daily_reward"]
|
||||
BLACKJACK_MULTIPLIER: float = _settings["economy"]["blackjack_multiplier"]
|
||||
BLACKJACK_HIT_EMOJI: str = _settings["economy"]["blackjack_hit_emoji"]
|
||||
BLACKJACK_STAND_EMOJI: str = _settings["economy"]["blackjack_stand_emoji"]
|
||||
SLOTS_MULTIPLIERS: Dict[str, float] = _settings["economy"]["slots_multipliers"]
|
||||
|
||||
# art from git repository
|
||||
_fetch_url: str = _settings["art"]["fetch_url"]
|
||||
|
||||
LUMI_LOGO_OPAQUE: str = _fetch_url + _settings["art"]["logo"]["opaque"]
|
||||
LUMI_LOGO_TRANSPARENT: str = _fetch_url + _settings["art"]["logo"]["transparent"]
|
||||
BOOST_ICON: str = _fetch_url + _settings["art"]["icons"]["boost"]
|
||||
CHECK_ICON: str = _fetch_url + _settings["art"]["icons"]["check"]
|
||||
CROSS_ICON: str = _fetch_url + _settings["art"]["icons"]["cross"]
|
||||
EXCLAIM_ICON: str = _fetch_url + _settings["art"]["icons"]["exclaim"]
|
||||
HAMMER_ICON: str = _fetch_url + _settings["art"]["icons"]["hammer"]
|
||||
MONEY_BAG_ICON: str = _fetch_url + _settings["art"]["icons"]["money_bag"]
|
||||
MONEY_COINS_ICON: str = _fetch_url + _settings["art"]["icons"]["money_coins"]
|
||||
QUESTION_ICON: str = _fetch_url + _settings["art"]["icons"]["question"]
|
||||
STREAK_ICON: str = _fetch_url + _settings["art"]["icons"]["streak"]
|
||||
STREAK_BRONZE_ICON: str = _fetch_url + _settings["art"]["icons"]["streak_bronze"]
|
||||
STREAK_GOLD_ICON: str = _fetch_url + _settings["art"]["icons"]["streak_gold"]
|
||||
STREAK_SILVER_ICON: str = _fetch_url + _settings["art"]["icons"]["streak_silver"]
|
||||
WARNING_ICON: str = _fetch_url + _settings["art"]["icons"]["warning"]
|
||||
|
||||
# art from imgur
|
||||
FLOWERS_ART: str = _settings["art"]["juicybblue"]["flowers"]
|
||||
TEAPOT_ART: str = _settings["art"]["juicybblue"]["teapot"]
|
||||
MUFFIN_ART: str = _settings["art"]["juicybblue"]["muffin"]
|
||||
CLOUD_ART: str = _settings["art"]["other"]["cloud"]
|
||||
TROPHY_ART: str = _settings["art"]["other"]["trophy"]
|
||||
|
||||
# emotes
|
||||
EMOTES_SERVER_ID: int = _settings["emotes"]["guild_id"]
|
||||
EMOTE_IDS: Dict[str, int] = _settings["emotes"]["emote_ids"]
|
||||
|
||||
# introductions (currently only usable in ONE guild)
|
||||
INTRODUCTIONS_GUILD_ID: int = _settings["introductions"]["intro_guild_id"]
|
||||
INTRODUCTIONS_CHANNEL_ID: int = _settings["introductions"]["intro_channel_id"]
|
||||
INTRODUCTIONS_QUESTION_MAPPING: Dict[str, str] = _settings["introductions"][
|
||||
"intro_question_mapping"
|
||||
]
|
||||
|
||||
# Response strings
|
||||
# TODO: Implement switching between languages
|
||||
STRINGS = _p.read_json("responses/strings.en-US")
|
||||
LEVEL_MESSAGES = _p.read_json("responses/levels.en-US")
|
||||
|
||||
# birthday messages
|
||||
_bday = _p.read_json("responses/bdays.en-US")
|
||||
BIRTHDAY_MESSAGES = _bday["birthday_messages"]
|
||||
BIRTHDAY_MONTHS = _bday["months"]
|
||||
|
||||
|
||||
CONST = Constants()
|
|
@ -1,206 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
|
||||
|
||||
class EmbedBuilder:
|
||||
@staticmethod
|
||||
def create_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
color=None,
|
||||
footer_text=None,
|
||||
footer_icon_url=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
if not hide_author:
|
||||
if not author_text:
|
||||
author_text = ctx.author.name
|
||||
elif show_name:
|
||||
description = f"**{ctx.author.name}** {description}"
|
||||
|
||||
if not hide_author_icon and not author_icon_url:
|
||||
author_icon_url = ctx.author.display_avatar.url
|
||||
|
||||
if not footer_text:
|
||||
footer_text = "Luminara"
|
||||
if not footer_icon_url:
|
||||
footer_icon_url = CONST.LUMI_LOGO_TRANSPARENT
|
||||
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
description=description,
|
||||
color=color or CONST.COLOR_DEFAULT,
|
||||
)
|
||||
if not hide_author:
|
||||
embed.set_author(
|
||||
name=author_text,
|
||||
icon_url=None if hide_author_icon else author_icon_url,
|
||||
url=author_url,
|
||||
)
|
||||
embed.set_footer(text=footer_text, icon_url=footer_icon_url)
|
||||
if not hide_timestamp:
|
||||
embed.timestamp = timestamp or datetime.datetime.now()
|
||||
|
||||
if image_url:
|
||||
embed.set_image(url=image_url)
|
||||
if thumbnail_url:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
|
||||
return embed
|
||||
|
||||
@staticmethod
|
||||
def create_error_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.CROSS_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_ERROR,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_success_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.CHECK_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_DEFAULT,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_info_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.EXCLAIM_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_DEFAULT,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_warning_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.WARNING_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_WARNING,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
|
@ -1,31 +0,0 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
|
||||
|
||||
class BirthdaysDisabled(commands.CheckFailure):
|
||||
"""
|
||||
Raised when the birthdays module is disabled in ctx.guild.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class LumiException(commands.CommandError):
|
||||
"""
|
||||
A generic exception to raise for quick error handling.
|
||||
"""
|
||||
|
||||
def __init__(self, message=CONST.STRINGS["lumi_exception_generic"]):
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
|
||||
class Blacklisted(commands.CommandError):
|
||||
"""
|
||||
Raised when a user is blacklisted.
|
||||
"""
|
||||
|
||||
def __init__(self, message=CONST.STRINGS["lumi_exception_blacklisted"]):
|
||||
self.message = message
|
||||
super().__init__(message)
|
138
lib/formatter.py
138
lib/formatter.py
|
@ -1,138 +0,0 @@
|
|||
import textwrap
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from pytimeparse import parse
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
def template(text: str, username: str, level: int | None = None) -> str:
|
||||
"""
|
||||
Replaces placeholders in the given text with actual values.
|
||||
|
||||
Args:
|
||||
text (str): The template text containing placeholders.
|
||||
username (str): The username to replace the "{user}" placeholder.
|
||||
level (int | None, optional): The level to replace the "{level}" placeholder. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: The formatted text with placeholders replaced by actual values.
|
||||
"""
|
||||
replacements: dict[str, str] = {
|
||||
"{user}": username,
|
||||
"{level}": str(level) if level else "",
|
||||
}
|
||||
|
||||
for placeholder, value in replacements.items():
|
||||
text = text.replace(placeholder, value)
|
||||
|
||||
return text
|
||||
|
||||
|
||||
def shorten(text: str, width: int = 200) -> str:
|
||||
"""
|
||||
Shortens the input text to the specified width by adding a placeholder at the end if the text exceeds the width.
|
||||
|
||||
Args:
|
||||
text (str): The text to be shortened.
|
||||
width (int): The maximum width of the shortened text (default is 200).
|
||||
|
||||
Returns:
|
||||
str: The shortened text.
|
||||
|
||||
Examples:
|
||||
shortened_text = shorten("Lorem ipsum dolor sit amet", 10)
|
||||
"""
|
||||
return textwrap.shorten(text, width=width, placeholder="...")
|
||||
|
||||
|
||||
def format_case_number(case_number: int) -> str:
|
||||
"""
|
||||
Formats a case number as a string with leading zeros if necessary.
|
||||
|
||||
Args:
|
||||
case_number (int): The case number to format.
|
||||
|
||||
Returns:
|
||||
str: The formatted case number as a string.
|
||||
If the case number is less than 1000, it will be padded with leading zeros to three digits.
|
||||
If the case number is 1000 or greater, it will be returned as a regular string.
|
||||
|
||||
Examples:
|
||||
>>> format_case_number(1)
|
||||
'001'
|
||||
>>> format_case_number(42)
|
||||
'042'
|
||||
>>> format_case_number(999)
|
||||
'999'
|
||||
>>> format_case_number(1000)
|
||||
'1000'
|
||||
"""
|
||||
return f"{case_number:03d}" if case_number < 1000 else str(case_number)
|
||||
|
||||
|
||||
def get_prefix(ctx: commands.Context) -> str:
|
||||
"""
|
||||
Attempts to retrieve the prefix for the given guild context.
|
||||
|
||||
Args:
|
||||
ctx (discord.ext.commands.Context): The context of the command invocation.
|
||||
|
||||
Returns:
|
||||
str: The prefix for the guild. Defaults to "." if the guild or prefix is not found.
|
||||
"""
|
||||
try:
|
||||
return GuildConfig.get_prefix(ctx.guild.id if ctx.guild else 0)
|
||||
except (AttributeError, TypeError):
|
||||
return "."
|
||||
|
||||
|
||||
def get_invoked_name(ctx: commands.Context) -> str | None:
|
||||
"""
|
||||
Attempts to get the alias of the command used. If the user used a SlashCommand, return the command name.
|
||||
|
||||
Args:
|
||||
ctx (discord.ext.commands.Context): The context of the command invocation.
|
||||
|
||||
Returns:
|
||||
str: The alias or name of the invoked command.
|
||||
"""
|
||||
try:
|
||||
return ctx.invoked_with
|
||||
except (discord.ApplicationCommandInvokeError, AttributeError):
|
||||
return ctx.command.name if ctx.command else None
|
||||
|
||||
|
||||
def format_duration_to_seconds(duration: str) -> int:
|
||||
"""
|
||||
Formats a duration in seconds to a human-readable string.
|
||||
"""
|
||||
parsed_duration = parse(duration)
|
||||
|
||||
if isinstance(parsed_duration, int):
|
||||
return parsed_duration
|
||||
else:
|
||||
raise LumiException(CONST.STRINGS["error_invalid_duration"].format(duration))
|
||||
|
||||
|
||||
def format_seconds_to_duration_string(seconds: int) -> str:
|
||||
"""
|
||||
Formats a duration in seconds to a human-readable string.
|
||||
Returns seconds if shorter than a minute.
|
||||
"""
|
||||
if seconds < 60:
|
||||
return f"{seconds}s"
|
||||
|
||||
days = seconds // 86400
|
||||
hours = (seconds % 86400) // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
|
||||
if days > 0:
|
||||
return f"{days}d{hours}h" if hours > 0 else f"{days}d"
|
||||
elif hours > 0:
|
||||
return f"{hours}h{minutes}m" if minutes > 0 else f"{hours}h"
|
||||
else:
|
||||
return f"{minutes}m"
|
|
@ -1,34 +0,0 @@
|
|||
import discord
|
||||
from discord.ui import View
|
||||
|
||||
|
||||
class ExchangeConfirmation(View):
|
||||
def __init__(self, ctx):
|
||||
super().__init__(timeout=180)
|
||||
self.ctx = ctx
|
||||
self.clickedConfirm = False
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
await self.message.edit(view=None)
|
||||
|
||||
@discord.ui.button(label="Confirm", style=discord.ButtonStyle.green)
|
||||
async def confirm_button_callback(self, button, interaction):
|
||||
await interaction.response.edit_message(view=None)
|
||||
self.clickedConfirm = True
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Stop", style=discord.ButtonStyle.red)
|
||||
async def stop_button_callback(self, button, interaction):
|
||||
await interaction.response.edit_message(view=None)
|
||||
self.stop()
|
||||
|
||||
async def interaction_check(self, interaction) -> bool:
|
||||
if interaction.user == self.ctx.author:
|
||||
return True
|
||||
await interaction.response.send_message(
|
||||
"You can't use these buttons, they're someone else's!",
|
||||
ephemeral=True,
|
||||
)
|
||||
return False
|
|
@ -1,78 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import bridge
|
||||
from discord.ui import View
|
||||
|
||||
|
||||
class IntroductionStartButtons(View):
|
||||
def __init__(self, ctx):
|
||||
super().__init__(timeout=60)
|
||||
self.ctx = ctx
|
||||
self.clickedStart = False
|
||||
self.clickedStop = False
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
if isinstance(child, discord.ui.Button):
|
||||
child.disabled = True
|
||||
if self.message:
|
||||
await self.message.edit(view=None)
|
||||
|
||||
@discord.ui.button(label="Start", style=discord.ButtonStyle.primary)
|
||||
async def short_button_callback(self, button, interaction):
|
||||
await interaction.response.edit_message(view=None)
|
||||
self.clickedStart = True
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Stop", style=discord.ButtonStyle.red)
|
||||
async def stop_button_callback(self, button, interaction):
|
||||
await interaction.response.edit_message(view=None)
|
||||
self.clickedStop = True
|
||||
self.stop()
|
||||
|
||||
|
||||
class IntroductionFinishButtons(View):
|
||||
def __init__(self, ctx: bridge.Context) -> None:
|
||||
"""
|
||||
Initializes the IntroductionFinishConfirm view.
|
||||
"""
|
||||
super().__init__(timeout=60)
|
||||
self.ctx = ctx
|
||||
self.clickedConfirm: bool = False
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
"""
|
||||
Called when the view times out. Disables all buttons and edits the message to remove the view.
|
||||
"""
|
||||
for child in self.children:
|
||||
if isinstance(child, discord.ui.Button):
|
||||
child.disabled = True
|
||||
if self.message:
|
||||
await self.message.edit(view=None)
|
||||
|
||||
@discord.ui.button(label="Post it!", style=discord.ButtonStyle.green)
|
||||
async def short_button_callback(
|
||||
self,
|
||||
button: discord.ui.Button,
|
||||
interaction: discord.Interaction,
|
||||
) -> None:
|
||||
await interaction.response.edit_message(view=None)
|
||||
self.clickedConfirm = True
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(label="Stop", style=discord.ButtonStyle.red)
|
||||
async def extended_button_callback(
|
||||
self,
|
||||
button: discord.ui.Button,
|
||||
interaction: discord.Interaction,
|
||||
) -> None:
|
||||
await interaction.response.edit_message(view=None)
|
||||
self.stop()
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
if interaction.user == self.ctx.author:
|
||||
return True
|
||||
await interaction.response.send_message(
|
||||
"You can't use these buttons.",
|
||||
ephemeral=True,
|
||||
)
|
||||
return False
|
|
@ -1,36 +0,0 @@
|
|||
import random
|
||||
|
||||
|
||||
class ReactionHandler:
|
||||
def __init__(self):
|
||||
self.eightball = [
|
||||
"It is certain.",
|
||||
"It is decidedly so.",
|
||||
"Without a doubt.",
|
||||
"Yes - definitely.",
|
||||
"You may rely on it.",
|
||||
"As I see it, yes.",
|
||||
"Most likely.",
|
||||
"Outlook good.",
|
||||
"Yes.",
|
||||
"Signs point to yes.",
|
||||
"Reply hazy, try again.",
|
||||
"Ask again later.",
|
||||
"Better not tell you now.",
|
||||
"Cannot predict now.",
|
||||
"Concentrate and ask again.",
|
||||
"Don't count on it.",
|
||||
"My reply is no.",
|
||||
"My sources say no.",
|
||||
"Outlook not so good.",
|
||||
"Very doubtful.",
|
||||
]
|
||||
|
||||
async def handle_message(self, message):
|
||||
content = message.content.lower()
|
||||
|
||||
if (
|
||||
content.startswith("Lumi ") or content.startswith("Lumi, ")
|
||||
) and content.endswith("?"):
|
||||
response = random.choice(self.eightball)
|
||||
await message.reply(content=response)
|
19
lib/time.py
19
lib/time.py
|
@ -1,19 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
|
||||
def seconds_until(hours, minutes):
|
||||
eastern_timezone = pytz.timezone("US/Eastern")
|
||||
|
||||
now = datetime.datetime.now(eastern_timezone)
|
||||
|
||||
# Create a datetime object for the given time in the Eastern Timezone
|
||||
given_time = datetime.time(hours, minutes)
|
||||
future_exec = eastern_timezone.localize(datetime.datetime.combine(now, given_time))
|
||||
|
||||
# If the given time is before the current time, add one day to the future execution time
|
||||
if future_exec < now:
|
||||
future_exec += datetime.timedelta(days=1)
|
||||
|
||||
return (future_exec - now).total_seconds()
|
|
@ -1,45 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from modules.admin import award, blacklist, sql, sync
|
||||
|
||||
|
||||
class BotAdmin(commands.Cog, name="Bot Admin"):
|
||||
"""
|
||||
This module is intended for commands that only bot owners can do.
|
||||
For server configuration with Lumi, see the "config" module.
|
||||
"""
|
||||
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@commands.command(name="award")
|
||||
@commands.is_owner()
|
||||
async def award_command(self, ctx, user: discord.User, *, amount: int):
|
||||
return await award.cmd(ctx, user, amount)
|
||||
|
||||
@commands.command(name="sqlselect", aliases=["sqls"])
|
||||
@commands.is_owner()
|
||||
async def select(self, ctx, *, query: str):
|
||||
return await sql.select_cmd(ctx, query)
|
||||
|
||||
@commands.command(name="sqlinject", aliases=["sqli"])
|
||||
@commands.is_owner()
|
||||
async def inject(self, ctx, *, query: str):
|
||||
return await sql.inject_cmd(ctx, query)
|
||||
|
||||
@commands.command(name="blacklist")
|
||||
@commands.is_owner()
|
||||
async def blacklist(self, ctx, user: discord.User, *, reason: Optional[str] = None):
|
||||
return await blacklist.blacklist_user(ctx, user, reason)
|
||||
|
||||
@commands.command(name="sync")
|
||||
@commands.is_owner()
|
||||
async def sync_command(self, ctx):
|
||||
await sync.sync_commands(self.client, ctx)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(BotAdmin(client))
|
|
@ -1,23 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.currency_service import Currency
|
||||
|
||||
|
||||
async def cmd(ctx, user: discord.User, amount: int):
|
||||
# Currency handler
|
||||
curr = Currency(user.id)
|
||||
curr.add_balance(amount)
|
||||
curr.push()
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_award_title"],
|
||||
description=CONST.STRINGS["admin_award_description"].format(
|
||||
Currency.format(amount),
|
||||
user.name,
|
||||
),
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,26 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
|
||||
|
||||
async def blacklist_user(
|
||||
ctx,
|
||||
user: discord.User,
|
||||
reason: Optional[str] = None,
|
||||
) -> None:
|
||||
blacklist_service = BlacklistUserService(user.id)
|
||||
blacklist_service.add_to_blacklist(reason)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_blacklist_author"],
|
||||
description=CONST.STRINGS["admin_blacklist_description"].format(user.name),
|
||||
footer_text=CONST.STRINGS["admin_blacklist_footer"],
|
||||
hide_timestamp=True,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
|
@ -1,60 +0,0 @@
|
|||
import mysql.connector
|
||||
|
||||
from db import database
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.formatter import shorten
|
||||
|
||||
|
||||
async def select_cmd(ctx, query: str):
|
||||
if query.lower().startswith("select "):
|
||||
query = query[7:]
|
||||
|
||||
try:
|
||||
results = database.select_query(f"SELECT {query}")
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_select_title"],
|
||||
description=CONST.STRINGS["admin_sql_select_description"].format(
|
||||
shorten(query, 200),
|
||||
shorten(str(results), 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
except mysql.connector.Error as error:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_select_error_title"],
|
||||
description=CONST.STRINGS["admin_sql_select_error_description"].format(
|
||||
shorten(query, 200),
|
||||
shorten(str(error), 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed, ephemeral=True)
|
||||
|
||||
|
||||
async def inject_cmd(ctx, query: str):
|
||||
try:
|
||||
database.execute_query(query)
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_inject_title"],
|
||||
description=CONST.STRINGS["admin_sql_inject_description"].format(
|
||||
shorten(query, 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
except mysql.connector.Error as error:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_inject_error_title"],
|
||||
description=CONST.STRINGS["admin_sql_inject_error_description"].format(
|
||||
shorten(query, 200),
|
||||
shorten(str(error), 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed, ephemeral=True)
|
|
@ -1,20 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
|
||||
|
||||
async def sync_commands(client, ctx):
|
||||
try:
|
||||
await client.sync_commands()
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sync_title"],
|
||||
description=CONST.STRINGS["admin_sync_description"],
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
except discord.HTTPException as e:
|
||||
raise LumiException(
|
||||
CONST.STRINGS["admin_sync_error_description"].format(e),
|
||||
) from e
|
|
@ -1,46 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import discord
|
||||
import pytz
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
from lib import checks
|
||||
from lib.constants import CONST
|
||||
from modules.birthdays import birthday, daily_check
|
||||
|
||||
|
||||
class Birthdays(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.daily_birthday_check.start()
|
||||
|
||||
birthday = SlashCommandGroup(
|
||||
name="birthday",
|
||||
description="Birthday commands.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
|
||||
@birthday.command(name="set", description="Set your birthday in this server.")
|
||||
@checks.birthdays_enabled()
|
||||
@discord.commands.option(name="month", choices=CONST.BIRTHDAY_MONTHS)
|
||||
async def set_birthday(self, ctx, month, day: int):
|
||||
index = CONST.BIRTHDAY_MONTHS.index(month) + 1
|
||||
await birthday.add(ctx, month, index, day)
|
||||
|
||||
@birthday.command(name="delete", description="Delete your birthday in this server.")
|
||||
async def delete_birthday(self, ctx):
|
||||
await birthday.delete(ctx)
|
||||
|
||||
@birthday.command(name="upcoming", description="Shows the upcoming birthdays.")
|
||||
@checks.birthdays_enabled()
|
||||
async def upcoming_birthdays(self, ctx):
|
||||
await birthday.upcoming(ctx)
|
||||
|
||||
@tasks.loop(time=datetime.time(hour=12, minute=0, tzinfo=pytz.UTC)) # 12 PM UTC
|
||||
async def daily_birthday_check(self):
|
||||
await daily_check.daily_birthday_check(self.client)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Birthdays(client))
|
|
@ -1,85 +0,0 @@
|
|||
import calendar
|
||||
import datetime
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.birthday_service import Birthday
|
||||
|
||||
|
||||
async def add(ctx, month, month_index, day):
|
||||
leap_year = 2020
|
||||
max_days = calendar.monthrange(leap_year, month_index)[1]
|
||||
|
||||
if not 1 <= day <= max_days:
|
||||
raise commands.BadArgument(CONST.STRINGS["birthday_add_invalid_date"])
|
||||
|
||||
date_obj = datetime.datetime(leap_year, month_index, day)
|
||||
|
||||
birthday = Birthday(ctx.author.id, ctx.guild.id)
|
||||
birthday.set(date_obj)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["birthday_add_success_author"],
|
||||
description=CONST.STRINGS["birthday_add_success_description"].format(
|
||||
month,
|
||||
day,
|
||||
),
|
||||
show_name=True,
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def delete(ctx):
|
||||
Birthday(ctx.author.id, ctx.guild.id).delete()
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["birthday_delete_success_author"],
|
||||
description=CONST.STRINGS["birthday_delete_success_description"],
|
||||
show_name=True,
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def upcoming(ctx):
|
||||
upcoming_birthdays = Birthday.get_upcoming_birthdays(ctx.guild.id)
|
||||
|
||||
if not upcoming_birthdays:
|
||||
embed = EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["birthday_upcoming_no_birthdays_author"],
|
||||
description=CONST.STRINGS["birthday_upcoming_no_birthdays"],
|
||||
show_name=True,
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
return
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["birthday_upcoming_author"],
|
||||
description="",
|
||||
show_name=False,
|
||||
)
|
||||
embed.set_thumbnail(url=CONST.LUMI_LOGO_TRANSPARENT)
|
||||
|
||||
birthday_lines = []
|
||||
for user_id, birthday in upcoming_birthdays[:10]:
|
||||
try:
|
||||
member = await ctx.guild.fetch_member(user_id)
|
||||
birthday_date = datetime.datetime.strptime(birthday, "%m-%d")
|
||||
formatted_birthday = birthday_date.strftime("%B %-d")
|
||||
birthday_lines.append(
|
||||
CONST.STRINGS["birthday_upcoming_description_line"].format(
|
||||
member.name,
|
||||
formatted_birthday,
|
||||
),
|
||||
)
|
||||
except (discord.HTTPException, ValueError):
|
||||
continue
|
||||
|
||||
embed.description = "\n".join(birthday_lines)
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,65 +0,0 @@
|
|||
import asyncio
|
||||
import random
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.birthday_service import Birthday
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def daily_birthday_check(client):
|
||||
logger.info(CONST.STRINGS["birthday_check_started"])
|
||||
birthdays_today = Birthday.get_birthdays_today()
|
||||
processed_birthdays = 0
|
||||
failed_birthdays = 0
|
||||
|
||||
if birthdays_today:
|
||||
for user_id, guild_id in birthdays_today:
|
||||
try:
|
||||
guild = await client.fetch_guild(guild_id)
|
||||
member = await guild.fetch_member(user_id)
|
||||
guild_config = GuildConfig(guild.id)
|
||||
|
||||
if not guild_config.birthday_channel_id:
|
||||
logger.debug(
|
||||
CONST.STRINGS["birthday_check_skipped"].format(guild.id),
|
||||
)
|
||||
continue
|
||||
|
||||
message = random.choice(CONST.BIRTHDAY_MESSAGES)
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
None,
|
||||
author_text="Happy Birthday!",
|
||||
description=message.format(member.name),
|
||||
show_name=False,
|
||||
)
|
||||
embed.set_image(url=CONST.BIRTHDAY_GIF_URL)
|
||||
|
||||
channel = await guild.fetch_channel(guild_config.birthday_channel_id)
|
||||
await channel.send(embed=embed, content=member.mention)
|
||||
logger.debug(
|
||||
CONST.STRINGS["birthday_check_success"].format(
|
||||
member.id,
|
||||
guild.id,
|
||||
channel.id,
|
||||
),
|
||||
)
|
||||
processed_birthdays += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
CONST.STRINGS["birthday_check_error"].format(user_id, guild_id, e),
|
||||
)
|
||||
failed_birthdays += 1
|
||||
|
||||
# wait one second to avoid rate limits
|
||||
await asyncio.sleep(1)
|
||||
|
||||
logger.info(
|
||||
CONST.STRINGS["birthday_check_finished"].format(
|
||||
processed_birthdays,
|
||||
failed_birthdays,
|
||||
),
|
||||
)
|
|
@ -1,174 +0,0 @@
|
|||
import discord
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.config import (
|
||||
c_birthday,
|
||||
c_boost,
|
||||
c_greet,
|
||||
c_level,
|
||||
c_moderation,
|
||||
c_prefix,
|
||||
c_show,
|
||||
xp_reward,
|
||||
)
|
||||
|
||||
|
||||
class Config(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="xprewards",
|
||||
aliases=["xpr"],
|
||||
description="Show your server's XP rewards list.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def xp_reward_command_show(self, ctx):
|
||||
await xp_reward.show(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="addxpreward",
|
||||
aliases=["axpr"],
|
||||
description="Add a Lumi XP reward.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def xp_reward_command_add(
|
||||
self,
|
||||
ctx,
|
||||
level: int,
|
||||
role: discord.Role,
|
||||
persistent: bool = False,
|
||||
):
|
||||
await xp_reward.add_reward(ctx, level, role.id, persistent)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="removexpreward",
|
||||
aliases=["rxpr"],
|
||||
description="Remove a Lumi XP reward.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def xp_reward_command_remove(self, ctx, level: int):
|
||||
await xp_reward.remove_reward(ctx, level)
|
||||
|
||||
"""
|
||||
CONFIG GROUPS
|
||||
The 'config' group consists of many different configuration types, each being guild-specific and guild-only.
|
||||
All commands in this group are exclusively available as slash-commands.
|
||||
Only administrators can access commands in this group.
|
||||
|
||||
- Birthdays
|
||||
- Welcome
|
||||
- Boosts
|
||||
- Levels
|
||||
- Prefix
|
||||
- Modlog channel
|
||||
- Permissions preset (coming soon)
|
||||
|
||||
Running '/config show' will show a list of all available configuration types.
|
||||
"""
|
||||
config = SlashCommandGroup(
|
||||
"config",
|
||||
"server config commands.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
default_member_permissions=discord.Permissions(administrator=True),
|
||||
)
|
||||
|
||||
@config.command(name="show")
|
||||
async def config_command(self, ctx):
|
||||
await c_show.cmd(ctx)
|
||||
|
||||
birthday_config = config.create_subgroup(name="birthdays")
|
||||
|
||||
@birthday_config.command(name="channel")
|
||||
async def config_birthdays_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_birthday.set_birthday_channel(ctx, channel)
|
||||
|
||||
@birthday_config.command(name="disable")
|
||||
async def config_birthdays_disable(self, ctx):
|
||||
await c_birthday.disable_birthday_module(ctx)
|
||||
|
||||
welcome_config = config.create_subgroup(name="greetings")
|
||||
|
||||
@welcome_config.command(name="channel")
|
||||
async def config_welcome_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_greet.set_welcome_channel(ctx, channel)
|
||||
|
||||
@welcome_config.command(name="disable")
|
||||
async def config_welcome_disable(self, ctx):
|
||||
await c_greet.disable_welcome_module(ctx)
|
||||
|
||||
@welcome_config.command(name="template")
|
||||
@discord.commands.option(name="text", type=str, max_length=2000)
|
||||
async def config_welcome_template(self, ctx, text):
|
||||
await c_greet.set_welcome_template(ctx, text)
|
||||
|
||||
boost_config = config.create_subgroup(name="boosts")
|
||||
|
||||
@boost_config.command(name="channel")
|
||||
async def config_boosts_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_boost.set_boost_channel(ctx, channel)
|
||||
|
||||
@boost_config.command(name="disable")
|
||||
async def config_boosts_disable(self, ctx):
|
||||
await c_boost.disable_boost_module(ctx)
|
||||
|
||||
@boost_config.command(name="template")
|
||||
@discord.commands.option(name="text", type=str, max_length=2000)
|
||||
async def config_boosts_template(self, ctx, text):
|
||||
await c_boost.set_boost_template(ctx, text)
|
||||
|
||||
@boost_config.command(name="image")
|
||||
@discord.commands.option(name="url", type=str, max_length=2000)
|
||||
async def config_boosts_image(self, ctx, url):
|
||||
await c_boost.set_boost_image(ctx, url)
|
||||
|
||||
level_config = config.create_subgroup(name="levels")
|
||||
|
||||
@level_config.command(name="channel")
|
||||
async def config_level_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_level.set_level_channel(ctx, channel)
|
||||
|
||||
@level_config.command(name="currentchannel")
|
||||
async def config_level_samechannel(self, ctx):
|
||||
await c_level.set_level_current_channel(ctx)
|
||||
|
||||
@level_config.command(name="disable")
|
||||
async def config_level_disable(self, ctx):
|
||||
await c_level.disable_level_module(ctx)
|
||||
|
||||
@level_config.command(name="enable")
|
||||
async def config_level_enable(self, ctx):
|
||||
await c_level.enable_level_module(ctx)
|
||||
|
||||
@level_config.command(name="type")
|
||||
@discord.commands.option(name="type", choices=["whimsical", "generic"])
|
||||
async def config_level_type(self, ctx, type):
|
||||
await c_level.set_level_type(ctx, type)
|
||||
|
||||
@level_config.command(name="template")
|
||||
async def config_level_template(self, ctx, text: str):
|
||||
await c_level.set_level_template(ctx, text)
|
||||
|
||||
prefix_config = config.create_subgroup(name="prefix")
|
||||
|
||||
@prefix_config.command(name="set")
|
||||
async def config_prefix_set(self, ctx, prefix: str):
|
||||
await c_prefix.set_prefix(ctx, prefix)
|
||||
|
||||
modlog = config.create_subgroup(name="moderation")
|
||||
|
||||
@modlog.command(name="log")
|
||||
async def config_moderation_log_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_moderation.set_mod_log_channel(ctx, channel)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Config(client))
|
|
@ -1,43 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_birthday_channel(ctx, channel: discord.TextChannel):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.birthday_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_channel_set"].format(
|
||||
channel.mention,
|
||||
),
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_birthday_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.birthday_channel_id:
|
||||
embed = EmbedBuilder().create_warning_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_module_already_disabled"],
|
||||
)
|
||||
|
||||
else:
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_module_disabled"],
|
||||
)
|
||||
guild_config.birthday_channel_id = None
|
||||
guild_config.push()
|
||||
|
||||
return await ctx.respond(embed=embed)
|
|
@ -1,125 +0,0 @@
|
|||
import discord
|
||||
|
||||
import lib.formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_boost_channel(ctx, channel: discord.TextChannel):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.boost_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_boost_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.boost_channel_id:
|
||||
embed = EmbedBuilder().create_warning_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_module_already_disabled"],
|
||||
)
|
||||
else:
|
||||
guild_config.boost_channel_id = None
|
||||
guild_config.boost_message = None
|
||||
guild_config.push()
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_module_disabled"],
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_boost_template(ctx, text: str):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.boost_message = text
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_template_updated"],
|
||||
footer_text=CONST.STRINGS["config_example_next_footer"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_boost_template_field"],
|
||||
value=f"```{text}```",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
example_embed = create_boost_embed(ctx.author, text, guild_config.boost_image_url)
|
||||
return await ctx.send(embed=example_embed, content=ctx.author.mention)
|
||||
|
||||
|
||||
async def set_boost_image(ctx, image_url: str | None):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if image_url is None or image_url.lower() == "original":
|
||||
guild_config.boost_image_url = None
|
||||
guild_config.push()
|
||||
image_url = None
|
||||
elif not image_url.endswith(CONST.ALLOWED_IMAGE_EXTENSIONS):
|
||||
raise LumiException(CONST.STRINGS["error_boost_image_url_invalid"])
|
||||
elif not image_url.startswith(("http://", "https://")):
|
||||
raise LumiException(CONST.STRINGS["error_image_url_invalid"])
|
||||
else:
|
||||
guild_config.boost_image_url = image_url
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_image_updated"],
|
||||
footer_text=CONST.STRINGS["config_example_next_footer"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_boost_image_field"],
|
||||
value=image_url or CONST.STRINGS["config_boost_image_original"],
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
example_embed = create_boost_embed(
|
||||
ctx.author,
|
||||
guild_config.boost_message,
|
||||
image_url,
|
||||
)
|
||||
return await ctx.send(embed=example_embed, content=ctx.author.mention)
|
||||
|
||||
|
||||
async def create_boost_embed(
|
||||
member: discord.Member,
|
||||
template: str | None = None,
|
||||
image_url: str | None = None,
|
||||
):
|
||||
embed = discord.Embed(
|
||||
color=discord.Color.nitro_pink(),
|
||||
title=CONST.STRINGS["boost_default_title"],
|
||||
description=CONST.STRINGS["boost_default_description"].format(member.name),
|
||||
)
|
||||
|
||||
if template:
|
||||
embed.description = lib.formatter.template(template, member.name)
|
||||
|
||||
embed.set_author(name=member.name, icon_url=member.display_avatar)
|
||||
embed.set_image(url=image_url or CONST.BOOST_ICON)
|
||||
embed.set_footer(
|
||||
text=f"Total server boosts: {member.guild.premium_subscription_count}",
|
||||
icon_url=CONST.EXCLAIM_ICON,
|
||||
)
|
|
@ -1,96 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import MemberConverter
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_welcome_channel(ctx, channel: discord.TextChannel) -> None:
|
||||
if not ctx.guild:
|
||||
raise LumiException()
|
||||
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
guild_config.welcome_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_welcome_module(ctx) -> None:
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.welcome_channel_id:
|
||||
embed: discord.Embed = EmbedBuilder().create_warning_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_module_already_disabled"],
|
||||
)
|
||||
else:
|
||||
guild_config.welcome_channel_id = None
|
||||
guild_config.welcome_message = None
|
||||
guild_config.push()
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_module_disabled"],
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_welcome_template(ctx, text: str) -> None:
|
||||
if not ctx.guild:
|
||||
raise LumiException()
|
||||
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
guild_config.welcome_message = text
|
||||
guild_config.push()
|
||||
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_template_updated"],
|
||||
footer_text=CONST.STRINGS["config_example_next_footer"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_welcome_template_field"],
|
||||
value=f"```{text}```",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
greet_member: discord.Member = await MemberConverter().convert(ctx, str(ctx.author))
|
||||
example_embed: discord.Embed = create_greet_embed(greet_member, text)
|
||||
await ctx.send(embed=example_embed, content=ctx.author.mention)
|
||||
|
||||
|
||||
def create_greet_embed(
|
||||
member: discord.Member,
|
||||
template: Optional[str] = None,
|
||||
) -> discord.Embed:
|
||||
embed: discord.Embed = discord.Embed(
|
||||
color=discord.Color.embed_background(),
|
||||
description=CONST.STRINGS["greet_default_description"].format(
|
||||
member.guild.name,
|
||||
),
|
||||
)
|
||||
if template and embed.description is not None:
|
||||
embed.description += CONST.STRINGS["greet_template_description"].format(
|
||||
formatter.template(template, member.name),
|
||||
)
|
||||
|
||||
embed.set_thumbnail(url=member.display_avatar.url)
|
||||
|
||||
return embed
|
|
@ -1,137 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_level_channel(ctx, channel: discord.TextChannel):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
if guild_config.level_message_type == 0:
|
||||
embed.set_footer(text=CONST.STRINGS["config_level_module_disabled_warning"])
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_level_current_channel(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_channel_id = None
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_current_channel_set"],
|
||||
)
|
||||
|
||||
if guild_config.level_message_type == 0:
|
||||
embed.set_footer(text=CONST.STRINGS["config_level_module_disabled_warning"])
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_level_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_message_type = 0
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_disabled"],
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def enable_level_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if guild_config.level_message_type != 0:
|
||||
embed = EmbedBuilder().create_info_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_already_enabled"],
|
||||
)
|
||||
else:
|
||||
guild_config.level_message_type = 1
|
||||
guild_config.push()
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_enabled"],
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_level_type(ctx, type: str):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
)
|
||||
|
||||
guild_config.level_message = None
|
||||
if type == "whimsical":
|
||||
guild_config.level_message_type = 1
|
||||
guild_config.push()
|
||||
|
||||
embed.description = CONST.STRINGS["config_level_type_whimsical"]
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_type_example"],
|
||||
value=CONST.STRINGS["config_level_type_whimsical_example"],
|
||||
inline=False,
|
||||
)
|
||||
else:
|
||||
guild_config.level_message_type = 2
|
||||
guild_config.push()
|
||||
|
||||
embed.description = CONST.STRINGS["config_level_type_generic"]
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_type_example"],
|
||||
value=CONST.STRINGS["config_level_type_generic_example"],
|
||||
inline=False,
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_level_template(ctx, text: str):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_message = text
|
||||
guild_config.push()
|
||||
|
||||
preview = formatter.template(text, "Lucas", 15)
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_template_updated"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_template"],
|
||||
value=f"```{text}```",
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_type_example"],
|
||||
value=preview,
|
||||
inline=False,
|
||||
)
|
||||
|
||||
if guild_config.level_message_type == 0:
|
||||
embed.set_footer(text=CONST.STRINGS["config_level_module_disabled_warning"])
|
||||
|
||||
return await ctx.respond(embed=embed)
|
|
@ -1,44 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.moderation.modlog_service import ModLogService
|
||||
|
||||
|
||||
async def set_mod_log_channel(ctx, channel: discord.TextChannel):
|
||||
mod_log = ModLogService()
|
||||
|
||||
info_embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_modlog_info_author"],
|
||||
description=CONST.STRINGS["config_modlog_info_description"].format(
|
||||
ctx.guild.name,
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
info_embed.add_field(
|
||||
name=CONST.STRINGS["config_modlog_info_commands_name"],
|
||||
value=CONST.STRINGS["config_modlog_info_commands_value"],
|
||||
inline=False,
|
||||
)
|
||||
info_embed.add_field(
|
||||
name=CONST.STRINGS["config_modlog_info_warning_name"],
|
||||
value=CONST.STRINGS["config_modlog_info_warning_value"],
|
||||
inline=False,
|
||||
)
|
||||
|
||||
try:
|
||||
await channel.send(embed=info_embed)
|
||||
except discord.errors.Forbidden as e:
|
||||
raise LumiException(CONST.STRINGS["config_modlog_permission_error"]) from e
|
||||
|
||||
mod_log.set_modlog_channel(ctx.guild.id, channel.id)
|
||||
|
||||
success_embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_modlog_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=success_embed)
|
|
@ -1,35 +0,0 @@
|
|||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_prefix(ctx, prefix):
|
||||
if len(prefix) > 25:
|
||||
embed = EmbedBuilder().create_error_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_too_long"],
|
||||
)
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
guild_config = GuildConfig(
|
||||
ctx.guild.id,
|
||||
) # generate a guild_config for if it didn't already exist
|
||||
GuildConfig.set_prefix(guild_config.guild_id, prefix)
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_set"].format(prefix),
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def get_prefix(ctx):
|
||||
prefix = GuildConfig.get_prefix_from_guild_id(ctx.guild.id) if ctx.guild else "."
|
||||
embed = EmbedBuilder().create_info_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_get"].format(prefix),
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,75 +0,0 @@
|
|||
from typing import List, Tuple, Optional
|
||||
|
||||
import discord
|
||||
from discord import Guild
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
from services.moderation.modlog_service import ModLogService
|
||||
|
||||
|
||||
async def cmd(ctx) -> None:
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
guild: Guild = ctx.guild
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_show_author"].format(guild.name),
|
||||
thumbnail_url=guild.icon.url if guild.icon else CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
config_items: List[Tuple[str, bool, bool]] = [
|
||||
(
|
||||
CONST.STRINGS["config_show_birthdays"],
|
||||
bool(guild_config.birthday_channel_id),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CONST.STRINGS["config_show_new_member_greets"],
|
||||
bool(guild_config.welcome_channel_id),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CONST.STRINGS["config_show_boost_announcements"],
|
||||
bool(guild_config.boost_channel_id),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CONST.STRINGS["config_show_level_announcements"],
|
||||
guild_config.level_message_type != 0,
|
||||
False,
|
||||
),
|
||||
]
|
||||
|
||||
for name, enabled, default_enabled in config_items:
|
||||
status: str = (
|
||||
CONST.STRINGS["config_show_enabled"]
|
||||
if enabled
|
||||
else CONST.STRINGS["config_show_disabled"]
|
||||
)
|
||||
if not enabled and default_enabled:
|
||||
status = CONST.STRINGS["config_show_default_enabled"]
|
||||
embed.add_field(name=name, value=status, inline=False)
|
||||
|
||||
modlog_service: ModLogService = ModLogService()
|
||||
modlog_channel_id: Optional[int] = modlog_service.fetch_modlog_channel_id(guild.id)
|
||||
modlog_channel = guild.get_channel(modlog_channel_id) if modlog_channel_id else None
|
||||
|
||||
modlog_status: str
|
||||
if modlog_channel:
|
||||
modlog_status = CONST.STRINGS["config_show_moderation_log_enabled"].format(
|
||||
modlog_channel.mention,
|
||||
)
|
||||
elif modlog_channel_id:
|
||||
modlog_status = CONST.STRINGS["config_show_moderation_log_channel_deleted"]
|
||||
else:
|
||||
modlog_status = CONST.STRINGS["config_show_moderation_log_not_configured"]
|
||||
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_show_moderation_log"],
|
||||
value=modlog_status,
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,43 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from services.xp_service import XpRewardService
|
||||
|
||||
|
||||
async def show(ctx):
|
||||
level_reward = XpRewardService(ctx.guild.id)
|
||||
|
||||
embed = discord.Embed(
|
||||
color=discord.Color.embed_background(),
|
||||
description="",
|
||||
)
|
||||
|
||||
icon = ctx.guild.icon or CONST.LUMI_LOGO_OPAQUE
|
||||
embed.set_author(name="Level Rewards", icon_url=icon)
|
||||
for level in sorted(level_reward.rewards.keys()):
|
||||
role_id, persistent = level_reward.rewards[level]
|
||||
role = ctx.guild.get_role(role_id)
|
||||
|
||||
if embed.description is None:
|
||||
embed.description = ""
|
||||
|
||||
embed.description += (
|
||||
f"\n**Level {level}** -> {role.mention if role else 'Role not found'}"
|
||||
)
|
||||
|
||||
if persistent:
|
||||
embed.description += " (persistent)"
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def add_reward(ctx, level, role_id, persistent):
|
||||
level_reward = XpRewardService(ctx.guild.id)
|
||||
level_reward.add_reward(level, role_id, persistent)
|
||||
await show(ctx)
|
||||
|
||||
|
||||
async def remove_reward(ctx, level):
|
||||
level_reward = XpRewardService(ctx.guild.id)
|
||||
level_reward.remove_reward(level)
|
||||
await show(ctx)
|
|
@ -1,77 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.economy import balance, blackjack, daily, give, slots
|
||||
|
||||
|
||||
class Economy(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="balance",
|
||||
aliases=["bal", "$"],
|
||||
description="Shows your current Lumi balance.",
|
||||
help="Shows your current Lumi balance. The economy system is global, meaning your balance will be synced in "
|
||||
"all servers.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.cooldown(1, 10, commands.BucketType.user)
|
||||
async def balance_command(self, ctx):
|
||||
return await balance.cmd(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="blackjack",
|
||||
aliases=["bj"],
|
||||
description="Start a game of blackjack.",
|
||||
help="Start a game of blackjack.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def blackjack_command(self, ctx, *, bet: int):
|
||||
return await blackjack.cmd(ctx, bet)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="daily",
|
||||
aliases=["timely"],
|
||||
description="Claim your daily reward.",
|
||||
help="Claim your daily reward! Reset is at 7 AM EST.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def daily_command(self, ctx):
|
||||
return await daily.cmd(ctx)
|
||||
|
||||
@commands.slash_command(
|
||||
name="give",
|
||||
description="Give a server member some cash.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
async def give_command(self, ctx, *, user: discord.Member, amount: int):
|
||||
return await give.cmd(ctx, user, amount)
|
||||
|
||||
@commands.command(
|
||||
name="give",
|
||||
help="Give a server member some cash. You can use ID or mention them.",
|
||||
)
|
||||
@guild_only()
|
||||
async def give_command_prefixed(self, ctx, user: discord.User, *, amount: int):
|
||||
return await give.cmd(ctx, user, amount)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="slots",
|
||||
aliases=["slot"],
|
||||
description="Start a slots game.",
|
||||
help="Spin the slots for a chance to win the jackpot!",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.cooldown(1, 5, commands.BucketType.user)
|
||||
async def slots_command(self, ctx, *, bet: int):
|
||||
return await slots.cmd(self, ctx, bet)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Economy(client))
|
|
@ -1,22 +0,0 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from services.currency_service import Currency
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
|
||||
|
||||
async def cmd(ctx: commands.Context[commands.Bot]) -> None:
|
||||
ctx_currency = Currency(ctx.author.id)
|
||||
balance = Currency.format(ctx_currency.balance)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["balance_author"].format(ctx.author.name),
|
||||
author_icon_url=ctx.author.display_avatar.url,
|
||||
description=CONST.STRINGS["balance_cash"].format(balance),
|
||||
footer_text=CONST.STRINGS["balance_footer"],
|
||||
show_name=False,
|
||||
hide_timestamp=True,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,324 +0,0 @@
|
|||
import random
|
||||
from typing import List, Tuple
|
||||
from loguru import logger
|
||||
|
||||
import discord
|
||||
from discord.ui import View
|
||||
import pytz
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.currency_service import Currency
|
||||
from services.stats_service import BlackJackStats
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
|
||||
EST = pytz.timezone("US/Eastern")
|
||||
ACTIVE_BLACKJACK_GAMES: dict[int, bool] = {}
|
||||
|
||||
Card = str
|
||||
Hand = List[Card]
|
||||
|
||||
|
||||
async def cmd(ctx: commands.Context, bet: int) -> None:
|
||||
if ctx.author.id in ACTIVE_BLACKJACK_GAMES:
|
||||
raise LumiException(CONST.STRINGS["error_already_playing_blackjack"])
|
||||
|
||||
currency = Currency(ctx.author.id)
|
||||
if bet > currency.balance:
|
||||
raise LumiException(CONST.STRINGS["error_not_enough_cash"])
|
||||
if bet <= 0:
|
||||
raise LumiException(CONST.STRINGS["error_invalid_bet"])
|
||||
|
||||
ACTIVE_BLACKJACK_GAMES[ctx.author.id] = True
|
||||
|
||||
try:
|
||||
await play_blackjack(ctx, currency, bet)
|
||||
except Exception as e:
|
||||
logger.exception(f"Error in blackjack game: {e}")
|
||||
raise LumiException(CONST.STRINGS["error_blackjack_game_error"]) from e
|
||||
finally:
|
||||
del ACTIVE_BLACKJACK_GAMES[ctx.author.id]
|
||||
|
||||
|
||||
async def play_blackjack(ctx: commands.Context, currency: Currency, bet: int) -> None:
|
||||
deck = get_new_deck()
|
||||
player_hand, dealer_hand = initial_deal(deck)
|
||||
multiplier = CONST.BLACKJACK_MULTIPLIER
|
||||
|
||||
player_value = calculate_hand_value(player_hand)
|
||||
status = 5 if player_value == 21 else 0
|
||||
view = BlackJackButtons(ctx)
|
||||
playing_embed = False
|
||||
|
||||
while status == 0:
|
||||
dealer_value = calculate_hand_value(dealer_hand)
|
||||
|
||||
embed = create_game_embed(
|
||||
ctx,
|
||||
bet,
|
||||
player_hand,
|
||||
dealer_hand,
|
||||
player_value,
|
||||
dealer_value,
|
||||
)
|
||||
if not playing_embed:
|
||||
await ctx.respond(embed=embed, view=view, content=ctx.author.mention)
|
||||
playing_embed = True
|
||||
else:
|
||||
await ctx.edit(embed=embed, view=view)
|
||||
|
||||
await view.wait()
|
||||
|
||||
if view.clickedHit:
|
||||
player_hand.append(deal_card(deck))
|
||||
player_value = calculate_hand_value(player_hand)
|
||||
if player_value > 21:
|
||||
status = 1
|
||||
break
|
||||
elif player_value == 21:
|
||||
status = 2
|
||||
break
|
||||
elif view.clickedStand:
|
||||
status = dealer_play(deck, dealer_hand, player_value)
|
||||
break
|
||||
else:
|
||||
currency.take_balance(bet)
|
||||
currency.push()
|
||||
raise LumiException(CONST.STRINGS["error_out_of_time_economy"])
|
||||
|
||||
view = BlackJackButtons(ctx)
|
||||
|
||||
await handle_game_end(
|
||||
ctx,
|
||||
currency,
|
||||
bet,
|
||||
player_hand,
|
||||
dealer_hand,
|
||||
status,
|
||||
multiplier,
|
||||
playing_embed,
|
||||
)
|
||||
|
||||
|
||||
def initial_deal(deck: List[Card]) -> Tuple[Hand, Hand]:
|
||||
return [deal_card(deck) for _ in range(2)], [deal_card(deck)]
|
||||
|
||||
|
||||
def dealer_play(deck: List[Card], dealer_hand: Hand, player_value: int) -> int:
|
||||
while calculate_hand_value(dealer_hand) <= player_value:
|
||||
dealer_hand.append(deal_card(deck))
|
||||
return 3 if calculate_hand_value(dealer_hand) > 21 else 4
|
||||
|
||||
|
||||
async def handle_game_end(
|
||||
ctx: commands.Context,
|
||||
currency: Currency,
|
||||
bet: int,
|
||||
player_hand: Hand,
|
||||
dealer_hand: Hand,
|
||||
status: int,
|
||||
multiplier: float,
|
||||
playing_embed: bool,
|
||||
) -> None:
|
||||
player_value = calculate_hand_value(player_hand)
|
||||
dealer_value = calculate_hand_value(dealer_hand)
|
||||
payout = bet * (2 if status == 5 else multiplier)
|
||||
is_won = status not in [1, 4]
|
||||
|
||||
embed = create_end_game_embed(ctx, bet, player_value, dealer_value, payout, status)
|
||||
|
||||
if playing_embed:
|
||||
await ctx.edit(embed=embed, view=None)
|
||||
else:
|
||||
await ctx.respond(embed=embed, view=None, content=ctx.author.mention)
|
||||
|
||||
currency.add_balance(payout) if is_won else currency.take_balance(bet)
|
||||
currency.push()
|
||||
|
||||
BlackJackStats(
|
||||
user_id=ctx.author.id,
|
||||
is_won=is_won,
|
||||
bet=bet,
|
||||
payout=payout if is_won else 0,
|
||||
hand_player=player_hand,
|
||||
hand_dealer=dealer_hand,
|
||||
).push()
|
||||
|
||||
|
||||
def create_game_embed(
|
||||
ctx: commands.Context,
|
||||
bet: int,
|
||||
player_hand: Hand,
|
||||
dealer_hand: Hand,
|
||||
player_value: int,
|
||||
dealer_value: int,
|
||||
) -> discord.Embed:
|
||||
player_hand_str = " + ".join(player_hand)
|
||||
dealer_hand_str = f"{dealer_hand[0]} + " + (
|
||||
CONST.STRINGS["blackjack_dealer_hidden"]
|
||||
if len(dealer_hand) < 2
|
||||
else " + ".join(dealer_hand[1:])
|
||||
)
|
||||
|
||||
description = (
|
||||
f"{CONST.STRINGS['blackjack_player_hand'].format(player_value, player_hand_str)}\n\n"
|
||||
f"{CONST.STRINGS['blackjack_dealer_hand'].format(dealer_value, dealer_hand_str)}"
|
||||
)
|
||||
|
||||
footer_text = (
|
||||
f"{CONST.STRINGS['blackjack_bet'].format(Currency.format_human(bet))} • "
|
||||
f"{CONST.STRINGS['blackjack_deck_shuffled']}"
|
||||
)
|
||||
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=CONST.STRINGS["blackjack_title"],
|
||||
color=discord.Colour.embed_background(),
|
||||
description=description,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.MUFFIN_ART,
|
||||
show_name=False,
|
||||
hide_timestamp=True,
|
||||
)
|
||||
|
||||
|
||||
def create_end_game_embed(
|
||||
ctx: commands.Context,
|
||||
bet: int,
|
||||
player_value: int,
|
||||
dealer_value: int,
|
||||
payout: int,
|
||||
status: int,
|
||||
) -> discord.Embed:
|
||||
embed = EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=CONST.STRINGS["blackjack_title"],
|
||||
color=discord.Colour.embed_background(),
|
||||
description=CONST.STRINGS["blackjack_description"].format(
|
||||
player_value,
|
||||
dealer_value,
|
||||
),
|
||||
footer_text=CONST.STRINGS["blackjack_footer"],
|
||||
footer_icon_url=CONST.MUFFIN_ART,
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
result = {
|
||||
1: (
|
||||
CONST.STRINGS["blackjack_busted"],
|
||||
CONST.STRINGS["blackjack_lost"].format(Currency.format_human(bet)),
|
||||
discord.Color.red(),
|
||||
CONST.CLOUD_ART,
|
||||
),
|
||||
2: (
|
||||
CONST.STRINGS["blackjack_won_21"],
|
||||
CONST.STRINGS["blackjack_won_payout"].format(Currency.format_human(payout)),
|
||||
discord.Color.green(),
|
||||
CONST.TROPHY_ART,
|
||||
),
|
||||
3: (
|
||||
CONST.STRINGS["blackjack_dealer_busted"],
|
||||
CONST.STRINGS["blackjack_won_payout"].format(Currency.format_human(payout)),
|
||||
discord.Color.green(),
|
||||
CONST.TROPHY_ART,
|
||||
),
|
||||
4: (
|
||||
CONST.STRINGS["blackjack_lost_generic"],
|
||||
CONST.STRINGS["blackjack_lost"].format(Currency.format_human(bet)),
|
||||
discord.Color.red(),
|
||||
CONST.CLOUD_ART,
|
||||
),
|
||||
5: (
|
||||
CONST.STRINGS["blackjack_won_natural"],
|
||||
CONST.STRINGS["blackjack_won_payout"].format(Currency.format_human(payout)),
|
||||
discord.Color.green(),
|
||||
CONST.TROPHY_ART,
|
||||
),
|
||||
}.get(
|
||||
status,
|
||||
(
|
||||
CONST.STRINGS["blackjack_error"],
|
||||
CONST.STRINGS["blackjack_error_description"],
|
||||
discord.Color.red(),
|
||||
None,
|
||||
),
|
||||
)
|
||||
|
||||
name, value, color, thumbnail_url = result
|
||||
embed.add_field(name=name, value=value, inline=False)
|
||||
embed.colour = color
|
||||
if thumbnail_url:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
def get_new_deck() -> List[Card]:
|
||||
deck = [
|
||||
rank + suit
|
||||
for suit in ["♠", "♡", "♢", "♣"]
|
||||
for rank in ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"]
|
||||
]
|
||||
random.shuffle(deck)
|
||||
return deck
|
||||
|
||||
|
||||
def deal_card(deck: List[Card]) -> Card:
|
||||
return deck.pop()
|
||||
|
||||
|
||||
def calculate_hand_value(hand: Hand) -> int:
|
||||
value = sum(
|
||||
10 if rank in "JQK" else 11 if rank == "A" else int(rank)
|
||||
for card in hand
|
||||
for rank in card[:-1]
|
||||
)
|
||||
aces = sum(card[0] == "A" for card in hand)
|
||||
while value > 21 and aces:
|
||||
value -= 10
|
||||
aces -= 1
|
||||
return value
|
||||
|
||||
|
||||
class BlackJackButtons(View):
|
||||
def __init__(self, ctx):
|
||||
super().__init__(timeout=180)
|
||||
self.ctx = ctx
|
||||
self.clickedHit = False
|
||||
self.clickedStand = False
|
||||
self.clickedDoubleDown = False
|
||||
|
||||
async def on_timeout(self):
|
||||
for child in self.children:
|
||||
child.disabled = True
|
||||
await self.message.edit(view=None)
|
||||
|
||||
@discord.ui.button(
|
||||
label=CONST.STRINGS["blackjack_hit"],
|
||||
style=discord.ButtonStyle.gray,
|
||||
emoji=CONST.BLACKJACK_HIT_EMOJI,
|
||||
)
|
||||
async def hit_button_callback(self, button, interaction):
|
||||
self.clickedHit = True
|
||||
await interaction.response.defer()
|
||||
self.stop()
|
||||
|
||||
@discord.ui.button(
|
||||
label=CONST.STRINGS["blackjack_stand"],
|
||||
style=discord.ButtonStyle.gray,
|
||||
emoji=CONST.BLACKJACK_STAND_EMOJI,
|
||||
)
|
||||
async def stand_button_callback(self, button, interaction):
|
||||
self.clickedStand = True
|
||||
await interaction.response.defer()
|
||||
self.stop()
|
||||
|
||||
async def interaction_check(self, interaction) -> bool:
|
||||
if interaction.user == self.ctx.author:
|
||||
return True
|
||||
await interaction.response.send_message(
|
||||
CONST.STRINGS["error_cant_use_buttons"],
|
||||
ephemeral=True,
|
||||
)
|
||||
return False
|
|
@ -1,43 +0,0 @@
|
|||
from datetime import datetime, timedelta
|
||||
|
||||
import lib.time
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.currency_service import Currency
|
||||
from services.daily_service import Dailies
|
||||
|
||||
|
||||
async def cmd(ctx) -> None:
|
||||
ctx_daily = Dailies(ctx.author.id)
|
||||
|
||||
if not ctx_daily.can_be_claimed():
|
||||
wait_time = datetime.now() + timedelta(seconds=lib.time.seconds_until(7, 0))
|
||||
unix_time = int(round(wait_time.timestamp()))
|
||||
error_embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["daily_already_claimed_author"],
|
||||
description=CONST.STRINGS["daily_already_claimed_description"].format(
|
||||
unix_time,
|
||||
),
|
||||
footer_text=CONST.STRINGS["daily_already_claimed_footer"],
|
||||
)
|
||||
await ctx.respond(embed=error_embed)
|
||||
return
|
||||
ctx_daily.streak = ctx_daily.streak + 1 if ctx_daily.streak_check() else 1
|
||||
ctx_daily.claimed_at = datetime.now(tz=ctx_daily.tz)
|
||||
ctx_daily.amount = 100 * 12 * (ctx_daily.streak - 1)
|
||||
|
||||
ctx_daily.refresh()
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["daily_success_claim_author"],
|
||||
description=CONST.STRINGS["daily_success_claim_description"].format(
|
||||
Currency.format(ctx_daily.amount),
|
||||
),
|
||||
footer_text=CONST.STRINGS["daily_streak_footer"].format(ctx_daily.streak)
|
||||
if ctx_daily.streak > 1
|
||||
else None,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,39 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from services.currency_service import Currency
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
|
||||
|
||||
async def cmd(ctx: commands.Context, user: discord.User, amount: int) -> None:
|
||||
if ctx.author.id == user.id:
|
||||
raise LumiException(CONST.STRINGS["give_error_self"])
|
||||
if user.bot:
|
||||
raise LumiException(CONST.STRINGS["give_error_bot"])
|
||||
if amount <= 0:
|
||||
raise LumiException(CONST.STRINGS["give_error_invalid_amount"])
|
||||
|
||||
ctx_currency = Currency(ctx.author.id)
|
||||
target_currency = Currency(user.id)
|
||||
|
||||
if ctx_currency.balance < amount:
|
||||
raise LumiException(CONST.STRINGS["give_error_insufficient_funds"])
|
||||
|
||||
target_currency.add_balance(amount)
|
||||
ctx_currency.take_balance(amount)
|
||||
|
||||
ctx_currency.push()
|
||||
target_currency.push()
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
description=CONST.STRINGS["give_success"].format(
|
||||
ctx.author.name,
|
||||
Currency.format(amount),
|
||||
user.name,
|
||||
),
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,214 +0,0 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
import random
|
||||
from collections import Counter
|
||||
|
||||
import discord
|
||||
import pytz
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from services.currency_service import Currency
|
||||
from services.stats_service import SlotsStats
|
||||
|
||||
est = pytz.timezone("US/Eastern")
|
||||
|
||||
|
||||
async def cmd(self, ctx, bet):
|
||||
ctx_currency = Currency(ctx.author.id)
|
||||
|
||||
player_balance = ctx_currency.balance
|
||||
if bet > player_balance:
|
||||
raise commands.BadArgument("you don't have enough cash.")
|
||||
elif bet <= 0:
|
||||
raise commands.BadArgument("the bet you entered is invalid.")
|
||||
|
||||
results = [random.randint(0, 6) for _ in range(3)]
|
||||
calculated_results = calculate_slots_results(bet, results)
|
||||
|
||||
(result_type, payout, multiplier) = calculated_results
|
||||
is_won = result_type != "lost"
|
||||
# only get the emojis once
|
||||
emojis = get_emotes(self.client)
|
||||
|
||||
# start with default "spinning" embed
|
||||
await ctx.respond(
|
||||
embed=slots_spinning(ctx, 3, Currency.format_human(bet), results, emojis),
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
for i in range(2, 0, -1):
|
||||
await ctx.edit(
|
||||
embed=slots_spinning(ctx, i, Currency.format_human(bet), results, emojis),
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# output final result
|
||||
finished_output = slots_finished(
|
||||
ctx,
|
||||
result_type,
|
||||
Currency.format_human(bet),
|
||||
Currency.format_human(payout),
|
||||
results,
|
||||
emojis,
|
||||
)
|
||||
|
||||
await ctx.edit(embed=finished_output)
|
||||
|
||||
# user payout
|
||||
if payout > 0:
|
||||
ctx_currency.add_balance(payout)
|
||||
else:
|
||||
ctx_currency.take_balance(bet)
|
||||
|
||||
stats = SlotsStats(
|
||||
user_id=ctx.author.id,
|
||||
is_won=is_won,
|
||||
bet=bet,
|
||||
payout=payout,
|
||||
spin_type=result_type,
|
||||
icons=results,
|
||||
)
|
||||
|
||||
ctx_currency.push()
|
||||
stats.push()
|
||||
|
||||
|
||||
def get_emotes(client):
|
||||
emotes = CONST.EMOTE_IDS
|
||||
return {name: client.get_emoji(emoji_id) for name, emoji_id in emotes.items()}
|
||||
|
||||
|
||||
def calculate_slots_results(bet, results):
|
||||
result_type = None
|
||||
multiplier = None
|
||||
rewards = CONST.SLOTS_MULTIPLIERS
|
||||
|
||||
# count occurrences of each item in the list
|
||||
counts = Counter(results)
|
||||
|
||||
# no icons match
|
||||
if len(counts) == 3:
|
||||
result_type = "lost"
|
||||
multiplier = 0
|
||||
|
||||
elif len(counts) == 2:
|
||||
result_type = "pair"
|
||||
multiplier = rewards[result_type]
|
||||
|
||||
elif len(counts) == 1:
|
||||
if results[0] == 5:
|
||||
result_type = "three_diamonds"
|
||||
elif results[0] == 6:
|
||||
result_type = "jackpot"
|
||||
else:
|
||||
result_type = "three_of_a_kind"
|
||||
multiplier = rewards[result_type]
|
||||
|
||||
payout = bet * multiplier
|
||||
return result_type, int(payout), multiplier
|
||||
|
||||
|
||||
def slots_spinning(ctx, spinning_icons_amount, bet, results, emojis):
|
||||
first_slots_emote = emojis.get(f"slots_{results[0]}_id")
|
||||
second_slots_emote = emojis.get(f"slots_{results[1]}_id")
|
||||
slots_animated_emote = emojis.get("slots_animated_id")
|
||||
|
||||
current_time = datetime.datetime.now(est).strftime("%I:%M %p")
|
||||
one = slots_animated_emote
|
||||
two = slots_animated_emote
|
||||
three = slots_animated_emote
|
||||
|
||||
if spinning_icons_amount == 3:
|
||||
pass
|
||||
elif spinning_icons_amount == 2:
|
||||
one = first_slots_emote
|
||||
elif spinning_icons_amount == 1:
|
||||
one = first_slots_emote
|
||||
two = second_slots_emote
|
||||
|
||||
description = (
|
||||
f"🎰{emojis['S_Wide']}{emojis['L_Wide']}{emojis['O_Wide']}{emojis['T_Wide']}{emojis['S_Wide']}🎰\n"
|
||||
f"{emojis['CBorderTLeft']}{emojis['HBorderT']}{emojis['HBorderT']}{emojis['HBorderT']}"
|
||||
f"{emojis['HBorderT']}{emojis['HBorderT']}{emojis['CBorderTRight']}\n"
|
||||
f"{emojis['VBorder']}{one}{emojis['VBorder']}{two}{emojis['VBorder']}"
|
||||
f"{three}{emojis['VBorder']}\n"
|
||||
f"{emojis['CBorderBLeft']}{emojis['HBorderB']}{emojis['HBorderB']}{emojis['HBorderB']}"
|
||||
f"{emojis['HBorderB']}{emojis['HBorderB']}{emojis['CBorderBRight']}\n"
|
||||
f"{emojis['Blank']}{emojis['Blank']}❓❓❓{emojis['Blank']}{emojis['Blank']}{emojis['Blank']}"
|
||||
)
|
||||
|
||||
embed = discord.Embed(
|
||||
description=description,
|
||||
)
|
||||
embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar.url)
|
||||
embed.set_footer(
|
||||
text=f"Bet ${bet} • jackpot = x15 • {current_time}",
|
||||
icon_url="https://i.imgur.com/wFsgSnr.png",
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
def slots_finished(ctx, payout_type, bet, payout, results, emojis):
|
||||
first_slots_emote = emojis.get(f"slots_{results[0]}_id")
|
||||
second_slots_emote = emojis.get(f"slots_{results[1]}_id")
|
||||
third_slots_emote = emojis.get(f"slots_{results[2]}_id")
|
||||
current_time = datetime.datetime.now(est).strftime("%I:%M %p")
|
||||
|
||||
field_name = "You lost."
|
||||
field_value = f"You lost **${bet}**."
|
||||
color = discord.Color.red()
|
||||
is_lost = True
|
||||
|
||||
if payout_type == "pair":
|
||||
field_name = "Pair"
|
||||
field_value = f"You won **${payout}**."
|
||||
is_lost = False
|
||||
color = discord.Color.dark_green()
|
||||
elif payout_type == "three_of_a_kind":
|
||||
field_name = "3 of a kind"
|
||||
field_value = f"You won **${payout}**."
|
||||
is_lost = False
|
||||
color = discord.Color.dark_green()
|
||||
elif payout_type == "three_diamonds":
|
||||
field_name = "Triple Diamonds!"
|
||||
field_value = f"You won **${payout}**."
|
||||
is_lost = False
|
||||
color = discord.Color.green()
|
||||
elif payout_type == "jackpot":
|
||||
field_name = "JACKPOT!!"
|
||||
field_value = f"You won **${payout}**."
|
||||
is_lost = False
|
||||
color = discord.Color.green()
|
||||
|
||||
description = (
|
||||
f"🎰{emojis['S_Wide']}{emojis['L_Wide']}{emojis['O_Wide']}{emojis['T_Wide']}{emojis['S_Wide']}🎰\n"
|
||||
f"{emojis['CBorderTLeft']}{emojis['HBorderT']}{emojis['HBorderT']}{emojis['HBorderT']}"
|
||||
f"{emojis['HBorderT']}{emojis['HBorderT']}{emojis['CBorderTRight']}\n"
|
||||
f"{emojis['VBorder']}{first_slots_emote}{emojis['VBorder']}{second_slots_emote}"
|
||||
f"{emojis['VBorder']}{third_slots_emote}{emojis['VBorder']}\n"
|
||||
f"{emojis['CBorderBLeft']}{emojis['HBorderB']}{emojis['HBorderB']}{emojis['HBorderB']}"
|
||||
f"{emojis['HBorderB']}{emojis['HBorderB']}{emojis['CBorderBRight']}"
|
||||
)
|
||||
|
||||
if is_lost:
|
||||
description += (
|
||||
f"\n{emojis['Blank']}{emojis['LCentered']}{emojis['OCentered']}{emojis['SCentered']}"
|
||||
f"{emojis['ECentered']}{emojis['lost']}{emojis['Blank']}"
|
||||
)
|
||||
else:
|
||||
description += f"\n{emojis['Blank']}🎉{emojis['WSmall']}{emojis['ISmall']}{emojis['NSmall']}🎉{emojis['Blank']}"
|
||||
|
||||
embed = discord.Embed(
|
||||
color=color,
|
||||
description=description,
|
||||
)
|
||||
embed.add_field(name=field_name, value=field_value, inline=False)
|
||||
embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar.url)
|
||||
embed.set_footer(
|
||||
text=f"Game finished • {current_time}",
|
||||
icon_url="https://i.imgur.com/wFsgSnr.png",
|
||||
)
|
||||
|
||||
return embed
|
|
@ -1,24 +0,0 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from lib import constants, embed_builder, formatter
|
||||
|
||||
|
||||
class Help(commands.Cog):
|
||||
def __init__(self, client: commands.Bot) -> None:
|
||||
self.client = client
|
||||
|
||||
@commands.slash_command(
|
||||
name="help",
|
||||
description="Get Lumi help.",
|
||||
)
|
||||
async def help_command(self, ctx) -> None:
|
||||
prefix = formatter.get_prefix(ctx)
|
||||
embed = embed_builder.EmbedBuilder.create_warning_embed(
|
||||
ctx=ctx,
|
||||
description=constants.CONST.STRINGS["help_use_prefix"].format(prefix),
|
||||
)
|
||||
await ctx.respond(embed=embed, ephemeral=True)
|
||||
|
||||
|
||||
def setup(client: commands.Bot) -> None:
|
||||
client.add_cog(Help(client))
|
|
@ -1,36 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.levels import leaderboard, level
|
||||
|
||||
|
||||
class Levels(commands.Cog):
|
||||
def __init__(self, client: commands.Bot) -> None:
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="level",
|
||||
aliases=["rank", "xp"],
|
||||
description="Displays your level and server rank.",
|
||||
help="Displays your level and server rank.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def level_command(self, ctx) -> None:
|
||||
await level.rank(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="leaderboard",
|
||||
aliases=["lb", "xplb"],
|
||||
description="See the Lumi leaderboards.",
|
||||
help="Shows three different leaderboards: levels, currency and daily streaks.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def leaderboard_command(self, ctx) -> None:
|
||||
await leaderboard.cmd(ctx)
|
||||
|
||||
|
||||
def setup(client: commands.Bot) -> None:
|
||||
client.add_cog(Levels(client))
|
|
@ -1,202 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.currency_service import Currency
|
||||
from services.daily_service import Dailies
|
||||
from services.xp_service import XpService
|
||||
|
||||
|
||||
async def cmd(ctx: bridge.Context) -> None:
|
||||
if not ctx.guild:
|
||||
return
|
||||
|
||||
options = LeaderboardCommandOptions()
|
||||
view = LeaderboardCommandView(ctx, options)
|
||||
|
||||
# default leaderboard
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx=ctx,
|
||||
thumbnail_url=CONST.FLOWERS_ART,
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
icon = ctx.guild.icon.url if ctx.guild.icon else CONST.FLOWERS_ART
|
||||
await view.populate_leaderboard("xp", embed, icon)
|
||||
|
||||
await ctx.respond(embed=embed, view=view)
|
||||
|
||||
|
||||
class LeaderboardCommandOptions(discord.ui.Select):
|
||||
"""
|
||||
This class specifies the options for the leaderboard command:
|
||||
- XP
|
||||
- Currency
|
||||
- Daily streak
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
placeholder="Select a leaderboard",
|
||||
min_values=1,
|
||||
max_values=1,
|
||||
options=[
|
||||
discord.SelectOption(
|
||||
label="Levels",
|
||||
description="See the top chatters of this server!",
|
||||
emoji="🆙",
|
||||
value="xp",
|
||||
),
|
||||
discord.SelectOption(
|
||||
label="Currency",
|
||||
description="Who is the richest Lumi user?",
|
||||
value="currency",
|
||||
emoji="💸",
|
||||
),
|
||||
discord.SelectOption(
|
||||
label="Dailies",
|
||||
description="See who has the biggest streak!",
|
||||
value="dailies",
|
||||
emoji="📅",
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
if self.view:
|
||||
await self.view.on_select(self.values[0], interaction)
|
||||
|
||||
|
||||
class LeaderboardCommandView(discord.ui.View):
|
||||
"""
|
||||
This view represents a dropdown menu to choose
|
||||
what kind of leaderboard to show.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx: bridge.Context, options: LeaderboardCommandOptions) -> None:
|
||||
self.ctx = ctx
|
||||
self.options = options
|
||||
|
||||
super().__init__(timeout=180)
|
||||
self.add_item(self.options)
|
||||
|
||||
async def on_timeout(self) -> None:
|
||||
if self.message:
|
||||
await self.message.edit(view=None)
|
||||
self.stop()
|
||||
|
||||
async def interaction_check(self, interaction: discord.Interaction) -> bool:
|
||||
if interaction.user and interaction.user != self.ctx.author:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx=self.ctx,
|
||||
author_text=interaction.user.name,
|
||||
description=CONST.STRINGS["xp_lb_cant_use_dropdown"],
|
||||
show_name=False,
|
||||
)
|
||||
await interaction.response.send_message(embed=embed, ephemeral=True)
|
||||
return False
|
||||
return True
|
||||
|
||||
async def on_select(self, item: str, interaction: discord.Interaction) -> None:
|
||||
if not self.ctx.guild:
|
||||
return
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx=self.ctx,
|
||||
thumbnail_url=CONST.FLOWERS_ART,
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
icon = self.ctx.guild.icon.url if self.ctx.guild.icon else CONST.FLOWERS_ART
|
||||
|
||||
await self.populate_leaderboard(item, embed, icon)
|
||||
|
||||
await interaction.response.edit_message(embed=embed)
|
||||
|
||||
async def populate_leaderboard(self, item: str, embed, icon):
|
||||
leaderboard_methods = {
|
||||
"xp": self._populate_xp_leaderboard,
|
||||
"currency": self._populate_currency_leaderboard,
|
||||
"dailies": self._populate_dailies_leaderboard,
|
||||
}
|
||||
await leaderboard_methods[item](embed, icon)
|
||||
|
||||
async def _populate_xp_leaderboard(self, embed, icon):
|
||||
if not self.ctx.guild:
|
||||
return
|
||||
|
||||
xp_lb = XpService.load_leaderboard(self.ctx.guild.id)
|
||||
embed.set_author(name=CONST.STRINGS["xp_lb_author"], icon_url=icon)
|
||||
|
||||
for rank, (user_id, xp, level, xp_needed_for_next_level) in enumerate(
|
||||
xp_lb[:5],
|
||||
start=1,
|
||||
):
|
||||
try:
|
||||
member = await self.ctx.guild.fetch_member(user_id)
|
||||
except discord.HTTPException:
|
||||
continue # skip user if not in guild
|
||||
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["xp_lb_field_name"].format(rank, member.name),
|
||||
value=CONST.STRINGS["xp_lb_field_value"].format(
|
||||
level,
|
||||
xp,
|
||||
xp_needed_for_next_level,
|
||||
),
|
||||
inline=False,
|
||||
)
|
||||
|
||||
async def _populate_currency_leaderboard(self, embed, icon):
|
||||
if not self.ctx.guild:
|
||||
return
|
||||
|
||||
cash_lb = Currency.load_leaderboard()
|
||||
embed.set_author(name=CONST.STRINGS["xp_lb_currency_author"], icon_url=icon)
|
||||
embed.set_thumbnail(url=CONST.TEAPOT_ART)
|
||||
|
||||
for user_id, balance, rank in cash_lb[:5]:
|
||||
try:
|
||||
member = await self.ctx.guild.fetch_member(user_id)
|
||||
except discord.HTTPException:
|
||||
member = None
|
||||
|
||||
name = member.name if member else str(user_id)
|
||||
|
||||
embed.add_field(
|
||||
name=f"#{rank} - {name}",
|
||||
value=CONST.STRINGS["xp_lb_currency_field_value"].format(
|
||||
Currency.format(balance),
|
||||
),
|
||||
inline=False,
|
||||
)
|
||||
|
||||
async def _populate_dailies_leaderboard(self, embed, icon):
|
||||
if not self.ctx.guild:
|
||||
return
|
||||
|
||||
daily_lb = Dailies.load_leaderboard()
|
||||
embed.set_author(name=CONST.STRINGS["xp_lb_dailies_author"], icon_url=icon)
|
||||
embed.set_thumbnail(url=CONST.MUFFIN_ART)
|
||||
|
||||
for user_id, streak, claimed_at, rank in daily_lb[:5]:
|
||||
try:
|
||||
member = await self.ctx.guild.fetch_member(user_id)
|
||||
except discord.HTTPException:
|
||||
member = None
|
||||
|
||||
name = member.name if member else user_id
|
||||
|
||||
claimed_at = datetime.fromisoformat(claimed_at).date()
|
||||
|
||||
embed.add_field(
|
||||
name=f"#{rank} - {name}",
|
||||
value=CONST.STRINGS["xp_lb_dailies_field_value"].format(
|
||||
streak,
|
||||
claimed_at,
|
||||
),
|
||||
inline=False,
|
||||
)
|
|
@ -1,31 +0,0 @@
|
|||
from discord import Embed
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.xp_service import XpService
|
||||
|
||||
|
||||
async def rank(ctx: bridge.Context) -> None:
|
||||
if not ctx.guild:
|
||||
return
|
||||
|
||||
xp_data: XpService = XpService(ctx.author.id, ctx.guild.id)
|
||||
|
||||
rank: str = str(xp_data.calculate_rank())
|
||||
needed_xp_for_next_level: int = XpService.xp_needed_for_next_level(xp_data.level)
|
||||
|
||||
embed: Embed = EmbedBuilder.create_success_embed(
|
||||
ctx=ctx,
|
||||
title=CONST.STRINGS["xp_level"].format(xp_data.level),
|
||||
footer_text=CONST.STRINGS["xp_server_rank"].format(rank or "NaN"),
|
||||
show_name=False,
|
||||
thumbnail_url=ctx.author.display_avatar.url,
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["xp_progress"],
|
||||
value=XpService.generate_progress_bar(xp_data.xp, needed_xp_for_next_level),
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,135 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import bridge, commands, tasks
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from Client import LumiBot
|
||||
from modules.config import c_prefix
|
||||
from modules.misc import avatar, backup, info, introduction, invite, ping, xkcd
|
||||
|
||||
|
||||
class Misc(commands.Cog):
|
||||
def __init__(self, client: LumiBot) -> None:
|
||||
self.client: LumiBot = client
|
||||
self.start_time: datetime = datetime.now()
|
||||
self.do_backup.start()
|
||||
|
||||
@tasks.loop(hours=1)
|
||||
async def do_backup(self) -> None:
|
||||
await backup.backup()
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="avatar",
|
||||
aliases=["av"],
|
||||
description="Get a user's avatar.",
|
||||
help="Get a user's avatar.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def avatar(self, ctx, user: discord.Member) -> None:
|
||||
return await avatar.get_avatar(ctx, user)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="ping",
|
||||
aliases=["p", "status"],
|
||||
description="Simple status check.",
|
||||
help="Simple status check.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def ping(self, ctx) -> None:
|
||||
await ping.ping(self, ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="uptime",
|
||||
description="See Lumi's uptime since the last update.",
|
||||
help="See how long Lumi has been online since his last update.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def uptime(self, ctx) -> None:
|
||||
await ping.uptime(self, ctx, self.start_time)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="invite",
|
||||
description="Generate an invite link.",
|
||||
help="Generate a link to invite Lumi to your own server!",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def invite_command(self, ctx) -> None:
|
||||
await invite.cmd(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="prefix",
|
||||
description="See the server's current prefix.",
|
||||
help="See the server's current prefix.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def prefix_command(self, ctx) -> None:
|
||||
await c_prefix.get_prefix(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="info",
|
||||
aliases=["stats"],
|
||||
description="Shows basic Lumi stats.",
|
||||
help="Shows basic Lumi stats.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def info_command(self, ctx) -> None:
|
||||
unix_timestamp: int = int(round(self.start_time.timestamp()))
|
||||
await info.cmd(self, ctx, unix_timestamp)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="introduction",
|
||||
aliases=["intro", "introduce"],
|
||||
description="This command can only be used in DMs.",
|
||||
help="Introduce yourself. For now this command "
|
||||
"can only be done in ONE server and only in Lumi's DMs.",
|
||||
contexts={discord.InteractionContextType.bot_dm},
|
||||
)
|
||||
@commands.dm_only()
|
||||
async def intro_command(self, ctx) -> None:
|
||||
await introduction.cmd(self, ctx)
|
||||
|
||||
"""
|
||||
xkcd submodule - slash command only
|
||||
"""
|
||||
xkcd: SlashCommandGroup = SlashCommandGroup(
|
||||
"xkcd",
|
||||
"A web comic of romance, sarcasm, math, and language.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
|
||||
@xkcd.command(name="latest", description="Get the latest xkcd comic.")
|
||||
async def xkcd_latest(self, ctx) -> None:
|
||||
await xkcd.print_comic(ctx, latest=True)
|
||||
|
||||
@xkcd.command(name="random", description="Get a random xkcd comic.")
|
||||
async def xkcd_random(self, ctx) -> None:
|
||||
await xkcd.print_comic(ctx)
|
||||
|
||||
@xkcd.command(name="search", description="Search for a xkcd comic by ID.")
|
||||
async def xkcd_search(self, ctx, *, id: int) -> None:
|
||||
await xkcd.print_comic(ctx, number=id)
|
||||
|
||||
|
||||
def setup(client: LumiBot) -> None:
|
||||
client.add_cog(Misc(client))
|
|
@ -1,58 +0,0 @@
|
|||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from discord import File, Member
|
||||
from discord.ext import bridge
|
||||
|
||||
client: httpx.AsyncClient = httpx.AsyncClient()
|
||||
|
||||
|
||||
async def get_avatar(ctx: bridge.Context, member: Member) -> None:
|
||||
"""
|
||||
Get the avatar of a member.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
ctx : ApplicationContext
|
||||
The discord context object.
|
||||
member : Member
|
||||
The member to get the avatar of.
|
||||
"""
|
||||
guild_avatar: Optional[str] = (
|
||||
member.guild_avatar.url if member.guild_avatar else None
|
||||
)
|
||||
profile_avatar: Optional[str] = member.avatar.url if member.avatar else None
|
||||
|
||||
files: list[File] = [
|
||||
await create_avatar_file(avatar)
|
||||
for avatar in [guild_avatar, profile_avatar]
|
||||
if avatar
|
||||
]
|
||||
|
||||
if files:
|
||||
await ctx.respond(files=files)
|
||||
else:
|
||||
await ctx.respond(content="member has no avatar.")
|
||||
|
||||
|
||||
async def create_avatar_file(url: str) -> File:
|
||||
"""
|
||||
Create a discord file from an avatar url.
|
||||
|
||||
Parameters:
|
||||
-----------
|
||||
url : str
|
||||
The url of the avatar.
|
||||
|
||||
Returns:
|
||||
--------
|
||||
File
|
||||
The discord file.
|
||||
"""
|
||||
response: httpx.Response = await client.get(url, timeout=10)
|
||||
response.raise_for_status()
|
||||
image_data: bytes = response.content
|
||||
image_file: BytesIO = BytesIO(image_data)
|
||||
image_file.seek(0)
|
||||
return File(image_file, filename="avatar.png")
|
|
@ -1,67 +0,0 @@
|
|||
import subprocess
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
|
||||
import dropbox
|
||||
from dropbox.files import FileMetadata
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
|
||||
# Initialize Dropbox client if instance is "main"
|
||||
_dbx: Optional[dropbox.Dropbox] = None
|
||||
if CONST.INSTANCE and CONST.INSTANCE.lower() == "main":
|
||||
_app_key: Optional[str] = CONST.DBX_APP_KEY
|
||||
_dbx_token: Optional[str] = CONST.DBX_TOKEN
|
||||
_app_secret: Optional[str] = CONST.DBX_APP_SECRET
|
||||
|
||||
_dbx = dropbox.Dropbox(
|
||||
app_key=_app_key,
|
||||
app_secret=_app_secret,
|
||||
oauth2_refresh_token=_dbx_token,
|
||||
)
|
||||
|
||||
|
||||
async def create_db_backup() -> None:
|
||||
if not _dbx:
|
||||
raise ValueError("Dropbox client is not initialized")
|
||||
|
||||
backup_name: str = datetime.today().strftime("%Y-%m-%d_%H%M") + "_lumi.sql"
|
||||
command: str = (
|
||||
f"mariadb-dump --user={CONST.MARIADB_USER} --password={CONST.MARIADB_PASSWORD} "
|
||||
f"--host=db --single-transaction --all-databases > ./db/migrations/100-dump.sql"
|
||||
)
|
||||
|
||||
subprocess.check_output(command, shell=True)
|
||||
|
||||
with open("./db/migrations/100-dump.sql", "rb") as f:
|
||||
_dbx.files_upload(f.read(), f"/{backup_name}")
|
||||
|
||||
|
||||
async def backup_cleanup() -> None:
|
||||
if not _dbx:
|
||||
raise ValueError("Dropbox client is not initialized")
|
||||
|
||||
result = _dbx.files_list_folder("")
|
||||
|
||||
all_backup_files: List[str] = [
|
||||
entry.name
|
||||
for entry in result.entries
|
||||
if isinstance(entry, FileMetadata) # type: ignore
|
||||
]
|
||||
|
||||
for file in sorted(all_backup_files)[:-48]:
|
||||
_dbx.files_delete_v2("/" + file)
|
||||
|
||||
|
||||
async def backup() -> None:
|
||||
if CONST.INSTANCE and CONST.INSTANCE.lower() == "main":
|
||||
logger.debug("Backing up the database.")
|
||||
try:
|
||||
await create_db_backup()
|
||||
await backup_cleanup()
|
||||
logger.debug("Backup successful.")
|
||||
except Exception as error:
|
||||
logger.error(f"Backup failed: {error}")
|
||||
else:
|
||||
logger.debug('No backup, instance not "MAIN".')
|
|
@ -1,42 +0,0 @@
|
|||
import os
|
||||
import platform
|
||||
|
||||
import discord
|
||||
import psutil
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.currency_service import Currency
|
||||
from services.stats_service import BlackJackStats
|
||||
|
||||
|
||||
async def cmd(self, ctx: bridge.Context, unix_timestamp: int) -> None:
|
||||
memory_usage_in_mb: float = psutil.Process().memory_info().rss / (1024 * 1024)
|
||||
total_rows: str = Currency.format(BlackJackStats.get_total_rows_count())
|
||||
|
||||
description: str = "".join(
|
||||
[
|
||||
CONST.STRINGS["info_uptime"].format(unix_timestamp),
|
||||
CONST.STRINGS["info_latency"].format(round(1000 * self.client.latency)),
|
||||
CONST.STRINGS["info_memory"].format(memory_usage_in_mb),
|
||||
CONST.STRINGS["info_system"].format(platform.system(), os.name),
|
||||
CONST.STRINGS["info_api_version"].format(discord.__version__),
|
||||
CONST.STRINGS["info_database_records"].format(total_rows),
|
||||
],
|
||||
)
|
||||
|
||||
embed: discord.Embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
description=description,
|
||||
footer_text=CONST.STRINGS["info_service_footer"],
|
||||
show_name=False,
|
||||
)
|
||||
embed.set_author(
|
||||
name=f"{CONST.TITLE} v{CONST.VERSION}",
|
||||
url=CONST.REPO_URL,
|
||||
icon_url=CONST.CHECK_ICON,
|
||||
)
|
||||
embed.set_thumbnail(url=CONST.LUMI_LOGO_OPAQUE)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,166 +0,0 @@
|
|||
import asyncio
|
||||
from typing import Dict, Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.interactions.introduction import (
|
||||
IntroductionFinishButtons,
|
||||
IntroductionStartButtons,
|
||||
)
|
||||
|
||||
|
||||
async def cmd(self, ctx: bridge.Context) -> None:
|
||||
guild: Optional[discord.Guild] = self.client.get_guild(CONST.KRC_GUILD_ID)
|
||||
member: Optional[discord.Member] = (
|
||||
guild.get_member(ctx.author.id) if guild else None
|
||||
)
|
||||
|
||||
if not guild or not member:
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["intro_no_guild_author"],
|
||||
description=CONST.STRINGS["intro_no_guild"],
|
||||
footer_text=CONST.STRINGS["intro_service_name"],
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
question_mapping: Dict[str, str] = CONST.KRC_QUESTION_MAPPING
|
||||
channel: Optional[discord.abc.GuildChannel] = guild.get_channel(
|
||||
CONST.KRC_INTRO_CHANNEL_ID,
|
||||
)
|
||||
|
||||
if not channel or isinstance(
|
||||
channel,
|
||||
(discord.ForumChannel, discord.CategoryChannel),
|
||||
):
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["intro_no_channel_author"],
|
||||
description=CONST.STRINGS["intro_no_channel"],
|
||||
footer_text=CONST.STRINGS["intro_service_name"],
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
view: IntroductionStartButtons | IntroductionFinishButtons = (
|
||||
IntroductionStartButtons(ctx)
|
||||
)
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["intro_service_name"],
|
||||
description=CONST.STRINGS["intro_start"].format(channel.mention),
|
||||
footer_text=CONST.STRINGS["intro_start_footer"],
|
||||
),
|
||||
view=view,
|
||||
)
|
||||
await view.wait()
|
||||
|
||||
if view.clickedStop:
|
||||
await ctx.send(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["intro_stopped_author"],
|
||||
description=CONST.STRINGS["intro_stopped"],
|
||||
footer_text=CONST.STRINGS["intro_service_name"],
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
if view.clickedStart:
|
||||
|
||||
def check(message: discord.Message) -> bool:
|
||||
return message.author == ctx.author and isinstance(
|
||||
message.channel,
|
||||
discord.DMChannel,
|
||||
)
|
||||
|
||||
answer_mapping: Dict[str, str] = {}
|
||||
|
||||
for key, question in question_mapping.items():
|
||||
await ctx.send(
|
||||
embed=EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
author_text=key,
|
||||
description=question,
|
||||
footer_text=CONST.STRINGS["intro_question_footer"],
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
answer: discord.Message = await self.client.wait_for(
|
||||
"message",
|
||||
check=check,
|
||||
timeout=300,
|
||||
)
|
||||
answer_content: str = answer.content.replace("\n", " ")
|
||||
|
||||
if len(answer_content) > 200:
|
||||
await ctx.send(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["intro_too_long_author"],
|
||||
description=CONST.STRINGS["intro_too_long"],
|
||||
footer_text=CONST.STRINGS["intro_service_name"],
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
answer_mapping[key] = answer_content
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["intro_timeout_author"],
|
||||
description=CONST.STRINGS["intro_timeout"],
|
||||
footer_text=CONST.STRINGS["intro_service_name"],
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
description: str = "".join(
|
||||
CONST.STRINGS["intro_preview_field"].format(key, value)
|
||||
for key, value in answer_mapping.items()
|
||||
)
|
||||
|
||||
preview: discord.Embed = EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
author_text=ctx.author.name,
|
||||
author_icon_url=ctx.author.display_avatar.url,
|
||||
description=description,
|
||||
footer_text=CONST.STRINGS["intro_content_footer"],
|
||||
)
|
||||
view = IntroductionFinishButtons(ctx)
|
||||
|
||||
await ctx.send(embed=preview, view=view)
|
||||
await view.wait()
|
||||
|
||||
if view.clickedConfirm:
|
||||
await channel.send(
|
||||
embed=preview,
|
||||
content=CONST.STRINGS["intro_content"].format(ctx.author.mention),
|
||||
)
|
||||
await ctx.send(
|
||||
embed=EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
description=CONST.STRINGS["intro_post_confirmation"].format(
|
||||
channel.mention,
|
||||
),
|
||||
),
|
||||
)
|
||||
else:
|
||||
await ctx.send(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["intro_stopped_author"],
|
||||
description=CONST.STRINGS["intro_stopped"],
|
||||
footer_text=CONST.STRINGS["intro_service_name"],
|
||||
),
|
||||
)
|
|
@ -1,27 +0,0 @@
|
|||
from discord import ButtonStyle
|
||||
from discord.ext import bridge
|
||||
from discord.ui import Button, View
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
|
||||
|
||||
async def cmd(ctx: bridge.BridgeContext) -> None:
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
description=CONST.STRINGS["invite_description"],
|
||||
),
|
||||
view=InviteButton(),
|
||||
)
|
||||
|
||||
|
||||
class InviteButton(View):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(timeout=None)
|
||||
invite_button: Button = Button(
|
||||
label=CONST.STRINGS["invite_button_text"],
|
||||
style=ButtonStyle.url,
|
||||
url=CONST.INVITE_LINK,
|
||||
)
|
||||
self.add_item(invite_button)
|
|
@ -1,32 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
|
||||
|
||||
async def ping(self, ctx: bridge.BridgeContext) -> None:
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["ping_author"],
|
||||
description=CONST.STRINGS["ping_pong"],
|
||||
footer_text=CONST.STRINGS["ping_footer"].format(
|
||||
round(1000 * self.client.latency),
|
||||
),
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def uptime(self, ctx: bridge.BridgeContext, start_time: datetime) -> None:
|
||||
unix_timestamp: int = int(round(self.start_time.timestamp()))
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["ping_author"],
|
||||
description=CONST.STRINGS["ping_uptime"].format(unix_timestamp),
|
||||
footer_text=CONST.STRINGS["ping_footer"].format(
|
||||
round(1000 * self.client.latency),
|
||||
),
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,47 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.xkcd_service import Client, HttpError
|
||||
|
||||
_xkcd = Client()
|
||||
|
||||
|
||||
async def print_comic(
|
||||
ctx: bridge.Context,
|
||||
latest: bool = False,
|
||||
number: Optional[int] = None,
|
||||
) -> None:
|
||||
try:
|
||||
if latest:
|
||||
comic = _xkcd.get_latest_comic(raw_comic_image=True)
|
||||
elif number is not None:
|
||||
comic = _xkcd.get_comic(number, raw_comic_image=True)
|
||||
else:
|
||||
comic = _xkcd.get_random_comic(raw_comic_image=True)
|
||||
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["xkcd_title"].format(comic.id, comic.title),
|
||||
description=CONST.STRINGS["xkcd_description"].format(
|
||||
comic.explanation_url,
|
||||
comic.comic_url,
|
||||
),
|
||||
footer_text=CONST.STRINGS["xkcd_footer"],
|
||||
image_url=comic.image_url,
|
||||
show_name=False,
|
||||
),
|
||||
)
|
||||
|
||||
except HttpError:
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["xkcd_not_found_author"],
|
||||
description=CONST.STRINGS["xkcd_not_found"],
|
||||
footer_text=CONST.STRINGS["xkcd_footer"],
|
||||
),
|
||||
)
|
|
@ -1,195 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.moderation import ban, cases, kick, softban, timeout, warn
|
||||
|
||||
|
||||
class Moderation(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="ban",
|
||||
aliases=["b"],
|
||||
description="Ban a user from the server.",
|
||||
help="Bans a user from the server, you can use ID or mention them.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@guild_only()
|
||||
async def ban_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.User,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await ban.ban_user(self, ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="case",
|
||||
aliases=["c"],
|
||||
description="View a case by its number.",
|
||||
help="Views a case by its number in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def case_command(self, ctx, case_number: int):
|
||||
await cases.view_case_by_number(ctx, ctx.guild.id, case_number)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="cases",
|
||||
aliases=["caselist"],
|
||||
description="View all cases in the server.",
|
||||
help="Lists all moderation cases for the current server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def cases_command(self, ctx):
|
||||
await cases.view_all_cases_in_guild(ctx, ctx.guild.id)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="editcase",
|
||||
aliases=["uc", "ec"],
|
||||
description="Edit the reason for a case.",
|
||||
help="Updates the reason for a specific case in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def edit_case_command(self, ctx, case_number: int, *, new_reason: str):
|
||||
await cases.edit_case_reason(ctx, ctx.guild.id, case_number, new_reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="kick",
|
||||
aliases=["k"],
|
||||
description="Kick a user from the server.",
|
||||
help="Kicks a user from the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(kick_members=True)
|
||||
@commands.bot_has_permissions(kick_members=True)
|
||||
@guild_only()
|
||||
async def kick_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await kick.kick_user(self, ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="modcases",
|
||||
aliases=["moderatorcases", "mc"],
|
||||
description="View all cases by a specific moderator.",
|
||||
help="Lists all moderation cases handled by a specific moderator in the current server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def moderator_cases_command(self, ctx, moderator: discord.Member):
|
||||
await cases.view_all_cases_by_mod(ctx, ctx.guild.id, moderator)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="softban",
|
||||
aliases=["sb"],
|
||||
description="Softban a user from the server.",
|
||||
help="Softbans a user from the server (ban and immediately unban to delete messages).",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@guild_only()
|
||||
async def softban_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await softban.softban_user(ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="timeout",
|
||||
aliases=["t", "to"],
|
||||
description="Timeout a user.",
|
||||
help="Timeouts a user in the server for a specified duration.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(moderate_members=True)
|
||||
@commands.bot_has_permissions(moderate_members=True)
|
||||
@guild_only()
|
||||
async def timeout_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
duration: str,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await timeout.timeout_user(self, ctx, target, duration, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="unban",
|
||||
aliases=["ub", "pardon"],
|
||||
description="Unbans a user from the server.",
|
||||
help="Unbans a user from the server, you can use ID or provide their username.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@guild_only()
|
||||
async def unban_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.User,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await ban.unban_user(ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="untimeout",
|
||||
aliases=["removetimeout", "rto", "uto"],
|
||||
description="Remove timeout from a user.",
|
||||
help="Removes the timeout from a user in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(moderate_members=True)
|
||||
@commands.bot_has_permissions(moderate_members=True)
|
||||
@guild_only()
|
||||
async def untimeout_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await timeout.untimeout_user(ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="warn",
|
||||
aliases=["w"],
|
||||
description="Warn a user.",
|
||||
help="Warns a user in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(kick_members=True)
|
||||
@guild_only()
|
||||
async def warn_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await warn.warn_user(ctx, target, reason)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Moderation(client))
|
|
@ -1,114 +0,0 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import MemberConverter
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from modules.moderation.utils.actionable import async_actionable
|
||||
from modules.moderation.utils.case_handler import create_case
|
||||
|
||||
|
||||
async def ban_user(cog, ctx, target: discord.User, reason: Optional[str] = None):
|
||||
# see if user is in guild
|
||||
member = await MemberConverter().convert(ctx, str(target.id))
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
# member -> user is in the guild, check role hierarchy
|
||||
if member:
|
||||
bot_member = await MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(member, ctx.author, bot_member)
|
||||
|
||||
try:
|
||||
await member.send(
|
||||
embed=EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_banned_author"],
|
||||
description=CONST.STRINGS["mod_ban_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
output_reason,
|
||||
),
|
||||
show_name=False,
|
||||
),
|
||||
)
|
||||
dm_sent = True
|
||||
|
||||
except (discord.HTTPException, discord.Forbidden):
|
||||
dm_sent = False
|
||||
|
||||
await member.ban(
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
delete_message_seconds=86400,
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_banned_author"],
|
||||
description=CONST.STRINGS["mod_banned_user"].format(target.id),
|
||||
footer_text=CONST.STRINGS["mod_dm_sent"]
|
||||
if dm_sent
|
||||
else CONST.STRINGS["mod_dm_not_sent"],
|
||||
),
|
||||
)
|
||||
create_case_task = create_case(ctx, target, "BAN", reason)
|
||||
await asyncio.gather(respond_task, create_case_task, return_exceptions=True)
|
||||
|
||||
# not a member in this guild, so ban right away
|
||||
else:
|
||||
await ctx.guild.ban(
|
||||
target,
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_banned_author"],
|
||||
description=CONST.STRINGS["mod_banned_user"].format(target.id),
|
||||
),
|
||||
)
|
||||
create_case_task = create_case(ctx, target, "BAN", reason)
|
||||
await asyncio.gather(respond_task, create_case_task)
|
||||
|
||||
|
||||
async def unban_user(ctx, target: discord.User, reason: Optional[str] = None):
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
try:
|
||||
await ctx.guild.unban(
|
||||
target,
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_unbanned_author"],
|
||||
description=CONST.STRINGS["mod_unbanned"].format(target.id),
|
||||
),
|
||||
)
|
||||
create_case_task = create_case(ctx, target, "UNBAN", reason)
|
||||
await asyncio.gather(respond_task, create_case_task)
|
||||
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
return await ctx.respond(
|
||||
embed=EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_not_banned_author"],
|
||||
description=CONST.STRINGS["mod_not_banned"].format(target.id),
|
||||
),
|
||||
)
|
|
@ -1,115 +0,0 @@
|
|||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import pages
|
||||
from discord.ext.commands import UserConverter
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.formatter import format_case_number
|
||||
from modules.moderation.utils.case_embed import (
|
||||
create_case_embed,
|
||||
create_case_list_embed,
|
||||
)
|
||||
from modules.moderation.utils.case_handler import edit_case_modlog
|
||||
from services.moderation.case_service import CaseService
|
||||
|
||||
case_service = CaseService()
|
||||
|
||||
|
||||
async def view_case_by_number(ctx, guild_id: int, case_number: int):
|
||||
case = case_service.fetch_case_by_guild_and_number(guild_id, case_number)
|
||||
|
||||
if not case:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["error_no_case_found_author"],
|
||||
description=CONST.STRINGS["error_no_case_found_description"],
|
||||
)
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
target = await UserConverter().convert(ctx, str(case["target_id"]))
|
||||
embed: discord.Embed = create_case_embed(
|
||||
ctx=ctx,
|
||||
target=target,
|
||||
case_number=case["case_number"],
|
||||
action_type=case["action_type"],
|
||||
reason=case["reason"],
|
||||
timestamp=case["created_at"],
|
||||
duration=case["duration"] or None,
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def view_all_cases_in_guild(ctx, guild_id: int):
|
||||
cases = case_service.fetch_cases_by_guild(guild_id)
|
||||
|
||||
if not cases:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["case_guild_no_cases_author"],
|
||||
description=CONST.STRINGS["case_guild_no_cases"],
|
||||
)
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
pages_list = []
|
||||
for i in range(0, len(cases), 10):
|
||||
chunk = cases[i : i + 10]
|
||||
embed = create_case_list_embed(
|
||||
ctx,
|
||||
chunk,
|
||||
CONST.STRINGS["case_guild_cases_author"],
|
||||
)
|
||||
pages_list.append(embed)
|
||||
|
||||
paginator = pages.Paginator(pages=pages_list)
|
||||
await paginator.respond(ctx)
|
||||
|
||||
|
||||
async def view_all_cases_by_mod(ctx, guild_id: int, moderator: discord.Member):
|
||||
cases = case_service.fetch_cases_by_moderator(guild_id, moderator.id)
|
||||
|
||||
if not cases:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["case_mod_no_cases_author"],
|
||||
description=CONST.STRINGS["case_mod_no_cases"],
|
||||
)
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
pages_list = []
|
||||
for i in range(0, len(cases), 10):
|
||||
chunk = cases[i : i + 10]
|
||||
embed = create_case_list_embed(
|
||||
ctx,
|
||||
chunk,
|
||||
CONST.STRINGS["case_mod_cases_author"].format(moderator.name),
|
||||
)
|
||||
pages_list.append(embed)
|
||||
|
||||
paginator = pages.Paginator(pages=pages_list)
|
||||
await paginator.respond(ctx)
|
||||
|
||||
|
||||
async def edit_case_reason(ctx, guild_id: int, case_number: int, new_reason: str):
|
||||
case_service.edit_case_reason(
|
||||
guild_id,
|
||||
case_number,
|
||||
new_reason,
|
||||
)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["case_reason_update_author"],
|
||||
description=CONST.STRINGS["case_reason_update_description"].format(
|
||||
format_case_number(case_number),
|
||||
),
|
||||
)
|
||||
|
||||
async def update_tasks():
|
||||
await asyncio.gather(
|
||||
ctx.respond(embed=embed),
|
||||
edit_case_modlog(ctx, guild_id, case_number, new_reason),
|
||||
)
|
||||
|
||||
await update_tasks()
|
|
@ -1,58 +0,0 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import UserConverter, MemberConverter
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from modules.moderation.utils.actionable import async_actionable
|
||||
from modules.moderation.utils.case_handler import create_case
|
||||
|
||||
|
||||
async def kick_user(cog, ctx, target: discord.Member, reason: Optional[str] = None):
|
||||
bot_member = await MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, ctx.author, bot_member)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
try:
|
||||
await target.send(
|
||||
embed=EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_kicked_author"],
|
||||
description=CONST.STRINGS["mod_kick_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
output_reason,
|
||||
),
|
||||
show_name=False,
|
||||
),
|
||||
)
|
||||
dm_sent = True
|
||||
|
||||
except (discord.HTTPException, discord.Forbidden):
|
||||
dm_sent = False
|
||||
|
||||
await target.kick(
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_kicked_author"],
|
||||
description=CONST.STRINGS["mod_kicked_user"].format(target.name),
|
||||
footer_text=CONST.STRINGS["mod_dm_sent"]
|
||||
if dm_sent
|
||||
else CONST.STRINGS["mod_dm_not_sent"],
|
||||
),
|
||||
)
|
||||
|
||||
target_user = await UserConverter().convert(ctx, str(target.id))
|
||||
create_case_task = create_case(ctx, target_user, "KICK", reason)
|
||||
await asyncio.gather(respond_task, create_case_task, return_exceptions=True)
|
|
@ -1,66 +0,0 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import MemberConverter, UserConverter
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from modules.moderation.utils.actionable import async_actionable
|
||||
from modules.moderation.utils.case_handler import create_case
|
||||
|
||||
|
||||
async def softban_user(ctx, target: discord.Member, reason: Optional[str] = None):
|
||||
bot_member = await MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, ctx.author, bot_member)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
try:
|
||||
await target.send(
|
||||
embed=EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_softbanned_author"],
|
||||
description=CONST.STRINGS["mod_softban_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
output_reason,
|
||||
),
|
||||
show_name=False,
|
||||
),
|
||||
)
|
||||
dm_sent = True
|
||||
except (discord.HTTPException, discord.Forbidden):
|
||||
dm_sent = False
|
||||
|
||||
await ctx.guild.ban(
|
||||
target,
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
delete_message_seconds=86400,
|
||||
)
|
||||
|
||||
await ctx.guild.unban(
|
||||
target,
|
||||
reason=CONST.STRINGS["mod_softban_unban_reason"].format(
|
||||
ctx.author.name,
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_softbanned_author"],
|
||||
description=CONST.STRINGS["mod_softbanned_user"].format(target.name),
|
||||
footer_text=CONST.STRINGS["mod_dm_sent"]
|
||||
if dm_sent
|
||||
else CONST.STRINGS["mod_dm_not_sent"],
|
||||
),
|
||||
)
|
||||
|
||||
target_user = await UserConverter().convert(ctx, str(target.id))
|
||||
create_case_task = create_case(ctx, target_user, "SOFTBAN", reason)
|
||||
await asyncio.gather(respond_task, create_case_task, return_exceptions=True)
|
|
@ -1,103 +0,0 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import UserConverter, MemberConverter
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.formatter import format_duration_to_seconds, format_seconds_to_duration_string
|
||||
from modules.moderation.utils.actionable import async_actionable
|
||||
from modules.moderation.utils.case_handler import create_case
|
||||
|
||||
|
||||
async def timeout_user(
|
||||
cog,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
duration: str,
|
||||
reason: Optional[str] = None,
|
||||
):
|
||||
bot_member = await MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, ctx.author, bot_member)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
# Parse duration to minutes and validate
|
||||
duration_int = format_duration_to_seconds(duration)
|
||||
duration_str = format_seconds_to_duration_string(duration_int)
|
||||
|
||||
await target.timeout_for(
|
||||
duration=datetime.timedelta(seconds=duration_int),
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
|
||||
dm_task = target.send(
|
||||
embed=EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_timed_out_author"],
|
||||
description=CONST.STRINGS["mod_timeout_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
duration_str,
|
||||
output_reason,
|
||||
),
|
||||
show_name=False,
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_timed_out_author"],
|
||||
description=CONST.STRINGS["mod_timed_out_user"].format(target.name),
|
||||
),
|
||||
)
|
||||
|
||||
target_user = await UserConverter().convert(ctx, str(target.id))
|
||||
create_case_task = create_case(ctx, target_user, "TIMEOUT", reason, duration_int)
|
||||
|
||||
await asyncio.gather(
|
||||
dm_task,
|
||||
respond_task,
|
||||
create_case_task,
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
|
||||
async def untimeout_user(ctx, target: discord.Member, reason: Optional[str] = None):
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
try:
|
||||
await target.remove_timeout(
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_untimed_out_author"],
|
||||
description=CONST.STRINGS["mod_untimed_out"].format(target.name),
|
||||
),
|
||||
)
|
||||
|
||||
target_user = await UserConverter().convert(ctx, str(target.id))
|
||||
create_case_task = create_case(ctx, target_user, "UNTIMEOUT", reason)
|
||||
await asyncio.gather(respond_task, create_case_task, return_exceptions=True)
|
||||
|
||||
except discord.HTTPException:
|
||||
return await ctx.respond(
|
||||
embed=EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_not_timed_out_author"],
|
||||
description=CONST.STRINGS["mod_not_timed_out"].format(target.name),
|
||||
),
|
||||
)
|
|
@ -1,30 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
|
||||
|
||||
async def async_actionable(
|
||||
target: discord.Member,
|
||||
invoker: discord.Member,
|
||||
bot_user: discord.Member,
|
||||
) -> None:
|
||||
"""
|
||||
Checks if the invoker and client have a higher role than the target user.
|
||||
|
||||
Args:
|
||||
target: The member object of the target user.
|
||||
invoker: The member object of the user who invoked the command.
|
||||
bot_user: The discord.Bot.user object representing the bot itself.
|
||||
|
||||
Returns:
|
||||
True if the client's highest role AND the invoker's highest role are higher than the target.
|
||||
"""
|
||||
if target == invoker:
|
||||
raise LumiException(CONST.STRINGS["error_actionable_self"])
|
||||
|
||||
if target.top_role >= invoker.top_role and invoker != invoker.guild.owner:
|
||||
raise LumiException(CONST.STRINGS["error_actionable_hierarchy_user"])
|
||||
|
||||
if target.top_role >= bot_user.top_role:
|
||||
raise LumiException(CONST.STRINGS["error_actionable_hierarchy_bot"])
|
|
@ -1,102 +0,0 @@
|
|||
import datetime
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.formatter import format_case_number
|
||||
from lib.formatter import format_seconds_to_duration_string
|
||||
|
||||
|
||||
def create_case_embed(
|
||||
ctx,
|
||||
target: discord.User,
|
||||
case_number: int,
|
||||
action_type: str,
|
||||
reason: Optional[str],
|
||||
timestamp: Optional[datetime.datetime] = None,
|
||||
duration: Optional[int] = None,
|
||||
) -> discord.Embed:
|
||||
embed = EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["case_new_case_author"],
|
||||
thumbnail_url=target.display_avatar.url,
|
||||
show_name=False,
|
||||
timestamp=timestamp,
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_case_field"],
|
||||
value=CONST.STRINGS["case_case_field_value"].format(
|
||||
format_case_number(case_number),
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
|
||||
if not duration:
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_type_field"],
|
||||
value=CONST.STRINGS["case_type_field_value"].format(
|
||||
action_type.lower().capitalize(),
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_type_field"],
|
||||
value=CONST.STRINGS["case_type_field_value_with_duration"].format(
|
||||
action_type.lower().capitalize(),
|
||||
format_seconds_to_duration_string(duration),
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_moderator_field"],
|
||||
value=CONST.STRINGS["case_moderator_field_value"].format(
|
||||
ctx.author.name,
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_target_field"],
|
||||
value=CONST.STRINGS["case_target_field_value"].format(target.name),
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["case_reason_field"],
|
||||
value=CONST.STRINGS["case_reason_field_value"].format(
|
||||
reason or CONST.STRINGS["mod_no_reason"],
|
||||
),
|
||||
inline=False,
|
||||
)
|
||||
return embed
|
||||
|
||||
|
||||
def create_case_list_embed(ctx, cases: list, author_text: str) -> discord.Embed:
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=author_text,
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
for case in cases:
|
||||
status_emoji = "❌" if case.get("is_closed") else "✅"
|
||||
case_number = case.get("case_number", "N/A")
|
||||
|
||||
if isinstance(case_number, int):
|
||||
case_number = format_case_number(case_number)
|
||||
|
||||
action_type = case.get("action_type", "Unknown")
|
||||
timestamp = case.get("created_at", "Unknown")
|
||||
|
||||
if isinstance(timestamp, datetime.datetime):
|
||||
formatted_timestamp = f"<t:{int(timestamp.timestamp())}:R>"
|
||||
else:
|
||||
formatted_timestamp = str(timestamp)
|
||||
|
||||
if embed.description is None:
|
||||
embed.description = ""
|
||||
embed.description += f"{status_emoji} `{case_number}` **[{action_type}]** {formatted_timestamp}\n"
|
||||
|
||||
return embed
|
|
@ -1,148 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import TextChannelConverter, UserConverter
|
||||
from loguru import logger
|
||||
|
||||
from modules.moderation.utils.case_embed import create_case_embed
|
||||
from services.moderation.case_service import CaseService
|
||||
from services.moderation.modlog_service import ModLogService
|
||||
|
||||
case_service = CaseService()
|
||||
modlog_service = ModLogService()
|
||||
|
||||
|
||||
async def create_case(
|
||||
ctx,
|
||||
target: discord.User,
|
||||
action_type: str,
|
||||
reason: Optional[str] = None,
|
||||
duration: Optional[int] = None,
|
||||
expires_at: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Creates a new moderation case and logs it to the modlog channel if configured.
|
||||
|
||||
Args:
|
||||
ctx: The context of the command invocation.
|
||||
target (discord.User): The user who is the subject of the moderation action.
|
||||
action_type (str): The type of moderation action (e.g., "ban", "kick", "warn").
|
||||
reason (Optional[str]): The reason for the moderation action. Defaults to None.
|
||||
duration (Optional[int]): The duration of the action in seconds, if applicable. Defaults to None.
|
||||
expires_at (Optional[str]): The expiration date of the action, if applicable. Defaults to None.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Raises:
|
||||
Exception: If there's an error sending the case to the modlog channel.
|
||||
|
||||
This function performs the following steps:
|
||||
1. Creates a new case in the database using the CaseService.
|
||||
2. Logs the case creation using the logger.
|
||||
3. If a modlog channel is configured, it sends an embed with the case details to that channel.
|
||||
4. If the embed is successfully sent to the modlog channel, it updates the case with the message ID for later edits.
|
||||
"""
|
||||
guild_id = ctx.guild.id
|
||||
moderator_id = ctx.author.id
|
||||
target_id = target.id
|
||||
|
||||
# Create the case
|
||||
case_number: int = case_service.create_case(
|
||||
guild_id=guild_id,
|
||||
target_id=target_id,
|
||||
moderator_id=moderator_id,
|
||||
action_type=action_type,
|
||||
reason=reason,
|
||||
duration=duration,
|
||||
expires_at=expires_at,
|
||||
modlog_message_id=None,
|
||||
)
|
||||
|
||||
logger.info(f"Created case {case_number} for {target.name} in guild {guild_id}")
|
||||
|
||||
if mod_log_channel_id := modlog_service.fetch_modlog_channel_id(guild_id):
|
||||
try:
|
||||
mod_log_channel = await TextChannelConverter().convert(
|
||||
ctx,
|
||||
str(mod_log_channel_id),
|
||||
)
|
||||
embed: discord.Embed = create_case_embed(
|
||||
ctx=ctx,
|
||||
target=target,
|
||||
case_number=case_number,
|
||||
action_type=action_type,
|
||||
reason=reason,
|
||||
timestamp=None,
|
||||
duration=duration,
|
||||
)
|
||||
message = await mod_log_channel.send(embed=embed)
|
||||
|
||||
# Update the case with the modlog_message_id
|
||||
case_service.edit_case(
|
||||
guild_id=guild_id,
|
||||
case_number=case_number,
|
||||
changes={"modlog_message_id": message.id},
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send case to modlog channel: {e}")
|
||||
|
||||
|
||||
async def edit_case_modlog(
|
||||
ctx,
|
||||
guild_id: int,
|
||||
case_number: int,
|
||||
new_reason: str,
|
||||
) -> bool:
|
||||
"""
|
||||
Edits the reason for an existing case and updates the modlog message if it exists.
|
||||
|
||||
Args:
|
||||
ctx: The context of the command invocation.
|
||||
guild_id: The ID of the guild where the case exists.
|
||||
case_number: The number of the case to edit.
|
||||
new_reason: The new reason for the case.
|
||||
|
||||
Raises:
|
||||
ValueError: If the case is not found.
|
||||
Exception: If there's an error updating the modlog message.
|
||||
"""
|
||||
case = case_service.fetch_case_by_guild_and_number(guild_id, case_number)
|
||||
if not case:
|
||||
raise ValueError(f"Case {case_number} not found in guild {guild_id}")
|
||||
|
||||
modlog_message_id = case.get("modlog_message_id")
|
||||
if not modlog_message_id:
|
||||
return False
|
||||
|
||||
mod_log_channel_id = modlog_service.fetch_modlog_channel_id(guild_id)
|
||||
if not mod_log_channel_id:
|
||||
return False
|
||||
|
||||
try:
|
||||
mod_log_channel = await TextChannelConverter().convert(
|
||||
ctx,
|
||||
str(mod_log_channel_id),
|
||||
)
|
||||
message = await mod_log_channel.fetch_message(modlog_message_id)
|
||||
target = await UserConverter().convert(ctx, str(case["target_id"]))
|
||||
|
||||
updated_embed: discord.Embed = create_case_embed(
|
||||
ctx=ctx,
|
||||
target=target,
|
||||
case_number=case_number,
|
||||
action_type=case["action_type"],
|
||||
reason=new_reason,
|
||||
timestamp=case["created_at"],
|
||||
duration=case["duration"] or None,
|
||||
)
|
||||
|
||||
await message.edit(embed=updated_embed)
|
||||
logger.info(f"Updated case {case_number} in guild {guild_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to update modlog message for case {case_number}: {e}")
|
||||
return False
|
||||
|
||||
return True
|
|
@ -1,48 +0,0 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import UserConverter, MemberConverter
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from modules.moderation.utils.actionable import async_actionable
|
||||
from modules.moderation.utils.case_handler import create_case
|
||||
|
||||
|
||||
async def warn_user(ctx, target: discord.Member, reason: Optional[str]):
|
||||
bot_member = await MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, ctx.author, bot_member)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
dm_task = target.send(
|
||||
embed=EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_warned_author"],
|
||||
description=CONST.STRINGS["mod_warn_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
output_reason,
|
||||
),
|
||||
show_name=False,
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["mod_warned_author"],
|
||||
description=CONST.STRINGS["mod_warned_user"].format(target.name),
|
||||
),
|
||||
)
|
||||
|
||||
target_user = await UserConverter().convert(ctx, str(target.id))
|
||||
create_case_task = create_case(ctx, target_user, "WARN", reason)
|
||||
|
||||
await asyncio.gather(
|
||||
dm_task,
|
||||
respond_task,
|
||||
create_case_task,
|
||||
return_exceptions=True,
|
||||
)
|
|
@ -1,81 +0,0 @@
|
|||
import discord
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from Client import LumiBot
|
||||
from modules.triggers.add import add_reaction
|
||||
from modules.triggers.delete import delete_reaction
|
||||
from modules.triggers.list import list_reactions
|
||||
|
||||
|
||||
class Triggers(commands.Cog):
|
||||
def __init__(self, client: LumiBot):
|
||||
self.client = client
|
||||
|
||||
trigger = SlashCommandGroup(
|
||||
"trigger",
|
||||
"Manage custom reactions.",
|
||||
default_member_permissions=discord.Permissions(manage_guild=True),
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
add = trigger.create_subgroup("add", "Add new custom reactions.")
|
||||
|
||||
@add.command(
|
||||
name="response",
|
||||
description="Add a new custom text reaction.",
|
||||
help="Add a new custom text reaction to the database.",
|
||||
)
|
||||
@guild_only()
|
||||
async def add_text_reaction_command(
|
||||
self,
|
||||
ctx,
|
||||
trigger_text: str,
|
||||
response: str,
|
||||
is_full_match: bool,
|
||||
):
|
||||
await add_reaction(ctx, trigger_text, response, None, False, is_full_match)
|
||||
|
||||
@add.command(
|
||||
name="emoji",
|
||||
description="Add a new custom emoji reaction.",
|
||||
help="Add a new custom emoji reaction to the database.",
|
||||
)
|
||||
@guild_only()
|
||||
async def add_emoji_reaction_command(
|
||||
self,
|
||||
ctx,
|
||||
trigger_text: str,
|
||||
emoji: discord.Emoji,
|
||||
is_full_match: bool,
|
||||
):
|
||||
await add_reaction(ctx, trigger_text, None, emoji.id, True, is_full_match)
|
||||
|
||||
@trigger.command(
|
||||
name="delete",
|
||||
description="Delete an existing custom reaction.",
|
||||
help="Delete an existing custom reaction from the database.",
|
||||
)
|
||||
@guild_only()
|
||||
async def delete_reaction_command(
|
||||
self,
|
||||
ctx,
|
||||
reaction_id: int,
|
||||
):
|
||||
await delete_reaction(ctx, reaction_id)
|
||||
|
||||
@trigger.command(
|
||||
name="list",
|
||||
description="List all custom reactions.",
|
||||
help="List all custom reactions for the current guild.",
|
||||
)
|
||||
@guild_only()
|
||||
async def list_reactions_command(
|
||||
self,
|
||||
ctx,
|
||||
):
|
||||
await list_reactions(ctx)
|
||||
|
||||
|
||||
def setup(client: LumiBot):
|
||||
client.add_cog(Triggers(client))
|
|
@ -1,107 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
||||
async def add_reaction(
|
||||
ctx: bridge.Context,
|
||||
trigger_text: str,
|
||||
response: Optional[str],
|
||||
emoji_id: Optional[int],
|
||||
is_emoji: bool,
|
||||
is_full_match: bool,
|
||||
) -> None:
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
reaction_service = CustomReactionsService()
|
||||
guild_id: int = ctx.guild.id
|
||||
creator_id: int = ctx.author.id
|
||||
|
||||
if not await check_reaction_limit(
|
||||
reaction_service,
|
||||
guild_id,
|
||||
):
|
||||
return
|
||||
|
||||
if not await check_existing_trigger(
|
||||
reaction_service,
|
||||
guild_id,
|
||||
trigger_text,
|
||||
):
|
||||
return
|
||||
|
||||
success: bool = await reaction_service.create_custom_reaction(
|
||||
guild_id=guild_id,
|
||||
creator_id=creator_id,
|
||||
trigger_text=trigger_text,
|
||||
response=response,
|
||||
emoji_id=emoji_id,
|
||||
is_emoji=is_emoji,
|
||||
is_full_match=is_full_match,
|
||||
is_global=False,
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise LumiException(CONST.STRINGS["triggers_not_added"])
|
||||
|
||||
trigger_text = formatter.shorten(trigger_text, 50)
|
||||
|
||||
if response:
|
||||
response = formatter.shorten(response, 50)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["triggers_add_author"],
|
||||
description="",
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
embed.description += CONST.STRINGS["triggers_add_description"].format(
|
||||
trigger_text,
|
||||
CONST.STRINGS["triggers_type_emoji"]
|
||||
if is_emoji
|
||||
else CONST.STRINGS["triggers_type_text"],
|
||||
is_full_match,
|
||||
)
|
||||
|
||||
if is_emoji:
|
||||
embed.description += CONST.STRINGS["triggers_add_emoji_details"].format(
|
||||
emoji_id,
|
||||
)
|
||||
else:
|
||||
embed.description += CONST.STRINGS["triggers_add_text_details"].format(response)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def check_reaction_limit(
|
||||
reaction_service: CustomReactionsService,
|
||||
guild_id: int,
|
||||
) -> bool:
|
||||
limit_reached = await reaction_service.count_custom_reactions(guild_id) >= 100
|
||||
|
||||
if limit_reached:
|
||||
raise LumiException(CONST.STRINGS["trigger_limit_reached"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def check_existing_trigger(
|
||||
reaction_service: CustomReactionsService,
|
||||
guild_id: int,
|
||||
trigger_text: str,
|
||||
) -> bool:
|
||||
existing_trigger = await reaction_service.find_trigger(guild_id, trigger_text)
|
||||
|
||||
if existing_trigger:
|
||||
raise LumiException(CONST.STRINGS["trigger_already_exists"])
|
||||
|
||||
return True
|
|
@ -1,29 +0,0 @@
|
|||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
||||
async def delete_reaction(ctx: bridge.Context, reaction_id: int) -> None:
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
reaction_service = CustomReactionsService()
|
||||
guild_id: int = ctx.guild.id
|
||||
reaction = await reaction_service.find_id(reaction_id)
|
||||
|
||||
if reaction is None or reaction["guild_id"] != guild_id or reaction["is_global"]:
|
||||
raise LumiException(CONST.STRINGS["triggers_not_found"])
|
||||
|
||||
await reaction_service.delete_custom_reaction(reaction_id)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["triggers_delete_author"],
|
||||
description=CONST.STRINGS["triggers_delete_description"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,80 +0,0 @@
|
|||
from typing import Any, Dict, List
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge, pages
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
||||
async def list_reactions(ctx: bridge.Context) -> None:
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
reaction_service: CustomReactionsService = CustomReactionsService()
|
||||
guild_id: int = ctx.guild.id
|
||||
|
||||
reactions: List[Dict[str, Any]] = await reaction_service.find_all_by_guild(guild_id)
|
||||
if not reactions:
|
||||
embed: discord.Embed = EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["triggers_no_reactions_title"],
|
||||
description=CONST.STRINGS["triggers_no_reactions_description"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
show_name=False,
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
return
|
||||
|
||||
pages_list = []
|
||||
for reaction in reactions:
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
title=CONST.STRINGS["triggers_list_custom_reaction_id"].format(
|
||||
reaction["id"],
|
||||
),
|
||||
author_text=CONST.STRINGS["triggers_list_custom_reactions_title"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
description_lines = [
|
||||
CONST.STRINGS["triggers_list_trigger_text"].format(
|
||||
formatter.shorten(reaction["trigger_text"], 50),
|
||||
),
|
||||
CONST.STRINGS["triggers_list_reaction_type"].format(
|
||||
CONST.STRINGS["triggers_type_emoji"]
|
||||
if reaction["is_emoji"]
|
||||
else CONST.STRINGS["triggers_type_text"],
|
||||
),
|
||||
]
|
||||
|
||||
if reaction["is_emoji"]:
|
||||
description_lines.append(
|
||||
CONST.STRINGS["triggers_list_emoji_id"].format(reaction["emoji_id"]),
|
||||
)
|
||||
else:
|
||||
description_lines.append(
|
||||
CONST.STRINGS["triggers_list_response"].format(
|
||||
formatter.shorten(reaction["response"], 50),
|
||||
),
|
||||
)
|
||||
|
||||
description_lines.extend(
|
||||
[
|
||||
CONST.STRINGS["triggers_list_full_match"].format(
|
||||
"True" if reaction["is_full_match"] else "False",
|
||||
),
|
||||
CONST.STRINGS["triggers_list_usage_count"].format(
|
||||
reaction["usage_count"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
embed.description = "\n".join(description_lines)
|
||||
pages_list.append(embed)
|
||||
|
||||
paginator: pages.Paginator = pages.Paginator(pages=pages_list, timeout=180.0)
|
||||
await paginator.respond(ctx, ephemeral=False)
|
1142
poetry.lock
generated
1142
poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,27 +0,0 @@
|
|||
[tool.poetry]
|
||||
authors = ["wlinator <dokimakimaki@gmail.com>"]
|
||||
description = "A Discord application, can serve as a template for your own bot."
|
||||
license = "GNU General Public License v3.0"
|
||||
name = "lumi"
|
||||
package-mode = false
|
||||
readme = "README.md"
|
||||
version = "0.1.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
dropbox = "^12.0.2"
|
||||
httpx = "^0.27.0"
|
||||
loguru = "^0.7.2"
|
||||
mysql-connector-python = "^9.0.0"
|
||||
pre-commit = "^3.7.1"
|
||||
psutil = "^6.0.0"
|
||||
py-cord = "^2.5.0"
|
||||
pyright = "^1.1.371"
|
||||
python = "^3.12"
|
||||
python-dotenv = "^1.0.1"
|
||||
pytimeparse = "^1.1.8"
|
||||
pytz = "^2024.1"
|
||||
ruff = "^0.5.2"
|
||||
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["poetry-core"]
|
|
@ -1,54 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
class Birthday:
|
||||
def __init__(self, user_id, guild_id):
|
||||
self.user_id = user_id
|
||||
self.guild_id = guild_id
|
||||
|
||||
def set(self, birthday):
|
||||
query = """
|
||||
INSERT INTO birthdays (user_id, guild_id, birthday)
|
||||
VALUES (%s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE birthday = VALUES(birthday);
|
||||
"""
|
||||
|
||||
database.execute_query(query, (self.user_id, self.guild_id, birthday))
|
||||
|
||||
def delete(self):
|
||||
query = """
|
||||
DELETE FROM birthdays
|
||||
WHERE user_id = %s AND guild_id = %s;
|
||||
"""
|
||||
|
||||
database.execute_query(query, (self.user_id, self.guild_id))
|
||||
|
||||
@staticmethod
|
||||
def get_birthdays_today():
|
||||
query = """
|
||||
SELECT user_id, guild_id
|
||||
FROM birthdays
|
||||
WHERE DATE_FORMAT(birthday, '%m-%d') = %s
|
||||
"""
|
||||
|
||||
tz = pytz.timezone("US/Eastern")
|
||||
today = datetime.datetime.now(tz).strftime("%m-%d")
|
||||
|
||||
return database.select_query(query, (today,))
|
||||
|
||||
@staticmethod
|
||||
def get_upcoming_birthdays(guild_id):
|
||||
query = """
|
||||
SELECT user_id, DATE_FORMAT(birthday, '%m-%d') AS upcoming_birthday
|
||||
FROM birthdays
|
||||
WHERE guild_id = %s
|
||||
ORDER BY (DAYOFYEAR(birthday) - DAYOFYEAR(now()) + 366) % 366;
|
||||
"""
|
||||
|
||||
data = database.select_query(query, (guild_id,))
|
||||
|
||||
return [(row[0], row[1]) for row in data]
|
|
@ -1,41 +0,0 @@
|
|||
from typing import List, Optional, Tuple
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
class BlacklistUserService:
|
||||
def __init__(self, user_id: int) -> None:
|
||||
self.user_id: int = user_id
|
||||
|
||||
def add_to_blacklist(self, reason: Optional[str] = None) -> None:
|
||||
"""
|
||||
Adds a user to the blacklist with the given reason.
|
||||
|
||||
Args:
|
||||
reason (str): The reason for blacklisting the user.
|
||||
"""
|
||||
query: str = """
|
||||
INSERT INTO blacklist_user (user_id, reason)
|
||||
VALUES (%s, %s)
|
||||
ON DUPLICATE KEY UPDATE reason = VALUES(reason)
|
||||
"""
|
||||
database.execute_query(query, (self.user_id, reason))
|
||||
|
||||
@staticmethod
|
||||
def is_user_blacklisted(user_id: int) -> bool:
|
||||
"""
|
||||
Checks if a user is currently blacklisted.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the user is blacklisted, False otherwise.
|
||||
"""
|
||||
query: str = """
|
||||
SELECT active
|
||||
FROM blacklist_user
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
result: List[Tuple[bool]] = database.select_query(query, (user_id,))
|
||||
return any(active for (active,) in result)
|
|
@ -1,144 +0,0 @@
|
|||
from db import database
|
||||
|
||||
|
||||
class GuildConfig:
|
||||
def __init__(self, guild_id):
|
||||
self.guild_id = guild_id
|
||||
self.birthday_channel_id = None
|
||||
self.command_channel_id = None
|
||||
self.intro_channel_id = None
|
||||
self.welcome_channel_id = None
|
||||
self.welcome_message = None
|
||||
self.boost_channel_id = None
|
||||
self.boost_message = None
|
||||
self.boost_image_url = None
|
||||
self.level_channel_id = None
|
||||
self.level_message = None
|
||||
self.level_message_type = 1
|
||||
|
||||
self.fetch_or_create_config()
|
||||
|
||||
def fetch_or_create_config(self):
|
||||
"""
|
||||
Gets a Guild Config from the database or inserts a new row if it doesn't exist yet.
|
||||
"""
|
||||
query = """
|
||||
SELECT birthday_channel_id, command_channel_id, intro_channel_id,
|
||||
welcome_channel_id, welcome_message, boost_channel_id,
|
||||
boost_message, boost_image_url, level_channel_id,
|
||||
level_message, level_message_type
|
||||
FROM guild_config WHERE guild_id = %s
|
||||
"""
|
||||
|
||||
try:
|
||||
self._extracted_from_fetch_or_create_config_14(query)
|
||||
except (IndexError, TypeError):
|
||||
# No record found for the specified guild_id
|
||||
query = "INSERT INTO guild_config (guild_id) VALUES (%s)"
|
||||
database.execute_query(query, (self.guild_id,))
|
||||
|
||||
# TODO Rename this here and in `fetch_or_create_config`
|
||||
def _extracted_from_fetch_or_create_config_14(self, query):
|
||||
(
|
||||
birthday_channel_id,
|
||||
command_channel_id,
|
||||
intro_channel_id,
|
||||
welcome_channel_id,
|
||||
welcome_message,
|
||||
boost_channel_id,
|
||||
boost_message,
|
||||
boost_image_url,
|
||||
level_channel_id,
|
||||
level_message,
|
||||
level_message_type,
|
||||
) = database.select_query(query, (self.guild_id,))[0]
|
||||
|
||||
self.birthday_channel_id = birthday_channel_id
|
||||
self.command_channel_id = command_channel_id
|
||||
self.intro_channel_id = intro_channel_id
|
||||
self.welcome_channel_id = welcome_channel_id
|
||||
self.welcome_message = welcome_message
|
||||
self.boost_channel_id = boost_channel_id
|
||||
self.boost_message = boost_message
|
||||
self.boost_image_url = boost_image_url
|
||||
self.level_channel_id = level_channel_id
|
||||
self.level_message = level_message
|
||||
self.level_message_type = level_message_type
|
||||
|
||||
def push(self):
|
||||
query = """
|
||||
UPDATE guild_config
|
||||
SET
|
||||
birthday_channel_id = %s,
|
||||
command_channel_id = %s,
|
||||
intro_channel_id = %s,
|
||||
welcome_channel_id = %s,
|
||||
welcome_message = %s,
|
||||
boost_channel_id = %s,
|
||||
boost_message = %s,
|
||||
boost_image_url = %s,
|
||||
level_channel_id = %s,
|
||||
level_message = %s,
|
||||
level_message_type = %s
|
||||
WHERE guild_id = %s;
|
||||
"""
|
||||
|
||||
database.execute_query(
|
||||
query,
|
||||
(
|
||||
self.birthday_channel_id,
|
||||
self.command_channel_id,
|
||||
self.intro_channel_id,
|
||||
self.welcome_channel_id,
|
||||
self.welcome_message,
|
||||
self.boost_channel_id,
|
||||
self.boost_message,
|
||||
self.boost_image_url,
|
||||
self.level_channel_id,
|
||||
self.level_message,
|
||||
self.level_message_type,
|
||||
self.guild_id,
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_prefix(message):
|
||||
"""
|
||||
Gets the prefix from a given guild.
|
||||
This function is done as static method to make the prefix fetch process faster.
|
||||
"""
|
||||
query = """
|
||||
SELECT prefix
|
||||
FROM guild_config
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
|
||||
prefix = database.select_query_one(
|
||||
query,
|
||||
(message.guild.id if message.guild else None,),
|
||||
)
|
||||
|
||||
return prefix or "."
|
||||
|
||||
@staticmethod
|
||||
def get_prefix_from_guild_id(guild_id):
|
||||
query = """
|
||||
SELECT prefix
|
||||
FROM guild_config
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
|
||||
return database.select_query_one(query, (guild_id,)) or "."
|
||||
|
||||
@staticmethod
|
||||
def set_prefix(guild_id, prefix):
|
||||
"""
|
||||
Sets the prefix for a given guild.
|
||||
"""
|
||||
query = """
|
||||
UPDATE guild_config
|
||||
SET prefix = %s
|
||||
WHERE guild_id = %s;
|
||||
"""
|
||||
|
||||
database.execute_query(query, (prefix, guild_id))
|
|
@ -1,92 +0,0 @@
|
|||
import locale
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
class Currency:
|
||||
def __init__(self, user_id):
|
||||
self.user_id = user_id
|
||||
self.balance = Currency.fetch_or_create_balance(self.user_id)
|
||||
|
||||
def add_balance(self, amount):
|
||||
self.balance += abs(amount)
|
||||
|
||||
def take_balance(self, amount):
|
||||
self.balance -= abs(amount)
|
||||
|
||||
if self.balance < 0:
|
||||
self.balance = 0
|
||||
|
||||
def push(self):
|
||||
query = """
|
||||
UPDATE currency
|
||||
SET balance = %s
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
|
||||
database.execute_query(query, (round(self.balance), self.user_id))
|
||||
|
||||
@staticmethod
|
||||
def fetch_or_create_balance(user_id):
|
||||
query = """
|
||||
SELECT balance
|
||||
FROM currency
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
|
||||
try:
|
||||
balance = database.select_query_one(query, (user_id,))
|
||||
except (IndexError, TypeError):
|
||||
balance = None
|
||||
|
||||
# if the user doesn't have a balance yet -> create one
|
||||
# additionally if for some reason a balance becomes Null
|
||||
# re-generate the user's balance as fallback.
|
||||
if balance is None:
|
||||
query = """
|
||||
INSERT INTO currency (user_id, balance)
|
||||
VALUES (%s, 50)
|
||||
"""
|
||||
database.execute_query(query, (user_id,))
|
||||
return 50
|
||||
|
||||
return balance
|
||||
|
||||
@staticmethod
|
||||
def load_leaderboard():
|
||||
query = "SELECT user_id, balance FROM currency ORDER BY balance DESC"
|
||||
data = database.select_query(query)
|
||||
|
||||
return [(row[0], row[1], rank) for rank, row in enumerate(data, start=1)]
|
||||
|
||||
@staticmethod
|
||||
def format(num):
|
||||
locale.setlocale(locale.LC_ALL, "en_US.UTF-8")
|
||||
return locale.format_string("%d", num, grouping=True)
|
||||
|
||||
@staticmethod
|
||||
def format_human(num):
|
||||
num = float("{:.3g}".format(num))
|
||||
magnitude = 0
|
||||
while abs(num) >= 1000:
|
||||
magnitude += 1
|
||||
num /= 1000.0
|
||||
|
||||
return "{}{}".format(
|
||||
"{:f}".format(num).rstrip("0").rstrip("."),
|
||||
["", "K", "M", "B", "T", "Q", "Qi", "Sx", "Sp", "Oc", "No", "Dc"][
|
||||
magnitude
|
||||
],
|
||||
)
|
||||
|
||||
# A Thousand = K
|
||||
# Million = M
|
||||
# Billion = B
|
||||
# Trillion = T
|
||||
# Quadrillion: Q
|
||||
# Quintillion: Qi
|
||||
# Sextillion: Sx
|
||||
# Septillion: Sp
|
||||
# Octillion: Oc
|
||||
# Nonillion: No
|
||||
# Decillion: Dc
|
|
@ -1,117 +0,0 @@
|
|||
from datetime import datetime, timedelta
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import pytz
|
||||
|
||||
from db import database
|
||||
from lib.constants import CONST
|
||||
from services.currency_service import Currency
|
||||
|
||||
|
||||
class Dailies:
|
||||
def __init__(self, user_id: int) -> None:
|
||||
self.user_id: int = user_id
|
||||
self.amount: int = 0
|
||||
self.tz = pytz.timezone("US/Eastern")
|
||||
self.time_now: datetime = datetime.now(tz=self.tz)
|
||||
self.reset_time: datetime = self.time_now.replace(
|
||||
hour=7,
|
||||
minute=0,
|
||||
second=0,
|
||||
microsecond=0,
|
||||
)
|
||||
|
||||
data: Tuple[Optional[str], int] = Dailies.get_data(user_id)
|
||||
|
||||
if data[0] is not None:
|
||||
self.claimed_at: datetime = datetime.fromisoformat(data[0])
|
||||
else:
|
||||
# set date as yesterday to mock a valid claimed_at.
|
||||
self.claimed_at: datetime = datetime.now(tz=self.tz) - timedelta(days=2)
|
||||
|
||||
self.streak: int = int(data[1])
|
||||
|
||||
def refresh(self) -> None:
|
||||
if self.amount == 0:
|
||||
self.amount = CONST.DAILY_REWARD
|
||||
query: str = """
|
||||
INSERT INTO dailies (user_id, amount, claimed_at, streak)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
"""
|
||||
values: Tuple[int, int, str, int] = (
|
||||
self.user_id,
|
||||
self.amount,
|
||||
self.claimed_at.isoformat(),
|
||||
self.streak,
|
||||
)
|
||||
database.execute_query(query, values)
|
||||
|
||||
cash = Currency(self.user_id)
|
||||
cash.add_balance(self.amount)
|
||||
cash.push()
|
||||
|
||||
def can_be_claimed(self) -> bool:
|
||||
if self.claimed_at is None:
|
||||
return True
|
||||
|
||||
if self.time_now < self.reset_time:
|
||||
self.reset_time -= timedelta(days=1)
|
||||
|
||||
return self.claimed_at < self.reset_time <= self.time_now
|
||||
|
||||
def streak_check(self) -> bool:
|
||||
"""
|
||||
Three checks are performed, only one has to return True.
|
||||
1. the daily was claimed yesterday
|
||||
2. the daily was claimed the day before yesterday (users no longer lose their dailies as fast)
|
||||
3. the daily was claimed today but before the reset time (see __init__)
|
||||
:return:
|
||||
"""
|
||||
|
||||
check_1: bool = (
|
||||
self.claimed_at.date() == (self.time_now - timedelta(days=1)).date()
|
||||
)
|
||||
check_2: bool = (
|
||||
self.claimed_at.date() == (self.time_now - timedelta(days=2)).date()
|
||||
)
|
||||
check_3: bool = (
|
||||
self.claimed_at.date() == self.time_now.date()
|
||||
and self.claimed_at < self.reset_time
|
||||
)
|
||||
|
||||
return check_1 or check_2 or check_3
|
||||
|
||||
@staticmethod
|
||||
def get_data(user_id: int) -> Tuple[Optional[str], int]:
|
||||
query: str = """
|
||||
SELECT claimed_at, streak
|
||||
FROM dailies
|
||||
WHERE id = (
|
||||
SELECT MAX(id)
|
||||
FROM dailies
|
||||
WHERE user_id = %s
|
||||
)
|
||||
"""
|
||||
|
||||
try:
|
||||
(claimed_at, streak) = database.select_query(query, (user_id,))[0]
|
||||
except (IndexError, TypeError):
|
||||
(claimed_at, streak) = None, 0
|
||||
|
||||
return claimed_at, streak
|
||||
|
||||
@staticmethod
|
||||
def load_leaderboard() -> List[Tuple[int, int, str, int]]:
|
||||
query: str = """
|
||||
SELECT user_id, MAX(streak), claimed_at
|
||||
FROM dailies
|
||||
GROUP BY user_id
|
||||
ORDER BY MAX(streak) DESC;
|
||||
"""
|
||||
|
||||
data: List[Tuple[int, int, str]] = database.select_query(query)
|
||||
|
||||
leaderboard: List[Tuple[int, int, str, int]] = [
|
||||
(row[0], row[1], row[2], rank) for rank, row in enumerate(data, start=1)
|
||||
]
|
||||
return leaderboard
|
|
@ -1,128 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
|
||||
|
||||
class LumiHelp(commands.HelpCommand):
|
||||
def __init__(self, **options):
|
||||
super().__init__(**options)
|
||||
self.verify_checks = True
|
||||
self.command_attrs = {
|
||||
"aliases": ["h"],
|
||||
"help": "Show a list of commands, or information about a specific command when an argument is passed.",
|
||||
"name": "help",
|
||||
"hidden": True,
|
||||
}
|
||||
|
||||
def get_command_qualified_name(self, command):
|
||||
return f"`{self.context.clean_prefix}{command.qualified_name}`"
|
||||
|
||||
async def send_bot_help(self, mapping):
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx=self.context,
|
||||
author_text="Help Command",
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
for cog, lumi_commands in mapping.items():
|
||||
filtered = await self.filter_commands(lumi_commands, sort=True)
|
||||
|
||||
if command_signatures := [
|
||||
self.get_command_qualified_name(c) for c in filtered
|
||||
]:
|
||||
# Remove duplicates using set() and convert back to a list
|
||||
unique_command_signatures = list(set(command_signatures))
|
||||
cog_name = getattr(cog, "qualified_name", "Help")
|
||||
embed.add_field(
|
||||
name=cog_name,
|
||||
value=", ".join(sorted(unique_command_signatures)),
|
||||
inline=False,
|
||||
)
|
||||
|
||||
channel = self.get_destination()
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def send_command_help(self, command):
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx=self.context,
|
||||
author_text=f"{self.context.clean_prefix}{command.qualified_name}",
|
||||
description=command.help,
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
usage_value = (
|
||||
f"`{self.context.clean_prefix}{command.qualified_name} {command.signature}`"
|
||||
)
|
||||
for alias in command.aliases:
|
||||
usage_value += f"\n`{self.context.clean_prefix}{alias} {command.signature}`"
|
||||
embed.add_field(name="Usage", value=usage_value, inline=False)
|
||||
|
||||
channel = self.get_destination()
|
||||
await channel.send(embed=embed)
|
||||
|
||||
async def send_error_message(self, error):
|
||||
raise LumiException(error)
|
||||
|
||||
async def send_group_help(self, group):
|
||||
raise LumiException(
|
||||
CONST.STRINGS["error_command_not_found"].format(group.qualified_name),
|
||||
)
|
||||
|
||||
async def send_cog_help(self, cog):
|
||||
raise LumiException(
|
||||
CONST.STRINGS["error_command_not_found"].format(cog.qualified_name),
|
||||
)
|
||||
|
||||
async def command_callback(self, ctx, *, command=None):
|
||||
await self.prepare_help_command(ctx, command)
|
||||
bot = ctx.bot
|
||||
|
||||
if command is None:
|
||||
mapping = self.get_bot_mapping()
|
||||
return await self.send_bot_help(mapping)
|
||||
|
||||
# Check if it's a cog
|
||||
cog = bot.get_cog(command)
|
||||
if cog is not None:
|
||||
return await self.send_cog_help(cog)
|
||||
|
||||
maybe_coro = discord.utils.maybe_coroutine # type: ignore
|
||||
|
||||
# If it's not a cog then it's a command.
|
||||
# Since we want to have detailed errors when someone
|
||||
# passes an invalid subcommand, we need to walk through
|
||||
# the command group chain ourselves.
|
||||
keys = command.split(" ")
|
||||
|
||||
cmd = bot.all_commands.get(keys[0].removeprefix(self.context.prefix))
|
||||
if cmd is None:
|
||||
string = await maybe_coro(
|
||||
self.command_not_found,
|
||||
self.remove_mentions(keys[0]),
|
||||
)
|
||||
return await self.send_error_message(string)
|
||||
|
||||
for key in keys[1:]:
|
||||
try:
|
||||
found = cmd.all_commands.get(key)
|
||||
except AttributeError:
|
||||
string = await maybe_coro(
|
||||
self.subcommand_not_found,
|
||||
cmd,
|
||||
self.remove_mentions(key),
|
||||
)
|
||||
return await self.send_error_message(string)
|
||||
else:
|
||||
if found is None:
|
||||
string = await maybe_coro(
|
||||
self.subcommand_not_found,
|
||||
cmd,
|
||||
self.remove_mentions(key),
|
||||
)
|
||||
return await self.send_error_message(string)
|
||||
cmd = found
|
||||
|
||||
return await self.send_command_help(cmd)
|
|
@ -1,77 +0,0 @@
|
|||
from loguru import logger
|
||||
|
||||
from db import database
|
||||
from services import item_service
|
||||
|
||||
|
||||
class Inventory:
|
||||
def __init__(self, user_id):
|
||||
self.user_id = user_id
|
||||
|
||||
def add_item(self, item: item_service.Item, quantity=1):
|
||||
"""
|
||||
Adds an item with a specific count (default 1) to the database, if there are
|
||||
no records of this user having that item yet, it will just add a record with quantity=quantity.
|
||||
:param item:
|
||||
:param quantity:
|
||||
:return:
|
||||
"""
|
||||
|
||||
query = """
|
||||
INSERT INTO inventory (user_id, item_id, quantity)
|
||||
VALUES (%s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE quantity = quantity + %s;
|
||||
"""
|
||||
|
||||
database.execute_query(
|
||||
query,
|
||||
(self.user_id, item.id, abs(quantity), abs(quantity)),
|
||||
)
|
||||
|
||||
def take_item(self, item: item_service.Item, quantity=1):
|
||||
query = """
|
||||
INSERT INTO inventory (user_id, item_id, quantity)
|
||||
VALUES (%s, %s, 0)
|
||||
ON DUPLICATE KEY UPDATE quantity = CASE
|
||||
WHEN quantity - %s < 0 THEN 0
|
||||
ELSE quantity - %s
|
||||
END;
|
||||
"""
|
||||
|
||||
database.execute_query(
|
||||
query,
|
||||
(self.user_id, item.id, self.user_id, item.id, abs(quantity)),
|
||||
)
|
||||
|
||||
def get_inventory(self):
|
||||
query = "SELECT item_id, quantity FROM inventory WHERE user_id = %s AND quantity > 0"
|
||||
results = database.select_query(query, (self.user_id,))
|
||||
|
||||
items_dict = {}
|
||||
for row in results:
|
||||
item_id, quantity = row
|
||||
item = item_service.Item(item_id)
|
||||
items_dict[item] = quantity
|
||||
|
||||
return items_dict
|
||||
|
||||
def get_item_quantity(self, item: item_service.Item):
|
||||
query = "SELECT COALESCE(quantity, 0) FROM inventory WHERE user_id = %s AND item_id = %s"
|
||||
return database.select_query_one(query, (self.user_id, item.id))
|
||||
|
||||
def get_sell_data(self):
|
||||
query = """
|
||||
SELECT item.display_name
|
||||
FROM inventory
|
||||
JOIN ShopItem ON inventory.item_id = ShopItem.item_id
|
||||
JOIN item ON inventory.item_id = item.id
|
||||
WHERE inventory.user_id = %s AND inventory.quantity > 0 AND ShopItem.worth > 0
|
||||
"""
|
||||
|
||||
try:
|
||||
results = database.select_query(query, (self.user_id,))
|
||||
return [item[0] for item in results]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return []
|
|
@ -1,69 +0,0 @@
|
|||
import sqlite3
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
class Item:
|
||||
def __init__(self, item_id):
|
||||
self.id = item_id
|
||||
|
||||
data = self.get_item_data()
|
||||
|
||||
self.name = data[0]
|
||||
self.display_name = data[1]
|
||||
self.description = data[2]
|
||||
self.image_url = data[3]
|
||||
self.emote_id = data[4]
|
||||
self.quote = data[5]
|
||||
self.type = data[6]
|
||||
|
||||
def get_item_data(self):
|
||||
query = """
|
||||
SELECT name, display_name, description, image_url, emote_id, quote, type
|
||||
FROM item
|
||||
WHERE id = %s
|
||||
"""
|
||||
|
||||
return database.select_query(query, (self.id,))[0]
|
||||
|
||||
def get_quantity(self, author_id):
|
||||
query = """
|
||||
SELECT COALESCE((SELECT quantity FROM inventory WHERE user_id = %s AND item_id = %s), 0) AS quantity
|
||||
"""
|
||||
|
||||
return database.select_query_one(query, (author_id, self.id))
|
||||
|
||||
def get_item_worth(self):
|
||||
query = """
|
||||
SELECT worth
|
||||
FROM ShopItem
|
||||
WHERE item_id = %s
|
||||
"""
|
||||
|
||||
return database.select_query_one(query, (self.id,))
|
||||
|
||||
@staticmethod
|
||||
def get_all_item_names():
|
||||
query = "SELECT display_name FROM item"
|
||||
|
||||
try:
|
||||
items = database.select_query(query)
|
||||
return [item[0] for item in items]
|
||||
|
||||
except sqlite3.Error:
|
||||
logger.error(sqlite3.Error)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_item_by_display_name(display_name):
|
||||
query = "SELECT id FROM item WHERE display_name = %s"
|
||||
item_id = database.select_query_one(query, (display_name,))
|
||||
return Item(item_id)
|
||||
|
||||
@staticmethod
|
||||
def get_item_by_name(name):
|
||||
query = "SELECT id FROM item WHERE name = %s"
|
||||
item_id = database.select_query_one(query, (name,))
|
||||
return Item(item_id)
|
|
@ -1,171 +0,0 @@
|
|||
from typing import Optional, Dict, Any, List
|
||||
|
||||
from db.database import execute_query, select_query_one, select_query_dict
|
||||
|
||||
|
||||
class CaseService:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def create_case(
|
||||
self,
|
||||
guild_id: int,
|
||||
target_id: int,
|
||||
moderator_id: int,
|
||||
action_type: str,
|
||||
reason: Optional[str] = None,
|
||||
duration: Optional[int] = None,
|
||||
expires_at: Optional[str] = None,
|
||||
modlog_message_id: Optional[int] = None,
|
||||
) -> int:
|
||||
# Get the next case number for the guild
|
||||
query: str = """
|
||||
SELECT IFNULL(MAX(case_number), 0) + 1
|
||||
FROM cases
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
case_number = select_query_one(query, (guild_id,))
|
||||
|
||||
if case_number is None:
|
||||
raise ValueError("Failed to retrieve the next case number.")
|
||||
|
||||
# Insert the new case
|
||||
query: str = """
|
||||
INSERT INTO cases (
|
||||
guild_id, case_number, target_id, moderator_id, action_type, reason, duration, expires_at, modlog_message_id
|
||||
) VALUES (
|
||||
%s, %s, %s, %s, %s, %s, %s, %s, %s
|
||||
)
|
||||
"""
|
||||
execute_query(
|
||||
query,
|
||||
(
|
||||
guild_id,
|
||||
case_number,
|
||||
target_id,
|
||||
moderator_id,
|
||||
action_type.upper(),
|
||||
reason,
|
||||
duration,
|
||||
expires_at,
|
||||
modlog_message_id,
|
||||
),
|
||||
)
|
||||
|
||||
return int(case_number)
|
||||
|
||||
def close_case(self, guild_id, case_number):
|
||||
query = """
|
||||
UPDATE cases
|
||||
SET is_closed = TRUE, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE guild_id = %s AND case_number = %s
|
||||
"""
|
||||
execute_query(query, (guild_id, case_number))
|
||||
|
||||
def edit_case_reason(
|
||||
self,
|
||||
guild_id: int,
|
||||
case_number: int,
|
||||
new_reason: Optional[str] = None,
|
||||
) -> bool:
|
||||
query = """
|
||||
UPDATE cases
|
||||
SET reason = COALESCE(%s, reason),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
WHERE guild_id = %s AND case_number = %s
|
||||
"""
|
||||
execute_query(
|
||||
query,
|
||||
(
|
||||
new_reason,
|
||||
guild_id,
|
||||
case_number,
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
def edit_case(self, guild_id, case_number, changes: dict):
|
||||
set_clause = ", ".join([f"{key} = %s" for key in changes.keys()])
|
||||
query = f"""
|
||||
UPDATE cases
|
||||
SET {set_clause}, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE guild_id = %s AND case_number = %s
|
||||
"""
|
||||
execute_query(query, (*changes.values(), guild_id, case_number))
|
||||
|
||||
def fetch_case_by_id(self, case_id: int) -> Optional[Dict[str, Any]]:
|
||||
query: str = """
|
||||
SELECT * FROM cases
|
||||
WHERE id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
result: List[Dict[str, Any]] = select_query_dict(query, (case_id,))
|
||||
return result[0] if result else None
|
||||
|
||||
def fetch_case_by_guild_and_number(
|
||||
self,
|
||||
guild_id: int,
|
||||
case_number: int,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
query: str = """
|
||||
SELECT * FROM cases
|
||||
WHERE guild_id = %s AND case_number = %s
|
||||
ORDER BY case_number DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
result: List[Dict[str, Any]] = select_query_dict(query, (guild_id, case_number))
|
||||
return result[0] if result else None
|
||||
|
||||
def fetch_cases_by_guild(self, guild_id: int) -> List[Dict[str, Any]]:
|
||||
query: str = """
|
||||
SELECT * FROM cases
|
||||
WHERE guild_id = %s
|
||||
ORDER BY case_number DESC
|
||||
"""
|
||||
results: List[Dict[str, Any]] = select_query_dict(query, (guild_id,))
|
||||
return results
|
||||
|
||||
def fetch_cases_by_target(
|
||||
self,
|
||||
guild_id: int,
|
||||
target_id: int,
|
||||
) -> List[Dict[str, Any]]:
|
||||
query: str = """
|
||||
SELECT * FROM cases
|
||||
WHERE guild_id = %s AND target_id = %s
|
||||
ORDER BY case_number DESC
|
||||
"""
|
||||
results: List[Dict[str, Any]] = select_query_dict(query, (guild_id, target_id))
|
||||
return results
|
||||
|
||||
def fetch_cases_by_moderator(
|
||||
self,
|
||||
guild_id: int,
|
||||
moderator_id: int,
|
||||
) -> List[Dict[str, Any]]:
|
||||
query: str = """
|
||||
SELECT * FROM cases
|
||||
WHERE guild_id = %s AND moderator_id = %s
|
||||
ORDER BY case_number DESC
|
||||
"""
|
||||
results: List[Dict[str, Any]] = select_query_dict(
|
||||
query,
|
||||
(guild_id, moderator_id),
|
||||
)
|
||||
return results
|
||||
|
||||
def fetch_cases_by_action_type(
|
||||
self,
|
||||
guild_id: int,
|
||||
action_type: str,
|
||||
) -> List[Dict[str, Any]]:
|
||||
query: str = """
|
||||
SELECT * FROM cases
|
||||
WHERE guild_id = %s AND action_type = %s
|
||||
ORDER BY case_number DESC
|
||||
"""
|
||||
results: List[Dict[str, Any]] = select_query_dict(
|
||||
query,
|
||||
(guild_id, action_type.upper()),
|
||||
)
|
||||
return results
|
|
@ -1,32 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from db.database import execute_query, select_query_one
|
||||
|
||||
|
||||
class ModLogService:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def set_modlog_channel(self, guild_id: int, channel_id: int) -> None:
|
||||
query: str = """
|
||||
INSERT INTO mod_log (guild_id, channel_id, is_enabled)
|
||||
VALUES (%s, %s, TRUE)
|
||||
ON DUPLICATE KEY UPDATE channel_id = VALUES(channel_id), is_enabled = TRUE, updated_at = CURRENT_TIMESTAMP
|
||||
"""
|
||||
execute_query(query, (guild_id, channel_id))
|
||||
|
||||
def disable_modlog_channel(self, guild_id: int) -> None:
|
||||
query: str = """
|
||||
UPDATE mod_log
|
||||
SET is_enabled = FALSE, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
execute_query(query, (guild_id,))
|
||||
|
||||
def fetch_modlog_channel_id(self, guild_id: int) -> Optional[int]:
|
||||
query: str = """
|
||||
SELECT channel_id FROM mod_log
|
||||
WHERE guild_id = %s AND is_enabled = TRUE
|
||||
"""
|
||||
result = select_query_one(query, (guild_id,))
|
||||
return result or None
|
|
@ -1,191 +0,0 @@
|
|||
from datetime import datetime, timezone
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
class CustomReactionsService:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def find_trigger(
|
||||
self,
|
||||
guild_id: int,
|
||||
message_content: str,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
message_content = message_content.lower()
|
||||
query = """
|
||||
SELECT * FROM custom_reactions
|
||||
WHERE (guild_id = %s OR is_global = TRUE) AND (
|
||||
(is_full_match = TRUE AND trigger_text = %s) OR
|
||||
(is_full_match = FALSE AND %s LIKE CONCAT('%%', trigger_text, '%%'))
|
||||
)
|
||||
ORDER BY guild_id = %s DESC, is_global ASC
|
||||
LIMIT 1
|
||||
"""
|
||||
if result := database.select_query(
|
||||
query,
|
||||
(guild_id, message_content, message_content, guild_id),
|
||||
):
|
||||
reaction = result[0] # Get the first result from the list
|
||||
return {
|
||||
"id": reaction[0],
|
||||
"trigger_text": reaction[1],
|
||||
"response": reaction[2],
|
||||
"emoji_id": reaction[3],
|
||||
"is_emoji": reaction[4],
|
||||
"is_full_match": reaction[5],
|
||||
"is_global": reaction[6],
|
||||
"guild_id": reaction[7],
|
||||
"creator_id": reaction[8],
|
||||
"usage_count": reaction[9],
|
||||
"created_at": reaction[10],
|
||||
"updated_at": reaction[11],
|
||||
"type": "emoji" if reaction[4] else "text",
|
||||
}
|
||||
return None
|
||||
|
||||
async def find_id(self, reaction_id: int) -> Optional[Dict[str, Any]]:
|
||||
query = """
|
||||
SELECT * FROM custom_reactions
|
||||
WHERE id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
if result := database.select_query(query, (reaction_id,)):
|
||||
reaction = result[0] # Get the first result from the list
|
||||
return {
|
||||
"id": reaction[0],
|
||||
"trigger_text": reaction[1],
|
||||
"response": reaction[2],
|
||||
"emoji_id": reaction[3],
|
||||
"is_emoji": reaction[4],
|
||||
"is_full_match": reaction[5],
|
||||
"is_global": reaction[6],
|
||||
"guild_id": reaction[7],
|
||||
"creator_id": reaction[8],
|
||||
"usage_count": reaction[9],
|
||||
"created_at": reaction[10],
|
||||
"updated_at": reaction[11],
|
||||
"type": "emoji" if reaction[4] else "text",
|
||||
}
|
||||
return None
|
||||
|
||||
async def find_all_by_guild(self, guild_id: int) -> List[Dict[str, Any]]:
|
||||
query = """
|
||||
SELECT * FROM custom_reactions
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
results = database.select_query(query, (guild_id,))
|
||||
return [
|
||||
{
|
||||
"id": reaction[0],
|
||||
"trigger_text": reaction[1],
|
||||
"response": reaction[2],
|
||||
"emoji_id": reaction[3],
|
||||
"is_emoji": reaction[4],
|
||||
"is_full_match": reaction[5],
|
||||
"is_global": reaction[6],
|
||||
"guild_id": reaction[7],
|
||||
"creator_id": reaction[8],
|
||||
"usage_count": reaction[9],
|
||||
"created_at": reaction[10],
|
||||
"updated_at": reaction[11],
|
||||
"type": "emoji" if reaction[4] else "text",
|
||||
}
|
||||
for reaction in results
|
||||
]
|
||||
|
||||
async def create_custom_reaction(
|
||||
self,
|
||||
guild_id: int,
|
||||
creator_id: int,
|
||||
trigger_text: str,
|
||||
response: Optional[str] = None,
|
||||
emoji_id: Optional[int] = None,
|
||||
is_emoji: bool = False,
|
||||
is_full_match: bool = False,
|
||||
is_global: bool = True,
|
||||
) -> bool:
|
||||
if await self.count_custom_reactions(guild_id) >= 100:
|
||||
return False
|
||||
|
||||
query = """
|
||||
INSERT INTO custom_reactions (trigger_text, response, emoji_id, is_emoji, is_full_match, is_global, guild_id, creator_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE trigger_text=trigger_text
|
||||
"""
|
||||
database.execute_query(
|
||||
query,
|
||||
(
|
||||
trigger_text,
|
||||
response,
|
||||
emoji_id,
|
||||
is_emoji,
|
||||
is_full_match,
|
||||
is_global,
|
||||
guild_id,
|
||||
creator_id,
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
async def edit_custom_reaction(
|
||||
self,
|
||||
reaction_id: int,
|
||||
new_response: Optional[str] = None,
|
||||
new_emoji_id: Optional[int] = None,
|
||||
is_emoji: Optional[bool] = None,
|
||||
is_full_match: Optional[bool] = None,
|
||||
is_global: Optional[bool] = None,
|
||||
) -> bool:
|
||||
query = """
|
||||
UPDATE custom_reactions
|
||||
SET response = COALESCE(%s, response),
|
||||
emoji_id = COALESCE(%s, emoji_id),
|
||||
is_emoji = COALESCE(%s, is_emoji),
|
||||
is_full_match = COALESCE(%s, is_full_match),
|
||||
is_global = COALESCE(%s, is_global),
|
||||
updated_at = %s
|
||||
WHERE id = %s
|
||||
"""
|
||||
database.execute_query(
|
||||
query,
|
||||
(
|
||||
new_response,
|
||||
new_emoji_id,
|
||||
is_emoji,
|
||||
is_full_match,
|
||||
is_global,
|
||||
datetime.now(timezone.utc),
|
||||
reaction_id,
|
||||
),
|
||||
)
|
||||
return True
|
||||
|
||||
async def delete_custom_reaction(self, reaction_id: int) -> bool:
|
||||
query = """
|
||||
DELETE FROM custom_reactions
|
||||
WHERE id = %s
|
||||
"""
|
||||
database.execute_query(query, (reaction_id,))
|
||||
return True
|
||||
|
||||
async def count_custom_reactions(self, guild_id: int) -> int:
|
||||
query = """
|
||||
SELECT COUNT(*) FROM custom_reactions
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
count = database.select_query_one(query, (guild_id,))
|
||||
return count if count else 0
|
||||
|
||||
async def increment_reaction_usage(self, reaction_id: int) -> bool:
|
||||
query = """
|
||||
UPDATE custom_reactions
|
||||
SET usage_count = usage_count + 1
|
||||
WHERE id = %s
|
||||
"""
|
||||
database.execute_query(
|
||||
query,
|
||||
(reaction_id,),
|
||||
)
|
||||
return True
|
|
@ -1,139 +0,0 @@
|
|||
import json
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
class BlackJackStats:
|
||||
def __init__(self, user_id, is_won, bet, payout, hand_player, hand_dealer):
|
||||
self.user_id = user_id
|
||||
self.is_won = is_won
|
||||
self.bet = bet
|
||||
self.payout = payout
|
||||
self.hand_player = json.dumps(hand_player)
|
||||
self.hand_dealer = json.dumps(hand_dealer)
|
||||
|
||||
def push(self):
|
||||
query = """
|
||||
INSERT INTO blackjack (user_id, is_won, bet, payout, hand_player, hand_dealer)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
values = (
|
||||
self.user_id,
|
||||
self.is_won,
|
||||
self.bet,
|
||||
self.payout,
|
||||
self.hand_player,
|
||||
self.hand_dealer,
|
||||
)
|
||||
|
||||
database.execute_query(query, values)
|
||||
|
||||
@staticmethod
|
||||
def get_user_stats(user_id):
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) AS amount_of_games,
|
||||
SUM(bet) AS total_bet,
|
||||
SUM(payout) AS total_payout,
|
||||
SUM(CASE WHEN is_won = 1 THEN 1 ELSE 0 END) AS winning,
|
||||
SUM(CASE WHEN is_won = 0 THEN 1 ELSE 0 END) AS losing
|
||||
FROM blackjack
|
||||
WHERE user_id = %s;
|
||||
"""
|
||||
(
|
||||
amount_of_games,
|
||||
total_bet,
|
||||
total_payout,
|
||||
winning_amount,
|
||||
losing_amount,
|
||||
) = database.select_query(query, (user_id,))[0]
|
||||
|
||||
return {
|
||||
"amount_of_games": amount_of_games,
|
||||
"total_bet": total_bet,
|
||||
"total_payout": total_payout,
|
||||
"winning_amount": winning_amount,
|
||||
"losing_amount": losing_amount,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_total_rows_count():
|
||||
query = """
|
||||
SELECT SUM(TABLE_ROWS)
|
||||
FROM INFORMATION_SCHEMA.TABLES
|
||||
"""
|
||||
|
||||
return database.select_query_one(query)
|
||||
|
||||
|
||||
class SlotsStats:
|
||||
"""
|
||||
Handles statistics for the /slots command
|
||||
"""
|
||||
|
||||
def __init__(self, user_id, is_won, bet, payout, spin_type, icons):
|
||||
self.user_id = user_id
|
||||
self.is_won = is_won
|
||||
self.bet = bet
|
||||
self.payout = payout
|
||||
self.spin_type = spin_type
|
||||
self.icons = json.dumps(icons)
|
||||
|
||||
def push(self):
|
||||
"""
|
||||
Insert the services from any given slots game into the database
|
||||
"""
|
||||
query = """
|
||||
INSERT INTO slots (user_id, is_won, bet, payout, spin_type, icons)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
|
||||
values = (
|
||||
self.user_id,
|
||||
self.is_won,
|
||||
self.bet,
|
||||
self.payout,
|
||||
self.spin_type,
|
||||
self.icons,
|
||||
)
|
||||
|
||||
database.execute_query(query, values)
|
||||
|
||||
@staticmethod
|
||||
def get_user_stats(user_id):
|
||||
"""
|
||||
Retrieve the Slots stats for a given user from the database.
|
||||
"""
|
||||
query = """
|
||||
SELECT
|
||||
COUNT(*) AS amount_of_games,
|
||||
SUM(bet) AS total_bet,
|
||||
SUM(payout) AS total_payout,
|
||||
SUM(CASE WHEN spin_type = 'pair' AND is_won = 1 THEN 1 ELSE 0 END) AS games_won_pair,
|
||||
SUM(CASE WHEN spin_type = 'three_of_a_kind' AND is_won = 1 THEN 1 ELSE 0 END) AS games_won_three_of_a_kind,
|
||||
SUM(CASE WHEN spin_type = 'three_diamonds' AND is_won = 1 THEN 1 ELSE 0 END) AS games_won_three_diamonds,
|
||||
SUM(CASE WHEN spin_type = 'jackpot' AND is_won = 1 THEN 1 ELSE 0 END) AS games_won_jackpot
|
||||
FROM slots
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
|
||||
(
|
||||
amount_of_games,
|
||||
total_bet,
|
||||
total_payout,
|
||||
games_won_pair,
|
||||
games_won_three_of_a_kind,
|
||||
games_won_three_diamonds,
|
||||
games_won_jackpot,
|
||||
) = database.select_query(query, (user_id,))[0]
|
||||
|
||||
return {
|
||||
"amount_of_games": amount_of_games,
|
||||
"total_bet": total_bet,
|
||||
"total_payout": total_payout,
|
||||
"games_won_pair": games_won_pair,
|
||||
"games_won_three_of_a_kind": games_won_three_of_a_kind,
|
||||
"games_won_three_diamonds": games_won_three_diamonds,
|
||||
"games_won_jackpot": games_won_jackpot,
|
||||
}
|
|
@ -1,321 +0,0 @@
|
|||
import datetime
|
||||
import imghdr
|
||||
import json
|
||||
import random
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
class HttpError(Exception):
|
||||
def __init__(self, status_code: int, reason: str) -> None:
|
||||
"""
|
||||
Initialize the HttpError.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
status_code : int
|
||||
The status code of the error.
|
||||
reason : str
|
||||
The reason of the error.
|
||||
"""
|
||||
self.status_code = status_code
|
||||
self.reason = reason
|
||||
super().__init__(f"HTTP Error {status_code}: {reason}")
|
||||
|
||||
|
||||
class Comic:
|
||||
"""
|
||||
A class representing a xkcd comic.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
xkcd_dict: dict[str, Any],
|
||||
raw_image: bytes | None = None,
|
||||
comic_url: str | None = None,
|
||||
explanation_url: str | None = None,
|
||||
) -> None:
|
||||
self.id: int | None = xkcd_dict.get("num")
|
||||
self.date: datetime.date | None = self._determine_date(xkcd_dict)
|
||||
self.title: str | None = xkcd_dict.get("safe_title")
|
||||
self.description: str | None = xkcd_dict.get("alt")
|
||||
self.transcript: str | None = xkcd_dict.get("transcript")
|
||||
self.image: bytes | None = raw_image
|
||||
self.image_extension: str | None = self._determine_image_extension()
|
||||
self.image_url: str | None = xkcd_dict.get("img")
|
||||
self.comic_url: str | None = comic_url
|
||||
self.explanation_url: str | None = explanation_url
|
||||
|
||||
@staticmethod
|
||||
def _determine_date(xkcd_dict: dict[str, Any]) -> datetime.date | None:
|
||||
"""
|
||||
Determine the date of the comic.
|
||||
Args:
|
||||
xkcd_dict:
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
return datetime.date(
|
||||
int(xkcd_dict["year"]),
|
||||
int(xkcd_dict["month"]),
|
||||
int(xkcd_dict["day"]),
|
||||
)
|
||||
|
||||
except (KeyError, ValueError):
|
||||
return None
|
||||
|
||||
def _determine_image_extension(self) -> str | None:
|
||||
"""
|
||||
Determine the image extension of the comic.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str | None
|
||||
The extension of the image.
|
||||
"""
|
||||
return f".{imghdr.what(None, h=self.image)}" if self.image else None
|
||||
|
||||
def update_raw_image(self, raw_image: bytes) -> None:
|
||||
"""
|
||||
Update the raw image of the comic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_image : bytes
|
||||
The raw image data.
|
||||
"""
|
||||
self.image = raw_image
|
||||
self.image_extension = self._determine_image_extension()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return the representation of the comic.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The representation of the comic.
|
||||
"""
|
||||
return f"Comic({self.title})"
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(
|
||||
self,
|
||||
api_url: str = "https://xkcd.com",
|
||||
explanation_wiki_url: str = "https://www.explainxkcd.com/wiki/index.php/",
|
||||
) -> None:
|
||||
self._api_url = api_url
|
||||
self._explanation_wiki_url = explanation_wiki_url
|
||||
|
||||
def latest_comic_url(self) -> str:
|
||||
"""
|
||||
Get the URL for the latest comic.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The URL for the latest comic.
|
||||
"""
|
||||
return f"{self._api_url}/info.0.json"
|
||||
|
||||
def comic_id_url(self, comic_id: int) -> str:
|
||||
"""
|
||||
Get the URL for a specific comic ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
comic_id : int
|
||||
The ID of the comic.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The URL for the specific comic ID.
|
||||
"""
|
||||
return f"{self._api_url}/{comic_id}/info.0.json"
|
||||
|
||||
def _parse_response(self, response_text: str) -> Comic:
|
||||
"""
|
||||
Parse the response text into a Comic object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
response_text : str
|
||||
The response text to parse.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Comic
|
||||
The parsed comic object.
|
||||
"""
|
||||
response_dict: dict[str, Any] = json.loads(response_text)
|
||||
comic_url: str = f"{self._api_url}/{response_dict['num']}/"
|
||||
explanation_url: str = f"{self._explanation_wiki_url}{response_dict['num']}"
|
||||
|
||||
return Comic(
|
||||
response_dict,
|
||||
comic_url=comic_url,
|
||||
explanation_url=explanation_url,
|
||||
)
|
||||
|
||||
def _fetch_comic(self, comic_id: int, raw_comic_image: bool) -> Comic:
|
||||
"""
|
||||
Fetch a comic from the xkcd API.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
comic_id : int
|
||||
The ID of the comic to fetch.
|
||||
raw_comic_image : bool
|
||||
Whether to fetch the raw image data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Comic
|
||||
The fetched comic.
|
||||
"""
|
||||
comic = self._parse_response(self._request_comic(comic_id))
|
||||
|
||||
if raw_comic_image:
|
||||
raw_image = self._request_raw_image(comic.image_url)
|
||||
comic.update_raw_image(raw_image)
|
||||
|
||||
return comic
|
||||
|
||||
def get_latest_comic(self, raw_comic_image: bool = False) -> Comic:
|
||||
"""
|
||||
Get the latest xkcd comic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_comic_image : bool, optional
|
||||
Whether to fetch the raw image data, by default False
|
||||
|
||||
Returns
|
||||
-------
|
||||
Comic
|
||||
The latest xkcd comic.
|
||||
"""
|
||||
return self._fetch_comic(0, raw_comic_image)
|
||||
|
||||
def get_comic(self, comic_id: int, raw_comic_image: bool = False) -> Comic:
|
||||
"""
|
||||
Get a specific xkcd comic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
comic_id : int
|
||||
The ID of the comic to fetch.
|
||||
raw_comic_image : bool, optional
|
||||
Whether to fetch the raw image data, by default False
|
||||
|
||||
Returns
|
||||
-------
|
||||
Comic
|
||||
The fetched xkcd comic.
|
||||
"""
|
||||
return self._fetch_comic(comic_id, raw_comic_image)
|
||||
|
||||
def get_random_comic(self, raw_comic_image: bool = False) -> Comic:
|
||||
"""
|
||||
Get a random xkcd comic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_comic_image : bool, optional
|
||||
Whether to fetch the raw image data, by default False
|
||||
|
||||
Returns
|
||||
-------
|
||||
Comic
|
||||
The random xkcd comic.
|
||||
"""
|
||||
latest_comic_id: int = self._parse_response(self._request_comic(0)).id or 0
|
||||
random_id: int = random.randint(1, latest_comic_id)
|
||||
|
||||
return self._fetch_comic(random_id, raw_comic_image)
|
||||
|
||||
def _request_comic(self, comic_id: int) -> str:
|
||||
"""
|
||||
Request the comic data from the xkcd API.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
comic_id : int
|
||||
The ID of the comic to fetch.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The response text.
|
||||
|
||||
Raises
|
||||
------
|
||||
HttpError
|
||||
If the request fails.
|
||||
"""
|
||||
comic_url = (
|
||||
self.latest_comic_url() if comic_id <= 0 else self.comic_id_url(comic_id)
|
||||
)
|
||||
|
||||
try:
|
||||
response = httpx.get(comic_url)
|
||||
response.raise_for_status()
|
||||
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HttpError(
|
||||
exc.response.status_code,
|
||||
exc.response.reason_phrase,
|
||||
) from exc
|
||||
|
||||
return response.text
|
||||
|
||||
@staticmethod
|
||||
def _request_raw_image(raw_image_url: str | None) -> bytes:
|
||||
"""
|
||||
Request the raw image data from the xkcd API.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
raw_image_url : str | None
|
||||
The URL of the raw image data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The raw image data.
|
||||
|
||||
Raises
|
||||
------
|
||||
HttpError
|
||||
If the request fails.
|
||||
"""
|
||||
if not raw_image_url:
|
||||
raise HttpError(404, "Image URL not found")
|
||||
|
||||
try:
|
||||
response = httpx.get(raw_image_url)
|
||||
response.raise_for_status()
|
||||
|
||||
except httpx.HTTPStatusError as exc:
|
||||
raise HttpError(
|
||||
exc.response.status_code,
|
||||
exc.response.reason_phrase,
|
||||
) from exc
|
||||
|
||||
return response.content
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Return the representation of the client.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The representation of the client.
|
||||
"""
|
||||
return "Client()"
|
|
@ -1,295 +0,0 @@
|
|||
import time
|
||||
from typing import Callable, Dict, List, Optional, Tuple
|
||||
|
||||
from discord.ext import commands
|
||||
|
||||
from db import database
|
||||
from lib.constants import CONST
|
||||
|
||||
|
||||
class XpService:
|
||||
"""
|
||||
Manages XP for a user, including storing, retrieving, and updating XP in the database.
|
||||
"""
|
||||
|
||||
def __init__(self, user_id: int, guild_id: int) -> None:
|
||||
"""
|
||||
Initializes the XpService with user and guild IDs, and fetches or creates XP data.
|
||||
|
||||
Args:
|
||||
user_id (int): The ID of the user.
|
||||
guild_id (int): The ID of the guild.
|
||||
"""
|
||||
self.user_id: int = user_id
|
||||
self.guild_id: int = guild_id
|
||||
self.xp: int = 0
|
||||
self.level: int = 0
|
||||
self.cooldown_time: Optional[float] = None
|
||||
self.xp_gain: int = CONST.XP_GAIN_PER_MESSAGE
|
||||
self.new_cooldown: int = CONST.XP_GAIN_COOLDOWN
|
||||
|
||||
self.fetch_or_create_xp()
|
||||
|
||||
def push(self) -> None:
|
||||
"""
|
||||
Updates the XP and cooldown for a user in the database.
|
||||
"""
|
||||
query: str = """
|
||||
UPDATE xp
|
||||
SET user_xp = %s, user_level = %s, cooldown = %s
|
||||
WHERE user_id = %s AND guild_id = %s
|
||||
"""
|
||||
database.execute_query(
|
||||
query,
|
||||
(self.xp, self.level, self.cooldown_time, self.user_id, self.guild_id),
|
||||
)
|
||||
|
||||
def fetch_or_create_xp(self) -> None:
|
||||
"""
|
||||
Retrieves a user's XP from the database or inserts a new row if it doesn't exist yet.
|
||||
"""
|
||||
query: str = "SELECT user_xp, user_level, cooldown FROM xp WHERE user_id = %s AND guild_id = %s"
|
||||
|
||||
try:
|
||||
user_xp, user_level, cooldown = database.select_query(
|
||||
query,
|
||||
(self.user_id, self.guild_id),
|
||||
)[0]
|
||||
except (IndexError, TypeError):
|
||||
user_xp, user_level, cooldown = 0, 0, None
|
||||
|
||||
if any(var is None for var in [user_xp, user_level, cooldown]):
|
||||
query = """
|
||||
INSERT INTO xp (user_id, guild_id, user_xp, user_level, cooldown)
|
||||
VALUES (%s, %s, 0, 0, %s)
|
||||
"""
|
||||
database.execute_query(query, (self.user_id, self.guild_id, time.time()))
|
||||
user_xp, user_level, cooldown = 0, 0, time.time()
|
||||
|
||||
self.xp = user_xp
|
||||
self.level = user_level
|
||||
self.cooldown_time = cooldown
|
||||
|
||||
def calculate_rank(self) -> Optional[int]:
|
||||
"""
|
||||
Determines the rank of a user in the guild based on their XP and level.
|
||||
|
||||
Returns:
|
||||
Optional[int]: The rank of the user in the guild, or None if not found.
|
||||
"""
|
||||
query: str = """
|
||||
SELECT user_id, user_xp, user_level
|
||||
FROM xp
|
||||
WHERE guild_id = %s
|
||||
ORDER BY user_level DESC, user_xp DESC
|
||||
"""
|
||||
data: List[Tuple[int, int, int]] = database.select_query(
|
||||
query,
|
||||
(self.guild_id,),
|
||||
)
|
||||
|
||||
leaderboard: List[Tuple[int, int, int, int]] = [
|
||||
(row[0], row[1], row[2], rank) for rank, row in enumerate(data, start=1)
|
||||
]
|
||||
return next(
|
||||
(entry[3] for entry in leaderboard if entry[0] == self.user_id),
|
||||
None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def load_leaderboard(guild_id: int) -> List[Tuple[int, int, int, int]]:
|
||||
"""
|
||||
Retrieves the guild's XP leaderboard.
|
||||
|
||||
Args:
|
||||
guild_id (int): The ID of the guild.
|
||||
|
||||
Returns:
|
||||
List[Tuple[int, int, int, int]]: A list of tuples containing user_id, user_xp, user_level, and needed_xp_for_next_level.
|
||||
"""
|
||||
query: str = """
|
||||
SELECT user_id, user_xp, user_level
|
||||
FROM xp
|
||||
WHERE guild_id = %s
|
||||
ORDER BY user_level DESC, user_xp DESC
|
||||
"""
|
||||
data: List[Tuple[int, int, int]] = database.select_query(query, (guild_id,))
|
||||
|
||||
leaderboard: List[Tuple[int, int, int, int]] = []
|
||||
for row in data:
|
||||
row_user_id: int = row[0]
|
||||
user_xp: int = row[1]
|
||||
user_level: int = row[2]
|
||||
needed_xp_for_next_level: int = XpService.xp_needed_for_next_level(
|
||||
user_level,
|
||||
)
|
||||
|
||||
leaderboard.append(
|
||||
(row_user_id, user_xp, user_level, needed_xp_for_next_level),
|
||||
)
|
||||
|
||||
return leaderboard
|
||||
|
||||
@staticmethod
|
||||
def generate_progress_bar(
|
||||
current_value: int,
|
||||
target_value: int,
|
||||
bar_length: int = 10,
|
||||
) -> str:
|
||||
"""
|
||||
Generates an XP progress bar based on the current level and XP.
|
||||
|
||||
Args:
|
||||
current_value (int): The current XP value.
|
||||
target_value (int): The target XP value.
|
||||
bar_length (int, optional): The length of the progress bar. Defaults to 10.
|
||||
|
||||
Returns:
|
||||
str: The formatted progress bar.
|
||||
"""
|
||||
progress: float = current_value / target_value
|
||||
filled_length: int = int(bar_length * progress)
|
||||
empty_length: int = bar_length - filled_length
|
||||
bar: str = "▰" * filled_length + "▱" * empty_length
|
||||
return f"`{bar}` {current_value}/{target_value}"
|
||||
|
||||
@staticmethod
|
||||
def xp_needed_for_next_level(current_level: int) -> int:
|
||||
"""
|
||||
Calculates the amount of XP needed to reach the next level, based on the current level.
|
||||
|
||||
Args:
|
||||
current_level (int): The current level of the user.
|
||||
|
||||
Returns:
|
||||
int: The amount of XP needed for the next level.
|
||||
"""
|
||||
formula_mapping: Dict[Tuple[int, int], Callable[[int], int]] = {
|
||||
(10, 19): lambda level: 12 * level + 28,
|
||||
(20, 29): lambda level: 15 * level + 29,
|
||||
(30, 39): lambda level: 18 * level + 30,
|
||||
(40, 49): lambda level: 21 * level + 31,
|
||||
(50, 59): lambda level: 24 * level + 32,
|
||||
(60, 69): lambda level: 27 * level + 33,
|
||||
(70, 79): lambda level: 30 * level + 34,
|
||||
(80, 89): lambda level: 33 * level + 35,
|
||||
(90, 99): lambda level: 36 * level + 36,
|
||||
}
|
||||
|
||||
return next(
|
||||
(
|
||||
formula(current_level)
|
||||
for level_range, formula in formula_mapping.items()
|
||||
if level_range[0] <= current_level <= level_range[1]
|
||||
),
|
||||
(
|
||||
10 * current_level + 27
|
||||
if current_level < 10
|
||||
else 42 * current_level + 37
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class XpRewardService:
|
||||
"""
|
||||
Manages XP rewards for a guild, including storing, retrieving, and updating rewards in the database.
|
||||
"""
|
||||
|
||||
def __init__(self, guild_id: int) -> None:
|
||||
"""
|
||||
Initializes the XpRewardService with the guild ID and fetches rewards.
|
||||
|
||||
Args:
|
||||
guild_id (int): The ID of the guild.
|
||||
"""
|
||||
self.guild_id: int = guild_id
|
||||
self.rewards: Dict[int, Tuple[int, bool]] = self._fetch_rewards()
|
||||
|
||||
def _fetch_rewards(self) -> Dict[int, Tuple[int, bool]]:
|
||||
"""
|
||||
Retrieves the XP rewards for the guild from the database.
|
||||
|
||||
Returns:
|
||||
Dict[int, Tuple[int, bool]]: A dictionary of rewards with levels as keys and (role_id, persistent) as values.
|
||||
"""
|
||||
query: str = """
|
||||
SELECT level, role_id, persistent
|
||||
FROM level_rewards
|
||||
WHERE guild_id = %s
|
||||
ORDER BY level DESC
|
||||
"""
|
||||
data: List[Tuple[int, int, bool]] = database.select_query(
|
||||
query,
|
||||
(self.guild_id,),
|
||||
)
|
||||
return {level: (role_id, persistent) for level, role_id, persistent in data}
|
||||
|
||||
def add_reward(self, level: int, role_id: int, persistent: bool) -> None:
|
||||
"""
|
||||
Adds a new XP reward for the guild.
|
||||
|
||||
Args:
|
||||
level (int): The level at which the reward is given.
|
||||
role_id (int): The ID of the role to be awarded.
|
||||
persistent (bool): Whether the reward is persistent.
|
||||
|
||||
Raises:
|
||||
commands.BadArgument: If the server has more than 25 XP rewards.
|
||||
"""
|
||||
if len(self.rewards) >= 25:
|
||||
raise commands.BadArgument("A server can't have more than 25 XP rewards.")
|
||||
|
||||
query: str = """
|
||||
INSERT INTO level_rewards (guild_id, level, role_id, persistent)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE role_id = %s, persistent = %s;
|
||||
"""
|
||||
database.execute_query(
|
||||
query,
|
||||
(self.guild_id, level, role_id, persistent, role_id, persistent),
|
||||
)
|
||||
self.rewards[level] = (role_id, persistent)
|
||||
|
||||
def remove_reward(self, level: int) -> None:
|
||||
"""
|
||||
Removes an XP reward for the guild.
|
||||
|
||||
Args:
|
||||
level (int): The level at which the reward is to be removed.
|
||||
"""
|
||||
query: str = """
|
||||
DELETE FROM level_rewards
|
||||
WHERE guild_id = %s AND level = %s;
|
||||
"""
|
||||
database.execute_query(query, (self.guild_id, level))
|
||||
self.rewards.pop(level, None)
|
||||
|
||||
def get_role(self, level: int) -> Optional[int]:
|
||||
"""
|
||||
Retrieves the role ID for a given level.
|
||||
|
||||
Args:
|
||||
level (int): The level for which to retrieve the role ID.
|
||||
|
||||
Returns:
|
||||
Optional[int]: The role ID if found, otherwise None.
|
||||
"""
|
||||
return self.rewards.get(level, (None,))[0]
|
||||
|
||||
def should_replace_previous_reward(self, level: int) -> Tuple[Optional[int], bool]:
|
||||
"""
|
||||
Checks if the previous reward should be replaced based on the given level.
|
||||
|
||||
Args:
|
||||
level (int): The level to check for replacement.
|
||||
|
||||
Returns:
|
||||
Tuple[Optional[int], bool]: A tuple containing the previous reward and a boolean indicating if it should be replaced.
|
||||
"""
|
||||
previous_reward, replace = None, False
|
||||
if levels_below := [lvl for lvl in sorted(self.rewards) if lvl < level]:
|
||||
highest_level_below = max(levels_below)
|
||||
previous_reward, persistent = self.rewards[highest_level_below]
|
||||
replace = not persistent
|
||||
|
||||
return previous_reward, replace
|
|
@ -1,76 +0,0 @@
|
|||
{
|
||||
"months": [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December"
|
||||
],
|
||||
"birthday_messages": [
|
||||
"🎂 Happy Birthday, **{0}**! 🎉 Wishing you a day filled with joy and laughter.",
|
||||
"🎈 It's party time! Happy Birthday, **{0}**! 🎉",
|
||||
"🎉 Another year older, another year wiser! Happy Birthday, **{0}**! 🎂",
|
||||
"🌟 Today's the day you shine brighter than ever! Happy Birthday, **{0}**! 🌟",
|
||||
"🎁 Special day alert! It's **{0}**'s birthday! 🎁",
|
||||
"🎊 Hip, hip, hooray! It's **{0}**'s birthday today! 🎊",
|
||||
"🎂 Cake and confetti time! Happy Birthday, **{0}**! 🎉",
|
||||
"🌈 Sending you a rainbow of happiness on your birthday, **{0}**! 🎈",
|
||||
"🎉 Let's raise a toast to **{0}** on their birthday! Cheers to another fantastic year! 🥂",
|
||||
"🎈 Birthdays are like sprinkles on the cupcake of life! Happy Birthday, **{0}**! 🧁",
|
||||
"🎁 Gift-wrapped wishes for a wonderful birthday and an amazing year ahead, **{0}**! 🎁",
|
||||
"🎊 Time to blow out the candles and make a wish! Happy Birthday, **{0}**! 🎂",
|
||||
"🌟 It's your day to sparkle and shine, **{0}**! Happy Birthday! ✨",
|
||||
"🎈 May your birthday be as fabulous as you are, **{0}**! 🎉",
|
||||
"🎉 Here's to a year filled with success, happiness, and endless opportunities for **{0}**! Happy Birthday! 🥳",
|
||||
"🎁 Wishing **{0}** all the best on their special day! Happy Birthday! 🎁",
|
||||
"🎊 Another year of unforgettable memories begins today for **{0}**! Happy Birthday! 🎊",
|
||||
"🌟 Your birthday is the perfect excuse to pamper yourself, **{0}**! Enjoy your special day! 🎈",
|
||||
"🎂 Age is just a number, and you're looking more fabulous with each passing year, **{0}**! Happy Birthday! 💕",
|
||||
"🎉 Today, we celebrate the amazing person you are, **{0}**! Happy Birthday! 🎂",
|
||||
"🎈 Life's journey gets even more exciting as **{0}** celebrates another year of it! Happy Birthday! 🎉",
|
||||
"🌟 Happy Birthday to someone who makes every day brighter with their presence, **{0}**! 🌞",
|
||||
"🎁 May this birthday be the beginning of the most extraordinary year yet for **{0}**! 🚀",
|
||||
"🎊 Birthdays are nature's way of telling us to eat more cake! Enjoy your special treat, **{0}**! 🍰",
|
||||
"🎉 Time to pop the confetti and make some fabulous birthday memories, **{0}**! 🎂",
|
||||
"🎈 Today, the world received a gift in the form of **{0}**! Happy Birthday! 🎉",
|
||||
"🌟 Wishing **{0}** health, happiness, and all the things they desire on their birthday! 🎁",
|
||||
"🎂 Cheers to **{0}** on another year of being amazing! Happy Birthday! 🎉",
|
||||
"🎉 It's **{0}**'s big day, so let loose and enjoy every moment! Happy Birthday! 🎊",
|
||||
"🎈 Sending virtual hugs and lots of love to **{0}** on their special day! 🤗❤️",
|
||||
"🌟 On your birthday, the world becomes a better place because of your presence, **{0}**! 🎉",
|
||||
"🎁 As you blow out the candles, know that your wishes are heard and your dreams matter. Happy Birthday, **{0}**! 🌠",
|
||||
"🎊 Here's to a birthday filled with laughter, love, and all the things that make you happy, **{0}**! Cheers! 🥂",
|
||||
"🎂 May your birthday be filled with delightful surprises and sweet moments, **{0}**! Enjoy your special day! 🎈",
|
||||
"🎉 It's time for the world to celebrate the incredible person that is **{0}**! Happy Birthday! 🎊",
|
||||
"🎈 Another year, another chapter in the adventure of life for **{0}**! May this year be full of excitement and joy! 🌟",
|
||||
"🌟 May this birthday mark the beginning of extraordinary achievements and unforgettable memories for **{0}**! 🎁",
|
||||
"🎂 Here's to a birthday filled with love, happiness, and all the good things you deserve, **{0}**! 🎉",
|
||||
"🎉 Sending virtual confetti and a big smile to **{0}** on their birthday! Let's celebrate! 🎈",
|
||||
"🎈 Time to indulge in cake and celebrate the wonderful human that is **{0}**! Happy Birthday! 🎂",
|
||||
"🎊 Today, we honor the amazing journey of **{0}**'s life! Happy Birthday! 🌟",
|
||||
"🌟 Birthdays are a time for reflection, growth, and gratitude. Wishing **{0}** a wonderful birthday and year ahead! 🎁",
|
||||
"🎁 Another trip around the sun calls for a big celebration! Happy Birthday, **{0}**! 🎉",
|
||||
"🎂 As you blow out the candles, know that you are loved and cherished, **{0}**! Happy Birthday! 🎈",
|
||||
"🎉 It's a new age and a new opportunity to shine, **{0}**! May this year be your best yet! 🌟",
|
||||
"🎈 On your special day, may you be surrounded by love, happiness, and everything that brings you joy, **{0}**! 🎂",
|
||||
"🌟 Today, we celebrate **{0}** and the unique light they bring into the world! Happy Birthday! 🎉",
|
||||
"🎁 Wishing a fantastic birthday to the one and only **{0}**! May this day be filled with laughter and love! 🎈",
|
||||
"🎊 May your birthday be as extraordinary as the person you are, **{0}**! Cheers to you! 🥳",
|
||||
"🎂 You're not just a year older; you're a year more incredible! Happy Birthday, **{0}**! 🌟",
|
||||
"🎉 It's time to embrace the joy, love, and happiness that come with birthdays! Enjoy every moment, **{0}**! 🎈",
|
||||
"🎈 Another year, another chance to create beautiful memories. Happy Birthday, **{0}**! 🎂",
|
||||
"🌟 Your birthday is a reminder of how much you mean to all of us! Wishing you the best day ever, **{0}**! 🎁",
|
||||
"🎁 Sending warm birthday wishes and virtual hugs to **{0}** on their special day! 🤗❤️",
|
||||
"🎉 Today, we celebrate the unique and wonderful person that is **{0}**! Happy Birthday! 🎂",
|
||||
"🎈 Here's to a birthday filled with laughter, love, and all the things that make you smile, **{0}**! 🌟",
|
||||
"🎊 It's a day to be spoiled and celebrated, **{0}**! Wishing you the happiest of birthdays! 🎁",
|
||||
"🎂 As you turn another year older, know that you are loved and cherished, **{0}**! Happy Birthday! 🎉"
|
||||
]
|
||||
}
|
|
@ -1,79 +0,0 @@
|
|||
{
|
||||
"0-10": [
|
||||
"Behold, you've reached **Level {}**. Let's all try not to yawn too loudly.",
|
||||
"Congratulations on reaching **Level {}**. It's like leveling up, but without the fanfare.",
|
||||
"Congrats on reaching **Level {}**, you're slowly but surely ascending the ladder of \"success\"...",
|
||||
"Rejoice! You reached **Level {}**. It's time to throw a party with a side of meh.",
|
||||
"You've reached **Level {}**, where the bar is set low and the excitement is mild.",
|
||||
"Welcome to **Level {}**, the land of marginal achievements and faint praise.",
|
||||
"It's time to celebrate! You've unlocked the 'Slightly Better Than Before' achievement at **Level {}**.",
|
||||
"Congratulations on your promotion to **Level {}**. It's like climbing a tiny hill.",
|
||||
"At **Level {}**, you're steadily inching closer to the realm of almost impressive.",
|
||||
"You reached **Level {}**! Get ready for a ripple of apathetic applause.",
|
||||
"Alert! You reached **Level {}**. Don't worry, it's not that exciting.",
|
||||
"Rumor has it that reaching **Level {}** grants you the ability to mildly impress others.",
|
||||
"You got to **Level {}**! Prepare for a slightly raised eyebrow of acknowledgement.",
|
||||
"Congratulations on reaching **Level {}**. It's a modest achievement, to say the least.",
|
||||
"Congratulations on **Level {}**! You must be SO proud of yourself. \uD83D\uDE44",
|
||||
"You've reached **Level {}**! Your achievement is about as significant as a grain of sand.",
|
||||
"Congratulations on your ascent to **Level {}**. It's a small step for mankind.",
|
||||
"At **Level {}**, you're like a firework that fizzles out before it even begins.",
|
||||
"Oh, you've reached **Level {}**? Maybe it's time to see the sunlight.",
|
||||
"Wow, **Level {}**! How's the weather in your mom's basement?",
|
||||
"You've reached **Level {}**. Now go reach for a job application.",
|
||||
"Look at you, **Level {}**. You're really climbing that ladder to nowhere.",
|
||||
"You've hit **Level {}**. Your keyboard must be thrilled.",
|
||||
"Congrats on **Level {}**. Your social life, however, remains at Level 0.",
|
||||
"You've reached **Level {}**. But remember, in the game of life, you're still a beginner.",
|
||||
"You're now **Level {}**. I'd say 'get a life', but clearly, you've chosen Discord instead.",
|
||||
"You've achieved **Level {}**. Achievement unlocked: Professional Procrastinator.",
|
||||
"You're at **Level {}**. Do you also level up in avoiding responsibilities?",
|
||||
"You've reached **Level {}**. If only leveling up in real life was this easy, huh?",
|
||||
"You're now **Level {}**. If only your productivity levels matched your Lumi level."
|
||||
],
|
||||
"11-20": [
|
||||
"Congratulations motherfucker you leveled the fuck up to **Level {}**.",
|
||||
"levle **{}** cmoning in! Let's celbraet!",
|
||||
"yay you reach the level **{}** waw you are so cool many time",
|
||||
"reached **Level {}** but you'll never get on MY level HAAHAHAHAHA",
|
||||
"*elevator music* Welcome to **level {}**."
|
||||
],
|
||||
"21-40": [
|
||||
"**Level {}** 👍",
|
||||
"Look who's slacking off work to level up on Discord. **Level {}** and counting!",
|
||||
"**Level {}**? Have you considered that there might be an entire world outside of Discord?",
|
||||
"Wow, you've climbed to **level {}**. Is Discord your full-time job now?",
|
||||
"Oh look, it's **level {}**! Are you sure you're not secretly a bot in disguise?",
|
||||
"You've reached **level {}**. I hope you're using your Discord powers for good and not just spamming memes.",
|
||||
"**Level {}** and still going strong. Who needs a social life when you have Discord, right?",
|
||||
"Congratulations on leveling up to **level {}**. I hope Discord gives you a lifetime supply of virtual cookies.",
|
||||
"Look who's made it to **level {}**. I'm starting to think you're more Discord than human.",
|
||||
"Wow, **level {}**! Do you ever wonder if Discord should be paying you a salary at this point?",
|
||||
"Congratulations on reaching **level {}**. Your dedication to Discord is both awe-inspiring and mildly concerning.",
|
||||
"**Level {}**? I bet you have more Discord badges than real-life achievements."
|
||||
],
|
||||
"41-60": [
|
||||
"Well, well, well, **level {}**. Your Discord addiction is reaching legendary status.",
|
||||
"**Level {}**. If you don't stop leveling up, I might have to stage an intervention. Discord addiction is real!",
|
||||
"You've reached **Level {}**. Stop. Just stop. You've had enough of this app. Go away.",
|
||||
"Oh, look who's flexing their **Level {}** status. Don't strain a muscle.",
|
||||
"Congratulations on reaching **Level {}**. Are you trying to make the rest of us feel inadequate?",
|
||||
"Hats off to **Level {}**. Your dedication is truly admirable... or slightly concerning.",
|
||||
"Are you okay...? **Level {}** is seriously unhealthy bro. Sleep.",
|
||||
"STOP. LEVELING. LEAVE. ME. ALONE. Here's your damn level: **{}**",
|
||||
"HAS REACHED **LEVEL {}**, FUCK YEAH.",
|
||||
"**Level {}**. The second-hand embarrassment is real."
|
||||
],
|
||||
"61-100000": [
|
||||
"Lol it took you this long to reach **Level {}**.",
|
||||
"**{}**.",
|
||||
"**Level {}**???? Who are you? Gear?",
|
||||
"Yay you reached **Level {}**!! :3 UwU \uD83E\uDD8B \uD83D\uDDA4 (nobody cares)",
|
||||
"Conragulasions your level **{}** now.",
|
||||
"Hey man congrats on reaching **Level {}**. I mean it. GG.",
|
||||
"You reached **Level {}**!! What's it like being a loser?",
|
||||
"**Level {}**. BIG IF TRUE.",
|
||||
"CONGRATIONS LEVE **{}**",
|
||||
"Hahahahahahahahahhahahahaahahah. **Level {}**."
|
||||
]
|
||||
}
|
|
@ -1,296 +0,0 @@
|
|||
{
|
||||
"admin_award_description": "awarded **${0}** to {1}.",
|
||||
"admin_award_title": "Awarded Currency",
|
||||
"admin_blacklist_author": "User Blacklisted",
|
||||
"admin_blacklist_description": "user `{0}` has been blacklisted from Luminara.",
|
||||
"admin_blacklist_footer": "There is no process to reinstate a blacklisted user. Appeals are not considered.",
|
||||
"admin_sql_inject_description": "```sql\n{0}\n```",
|
||||
"admin_sql_inject_error_description": "```sql\n{0}\n```\n```\n{1}\n```",
|
||||
"admin_sql_inject_error_title": "SQL Query Error",
|
||||
"admin_sql_inject_title": "SQL Query Executed",
|
||||
"admin_sql_select_description": "```sql\nSELECT {0}\n```\n```\n{1}\n```",
|
||||
"admin_sql_select_error_description": "```sql\nSELECT {0}\n```\n```\n{1}\n```",
|
||||
"admin_sql_select_error_title": "SQL Select Query Error",
|
||||
"admin_sql_select_title": "SQL Select Query",
|
||||
"admin_sync_description": "command tree synced successfully.",
|
||||
"admin_sync_error_description": "An error occurred while syncing: {0}",
|
||||
"admin_sync_error_title": "Sync Error",
|
||||
"admin_sync_title": "Sync Successful",
|
||||
"bet_limit": "❌ | **{0}** you cannot place any bets above **${1}**.",
|
||||
"birthday_add_invalid_date": "The date you entered is invalid.",
|
||||
"birthday_add_success_author": "Birthday Set",
|
||||
"birthday_add_success_description": "your birthday has been set to **{0} {1}**.",
|
||||
"birthday_check_error": "Birthday announcement skipped processing user/guild {0}/{1} | {2}",
|
||||
"birthday_check_finished": "Daily birthday check finished. {0} birthdays processed. {1} birthdays failed.",
|
||||
"birthday_check_skipped": "Birthday announcements in guild with ID {0} skipped: no birthday channel.",
|
||||
"birthday_check_started": "Daily birthday check started.",
|
||||
"birthday_check_success": "Birthday announcement Success! user/guild/chan ID: {0}/{1}/{2}",
|
||||
"birthday_delete_success_author": "Birthday Deleted",
|
||||
"birthday_delete_success_description": "your birthday has been deleted from this server.",
|
||||
"birthday_leap_year": "February 29",
|
||||
"birthday_upcoming_author": "Upcoming Birthdays!",
|
||||
"birthday_upcoming_description_line": "🎂 {0} - {1}",
|
||||
"birthday_upcoming_no_birthdays": "there are no upcoming birthdays in this server.",
|
||||
"birthday_upcoming_no_birthdays_author": "No Upcoming Birthdays",
|
||||
"blackjack_bet": "Bet ${0}",
|
||||
"blackjack_busted": "Busted..",
|
||||
"blackjack_dealer_busted": "The dealer busted. You won!",
|
||||
"blackjack_dealer_hand": "**Dealer**\nScore: {0}\n*Hand: {1}*",
|
||||
"blackjack_dealer_hidden": "??",
|
||||
"blackjack_deck_shuffled": "deck shuffled",
|
||||
"blackjack_description": "You | Score: {0}\nDealer | Score: {1}",
|
||||
"blackjack_error": "I.. don't know if you won?",
|
||||
"blackjack_error_description": "This is an error, please report it.",
|
||||
"blackjack_footer": "Game finished",
|
||||
"blackjack_lost": "You lost **${0}**.",
|
||||
"blackjack_lost_generic": "You lost..",
|
||||
"blackjack_player_hand": "**You**\nScore: {0}\n*Hand: {1}*",
|
||||
"blackjack_title": "BlackJack",
|
||||
"blackjack_won_21": "You won with a score of 21!",
|
||||
"blackjack_won_natural": "You won with a natural hand!",
|
||||
"blackjack_won_payout": "You won **${0}**.",
|
||||
"blackjack_hit": "hit",
|
||||
"blackjack_stand": "stand",
|
||||
"boost_default_description": "Thanks for boosting, **{0}**!!",
|
||||
"boost_default_title": "New Booster",
|
||||
"case_case_field": "Case:",
|
||||
"case_case_field_value": "`{0}`",
|
||||
"case_duration_field": "Duration:",
|
||||
"case_duration_field_value": "`{0}`",
|
||||
"case_guild_cases_author": "All Cases in this Server",
|
||||
"case_guild_no_cases": "this server doesn't have any mod cases yet.",
|
||||
"case_guild_no_cases_author": "No Mod Cases",
|
||||
"case_mod_cases_author": "Cases by Moderator ({0})",
|
||||
"case_mod_no_cases": "this user has not handled any cases in this server.",
|
||||
"case_mod_no_cases_author": "No Mod Cases",
|
||||
"case_moderator_field": "Moderator:",
|
||||
"case_moderator_field_value": "`{0}`",
|
||||
"case_new_case_author": "New Case",
|
||||
"case_reason_field": "Reason:",
|
||||
"case_reason_field_value": "`{0}`",
|
||||
"case_reason_update_author": "Case Reason Updated",
|
||||
"case_reason_update_description": "case `{0}` reason has been updated.",
|
||||
"case_target_field": "Target:",
|
||||
"case_target_field_value": "`{0}` 🎯",
|
||||
"case_type_field": "Type:",
|
||||
"case_type_field_value": "`{0}`",
|
||||
"case_type_field_value_with_duration": "`{0} ({1})`",
|
||||
"config_author": "Server Configuration",
|
||||
"config_birthday_channel_set": "birthday announcements will be sent in {0}.",
|
||||
"config_birthday_module_already_disabled": "the birthday module was already disabled.",
|
||||
"config_birthday_module_disabled": "the birthday module was successfully disabled.",
|
||||
"config_boost_channel_set": "boost announcements will be sent in {0}.",
|
||||
"config_boost_image_field": "New Image URL:",
|
||||
"config_boost_image_original": "Original (default)",
|
||||
"config_boost_image_updated": "the boost image has been updated.",
|
||||
"config_boost_module_already_disabled": "the boost module was already disabled.",
|
||||
"config_boost_module_disabled": "the boost module was successfully disabled.",
|
||||
"config_boost_template_field": "New Template:",
|
||||
"config_boost_template_updated": "the boost message template has been updated.",
|
||||
"config_example_next_footer": "An example will be sent next.",
|
||||
"config_level_channel_set": "all level announcements will be sent in {0}.",
|
||||
"config_level_current_channel_set": "members will receive level announcements in their current channel.",
|
||||
"config_level_module_already_enabled": "the Lumi XP system was already enabled.",
|
||||
"config_level_module_disabled": "the Lumi XP system was successfully disabled.",
|
||||
"config_level_module_disabled_warning": "Warning: this module is disabled, please do '/config levels enable'",
|
||||
"config_level_module_enabled": "the Lumi XP system was successfully enabled.",
|
||||
"config_level_template": "Template:",
|
||||
"config_level_template_updated": "the level template was successfully updated.",
|
||||
"config_level_type_example": "Example:",
|
||||
"config_level_type_generic": "level announcements will be **generic messages**.",
|
||||
"config_level_type_generic_example": "📈 | **lucas** you have reached **Level 15**.",
|
||||
"config_level_type_whimsical": "level announcements will be **sarcastic comments**.",
|
||||
"config_level_type_whimsical_example": "📈 | **lucas** Lol it took you this long to reach **Level 15**.",
|
||||
"config_modlog_channel_set": "moderation logs will be sent in {0}.",
|
||||
"config_modlog_info_author": "Moderation Log Configuration",
|
||||
"config_modlog_info_commands_name": "📖 Case commands",
|
||||
"config_modlog_info_commands_value": "`/cases` - View all cases in this server\n`/case <case_id>` - View a specific case\n`/editcase <case_id> <new_reason>` - Update a case reason",
|
||||
"config_modlog_info_description": "This channel has been set as the moderation log channel for **{0}**. All moderation actions issued with Lumi will be logged here as cases.",
|
||||
"config_modlog_info_warning_name": "⚠️ Warning",
|
||||
"config_modlog_info_warning_value": "Changing the mod-log channel in the future will make old cases uneditable in this channel.",
|
||||
"config_modlog_permission_error": "I don't have perms to send messages in that channel. Please fix & try again.",
|
||||
"config_prefix_get": "the current prefix for this server is `{0}`",
|
||||
"config_prefix_set": "the prefix has been set to `{0}`",
|
||||
"config_prefix_too_long": "the prefix must be 25 characters or less.",
|
||||
"config_show_author": "{0} Configuration",
|
||||
"config_show_birthdays": "Birthdays",
|
||||
"config_show_boost_announcements": "Boost announcements",
|
||||
"config_show_default_enabled": "✅ Enabled (default)",
|
||||
"config_show_disabled": "❌ Disabled",
|
||||
"config_show_enabled": "✅ Enabled",
|
||||
"config_show_guide": "Guide: {0}",
|
||||
"config_show_level_announcements": "Level announcements",
|
||||
"config_show_moderation_log": "Moderation Log",
|
||||
"config_show_moderation_log_channel_deleted": "⚠️ **Not configured** (channel deleted?)",
|
||||
"config_show_moderation_log_enabled": "✅ {0}",
|
||||
"config_show_moderation_log_not_configured": "⚠️ **Not configured yet**",
|
||||
"config_show_new_member_greets": "New member greets",
|
||||
"config_welcome_channel_set": "I will announce new members in {0}.",
|
||||
"config_welcome_module_already_disabled": "the greeting module was already disabled.",
|
||||
"config_welcome_module_disabled": "the greeting module was successfully disabled.",
|
||||
"config_welcome_template_field": "New Template:",
|
||||
"config_welcome_template_updated": "the welcome message template has been updated.",
|
||||
"daily_already_claimed_author": "Already Claimed",
|
||||
"daily_already_claimed_description": "you can claim your daily reward again <t:{0}:R>.",
|
||||
"daily_already_claimed_footer": "Daily reset is at 7 AM EST",
|
||||
"daily_streak_footer": "You're on a streak of {0} days",
|
||||
"daily_success_claim_author": "Reward Claimed",
|
||||
"daily_success_claim_description": "you claimed your reward of **${0}**!",
|
||||
"default_level_up_message": "**{0}** you have reached **Level {1}**.",
|
||||
"error_actionable_hierarchy_bot": "I don't have permission to perform this action on this user due to role hierarchy.",
|
||||
"error_actionable_hierarchy_user": "you don't have permission to perform this action on this user due to role hierarchy.",
|
||||
"error_actionable_self": "you can't perform this action on yourself.",
|
||||
"error_already_playing_blackjack": "you already have a game of blackjack running.",
|
||||
"error_bad_argument_author": "Bad Argument",
|
||||
"error_bad_argument_description": "{0}",
|
||||
"error_birthdays_disabled_author": "Birthdays Disabled",
|
||||
"error_birthdays_disabled_description": "birthdays are disabled in this server.",
|
||||
"error_birthdays_disabled_footer": "Contact a mod to enable them.",
|
||||
"error_blackjack_game_error": "something went wrong while playing blackjack.",
|
||||
"error_boost_image_url_invalid": "the image URL must end with `.jpg` or `.png`.",
|
||||
"error_bot_missing_permissions_author": "Bot Missing Permissions",
|
||||
"error_bot_missing_permissions_description": "Lumi lacks the required permissions to run this command.",
|
||||
"error_command_cooldown_author": "Command Cooldown",
|
||||
"error_command_cooldown_description": "try again in **{0:02d}:{1:02d}**.",
|
||||
"error_command_not_found": "No command called \"{0}\" found.",
|
||||
"error_image_url_invalid": "invalid image URL.",
|
||||
"error_invalid_bet": "the bet you entered is invalid.",
|
||||
"error_invalid_duration": "Invalid duration: {0}",
|
||||
"error_invalid_duration_author": "Invalid Duration",
|
||||
"error_invalid_duration_description": "Please provide a valid duration between 1 minute and 30 days.",
|
||||
"error_lumi_exception_author": "Lumi Exception",
|
||||
"error_lumi_exception_description": "{0}",
|
||||
"error_missing_permissions_author": "Missing Permissions",
|
||||
"error_missing_permissions_description": "you lack the required permissions to run this command.",
|
||||
"error_no_case_found_author": "Case Not Found",
|
||||
"error_no_case_found_description": "no case found with that ID.",
|
||||
"error_no_private_message_author": "Guild Only",
|
||||
"error_no_private_message_description": "this command can only be used in servers.",
|
||||
"error_not_enough_cash": "you don't have enough cash.",
|
||||
"error_not_owner_author": "Owner Only",
|
||||
"error_not_owner_description": "this command requires Lumi ownership permissions.",
|
||||
"error_out_of_time": "you ran out of time.",
|
||||
"error_out_of_time_economy": "you ran out of time. Your bet was forfeited.",
|
||||
"error_private_message_only_author": "Private Message Only",
|
||||
"error_private_message_only_description": "this command can only be used in private messages.",
|
||||
"error_unknown_error_author": "Unknown Error",
|
||||
"error_unknown_error_description": "an unknown error occurred. Please try again later.",
|
||||
"greet_default_description": "_ _\n**Welcome** to **{0}**",
|
||||
"greet_template_description": "↓↓↓\n{0}",
|
||||
"help_use_prefix": "Please use Lumi's prefix to get help. Type `{0}help`",
|
||||
"info_api_version": "**API:** v{0}\n",
|
||||
"info_database_records": "**Database:** {0} records",
|
||||
"info_latency": "**Latency:** {0}ms\n",
|
||||
"info_memory": "**Memory:** {0:.2f} MB\n",
|
||||
"info_service_footer": "Info Service",
|
||||
"info_system": "**System:** {0} ({1})\n",
|
||||
"info_uptime": "**Uptime:** <t:{0}:R>\n",
|
||||
"intro_content": "Introduction by {0}",
|
||||
"intro_content_footer": "Type .intro in my DMs to start",
|
||||
"intro_no_channel": "the introduction channel is not set, please contact a moderator.",
|
||||
"intro_no_channel_author": "Channel Not Set",
|
||||
"intro_no_guild": "you're not in a server that supports introductions.",
|
||||
"intro_no_guild_author": "Server Not Supported",
|
||||
"intro_post_confirmation": "your introduction has been posted in {0}!",
|
||||
"intro_preview_field": "**{0}:** {1}\n\n",
|
||||
"intro_question_footer": "Type your answer below.",
|
||||
"intro_service_name": "Introduction Service",
|
||||
"intro_start": "this command will serve as a questionnaire for your entry to {0}. Please keep your answers \"PG-13\" and don't abuse this command.",
|
||||
"intro_start_footer": "Click the button below to start",
|
||||
"intro_stopped": "the introduction command was stopped.",
|
||||
"intro_stopped_author": "Introduction Stopped",
|
||||
"intro_timeout": "you took too long to answer the question, please try again.",
|
||||
"intro_timeout_author": "Timeout",
|
||||
"intro_too_long": "your answer was too long, please keep it below 200 characters.",
|
||||
"intro_too_long_author": "Answer Too Long",
|
||||
"invite_button_text": "Invite Lumi",
|
||||
"invite_description": "Thanks for inviting me to your server!",
|
||||
"level_up": "📈 | **{0}** you have reached **Level {1}**.",
|
||||
"level_up_prefix": "📈 | **{0}** ",
|
||||
"lumi_exception_blacklisted": "User is blacklisted",
|
||||
"lumi_exception_generic": "An error occurred.",
|
||||
"mod_ban_dm": "**{0}** you have been banned from `{1}`.\n\n**Reason:** `{2}`",
|
||||
"mod_banned_author": "User Banned",
|
||||
"mod_banned_user": "user with ID `{0}` has been banned.",
|
||||
"mod_dm_not_sent": "Failed to notify them in DM",
|
||||
"mod_dm_sent": "notified them in DM",
|
||||
"mod_kick_dm": "**{0}** you have been kicked from `{1}`.\n\n**Reason:** `{2}`",
|
||||
"mod_kicked_author": "User Kicked",
|
||||
"mod_kicked_user": "user `{0}` has been kicked.",
|
||||
"mod_no_reason": "No reason provided.",
|
||||
"mod_not_banned": "user with ID `{0}` is not banned.",
|
||||
"mod_not_banned_author": "User Not Banned",
|
||||
"mod_not_timed_out": "user `{0}` is not timed out.",
|
||||
"mod_not_timed_out_author": "User Not Timed Out",
|
||||
"mod_reason": "Moderator: {0} | Reason: {1}",
|
||||
"mod_softban_dm": "**{0}** you have been softbanned from `{1}`.\n\n**Reason:** `{2}`",
|
||||
"mod_softban_unban_reason": "Softban by {0}",
|
||||
"mod_softbanned_author": "User Softbanned",
|
||||
"mod_softbanned_user": "user `{0}` has been softbanned.",
|
||||
"mod_timed_out_author": "User Timed Out",
|
||||
"mod_timed_out_user": "user `{0}` has been timed out.",
|
||||
"mod_timeout_dm": "**{0}** you have been timed out in `{1}` for `{2}`.\n\n**Reason:** `{3}`",
|
||||
"mod_unbanned": "user with ID `{0}` has been unbanned.",
|
||||
"mod_unbanned_author": "User Unbanned",
|
||||
"mod_untimed_out": "timeout has been removed for user `{0}`.",
|
||||
"mod_untimed_out_author": "User Timeout Removed",
|
||||
"mod_warn_dm": "**{0}** you have been warned in `{1}`.\n\n**Reason:** `{2}`",
|
||||
"mod_warned_author": "User Warned",
|
||||
"mod_warned_user": "user `{0}` has been warned.",
|
||||
"ping_author": "I'm online!",
|
||||
"ping_footer": "Latency: {0}ms",
|
||||
"ping_pong": "Pong!",
|
||||
"ping_uptime": "I've been online since <t:{0}:R>.",
|
||||
"stats_blackjack": "🃏 | You've played **{0}** games of BlackJack, betting a total of **${1}**. You won **{2}** of those games with a total payout of **${3}**.",
|
||||
"stats_slots": "🎰 | You've played **{0}** games of Slots, betting a total of **${1}**. Your total payout was **${2}**.",
|
||||
"trigger_already_exists": "Failed to add custom reaction. This text already contains another trigger. To avoid unexpected behavior, please delete it before adding a new one.",
|
||||
"trigger_limit_reached": "Failed to add custom reaction. You have reached the limit of 100 custom reactions for this server.",
|
||||
"triggers_add_author": "Custom Reaction Created",
|
||||
"triggers_add_description": "**Trigger Text:** `{0}`\n**Reaction Type:** {1}\n**Full Match:** `{2}`\n",
|
||||
"triggers_add_emoji_details": "**Emoji ID:** `{0}`",
|
||||
"triggers_add_text_details": "**Response:** `{0}`",
|
||||
"triggers_delete_author": "Custom Reaction Deleted",
|
||||
"triggers_delete_description": "custom reaction has been successfully deleted.",
|
||||
"triggers_delete_not_found_author": "Custom Reaction Not Found",
|
||||
"triggers_list_custom_reaction_id": "**ID:** {0}",
|
||||
"triggers_list_custom_reactions_title": "Custom Reactions",
|
||||
"triggers_list_emoji_id": "**Emoji ID:** `{0}`",
|
||||
"triggers_list_full_match": "**Full Match:** `{0}`",
|
||||
"triggers_list_reaction_type": "**Reaction Type:** {0}",
|
||||
"triggers_list_response": "**Response:** `{0}`",
|
||||
"triggers_list_trigger_text": "**Trigger Text:** `{0}`",
|
||||
"triggers_list_usage_count": "**Usage Count:** `{0}`",
|
||||
"triggers_no_reactions_description": "There are no custom reactions set up yet.\n\nTo create a new custom reaction, use the following commands:\n`/trigger add emoji` - Add a new custom emoji reaction.\n`/trigger add response` - Add a new custom text reaction.\n\n**Emoji Reaction:**\nAn emoji reaction will react with a specific emoji when the trigger text is detected.\n\n**Text Reaction:**\nA text reaction will respond with a specific text message when the trigger text is detected.",
|
||||
"triggers_no_reactions_title": "No Custom Reactions Found",
|
||||
"triggers_not_added": "failed to add custom reaction. Please try again.",
|
||||
"triggers_not_deleted": "something went wrong while trying to delete this trigger.",
|
||||
"triggers_not_found": "no custom reaction found with the provided ID.",
|
||||
"triggers_reaction_service_footer": "Reaction Service",
|
||||
"triggers_type_emoji": "Emoji",
|
||||
"triggers_type_text": "Text",
|
||||
"xkcd_description": "[Explainxkcd]({0}) | [Webpage]({1})",
|
||||
"xkcd_footer": "Xkcd Service",
|
||||
"xkcd_not_found": "failed to fetch this comic.",
|
||||
"xkcd_not_found_author": "Comic Not Found",
|
||||
"xkcd_title": "Xkcd {0} - {1}",
|
||||
"xp_lb_author": "Level Leaderboard",
|
||||
"xp_lb_cant_use_dropdown": "You can't use this menu, it's someone else's.",
|
||||
"xp_lb_currency_author": "Currency Leaderboard",
|
||||
"xp_lb_currency_field_value": "cash: **${0}**",
|
||||
"xp_lb_dailies_author": "Daily Streak Leaderboard",
|
||||
"xp_lb_dailies_field_value": "highest streak: **{0}**\nclaimed on: `{1}`",
|
||||
"xp_lb_field_name": "#{0} - {1}",
|
||||
"xp_lb_field_value": "level: **{0}**\nxp: `{1}/{2}`",
|
||||
"xp_level": "Level {0}",
|
||||
"xp_progress": "Progress to next level",
|
||||
"xp_server_rank": "Server Rank: #{0}",
|
||||
"balance_cash": "**Cash**: ${0}",
|
||||
"balance_author": "{0}'s wallet",
|
||||
"balance_footer": "check out /daily",
|
||||
"give_error_self": "you can't give money to yourself.",
|
||||
"give_error_bot": "you can't give money to a bot.",
|
||||
"give_error_invalid_amount": "invalid amount.",
|
||||
"give_error_insufficient_funds": "you don't have enough cash.",
|
||||
"give_success": "**{0}** gave **${1}** to {2}.",
|
||||
"error_cant_use_buttons": "You can't use these buttons, they're someone else's!"
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
---
|
||||
info:
|
||||
title: Luminara
|
||||
author: wlinator
|
||||
license: GNU General Public License v3.0
|
||||
version: "2.9.0" # "Settings & Customizability" update
|
||||
repository_url: https://git.wlinator.org/Luminara/Lumi
|
||||
|
||||
images:
|
||||
allowed_image_extensions:
|
||||
- .jpg
|
||||
- .png
|
||||
birthday_gif_url: https://media1.tenor.com/m/NXvU9jbBUGMAAAAC/fireworks.gif
|
||||
|
||||
colors:
|
||||
color_default: 0xFF8C00
|
||||
color_warning: 0xFF7600
|
||||
color_error: 0xFF4500
|
||||
|
||||
economy:
|
||||
daily_reward: 500
|
||||
blackjack_multiplier: 1.4
|
||||
blackjack_hit_emoji: <:hit:1119262723285467156>
|
||||
blackjack_stand_emoji: <:stand:1118923298298929154>
|
||||
slots_multipliers:
|
||||
pair: 1.5
|
||||
three_of_a_kind: 4
|
||||
three_diamonds: 6
|
||||
jackpot: 15
|
||||
|
||||
art:
|
||||
fetch_url: https://git.wlinator.org/Luminara/Art/raw/branch/main/
|
||||
logo:
|
||||
opaque: lumi_logo.png
|
||||
transparent: lumi_logo_transparent.png
|
||||
icons:
|
||||
boost: lumi_boost.png
|
||||
check: lumi_check.png
|
||||
cross: lumi_cross.png
|
||||
exclaim: lumi_exclaim.png
|
||||
hammer: lumi_hammer.png
|
||||
money_bag: lumi_money_bag.png
|
||||
money_coins: lumi_money_coins.png
|
||||
question: lumi_question.png
|
||||
streak: lumi_streak.png
|
||||
streak_bronze: lumi_streak_bronze.png
|
||||
streak_gold: lumi_streak_gold.png
|
||||
streak_silver: lumi_streak_silver.png
|
||||
warning: lumi_warning.png
|
||||
juicybblue:
|
||||
flowers: https://i.imgur.com/79XfsbS.png
|
||||
teapot: https://i.imgur.com/wFsgSnr.png
|
||||
muffin: https://i.imgur.com/hSauh7K.png
|
||||
other:
|
||||
cloud: https://i.imgur.com/rc68c43.png
|
||||
trophy: https://i.imgur.com/dvIIr2G.png
|
||||
|
||||
emotes:
|
||||
guild_id: 1038051105642401812
|
||||
emote_ids:
|
||||
slots_animated_id: 1119262805309259776
|
||||
slots_0_id: 1119262803816095825
|
||||
slots_1_id: 1119262801261760592
|
||||
slots_2_id: 1119262800049614939
|
||||
slots_3_id: 1119262796497039510
|
||||
slots_4_id: 1119262794676715681
|
||||
slots_5_id: 1119262792386621555
|
||||
slots_6_id: 1119262791061229669
|
||||
S_Wide: 1119286730302955651
|
||||
L_Wide: 1119286763802857533
|
||||
O_Wide: 1119286787169329203
|
||||
T_Wide: 1119286804634406942
|
||||
CBorderBLeft: 1119286973572595712
|
||||
CBorderBRight: 1119286918459445408
|
||||
CBorderTLeft: 1119287006464331806
|
||||
CBorderTRight: 1119286865284051035
|
||||
HBorderB: 1119286936155213835
|
||||
HBorderT: 1119287027662344322
|
||||
VBorder: 1119286889854279680
|
||||
WSmall: 1119288536282173490
|
||||
ISmall: 1119288552673517608
|
||||
NSmall: 1119288579382857830
|
||||
LCentered: 1119287296127156325
|
||||
OCentered: 1119287563245584394
|
||||
SCentered: 1119287327588634647
|
||||
ECentered: 1119287343833165945
|
||||
Blank: 1119287267001905283
|
||||
lost: 1119288454212243607
|
||||
|
||||
introductions:
|
||||
intro_guild_id: 719227135151046700
|
||||
intro_channel_id: 973619250507972600
|
||||
intro_question_mapping:
|
||||
Nickname: How would you like to be identified in the server?
|
||||
Age: How old are you?
|
||||
Region: Where do you live?
|
||||
Languages: Which languages do you speak?
|
||||
Pronouns: What are your preferred pronouns?
|
||||
Sexuality: What's your sexuality?
|
||||
Relationship status: What's your current relationship status?
|
||||
Likes & interests: Likes & interests
|
||||
Dislikes: Dislikes
|
||||
EXTRAS: "EXTRAS: job status, zodiac sign, hobbies, etc. Tell us about yourself!"
|
Loading…
Reference in a new issue