1
Fork 0
mirror of https://github.com/wlinator/luminara.git synced 2024-10-02 14:03:13 +00:00

Huge refactor

This commit is contained in:
wlinator 2024-08-29 04:48:14 -04:00
parent 3c6bc711d6
commit f87f8a0b39
46 changed files with 853 additions and 516 deletions

View file

@ -22,4 +22,4 @@ RUN rm -rf .venv
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
CMD [ "poetry", "run", "python", "-OO", "./main.py" ]
CMD [ "poetry", "run", "python", "-O", "./main.py" ]

0
db/__init__.py Normal file
View file

View file

@ -1,6 +1,7 @@
import os
import pathlib
import re
from typing import Any
import mysql.connector
from loguru import logger
@ -27,37 +28,33 @@ 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
raise
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 execute_query(query: str, values: tuple[Any, ...] | None = None) -> None:
with _cnxpool.get_connection() as conn, 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(query: str, values: tuple[Any, ...] | None = None) -> list[Any]:
with _cnxpool.get_connection() as conn, 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_one(query: str, values: tuple[Any, ...] | None = None) -> Any | None:
with _cnxpool.get_connection() as conn, 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 select_query_dict(query: str, values: tuple[Any, ...] | None = None) -> list[dict[str, Any]]:
with _cnxpool.get_connection() as conn, conn.cursor(dictionary=True) as cursor:
cursor.execute(query, values)
return cursor.fetchall()
def run_migrations():
@ -66,10 +63,9 @@ def run_migrations():
[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("""
with _cnxpool.get_connection() as conn, 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,
@ -77,39 +73,38 @@ def run_migrations():
)
""")
for migration_file in migration_files:
# Check if migration has already been applied
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(migrations_dir) / migration_file
migration_sql = migration_sql.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(
"SELECT COUNT(*) FROM migrations WHERE filename = %s",
"INSERT INTO migrations (filename) VALUES (%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
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.success("All database migrations completed.")

View file

0
handlers/__init__.py Normal file
View file

View file

@ -1,20 +1,20 @@
import sys
import traceback
from typing import Any
from discord.ext import commands
from discord.ext.commands import Cog
from loguru import logger
from lib import exceptions
from lib.const import CONST
from ui.embeds import builder
from lib import exceptions as LumiExceptions
from ui.embeds import Builder
async def handle_error(
ctx: commands.Context[commands.Bot],
error: commands.CommandError | commands.CheckFailure,
) -> None:
if isinstance(error, (commands.CommandNotFound, LumiExceptions.Blacklisted)):
if isinstance(error, commands.CommandNotFound | exceptions.Blacklisted):
return
author_text = None
@ -22,11 +22,7 @@ async def handle_error(
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):
if isinstance(error, commands.MissingRequiredArgument | commands.BadArgument):
author_text = CONST.STRINGS["error_bad_argument_author"]
description = CONST.STRINGS["error_bad_argument_description"].format(str(error))
@ -58,12 +54,12 @@ async def handle_error(
author_text = CONST.STRINGS["error_private_message_only_author"]
description = CONST.STRINGS["error_private_message_only_description"]
elif isinstance(error, LumiExceptions.BirthdaysDisabled):
elif isinstance(error, exceptions.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):
elif isinstance(error, exceptions.LumiException):
author_text = CONST.STRINGS["error_lumi_exception_author"]
description = CONST.STRINGS["error_lumi_exception_description"].format(
str(error),
@ -74,7 +70,7 @@ async def handle_error(
description = CONST.STRINGS["error_unknown_error_description"]
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=author_text,
@ -85,7 +81,7 @@ async def handle_error(
)
async def on_error(event: str, *args, **kwargs) -> None:
async def on_error(event: str, *args: Any, **kwargs: Any) -> None:
logger.exception(
f"on_error INFO: errors.event.{event} | '*args': {args} | '**kwargs': {kwargs}",
)
@ -93,20 +89,25 @@ async def on_error(event: str, *args, **kwargs) -> None:
traceback.print_exc()
class ErrorHandler(Cog):
class ErrorHandler(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@staticmethod
async def log_command_error(ctx, error, command_type):
log_msg = (
f"{ctx.author.name} executed {command_type}{ctx.command.qualified_name}"
)
async def log_command_error(
ctx: commands.Context[commands.Bot],
error: commands.CommandError | commands.CheckFailure,
command_type: str,
) -> None:
if ctx.command is None:
return
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.exception(f"{log_msg} | FAILED: {error}")
@Cog.listener()
@commands.Cog.listener()
async def on_command_error(
self,
ctx: commands.Context[commands.Bot],
@ -119,7 +120,7 @@ class ErrorHandler(Cog):
logger.exception(f"Error in on_command_error: {e}")
traceback.print_exc()
@Cog.listener()
@commands.Cog.listener()
async def on_app_command_error(
self,
ctx: commands.Context[commands.Bot],
@ -132,8 +133,8 @@ class ErrorHandler(Cog):
logger.exception(f"Error in on_app_command_error: {e}")
traceback.print_exc()
@Cog.listener()
async def on_error(self, event: str, *args, **kwargs) -> None:
@commands.Cog.listener()
async def on_error(self, event: str, *args: Any, **kwargs: Any) -> None:
await on_error(event, *args, **kwargs)

0
lib/__init__.py Normal file
View file

View file

@ -1,15 +1,18 @@
import asyncio
from typing import Any
from discord.ext import commands
from loguru import logger
import asyncio
from lib.loader import CogLoader
from db.database import run_migrations
from lib.loader import CogLoader
class Luminara(commands.Bot):
def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
self.is_shutting_down: bool = False
self.setup_task: asyncio.Task = asyncio.create_task(self.setup())
self.setup_task: asyncio.Task[None] = asyncio.create_task(self.setup())
self.strip_after_prefix = True
self.case_insensitive = True
@ -47,9 +50,7 @@ class Luminara(commands.Bot):
await self.close()
if tasks := [
task for task in asyncio.all_tasks() if task is not asyncio.current_task()
]:
if tasks := [task for task in asyncio.all_tasks() if task is not asyncio.current_task()]:
logger.debug(f"Cancelling {len(tasks)} outstanding tasks.")
for task in tasks:

View file

@ -1,127 +1,132 @@
import os
import json
import os
from collections.abc import Callable
from pathlib import Path
from typing import Any, Final
import yaml
from functools import lru_cache
from typing import Optional, Callable, Set, List, Dict
class _parser:
class Parser:
"""Internal parses class. Not intended to be used outside of this module."""
@lru_cache(maxsize=1024)
def read_s(self) -> dict:
return self._read_file("settings.yaml", yaml.safe_load)
def __init__(self):
self._cache: dict[str, Any] = {}
def read_json(self, path: str) -> dict:
return self._read_file(f"locales/{path}.json", json.load)
def read_s(self) -> dict[str, Any]:
if "settings" not in self._cache:
self._cache["settings"] = self._read_file("settings.yaml", yaml.safe_load)
return self._cache["settings"]
def _read_file(self, file_path: str, load_func: Callable) -> dict:
with open(file_path) as file:
def read_json(self, path: str) -> dict[str, Any]:
cache_key = f"json_{path}"
if cache_key not in self._cache:
self._cache[cache_key] = self._read_file(f"locales/{path}.json", json.load)
return self._cache[cache_key]
def _read_file(self, file_path: str, load_func: Callable[[Any], dict[str, Any]]) -> dict[str, Any]:
with Path(file_path).open() as file:
return load_func(file)
class _constants:
_p = _parser()
_s = _parser().read_s()
class Constants:
_p: Final = Parser()
_s: Final = Parser().read_s()
# bot credentials
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")
TOKEN: Final[str | None] = os.environ.get("TOKEN")
INSTANCE: Final[str | None] = os.environ.get("INSTANCE")
XP_GAIN_PER_MESSAGE: Final[int] = int(os.environ.get("XP_GAIN_PER_MESSAGE", 1))
XP_GAIN_COOLDOWN: Final[int] = int(os.environ.get("XP_GAIN_COOLDOWN", 8))
DBX_TOKEN: Final[str | None] = os.environ.get("DBX_OAUTH2_REFRESH_TOKEN")
DBX_APP_KEY: Final[str | None] = os.environ.get("DBX_APP_KEY")
DBX_APP_SECRET: Final[str | None] = os.environ.get("DBX_APP_SECRET")
MARIADB_USER: Final[str | None] = os.environ.get("MARIADB_USER")
MARIADB_PASSWORD: Final[str | None] = os.environ.get("MARIADB_PASSWORD")
MARIADB_ROOT_PASSWORD: Final[str | None] = os.environ.get("MARIADB_ROOT_PASSWORD")
MARIADB_DATABASE: Final[str | None] = os.environ.get("MARIADB_DATABASE")
OWNER_IDS: Optional[Set[int]] = (
{int(id.strip()) for id in os.environ.get("OWNER_IDS", "").split(",") if id}
OWNER_IDS: Final[set[int] | None] = (
{int(owner_id.strip()) for owner_id in os.environ.get("OWNER_IDS", "").split(",") if owner_id}
if "OWNER_IDS" in os.environ
else None
)
# metadata
TITLE: str = _s["info"]["title"]
AUTHOR: str = _s["info"]["author"]
LICENSE: str = _s["info"]["license"]
VERSION: str = _s["info"]["version"]
REPO_URL: str = _s["info"]["repository_url"]
INVITE_URL: str = _s["info"]["invite_url"]
TITLE: Final[str] = _s["info"]["title"]
AUTHOR: Final[str] = _s["info"]["author"]
LICENSE: Final[str] = _s["info"]["license"]
VERSION: Final[str] = _s["info"]["version"]
REPO_URL: Final[str] = _s["info"]["repository_url"]
INVITE_URL: Final[str] = _s["info"]["invite_url"]
# loguru
LOG_LEVEL: str = _s["logs"]["level"] or "DEBUG"
LOG_FORMAT: str = _s["logs"]["format"]
LOG_LEVEL: Final[str] = _s["logs"]["level"] or "DEBUG"
LOG_FORMAT: Final[str] = _s["logs"]["format"]
# cogs
COG_IGNORE_LIST: Set[str] = (
set(_s["cogs"]["ignore"]) if _s["cogs"]["ignore"] else set()
)
COG_IGNORE_LIST: Final[set[str]] = set(_s["cogs"]["ignore"]) if _s["cogs"]["ignore"] else set()
# images
ALLOWED_IMAGE_EXTENSIONS: List[str] = _s["images"]["allowed_image_extensions"]
BIRTHDAY_GIF_URL: str = _s["images"]["birthday_gif_url"]
ALLOWED_IMAGE_EXTENSIONS: Final[list[str]] = _s["images"]["allowed_image_extensions"]
BIRTHDAY_GIF_URL: Final[str] = _s["images"]["birthday_gif_url"]
# colors
COLOR_DEFAULT: int = _s["colors"]["color_default"]
COLOR_WARNING: int = _s["colors"]["color_warning"]
COLOR_ERROR: int = _s["colors"]["color_error"]
COLOR_DEFAULT: Final[int] = _s["colors"]["color_default"]
COLOR_WARNING: Final[int] = _s["colors"]["color_warning"]
COLOR_ERROR: Final[int] = _s["colors"]["color_error"]
# economy
DAILY_REWARD: int = _s["economy"]["daily_reward"]
BLACKJACK_MULTIPLIER: float = _s["economy"]["blackjack_multiplier"]
BLACKJACK_HIT_EMOJI: str = _s["economy"]["blackjack_hit_emoji"]
BLACKJACK_STAND_EMOJI: str = _s["economy"]["blackjack_stand_emoji"]
SLOTS_MULTIPLIERS: Dict[str, float] = _s["economy"]["slots_multipliers"]
DAILY_REWARD: Final[int] = _s["economy"]["daily_reward"]
BLACKJACK_MULTIPLIER: Final[float] = _s["economy"]["blackjack_multiplier"]
BLACKJACK_HIT_EMOJI: Final[str] = _s["economy"]["blackjack_hit_emoji"]
BLACKJACK_STAND_EMOJI: Final[str] = _s["economy"]["blackjack_stand_emoji"]
SLOTS_MULTIPLIERS: Final[dict[str, float]] = _s["economy"]["slots_multipliers"]
# art from git repository
_fetch_url: str = _s["art"]["fetch_url"]
_fetch_url: Final[str] = _s["art"]["fetch_url"]
LUMI_LOGO_OPAQUE: str = _fetch_url + _s["art"]["logo"]["opaque"]
LUMI_LOGO_TRANSPARENT: str = _fetch_url + _s["art"]["logo"]["transparent"]
BOOST_ICON: str = _fetch_url + _s["art"]["icons"]["boost"]
CHECK_ICON: str = _fetch_url + _s["art"]["icons"]["check"]
CROSS_ICON: str = _fetch_url + _s["art"]["icons"]["cross"]
EXCLAIM_ICON: str = _fetch_url + _s["art"]["icons"]["exclaim"]
INFO_ICON: str = _fetch_url + _s["art"]["icons"]["info"]
HAMMER_ICON: str = _fetch_url + _s["art"]["icons"]["hammer"]
MONEY_BAG_ICON: str = _fetch_url + _s["art"]["icons"]["money_bag"]
MONEY_COINS_ICON: str = _fetch_url + _s["art"]["icons"]["money_coins"]
QUESTION_ICON: str = _fetch_url + _s["art"]["icons"]["question"]
STREAK_ICON: str = _fetch_url + _s["art"]["icons"]["streak"]
STREAK_BRONZE_ICON: str = _fetch_url + _s["art"]["icons"]["streak_bronze"]
STREAK_GOLD_ICON: str = _fetch_url + _s["art"]["icons"]["streak_gold"]
STREAK_SILVER_ICON: str = _fetch_url + _s["art"]["icons"]["streak_silver"]
WARNING_ICON: str = _fetch_url + _s["art"]["icons"]["warning"]
LUMI_LOGO_OPAQUE: Final[str] = _fetch_url + _s["art"]["logo"]["opaque"]
LUMI_LOGO_TRANSPARENT: Final[str] = _fetch_url + _s["art"]["logo"]["transparent"]
BOOST_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["boost"]
CHECK_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["check"]
CROSS_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["cross"]
EXCLAIM_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["exclaim"]
INFO_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["info"]
HAMMER_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["hammer"]
MONEY_BAG_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["money_bag"]
MONEY_COINS_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["money_coins"]
QUESTION_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["question"]
STREAK_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["streak"]
STREAK_BRONZE_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["streak_bronze"]
STREAK_GOLD_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["streak_gold"]
STREAK_SILVER_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["streak_silver"]
WARNING_ICON: Final[str] = _fetch_url + _s["art"]["icons"]["warning"]
# art from imgur
FLOWERS_ART: str = _s["art"]["juicybblue"]["flowers"]
TEAPOT_ART: str = _s["art"]["juicybblue"]["teapot"]
MUFFIN_ART: str = _s["art"]["juicybblue"]["muffin"]
CLOUD_ART: str = _s["art"]["other"]["cloud"]
TROPHY_ART: str = _s["art"]["other"]["trophy"]
FLOWERS_ART: Final[str] = _s["art"]["juicybblue"]["flowers"]
TEAPOT_ART: Final[str] = _s["art"]["juicybblue"]["teapot"]
MUFFIN_ART: Final[str] = _s["art"]["juicybblue"]["muffin"]
CLOUD_ART: Final[str] = _s["art"]["other"]["cloud"]
TROPHY_ART: Final[str] = _s["art"]["other"]["trophy"]
# emotes
EMOTES_SERVER_ID: int = _s["emotes"]["guild_id"]
EMOTE_IDS: Dict[str, int] = _s["emotes"]["emote_ids"]
EMOTES_SERVER_ID: Final[int] = _s["emotes"]["guild_id"]
EMOTE_IDS: Final[dict[str, int]] = _s["emotes"]["emote_ids"]
# introductions (currently only usable in ONE guild)
INTRODUCTIONS_GUILD_ID: int = _s["introductions"]["intro_guild_id"]
INTRODUCTIONS_CHANNEL_ID: int = _s["introductions"]["intro_channel_id"]
INTRODUCTIONS_QUESTION_MAPPING: Dict[str, str] = _s["introductions"][
"intro_question_mapping"
]
INTRODUCTIONS_GUILD_ID: Final[int] = _s["introductions"]["intro_guild_id"]
INTRODUCTIONS_CHANNEL_ID: Final[int] = _s["introductions"]["intro_channel_id"]
INTRODUCTIONS_QUESTION_MAPPING: Final[dict[str, str]] = _s["introductions"]["intro_question_mapping"]
# Reponse strings
# TODO: Implement switching between languages
STRINGS = _p.read_json("strings.en-US")
LEVEL_MESSAGES = _p.read_json("levels.en-US")
STRINGS: Final = _p.read_json("strings.en-US")
LEVEL_MESSAGES: Final = _p.read_json("levels.en-US")
_bday = _p.read_json("bdays.en-US")
BIRTHDAY_MESSAGES = _bday["birthday_messages"]
BIRTHDAY_MONTHS = _bday["months"]
_bday: Final = _p.read_json("bdays.en-US")
BIRTHDAY_MESSAGES: Final = _bday["birthday_messages"]
BIRTHDAY_MONTHS: Final = _bday["months"]
CONST: _constants = _constants()
CONST = Constants()

View file

@ -8,15 +8,13 @@ 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"]):
def __init__(self, message: str = CONST.STRINGS["lumi_exception_generic"]):
self.message = message
super().__init__(message)
@ -26,6 +24,6 @@ class Blacklisted(commands.CommandError):
Raised when a user is blacklisted.
"""
def __init__(self, message=CONST.STRINGS["lumi_exception_blacklisted"]):
def __init__(self, message: str = CONST.STRINGS["lumi_exception_blacklisted"]):
self.message = message
super().__init__(message)

View file

@ -1,8 +1,10 @@
from loguru import logger
from discord.ext import commands
from lib.const import CONST
from pathlib import Path
import aiofiles.os
from discord.ext import commands
from loguru import logger
from lib.const import CONST
class CogLoader(commands.Cog):
@ -17,11 +19,7 @@ class CogLoader(commands.Cog):
logger.debug(f"Ignoring cog: {cog_name} because it is in the ignore list")
return False
return (
path.suffix == ".py"
and not path.name.startswith("_")
and await aiofiles.os.path.isfile(path)
)
return path.suffix == ".py" and not path.name.startswith("_") and await aiofiles.os.path.isfile(path)
async def load_cogs(self, path: Path) -> None:
try:
@ -34,9 +32,7 @@ class CogLoader(commands.Cog):
elif await self.is_cog(path):
relative_path: Path = path.relative_to(Path(__file__).parent.parent)
module: str = (
str(relative_path).replace("/", ".").replace("\\", ".")[:-3]
)
module: str = str(relative_path).replace("/", ".").replace("\\", ".")[:-3]
try:
await self.bot.load_extension(name=module)
logger.debug(f"Loaded cog: {module}")

View file

@ -252,6 +252,13 @@
"ping_footer": "Latency: {0}ms",
"ping_pong": "pong!",
"ping_uptime": "I've been online since <t:{0}:R>.",
"slowmode_author": "Slowmode",
"slowmode_channel_not_found": "Channel not found.",
"slowmode_duration_not_found": "Please provide a duration in seconds for the slowmode.",
"slowmode_footer": "Use 0 to disable slowmode",
"slowmode_forbidden": "I don't have permission to change the slowmode in that channel.",
"slowmode_invalid_duration": "Slowmode duration must be between 0 and 21600 seconds.",
"slowmode_success": "Slowmode set to {0} seconds in {1}.",
"stats_blackjack": "\ud83c\udccf | 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": "\ud83c\udfb0 | You've played **{0}** games of Slots, betting a total of **${1}**. Your total payout was **${2}**.",
"sync_author": "Synced Commands",

View file

@ -1,10 +1,12 @@
import sys
import asyncio
import sys
import discord
from discord.ext import commands
from loguru import logger
from lib.const import CONST
from lib.client import Luminara
from lib.const import CONST
logger.remove()
logger.add(sys.stdout, format=CONST.LOG_FORMAT, colorize=True, level=CONST.LOG_LEVEL)

0
modules/__init__.py Normal file
View file

View file

View file

@ -1,8 +1,8 @@
from discord.ext import commands
import discord
from typing import Optional
from ui.embeds import builder
from discord.ext import commands
from lib.const import CONST
from ui.embeds import Builder
class Sync(commands.Cog):
@ -18,7 +18,7 @@ class Sync(commands.Cog):
async def sync(
self,
ctx: commands.Context[commands.Bot],
guild: Optional[discord.Guild] = None,
guild: discord.Guild | None = None,
) -> None:
if not guild:
guild = ctx.guild
@ -28,7 +28,7 @@ class Sync(commands.Cog):
self.bot.tree.copy_global_to(guild=guild)
await self.bot.tree.sync(guild=guild)
embed = builder.create_embed(
embed = Builder.create_embed(
theme="success",
user_name=ctx.author.name,
author_text=CONST.STRINGS["sync_author"],

View file

View file

@ -1,8 +1,10 @@
from typing import Optional
from typing import cast
from discord import Embed, Guild, Member
from discord.ext import commands
from discord import Embed, Guild
from lib.const import CONST
from ui.embeds import builder
from ui.embeds import Builder
from ui.views.leaderboard import LeaderboardCommandOptions, LeaderboardCommandView
@ -16,17 +18,18 @@ class Leaderboard(commands.Cog):
usage="leaderboard",
)
async def leaderboard(self, ctx: commands.Context[commands.Bot]) -> None:
guild: Optional[Guild] = ctx.guild
guild: Guild | None = ctx.guild
if not guild:
return
options: LeaderboardCommandOptions = LeaderboardCommandOptions()
view: LeaderboardCommandView = LeaderboardCommandView(ctx, options)
embed: Embed = builder.create_embed(
author: Member = cast(Member, ctx.author)
embed: Embed = Builder.create_embed(
theme="info",
user_name=ctx.author.name,
thumbnail_url=ctx.author.display_avatar.url,
user_name=author.name,
thumbnail_url=author.display_avatar.url,
hide_name_in_description=True,
)

View file

@ -1,8 +1,9 @@
from discord.ext import commands
from discord import Embed
from discord.ext import commands
from lib.const import CONST
from ui.embeds import builder
from services.xp_service import XpService
from ui.embeds import Builder
class Level(commands.Cog):
@ -25,7 +26,7 @@ class Level(commands.Cog):
xp_data.level,
)
embed: Embed = builder.create_embed(
embed: Embed = Builder.create_embed(
theme="success",
user_name=ctx.author.name,
title=CONST.STRINGS["xp_level"].format(xp_data.level),

0
modules/misc/__init__.py Normal file
View file

View file

@ -1,10 +1,9 @@
from io import BytesIO
from discord.ext import commands
from discord.ext.commands import MemberConverter
from typing import Optional
import discord
from discord import File
import httpx
from discord import File
from discord.ext import commands
async def create_avatar_file(url: str) -> File:
@ -42,7 +41,7 @@ class Avatar(commands.Cog):
async def avatar(
self,
ctx: commands.Context[commands.Bot],
member: Optional[discord.Member] = None,
member: discord.Member | None = None,
) -> None:
"""
Get the avatar of a member.
@ -55,18 +54,12 @@ class Avatar(commands.Cog):
The member to get the avatar of.
"""
if member is None:
member = await MemberConverter().convert(ctx, str(ctx.author.id))
member = await commands.MemberConverter().convert(ctx, str(ctx.author.id))
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
guild_avatar: str | None = member.guild_avatar.url if member.guild_avatar else None
profile_avatar: str | None = 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
]
files: list[File] = [await create_avatar_file(avatar) for avatar in [guild_avatar, profile_avatar] if avatar]
if files:
await ctx.send(files=files)

View file

@ -1,20 +1,24 @@
import subprocess
from datetime import datetime
from typing import List, Optional
from pathlib import Path
import dropbox # type: ignore
import pytz
from discord.ext import commands, tasks
import dropbox
from dropbox.files import FileMetadata
from dropbox.files import FileMetadata # type: ignore
from loguru import logger
from lib.const import CONST
# Initialize timezone
tz = pytz.timezone("America/New_York")
# Initialize Dropbox client if instance is "main"
_dbx: Optional[dropbox.Dropbox] = None
_dbx: dropbox.Dropbox | None = 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
_app_key: str | None = CONST.DBX_APP_KEY
_dbx_token: str | None = CONST.DBX_TOKEN
_app_secret: str | None = CONST.DBX_APP_SECRET
_dbx = dropbox.Dropbox(
app_key=_app_key,
@ -23,36 +27,42 @@ if CONST.INSTANCE and CONST.INSTANCE.lower() == "main":
)
async def create_db_backup() -> None:
if not _dbx:
raise ValueError("Dropbox client is not initialized")
backup_name: str = datetime.now().strftime("%Y-%m-%d_%H%M") + "_lumi.sql"
def run_db_dump() -> None:
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}")
def upload_backup_to_dropbox(backup_name: str) -> None:
with Path("./db/migrations/100-dump.sql").open("rb") as f:
if _dbx:
_dbx.files_upload(f.read(), f"/{backup_name}") # type: ignore
async def create_db_backup() -> None:
if not _dbx:
msg = "Dropbox client is not initialized"
raise ValueError(msg)
backup_name: str = datetime.now(tz).strftime("%Y-%m-%d_%H%M") + "_lumi.sql"
run_db_dump()
upload_backup_to_dropbox(backup_name)
async def backup_cleanup() -> None:
if not _dbx:
raise ValueError("Dropbox client is not initialized")
msg = "Dropbox client is not initialized"
raise ValueError(msg)
result = _dbx.files_list_folder("")
result = _dbx.files_list_folder("") # type: ignore
all_backup_files: List[str] = [
entry.name
for entry in result.entries # type: ignore
if isinstance(entry, FileMetadata)
]
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(f"/{file}")
_dbx.files_delete_v2(f"/{file}") # type: ignore
async def backup() -> None:

View file

@ -1,10 +1,12 @@
from discord.ext import commands
from lib.const import CONST
from ui.embeds import builder
import discord
import os
import platform
import discord
import psutil
from discord.ext import commands
from lib.const import CONST
from ui.embeds import Builder
class Info(commands.Cog):
@ -29,7 +31,7 @@ class Info(commands.Cog):
],
)
embed: discord.Embed = builder.create_embed(
embed: discord.Embed = Builder.create_embed(
theme="info",
user_name=ctx.author.name,
author_text=f"{CONST.TITLE} v{CONST.VERSION}",

View file

@ -1,11 +1,8 @@
import asyncio
from typing import Dict, Optional
import discord
from discord.ext import commands
from lib.const import CONST
from ui.embeds import builder
from ui.embeds import Builder
from ui.views.introduction import (
IntroductionFinishButtons,
IntroductionStartButtons,
@ -22,16 +19,14 @@ class Introduction(commands.Cog):
usage="introduction",
)
async def introduction(self, ctx: commands.Context[commands.Bot]) -> None:
guild: Optional[discord.Guild] = self.bot.get_guild(
guild: discord.Guild | None = self.bot.get_guild(
CONST.INTRODUCTIONS_GUILD_ID,
)
member: Optional[discord.Member] = (
guild.get_member(ctx.author.id) if guild else None
)
member: discord.Member | None = guild.get_member(ctx.author.id) if guild else None
if not guild or not member:
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_no_guild_author"],
@ -41,17 +36,17 @@ class Introduction(commands.Cog):
)
return
question_mapping: Dict[str, str] = CONST.INTRODUCTIONS_QUESTION_MAPPING
channel: Optional[discord.abc.GuildChannel] = guild.get_channel(
question_mapping: dict[str, str] = CONST.INTRODUCTIONS_QUESTION_MAPPING
channel: discord.abc.GuildChannel | None = guild.get_channel(
CONST.INTRODUCTIONS_CHANNEL_ID,
)
if not channel or isinstance(
channel,
(discord.ForumChannel, discord.CategoryChannel),
discord.ForumChannel | discord.CategoryChannel,
):
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_no_channel_author"],
@ -61,11 +56,9 @@ class Introduction(commands.Cog):
)
return
view: IntroductionStartButtons | IntroductionFinishButtons = (
IntroductionStartButtons(ctx)
)
view: IntroductionStartButtons | IntroductionFinishButtons = IntroductionStartButtons(ctx)
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="info",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_service_name"],
@ -79,7 +72,7 @@ class Introduction(commands.Cog):
if view.clicked_stop:
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_stopped_author"],
@ -97,96 +90,95 @@ class Introduction(commands.Cog):
discord.DMChannel,
)
answer_mapping: Dict[str, str] = {}
answer_mapping: dict[str, str] = {}
for key, question in question_mapping.items():
await ctx.send(
embed=builder.create_embed(
theme="info",
user_name=ctx.author.name,
author_text=key,
description=question,
footer_text=CONST.STRINGS["intro_question_footer"],
),
)
try:
answer: discord.Message = await self.bot.wait_for(
"message",
check=check,
timeout=300,
for key, question in question_mapping.items():
await ctx.send(
embed=Builder.create_embed(
theme="info",
user_name=ctx.author.name,
author_text=key,
description=question,
footer_text=CONST.STRINGS["intro_question_footer"],
),
)
answer_content: str = answer.content.replace("\n", " ")
if len(answer_content) > 200:
try:
answer: discord.Message = await self.bot.wait_for(
"message",
check=check,
timeout=300,
)
answer_content: str = answer.content.replace("\n", " ")
if len(answer_content) > 200:
await ctx.send(
embed=Builder.create_embed(
theme="error",
user_name=ctx.author.name,
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 TimeoutError:
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_too_long_author"],
description=CONST.STRINGS["intro_too_long"],
author_text=CONST.STRINGS["intro_timeout_author"],
description=CONST.STRINGS["intro_timeout"],
footer_text=CONST.STRINGS["intro_service_name"],
),
)
return
answer_mapping[key] = answer_content
description: str = "".join(
CONST.STRINGS["intro_preview_field"].format(key, value) for key, value in answer_mapping.items()
)
except asyncio.TimeoutError:
preview: discord.Embed = Builder.create_embed(
theme="info",
user_name=ctx.author.name,
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.clicked_confirm:
await channel.send(
embed=preview,
content=CONST.STRINGS["intro_content"].format(ctx.author.mention),
)
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="info",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_post_confirmation_author"],
description=CONST.STRINGS["intro_post_confirmation"].format(
channel.mention,
),
),
)
else:
await ctx.send(
embed=Builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_timeout_author"],
description=CONST.STRINGS["intro_timeout"],
author_text=CONST.STRINGS["intro_stopped_author"],
description=CONST.STRINGS["intro_stopped"],
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 = builder.create_embed(
theme="info",
user_name=ctx.author.name,
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.clicked_confirm:
await channel.send(
embed=preview,
content=CONST.STRINGS["intro_content"].format(ctx.author.mention),
)
await ctx.send(
embed=builder.create_embed(
theme="info",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_post_confirmation_author"],
description=CONST.STRINGS["intro_post_confirmation"].format(
channel.mention,
),
),
)
else:
await ctx.send(
embed=builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=CONST.STRINGS["intro_stopped_author"],
description=CONST.STRINGS["intro_stopped"],
footer_text=CONST.STRINGS["intro_service_name"],
),
)
async def setup(bot: commands.Bot) -> None:

View file

@ -1,6 +1,7 @@
from discord.ext import commands
from lib.const import CONST
from ui.embeds import builder
from ui.embeds import Builder
from ui.views.invite import InviteButton
@ -15,7 +16,7 @@ class Invite(commands.Cog):
)
async def invite(self, ctx: commands.Context[commands.Bot]) -> None:
await ctx.send(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="success",
user_name=ctx.author.name,
author_text=CONST.STRINGS["invite_author"],

View file

@ -1,6 +1,7 @@
from discord.ext import commands
from lib.const import CONST
from ui.embeds import builder
from ui.embeds import Builder
class Ping(commands.Cog):
@ -12,7 +13,7 @@ class Ping(commands.Cog):
usage="ping",
)
async def ping(self, ctx: commands.Context[commands.Bot]) -> None:
embed = builder.create_embed(
embed = Builder.create_embed(
theme="success",
user_name=ctx.author.name,
author_text=CONST.STRINGS["ping_author"],

View file

@ -1,14 +1,17 @@
from discord.ext import commands
from discord import Embed
from lib.const import CONST
from ui.embeds import builder
from datetime import datetime
import discord
from discord import Embed
from discord.ext import commands
from lib.const import CONST
from ui.embeds import Builder
class Uptime(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot: commands.Bot = bot
self.start_time: datetime = datetime.now()
self.start_time: datetime = discord.utils.utcnow()
@commands.hybrid_command(
name="uptime",
@ -17,7 +20,7 @@ class Uptime(commands.Cog):
async def uptime(self, ctx: commands.Context[commands.Bot]) -> None:
unix_timestamp: int = int(self.start_time.timestamp())
embed: Embed = builder.create_embed(
embed: Embed = Builder.create_embed(
theme="info",
user_name=ctx.author.name,
author_text=CONST.STRINGS["ping_author"],

View file

@ -1,10 +1,10 @@
from discord.ext import commands
from lib.const import CONST
from ui.embeds import builder
from discord import app_commands
import discord
from discord import app_commands
from discord.ext import commands
from lib.const import CONST
from ui.embeds import Builder
from wrappers.xkcd import Client, HttpError
from typing import Optional
_xkcd = Client()
@ -12,7 +12,7 @@ _xkcd = Client()
async def print_comic(
interaction: discord.Interaction,
latest: bool = False,
number: Optional[int] = None,
number: int | None = None,
) -> None:
try:
if latest:
@ -23,7 +23,7 @@ async def print_comic(
comic = _xkcd.get_random_comic(raw_comic_image=True)
await interaction.response.send_message(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="info",
author_text=CONST.STRINGS["xkcd_title"].format(comic.id, comic.title),
description=CONST.STRINGS["xkcd_description"].format(
@ -37,7 +37,7 @@ async def print_comic(
except HttpError:
await interaction.response.send_message(
embed=builder.create_embed(
embed=Builder.create_embed(
theme="error",
author_text=CONST.STRINGS["xkcd_not_found_author"],
description=CONST.STRINGS["xkcd_not_found"],
@ -61,8 +61,8 @@ class Xkcd(commands.Cog):
await print_comic(interaction)
@xkcd.command(name="search", description="Search for an xkcd comic")
async def xkcd_search(self, interaction: discord.Interaction, id: int) -> None:
await print_comic(interaction, number=id)
async def xkcd_search(self, interaction: discord.Interaction, comic_id: int) -> None:
await print_comic(interaction, number=comic_id)
async def setup(bot: commands.Bot) -> None:

View file

View file

@ -0,0 +1,75 @@
import contextlib
import discord
from discord.ext import commands
from lib.const import CONST
from ui.embeds import Builder
class Slowmode(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
@commands.command(
name="slowmode",
aliases=["sm"],
usage="slowmode <duration> <channel>",
)
@commands.has_permissions(manage_channels=True)
async def slowmode(
self,
ctx: commands.Context[commands.Bot],
arg1: str,
arg2: str,
) -> None:
# define actual channel & duration
channel: discord.TextChannel | None = None
duration: int | None = None
for arg in (arg1, arg2):
if not channel:
try:
channel = await commands.TextChannelConverter().convert(ctx, arg)
except commands.BadArgument:
with contextlib.suppress(ValueError):
duration = int(arg)
else:
with contextlib.suppress(ValueError):
duration = int(arg)
if not channel:
await ctx.send(CONST.STRINGS["slowmode_channel_not_found"])
return
if duration is None:
await ctx.send(CONST.STRINGS["slowmode_duration_not_found"])
return
if duration < 0 or duration > 21600: # 21600 seconds = 6 hours (Discord's max slowmode)
await ctx.send("Slowmode duration must be between 0 and 21600 seconds.")
return
try:
await channel.edit(slowmode_delay=duration)
embed = Builder.create_embed(
theme="success",
user_name=ctx.author.name,
author_text=CONST.STRINGS["slowmode_author"],
description=CONST.STRINGS["slowmode_success"].format(duration, channel.mention),
footer_text=CONST.STRINGS["slowmode_footer"],
)
except discord.Forbidden:
embed = Builder.create_embed(
theme="error",
user_name=ctx.author.name,
author_text=CONST.STRINGS["slowmode_author"],
description=CONST.STRINGS["slowmode_forbidden"],
footer_text=CONST.STRINGS["slowmode_footer"],
)
await ctx.send(embed=embed)
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Slowmode(bot))

142
poetry.lock generated
View file

@ -174,6 +174,17 @@ files = [
[package.dependencies]
frozenlist = ">=1.1.0"
[[package]]
name = "annotated-types"
version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
]
[[package]]
name = "anyio"
version = "4.4.0"
@ -820,15 +831,138 @@ files = [
[package.extras]
test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "pydantic"
version = "2.8.2"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
]
[package.dependencies]
annotated-types = ">=0.4.0"
pydantic-core = "2.20.1"
typing-extensions = [
{version = ">=4.12.2", markers = "python_version >= \"3.13\""},
{version = ">=4.6.1", markers = "python_version < \"3.13\""},
]
[package.extras]
email = ["email-validator (>=2.0.0)"]
[[package]]
name = "pydantic-core"
version = "2.20.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
{file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
{file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
{file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
{file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
{file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
{file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
{file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
{file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
{file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
{file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
{file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
{file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
{file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
{file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
{file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
{file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
{file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
{file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
{file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
{file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
{file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
{file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
{file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
{file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
{file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
{file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
{file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
{file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
{file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
{file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
{file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
{file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
{file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
]
[package.dependencies]
typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pyright"
version = "1.1.377"
version = "1.1.378"
description = "Command line wrapper for pyright"
optional = false
python-versions = ">=3.7"
files = [
{file = "pyright-1.1.377-py3-none-any.whl", hash = "sha256:af0dd2b6b636c383a6569a083f8c5a8748ae4dcde5df7914b3f3f267e14dd162"},
{file = "pyright-1.1.377.tar.gz", hash = "sha256:aabc30fedce0ded34baa0c49b24f10e68f4bfc8f68ae7f3d175c4b0f256b4fcf"},
{file = "pyright-1.1.378-py3-none-any.whl", hash = "sha256:8853776138b01bc284da07ac481235be7cc89d3176b073d2dba73636cb95be79"},
{file = "pyright-1.1.378.tar.gz", hash = "sha256:78a043be2876d12d0af101d667e92c7734f3ebb9db71dccc2c220e7e7eb89ca2"},
]
[package.dependencies]
@ -1165,4 +1299,4 @@ multidict = ">=4.0"
[metadata]
lock-version = "2.0"
python-versions = "^3.12"
content-hash = "1bec4428d16328dd4054cda20654e446c54aa0463b79ef32ae4cfa10de7c0dfd"
content-hash = "e6ab702cf6efc2ec25a9c033029869f9c4c3631e2e6063c4241153ea8c1f8e79"

View file

@ -8,23 +8,148 @@ readme = "README.md"
version = "3"
[tool.poetry.dependencies]
aiocache = "^0.12.2"
aioconsole = "^0.7.1"
aiofiles = "^24.1.0"
discord-py = "^2.4.0"
dropbox = "^12.0.2"
httpx = "^0.27.2"
loguru = "^0.7.2"
mysql-connector-python = "^9.0.0"
pre-commit = "^3.8.0"
psutil = "^6.0.0"
pyright = "^1.1.377"
python = "^3.12"
pytz = "^2024.1"
pyyaml = "^6.0.2"
ruff = "^0.6.2"
typing-extensions = "^4.12.2"
aiofiles = "^24.1.0"
aiocache = "^0.12.2"
aioconsole = "^0.7.1"
psutil = "^6.0.0"
dropbox = "^12.0.2"
pytz = "^2024.1"
pydantic = "^2.8.2"
[build-system]
build-backend = "poetry.core.masonry.api"
requires = ["poetry-core"]
[tool.ruff]
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".git-rewrite",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"examples",
"tmp",
"tests",
".archive",
"stubs",
]
indent-width = 4
line-length = 120
target-version = "py312"
# Ruff Linting Configuration
[tool.ruff.lint]
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
fixable = ["ALL"]
ignore = ["E501", "N814", "PLR0913", "PLR2004"]
select = [
"I", # isort
"E", # pycodestyle-error
"F", # pyflakes
"PERF", # perflint
"N", # pep8-naming
"TRY", # tryceratops
"UP", # pyupgrade
"FURB", # refurb
"PL", # pylint
"B", # flake8-bugbear
"SIM", # flake8-simplify
"ASYNC", # flake8-async
"A", # flake8-builtins
"C4", # flake8-comprehensions
"DTZ", # flake8-datetimez
"EM", # flake8-errmsg
"PIE", # flake8-pie
"T20", # flake8-print
"Q", # flake8-quotes
"RET", # flake8-return
"PTH", # flake8-use-pathlib
"INP", # flake8-no-pep420
"RSE", # flake8-raise
"ICN", # flake8-import-conventions
"RUF", # ruff
]
unfixable = []
# Ruff Formatting Configuration
[tool.ruff.format]
docstring-code-format = true
docstring-code-line-length = "dynamic"
indent-style = "space"
line-ending = "lf"
quote-style = "double"
skip-magic-trailing-comma = false
# Pyright Configuration
[tool.pyright]
defineConstant = {DEBUG = true}
exclude = [
".direnv",
".eggs",
".git",
".hg",
".ipynb_checkpoints",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".svn",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"site-packages",
"venv",
"examples",
"tests",
".archive",
"stubs",
]
include = ["**/*.py"]
pythonPlatform = "Linux"
pythonVersion = "3.12"
reportMissingTypeStubs = true
reportShadowedImports = false
stubPath = "./stubs"
typeCheckingMode = "strict"
venv = ".venv"
venvPath = "."

0
services/__init__.py Normal file
View file

View file

@ -4,20 +4,19 @@ 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 __init__(self, user_id: int) -> None:
self.user_id: int = user_id
self.balance: int = Currency.fetch_or_create_balance(self.user_id)
def add_balance(self, amount):
def add_balance(self, amount: int) -> None:
self.balance += abs(amount)
def take_balance(self, amount):
def take_balance(self, amount: int) -> None:
self.balance -= abs(amount)
self.balance = max(self.balance, 0)
def push(self):
query = """
def push(self) -> None:
query: str = """
UPDATE currency
SET balance = %s
WHERE user_id = %s
@ -26,15 +25,15 @@ class Currency:
database.execute_query(query, (round(self.balance), self.user_id))
@staticmethod
def fetch_or_create_balance(user_id):
query = """
def fetch_or_create_balance(user_id: int) -> int:
query: str = """
SELECT balance
FROM currency
WHERE user_id = %s
"""
try:
balance = database.select_query_one(query, (user_id,))
balance: int | None = database.select_query_one(query, (user_id,))
except (IndexError, TypeError):
balance = None
@ -52,31 +51,27 @@ class Currency:
return balance
@staticmethod
def load_leaderboard():
query = "SELECT user_id, balance FROM currency ORDER BY balance DESC"
data = database.select_query(query)
def load_leaderboard() -> list[tuple[int, int, int]]:
query: str = "SELECT user_id, balance FROM currency ORDER BY balance DESC"
data: list[tuple[int, int]] = database.select_query(query)
return [(row[0], row[1], rank) for rank, row in enumerate(data, start=1)]
@staticmethod
def format(num):
def format(num: int) -> str:
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:
def format_human(num: int) -> str:
num_float: float = float(f"{num:.3g}")
magnitude: int = 0
while abs(num_float) >= 1000:
magnitude += 1
num /= 1000.0
num_float /= 1000.0
return "{}{}".format(
"{:f}".format(num).rstrip("0").rstrip("."),
["", "K", "M", "B", "T", "Q", "Qi", "Sx", "Sp", "Oc", "No", "Dc"][
magnitude
],
)
suffixes: list[str] = ["", "K", "M", "B", "T", "Q", "Qi", "Sx", "Sp", "Oc", "No", "Dc"]
return f'{f"{num_float:f}".rstrip("0").rstrip(".")}{suffixes[magnitude]}'
# A Thousand = K
# Million = M

View file

@ -1,5 +1,4 @@
from datetime import datetime, timedelta
from typing import List, Optional, Tuple
import pytz
@ -21,7 +20,7 @@ class Dailies:
microsecond=0,
)
data: Tuple[Optional[str], int] = Dailies.get_data(user_id)
data: tuple[str | None, int] = Dailies.get_data(user_id)
if data[0] is not None:
self.claimed_at: datetime = datetime.fromisoformat(data[0])
@ -38,7 +37,7 @@ class Dailies:
INSERT INTO dailies (user_id, amount, claimed_at, streak)
VALUES (%s, %s, %s, %s)
"""
values: Tuple[int, int, str, int] = (
values: tuple[int, int, str, int] = (
self.user_id,
self.amount,
self.claimed_at.isoformat(),
@ -51,9 +50,6 @@ class Dailies:
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)
@ -68,21 +64,14 @@ class Dailies:
: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
)
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]:
def get_data(user_id: int) -> tuple[str | None, int]:
query: str = """
SELECT claimed_at, streak
FROM dailies
@ -101,7 +90,7 @@ class Dailies:
return claimed_at, streak
@staticmethod
def load_leaderboard() -> List[Tuple[int, int, str, int]]:
def load_leaderboard() -> list[tuple[int, int, str, int]]:
query: str = """
SELECT user_id, MAX(streak), claimed_at
FROM dailies
@ -109,9 +98,9 @@ class Dailies:
ORDER BY MAX(streak) DESC;
"""
data: List[Tuple[int, int, str]] = database.select_query(query)
data: list[tuple[int, int, str]] = database.select_query(query)
leaderboard: List[Tuple[int, int, str, int]] = [
leaderboard: list[tuple[int, int, str, int]] = [
(row[0], row[1], row[2], rank) for rank, row in enumerate(data, start=1)
]
return leaderboard

View file

@ -1,5 +1,5 @@
import time
from typing import Callable, Dict, List, Optional, Tuple
from collections.abc import Callable
from discord.ext import commands
@ -24,7 +24,7 @@ class XpService:
self.guild_id: int = guild_id
self.xp: int = 0
self.level: int = 0
self.cooldown_time: Optional[float] = None
self.cooldown_time: float | None = None
self.xp_gain: int = CONST.XP_GAIN_PER_MESSAGE
self.new_cooldown: int = CONST.XP_GAIN_COOLDOWN
@ -70,7 +70,7 @@ class XpService:
self.level = user_level
self.cooldown_time = cooldown
def calculate_rank(self) -> Optional[int]:
def calculate_rank(self) -> int | None:
"""
Determines the rank of a user in the guild based on their XP and level.
@ -83,12 +83,12 @@ class XpService:
WHERE guild_id = %s
ORDER BY user_level DESC, user_xp DESC
"""
data: List[Tuple[int, int, int]] = database.select_query(
data: list[tuple[int, int, int]] = database.select_query(
query,
(self.guild_id,),
)
leaderboard: List[Tuple[int, int, int, int]] = [
leaderboard: list[tuple[int, int, int, int]] = [
(row[0], row[1], row[2], rank) for rank, row in enumerate(data, start=1)
]
return next(
@ -97,7 +97,7 @@ class XpService:
)
@staticmethod
def load_leaderboard(guild_id: int) -> List[Tuple[int, int, int, int]]:
def load_leaderboard(guild_id: int) -> list[tuple[int, int, int, int]]:
"""
Retrieves the guild's XP leaderboard.
@ -113,9 +113,9 @@ class XpService:
WHERE guild_id = %s
ORDER BY user_level DESC, user_xp DESC
"""
data: List[Tuple[int, int, int]] = database.select_query(query, (guild_id,))
data: list[tuple[int, int, int]] = database.select_query(query, (guild_id,))
leaderboard: List[Tuple[int, int, int, int]] = []
leaderboard: list[tuple[int, int, int, int]] = []
for row in data:
row_user_id: int = row[0]
user_xp: int = row[1]
@ -164,7 +164,7 @@ class XpService:
Returns:
int: The amount of XP needed for the next level.
"""
formula_mapping: Dict[Tuple[int, int], Callable[[int], int]] = {
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,
@ -182,11 +182,7 @@ class XpService:
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
),
(10 * current_level + 27 if current_level < 10 else 42 * current_level + 37),
)
@ -203,9 +199,9 @@ class XpRewardService:
guild_id (int): The ID of the guild.
"""
self.guild_id: int = guild_id
self.rewards: Dict[int, Tuple[int, bool]] = self._fetch_rewards()
self.rewards: dict[int, tuple[int, bool]] = self._fetch_rewards()
def _fetch_rewards(self) -> Dict[int, Tuple[int, bool]]:
def _fetch_rewards(self) -> dict[int, tuple[int, bool]]:
"""
Retrieves the XP rewards for the guild from the database.
@ -218,7 +214,7 @@ class XpRewardService:
WHERE guild_id = %s
ORDER BY level DESC
"""
data: List[Tuple[int, int, bool]] = database.select_query(
data: list[tuple[int, int, bool]] = database.select_query(
query,
(self.guild_id,),
)
@ -237,7 +233,8 @@ class XpRewardService:
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.")
msg = "A server can't have more than 25 XP rewards."
raise commands.BadArgument(msg)
query: str = """
INSERT INTO level_rewards (guild_id, level, role_id, persistent)
@ -264,7 +261,7 @@ class XpRewardService:
database.execute_query(query, (self.guild_id, level))
self.rewards.pop(level, None)
def get_role(self, level: int) -> Optional[int]:
def get_role(self, level: int) -> int | None:
"""
Retrieves the role ID for a given level.
@ -276,7 +273,7 @@ class XpRewardService:
"""
return self.rewards.get(level, (None,))[0]
def should_replace_previous_reward(self, level: int) -> Tuple[Optional[int], bool]:
def should_replace_previous_reward(self, level: int) -> tuple[int | None, bool]:
"""
Checks if the previous reward should be replaced based on the given level.

View file

@ -92,8 +92,8 @@ info:
invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot
introductions:
intro_guild_id: 719227135151046700
intro_channel_id: 973619250507972600
intro_guild_id: 1219635811977269371
intro_channel_id: 1219637688328523806
intro_question_mapping:
Nickname: how would you like to be identified in the server? (nickname)
Age: how old are you?

0
ui/__init__.py Normal file
View file

View file

@ -1,28 +1,28 @@
from datetime import datetime
from typing import Optional, Literal
from typing import Literal
import discord
from lib.const import CONST
class builder:
class Builder:
@staticmethod
def create_embed(
user_name: Optional[str] = None,
user_display_avatar_url: Optional[str] = None,
theme: Optional[Literal["error", "success", "info", "warning"]] = None,
title: Optional[str] = None,
author_text: Optional[str] = None,
author_icon_url: Optional[str] = None,
author_url: Optional[str] = None,
description: Optional[str] = None,
color: Optional[int] = None,
footer_text: Optional[str] = None,
footer_icon_url: Optional[str] = None,
image_url: Optional[str] = None,
thumbnail_url: Optional[str] = None,
timestamp: Optional[datetime] = None,
user_name: str | None = None,
user_display_avatar_url: str | None = None,
theme: Literal["error", "success", "info", "warning"] | None = None,
title: str | None = None,
author_text: str | None = None,
author_icon_url: str | None = None,
author_url: str | None = None,
description: str | None = None,
color: int | None = None,
footer_text: str | None = None,
footer_icon_url: str | None = None,
image_url: str | None = None,
thumbnail_url: str | None = None,
timestamp: datetime | None = None,
hide_name_in_description: bool = False,
hide_time: bool = False,
) -> discord.Embed:
@ -59,7 +59,7 @@ class builder:
icon_url=footer_icon_url or CONST.LUMI_LOGO_TRANSPARENT,
)
embed.timestamp = None if hide_time else (timestamp or datetime.now())
embed.timestamp = None if hide_time else (timestamp or discord.utils.utcnow())
if image_url:
embed.set_image(url=image_url)
if thumbnail_url:

0
ui/views/__init__.py Normal file
View file

View file

@ -1,8 +1,8 @@
import discord
from discord.ext import commands
from discord.ui import View, Button
from discord.ui import Button, View
from lib.const import CONST
from typing import List, Optional
class BlackJackButtons(View):
@ -12,14 +12,13 @@ class BlackJackButtons(View):
self.clickedHit: bool = False
self.clickedStand: bool = False
self.clickedDoubleDown: bool = False
self.message: Optional[discord.Message] = None
self.message: discord.Message | None = None
async def on_timeout(self) -> None:
self.children: List[discord.ui.Button] = []
self.children: list[discord.ui.Button[View]] = []
for child in self.children:
if isinstance(child, Button):
child.disabled = True
child.disabled = True
@discord.ui.button(
label=CONST.STRINGS["blackjack_hit"],
@ -29,7 +28,7 @@ class BlackJackButtons(View):
async def hit_button_callback(
self,
interaction: discord.Interaction,
button: Button,
button: Button[View],
) -> None:
self.clickedHit = True
await interaction.response.defer()
@ -43,7 +42,7 @@ class BlackJackButtons(View):
async def stand_button_callback(
self,
interaction: discord.Interaction,
button: Button,
button: Button[View],
) -> None:
self.clickedStand = True
await interaction.response.defer()

View file

@ -1,5 +1,3 @@
from typing import Optional
import discord
from discord.ext import commands
from discord.ui import Button, View
@ -11,7 +9,7 @@ class IntroductionStartButtons(View):
self.ctx: commands.Context[commands.Bot] = ctx
self.clicked_start: bool = False
self.clicked_stop: bool = False
self.message: Optional[discord.Message] = None
self.message: discord.Message | None = None
async def on_timeout(self) -> None:
for child in self.children:
@ -24,7 +22,7 @@ class IntroductionStartButtons(View):
async def start_button_callback(
self,
interaction: discord.Interaction,
button: Button,
button: Button[View],
) -> None:
await interaction.response.edit_message(view=None)
self.clicked_start = True
@ -34,7 +32,7 @@ class IntroductionStartButtons(View):
async def stop_button_callback(
self,
interaction: discord.Interaction,
button: Button,
button: Button[View],
) -> None:
await interaction.response.edit_message(view=None)
self.clicked_stop = True
@ -46,7 +44,7 @@ class IntroductionFinishButtons(View):
super().__init__(timeout=60)
self.ctx: commands.Context[commands.Bot] = ctx
self.clicked_confirm: bool = False
self.message: Optional[discord.Message] = None
self.message: discord.Message | None = None
async def on_timeout(self) -> None:
for child in self.children:
@ -59,7 +57,7 @@ class IntroductionFinishButtons(View):
async def confirm_button_callback(
self,
interaction: discord.Interaction,
button: Button,
button: Button[View],
) -> None:
await interaction.response.edit_message(view=None)
self.clicked_confirm = True
@ -69,7 +67,7 @@ class IntroductionFinishButtons(View):
async def stop_button_callback(
self,
interaction: discord.Interaction,
button: Button,
button: Button[View],
) -> None:
await interaction.response.edit_message(view=None)
self.stop()

View file

@ -1,12 +1,13 @@
from discord import ButtonStyle
from discord.ui import Button, View
from lib.const import CONST
class InviteButton(View):
def __init__(self) -> None:
super().__init__(timeout=None)
invite_button: Button = Button(
invite_button: Button[InviteButton] = Button(
label=CONST.STRINGS["invite_button_text"],
style=ButtonStyle.url,
url=CONST.INVITE_URL,

View file

@ -1,16 +1,17 @@
import contextlib
from datetime import datetime
import discord
from discord.ext import commands
from lib.const import CONST
from ui.embeds import builder
from services.currency_service import Currency
from services.daily_service import Dailies
from services.xp_service import XpService
from ui.embeds import Builder
class LeaderboardCommandOptions(discord.ui.Select):
class LeaderboardCommandOptions(discord.ui.Select[discord.ui.View]):
"""
This class specifies the options for the leaderboard command:
- XP
@ -19,34 +20,35 @@ class LeaderboardCommandOptions(discord.ui.Select):
"""
def __init__(self) -> None:
options: list[discord.SelectOption] = [
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="📅",
),
]
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="📅",
),
],
options=options,
)
async def callback(self, interaction: discord.Interaction) -> None:
if self.view:
if isinstance(self.view, LeaderboardCommandView):
await self.view.on_select(self.values[0], interaction)
@ -56,15 +58,17 @@ class LeaderboardCommandView(discord.ui.View):
what kind of leaderboard to show.
"""
ctx: commands.Context[commands.Bot]
options: LeaderboardCommandOptions
def __init__(
self,
ctx: commands.Context[commands.Bot],
options: LeaderboardCommandOptions,
) -> None:
super().__init__(timeout=180)
self.ctx = ctx
self.options = options
super().__init__(timeout=180)
self.add_item(self.options)
async def on_timeout(self) -> None:
@ -72,7 +76,7 @@ class LeaderboardCommandView(discord.ui.View):
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user and interaction.user != self.ctx.author:
embed = builder.create_embed(
embed = Builder.create_embed(
theme="error",
user_name=interaction.user.name,
description=CONST.STRINGS["xp_lb_cant_use_dropdown"],
@ -86,7 +90,7 @@ class LeaderboardCommandView(discord.ui.View):
if not self.ctx.guild:
return
embed = builder.create_embed(
embed = Builder.create_embed(
theme="success",
user_name=interaction.user.name,
thumbnail_url=CONST.FLOWERS_ART,
@ -99,7 +103,12 @@ class LeaderboardCommandView(discord.ui.View):
await interaction.response.edit_message(embed=embed)
async def populate_leaderboard(self, item: str, embed, icon):
async def populate_leaderboard(
self,
item: str,
embed: discord.Embed,
icon: str,
) -> None:
leaderboard_methods = {
"xp": self._populate_xp_leaderboard,
"currency": self._populate_currency_leaderboard,
@ -107,11 +116,13 @@ class LeaderboardCommandView(discord.ui.View):
}
await leaderboard_methods[item](embed, icon)
async def _populate_xp_leaderboard(self, embed, icon):
async def _populate_xp_leaderboard(self, embed: discord.Embed, icon: str) -> None:
if not self.ctx.guild:
return
xp_lb = XpService.load_leaderboard(self.ctx.guild.id)
xp_lb: list[tuple[int, int, int, int]] = 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(
@ -133,20 +144,22 @@ class LeaderboardCommandView(discord.ui.View):
inline=False,
)
async def _populate_currency_leaderboard(self, embed, icon):
async def _populate_currency_leaderboard(
self,
embed: discord.Embed,
icon: str,
) -> None:
if not self.ctx.guild:
return
cash_lb = Currency.load_leaderboard()
cash_lb: list[tuple[int, int, int]] = 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: discord.Member | None = None
with contextlib.suppress(discord.HTTPException):
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(
@ -157,29 +170,31 @@ class LeaderboardCommandView(discord.ui.View):
inline=False,
)
async def _populate_dailies_leaderboard(self, embed, icon):
async def _populate_dailies_leaderboard(
self,
embed: discord.Embed,
icon: str,
) -> None:
if not self.ctx.guild:
return
daily_lb = Dailies.load_leaderboard()
daily_lb: list[tuple[int, int, str, int]] = 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: discord.Member | None = None
with contextlib.suppress(discord.HTTPException):
member = await self.ctx.guild.fetch_member(user_id)
except discord.HTTPException:
member = None
name = member.name if member else str(user_id)
name = member.name if member else user_id
claimed_at = datetime.fromisoformat(claimed_at).date()
claimed_at_date = datetime.fromisoformat(claimed_at).date()
embed.add_field(
name=f"#{rank} - {name}",
value=CONST.STRINGS["xp_lb_dailies_field_value"].format(
streak,
claimed_at,
claimed_at_date,
),
inline=False,
)

0
wrappers/__init__.py Normal file
View file

View file

@ -258,9 +258,7 @@ class Client:
HttpError
If the request fails.
"""
comic_url = (
self.latest_comic_url() if comic_id <= 0 else self.comic_id_url(comic_id)
)
comic_url = self.latest_comic_url() if comic_id <= 0 else self.comic_id_url(comic_id)
try:
response = httpx.get(comic_url)