From 1c9e89cb0e336455b3f7604e604d7106bfcfe88e Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 04:58:40 -0400 Subject: [PATCH 001/102] Clean up dir tree in preparation for v3 --- .github/ISSUE_TEMPLATE/bug_report.md | 23 - .github/ISSUE_TEMPLATE/feature_request.md | 20 - .github/workflows/docker-image.yml | 55 - Client.py | 43 - Luminara.py | 101 +- db/database.py | 115 --- db/migrations/v2_5_8_init.sql | 109 -- db/migrations/v2_5_9_reactions.sql | 21 - db/migrations/v2_6_0_cases.sql | 44 - docker-compose.prod.yml | 28 - handlers/error_handler.py | 121 --- handlers/event_handler.py | 86 -- handlers/reaction_handler.py | 86 -- handlers/xp_handler.py | 263 ----- lib/checks.py | 19 - lib/constants.py | 120 --- lib/embed_builder.py | 206 ---- lib/exceptions/LumiExceptions.py | 31 - lib/formatter.py | 138 --- lib/interaction.py | 34 - lib/interactions/introduction.py | 78 -- lib/reactions.py | 36 - lib/time.py | 19 - modules/admin/__init__.py | 45 - modules/admin/award.py | 23 - modules/admin/blacklist.py | 26 - modules/admin/sql.py | 60 -- modules/admin/sync.py | 20 - modules/birthdays/__init__.py | 46 - modules/birthdays/birthday.py | 85 -- modules/birthdays/daily_check.py | 65 -- modules/config/__init__.py | 174 ---- modules/config/c_birthday.py | 43 - modules/config/c_boost.py | 125 --- modules/config/c_greet.py | 96 -- modules/config/c_level.py | 137 --- modules/config/c_moderation.py | 44 - modules/config/c_prefix.py | 35 - modules/config/c_show.py | 75 -- modules/config/xp_reward.py | 43 - modules/economy/__init__.py | 77 -- modules/economy/balance.py | 22 - modules/economy/blackjack.py | 324 ------ modules/economy/daily.py | 43 - modules/economy/give.py | 39 - modules/economy/slots.py | 214 ---- modules/help/__init__.py | 24 - modules/levels/__init__.py | 36 - modules/levels/leaderboard.py | 202 ---- modules/levels/level.py | 31 - modules/misc/__init__.py | 135 --- modules/misc/avatar.py | 58 -- modules/misc/backup.py | 67 -- modules/misc/info.py | 42 - modules/misc/introduction.py | 166 --- modules/misc/invite.py | 27 - modules/misc/ping.py | 32 - modules/misc/xkcd.py | 47 - modules/moderation/__init__.py | 195 ---- modules/moderation/ban.py | 114 -- modules/moderation/cases.py | 115 --- modules/moderation/kick.py | 58 -- modules/moderation/softban.py | 66 -- modules/moderation/timeout.py | 103 -- modules/moderation/utils/actionable.py | 30 - modules/moderation/utils/case_embed.py | 102 -- modules/moderation/utils/case_handler.py | 148 --- modules/moderation/warn.py | 48 - modules/triggers/__init__.py | 81 -- modules/triggers/add.py | 107 -- modules/triggers/delete.py | 29 - modules/triggers/list.py | 80 -- poetry.lock | 1142 --------------------- pyproject.toml | 27 - services/birthday_service.py | 54 - services/blacklist_service.py | 41 - services/config_service.py | 144 --- services/currency_service.py | 92 -- services/daily_service.py | 117 --- services/help_service.py | 128 --- services/inventory_service.py | 77 -- services/item_service.py | 69 -- services/moderation/case_service.py | 171 --- services/moderation/modlog_service.py | 32 - services/reactions_service.py | 191 ---- services/stats_service.py | 139 --- services/xkcd_service.py | 321 ------ services/xp_service.py | 295 ------ settings/responses/bdays.en-US.json | 76 -- settings/responses/levels.en-US.json | 79 -- settings/responses/strings.en-US.json | 296 ------ settings/settings.yaml | 103 -- 92 files changed, 1 insertion(+), 9393 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/workflows/docker-image.yml delete mode 100644 Client.py delete mode 100644 db/database.py delete mode 100644 db/migrations/v2_5_8_init.sql delete mode 100644 db/migrations/v2_5_9_reactions.sql delete mode 100644 db/migrations/v2_6_0_cases.sql delete mode 100644 docker-compose.prod.yml delete mode 100644 handlers/error_handler.py delete mode 100644 handlers/event_handler.py delete mode 100644 handlers/reaction_handler.py delete mode 100644 handlers/xp_handler.py delete mode 100644 lib/checks.py delete mode 100644 lib/constants.py delete mode 100644 lib/embed_builder.py delete mode 100644 lib/exceptions/LumiExceptions.py delete mode 100644 lib/formatter.py delete mode 100644 lib/interaction.py delete mode 100644 lib/interactions/introduction.py delete mode 100644 lib/reactions.py delete mode 100644 lib/time.py delete mode 100644 modules/admin/__init__.py delete mode 100644 modules/admin/award.py delete mode 100644 modules/admin/blacklist.py delete mode 100644 modules/admin/sql.py delete mode 100644 modules/admin/sync.py delete mode 100644 modules/birthdays/__init__.py delete mode 100644 modules/birthdays/birthday.py delete mode 100644 modules/birthdays/daily_check.py delete mode 100644 modules/config/__init__.py delete mode 100644 modules/config/c_birthday.py delete mode 100644 modules/config/c_boost.py delete mode 100644 modules/config/c_greet.py delete mode 100644 modules/config/c_level.py delete mode 100644 modules/config/c_moderation.py delete mode 100644 modules/config/c_prefix.py delete mode 100644 modules/config/c_show.py delete mode 100644 modules/config/xp_reward.py delete mode 100644 modules/economy/__init__.py delete mode 100644 modules/economy/balance.py delete mode 100644 modules/economy/blackjack.py delete mode 100644 modules/economy/daily.py delete mode 100644 modules/economy/give.py delete mode 100644 modules/economy/slots.py delete mode 100644 modules/help/__init__.py delete mode 100644 modules/levels/__init__.py delete mode 100644 modules/levels/leaderboard.py delete mode 100644 modules/levels/level.py delete mode 100644 modules/misc/__init__.py delete mode 100644 modules/misc/avatar.py delete mode 100644 modules/misc/backup.py delete mode 100644 modules/misc/info.py delete mode 100644 modules/misc/introduction.py delete mode 100644 modules/misc/invite.py delete mode 100644 modules/misc/ping.py delete mode 100644 modules/misc/xkcd.py delete mode 100644 modules/moderation/__init__.py delete mode 100644 modules/moderation/ban.py delete mode 100644 modules/moderation/cases.py delete mode 100644 modules/moderation/kick.py delete mode 100644 modules/moderation/softban.py delete mode 100644 modules/moderation/timeout.py delete mode 100644 modules/moderation/utils/actionable.py delete mode 100644 modules/moderation/utils/case_embed.py delete mode 100644 modules/moderation/utils/case_handler.py delete mode 100644 modules/moderation/warn.py delete mode 100644 modules/triggers/__init__.py delete mode 100644 modules/triggers/add.py delete mode 100644 modules/triggers/delete.py delete mode 100644 modules/triggers/list.py delete mode 100644 poetry.lock delete mode 100644 pyproject.toml delete mode 100644 services/birthday_service.py delete mode 100644 services/blacklist_service.py delete mode 100644 services/config_service.py delete mode 100644 services/currency_service.py delete mode 100644 services/daily_service.py delete mode 100644 services/help_service.py delete mode 100644 services/inventory_service.py delete mode 100644 services/item_service.py delete mode 100644 services/moderation/case_service.py delete mode 100644 services/moderation/modlog_service.py delete mode 100644 services/reactions_service.py delete mode 100644 services/stats_service.py delete mode 100644 services/xkcd_service.py delete mode 100644 services/xp_service.py delete mode 100644 settings/responses/bdays.en-US.json delete mode 100644 settings/responses/levels.en-US.json delete mode 100644 settings/responses/strings.en-US.json delete mode 100644 settings/settings.yaml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index e67df16..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -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. diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml deleted file mode 100644 index 53128fd..0000000 --- a/.github/workflows/docker-image.yml +++ /dev/null @@ -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 }} - - - diff --git a/Client.py b/Client.py deleted file mode 100644 index 0bae0a0..0000000 --- a/Client.py +++ /dev/null @@ -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) diff --git a/Luminara.py b/Luminara.py index 4872471..445fe2e 100644 --- a/Luminara.py +++ b/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 = ( - "{time:YY-MM-DD HH:mm:ss} | " - "{level: <8} | " - # "{name}:{function}:{line} - " - "{message}" -) -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 diff --git a/db/database.py b/db/database.py deleted file mode 100644 index 0277edc..0000000 --- a/db/database.py +++ /dev/null @@ -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.") diff --git a/db/migrations/v2_5_8_init.sql b/db/migrations/v2_5_8_init.sql deleted file mode 100644 index 8cd15e0..0000000 --- a/db/migrations/v2_5_8_init.sql +++ /dev/null @@ -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) -); diff --git a/db/migrations/v2_5_9_reactions.sql b/db/migrations/v2_5_9_reactions.sql deleted file mode 100644 index ca3839f..0000000 --- a/db/migrations/v2_5_9_reactions.sql +++ /dev/null @@ -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); diff --git a/db/migrations/v2_6_0_cases.sql b/db/migrations/v2_6_0_cases.sql deleted file mode 100644 index f6fcd60..0000000 --- a/db/migrations/v2_6_0_cases.sql +++ /dev/null @@ -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); \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml deleted file mode 100644 index c24272d..0000000 --- a/docker-compose.prod.yml +++ /dev/null @@ -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 diff --git a/handlers/error_handler.py b/handlers/error_handler.py deleted file mode 100644 index 8833371..0000000 --- a/handlers/error_handler.py +++ /dev/null @@ -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)) diff --git a/handlers/event_handler.py b/handlers/event_handler.py deleted file mode 100644 index edce572..0000000 --- a/handlers/event_handler.py +++ /dev/null @@ -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)) diff --git a/handlers/reaction_handler.py b/handlers/reaction_handler.py deleted file mode 100644 index 177efe8..0000000 --- a/handlers/reaction_handler.py +++ /dev/null @@ -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)) diff --git a/handlers/xp_handler.py b/handlers/xp_handler.py deleted file mode 100644 index c36e5b5..0000000 --- a/handlers/xp_handler.py +++ /dev/null @@ -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)) diff --git a/lib/checks.py b/lib/checks.py deleted file mode 100644 index c38e1de..0000000 --- a/lib/checks.py +++ /dev/null @@ -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) diff --git a/lib/constants.py b/lib/constants.py deleted file mode 100644 index 9e48593..0000000 --- a/lib/constants.py +++ /dev/null @@ -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() diff --git a/lib/embed_builder.py b/lib/embed_builder.py deleted file mode 100644 index 1470daa..0000000 --- a/lib/embed_builder.py +++ /dev/null @@ -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, - ) diff --git a/lib/exceptions/LumiExceptions.py b/lib/exceptions/LumiExceptions.py deleted file mode 100644 index 0136e31..0000000 --- a/lib/exceptions/LumiExceptions.py +++ /dev/null @@ -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) diff --git a/lib/formatter.py b/lib/formatter.py deleted file mode 100644 index 019613b..0000000 --- a/lib/formatter.py +++ /dev/null @@ -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" diff --git a/lib/interaction.py b/lib/interaction.py deleted file mode 100644 index b49443e..0000000 --- a/lib/interaction.py +++ /dev/null @@ -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 diff --git a/lib/interactions/introduction.py b/lib/interactions/introduction.py deleted file mode 100644 index f63627b..0000000 --- a/lib/interactions/introduction.py +++ /dev/null @@ -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 diff --git a/lib/reactions.py b/lib/reactions.py deleted file mode 100644 index 9ccb5b2..0000000 --- a/lib/reactions.py +++ /dev/null @@ -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) diff --git a/lib/time.py b/lib/time.py deleted file mode 100644 index 9d0b2f5..0000000 --- a/lib/time.py +++ /dev/null @@ -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() diff --git a/modules/admin/__init__.py b/modules/admin/__init__.py deleted file mode 100644 index a442d51..0000000 --- a/modules/admin/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/admin/award.py b/modules/admin/award.py deleted file mode 100644 index 9cd18ab..0000000 --- a/modules/admin/award.py +++ /dev/null @@ -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) diff --git a/modules/admin/blacklist.py b/modules/admin/blacklist.py deleted file mode 100644 index 815f649..0000000 --- a/modules/admin/blacklist.py +++ /dev/null @@ -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) diff --git a/modules/admin/sql.py b/modules/admin/sql.py deleted file mode 100644 index fd16daa..0000000 --- a/modules/admin/sql.py +++ /dev/null @@ -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) diff --git a/modules/admin/sync.py b/modules/admin/sync.py deleted file mode 100644 index 8195ede..0000000 --- a/modules/admin/sync.py +++ /dev/null @@ -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 diff --git a/modules/birthdays/__init__.py b/modules/birthdays/__init__.py deleted file mode 100644 index 2605e2f..0000000 --- a/modules/birthdays/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/birthdays/birthday.py b/modules/birthdays/birthday.py deleted file mode 100644 index 6fe136c..0000000 --- a/modules/birthdays/birthday.py +++ /dev/null @@ -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) diff --git a/modules/birthdays/daily_check.py b/modules/birthdays/daily_check.py deleted file mode 100644 index 0abd10b..0000000 --- a/modules/birthdays/daily_check.py +++ /dev/null @@ -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, - ), - ) diff --git a/modules/config/__init__.py b/modules/config/__init__.py deleted file mode 100644 index b2cff77..0000000 --- a/modules/config/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/config/c_birthday.py b/modules/config/c_birthday.py deleted file mode 100644 index 5a74f22..0000000 --- a/modules/config/c_birthday.py +++ /dev/null @@ -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) diff --git a/modules/config/c_boost.py b/modules/config/c_boost.py deleted file mode 100644 index ff0c271..0000000 --- a/modules/config/c_boost.py +++ /dev/null @@ -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, - ) diff --git a/modules/config/c_greet.py b/modules/config/c_greet.py deleted file mode 100644 index 547703c..0000000 --- a/modules/config/c_greet.py +++ /dev/null @@ -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 diff --git a/modules/config/c_level.py b/modules/config/c_level.py deleted file mode 100644 index c9c1bef..0000000 --- a/modules/config/c_level.py +++ /dev/null @@ -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) diff --git a/modules/config/c_moderation.py b/modules/config/c_moderation.py deleted file mode 100644 index d9003d0..0000000 --- a/modules/config/c_moderation.py +++ /dev/null @@ -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) diff --git a/modules/config/c_prefix.py b/modules/config/c_prefix.py deleted file mode 100644 index 655c520..0000000 --- a/modules/config/c_prefix.py +++ /dev/null @@ -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) diff --git a/modules/config/c_show.py b/modules/config/c_show.py deleted file mode 100644 index c19f9ed..0000000 --- a/modules/config/c_show.py +++ /dev/null @@ -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) diff --git a/modules/config/xp_reward.py b/modules/config/xp_reward.py deleted file mode 100644 index bb9eafd..0000000 --- a/modules/config/xp_reward.py +++ /dev/null @@ -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) diff --git a/modules/economy/__init__.py b/modules/economy/__init__.py deleted file mode 100644 index 9bc7237..0000000 --- a/modules/economy/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/economy/balance.py b/modules/economy/balance.py deleted file mode 100644 index fb04840..0000000 --- a/modules/economy/balance.py +++ /dev/null @@ -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) diff --git a/modules/economy/blackjack.py b/modules/economy/blackjack.py deleted file mode 100644 index bbc8c56..0000000 --- a/modules/economy/blackjack.py +++ /dev/null @@ -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 diff --git a/modules/economy/daily.py b/modules/economy/daily.py deleted file mode 100644 index b7777a4..0000000 --- a/modules/economy/daily.py +++ /dev/null @@ -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) diff --git a/modules/economy/give.py b/modules/economy/give.py deleted file mode 100644 index 07ffdeb..0000000 --- a/modules/economy/give.py +++ /dev/null @@ -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) diff --git a/modules/economy/slots.py b/modules/economy/slots.py deleted file mode 100644 index 3ab2b87..0000000 --- a/modules/economy/slots.py +++ /dev/null @@ -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 diff --git a/modules/help/__init__.py b/modules/help/__init__.py deleted file mode 100644 index 1957e2f..0000000 --- a/modules/help/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/levels/__init__.py b/modules/levels/__init__.py deleted file mode 100644 index 8a4d488..0000000 --- a/modules/levels/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/levels/leaderboard.py b/modules/levels/leaderboard.py deleted file mode 100644 index 381f26b..0000000 --- a/modules/levels/leaderboard.py +++ /dev/null @@ -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, - ) diff --git a/modules/levels/level.py b/modules/levels/level.py deleted file mode 100644 index e3c4e13..0000000 --- a/modules/levels/level.py +++ /dev/null @@ -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) diff --git a/modules/misc/__init__.py b/modules/misc/__init__.py deleted file mode 100644 index 77844c3..0000000 --- a/modules/misc/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/misc/avatar.py b/modules/misc/avatar.py deleted file mode 100644 index 1d8424c..0000000 --- a/modules/misc/avatar.py +++ /dev/null @@ -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") diff --git a/modules/misc/backup.py b/modules/misc/backup.py deleted file mode 100644 index 4604348..0000000 --- a/modules/misc/backup.py +++ /dev/null @@ -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".') diff --git a/modules/misc/info.py b/modules/misc/info.py deleted file mode 100644 index 3d991af..0000000 --- a/modules/misc/info.py +++ /dev/null @@ -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) diff --git a/modules/misc/introduction.py b/modules/misc/introduction.py deleted file mode 100644 index f0ee366..0000000 --- a/modules/misc/introduction.py +++ /dev/null @@ -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"], - ), - ) diff --git a/modules/misc/invite.py b/modules/misc/invite.py deleted file mode 100644 index 79df5e0..0000000 --- a/modules/misc/invite.py +++ /dev/null @@ -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) diff --git a/modules/misc/ping.py b/modules/misc/ping.py deleted file mode 100644 index d45c5c8..0000000 --- a/modules/misc/ping.py +++ /dev/null @@ -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) diff --git a/modules/misc/xkcd.py b/modules/misc/xkcd.py deleted file mode 100644 index 0b88120..0000000 --- a/modules/misc/xkcd.py +++ /dev/null @@ -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"], - ), - ) diff --git a/modules/moderation/__init__.py b/modules/moderation/__init__.py deleted file mode 100644 index 730d70a..0000000 --- a/modules/moderation/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/moderation/ban.py b/modules/moderation/ban.py deleted file mode 100644 index 4eedbd0..0000000 --- a/modules/moderation/ban.py +++ /dev/null @@ -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), - ), - ) diff --git a/modules/moderation/cases.py b/modules/moderation/cases.py deleted file mode 100644 index 5d56c58..0000000 --- a/modules/moderation/cases.py +++ /dev/null @@ -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() diff --git a/modules/moderation/kick.py b/modules/moderation/kick.py deleted file mode 100644 index 87e5d76..0000000 --- a/modules/moderation/kick.py +++ /dev/null @@ -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) diff --git a/modules/moderation/softban.py b/modules/moderation/softban.py deleted file mode 100644 index a6c3b07..0000000 --- a/modules/moderation/softban.py +++ /dev/null @@ -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) diff --git a/modules/moderation/timeout.py b/modules/moderation/timeout.py deleted file mode 100644 index 5075f8e..0000000 --- a/modules/moderation/timeout.py +++ /dev/null @@ -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), - ), - ) diff --git a/modules/moderation/utils/actionable.py b/modules/moderation/utils/actionable.py deleted file mode 100644 index 5b0d811..0000000 --- a/modules/moderation/utils/actionable.py +++ /dev/null @@ -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"]) diff --git a/modules/moderation/utils/case_embed.py b/modules/moderation/utils/case_embed.py deleted file mode 100644 index c0bd3fd..0000000 --- a/modules/moderation/utils/case_embed.py +++ /dev/null @@ -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"" - 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 diff --git a/modules/moderation/utils/case_handler.py b/modules/moderation/utils/case_handler.py deleted file mode 100644 index 024e040..0000000 --- a/modules/moderation/utils/case_handler.py +++ /dev/null @@ -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 diff --git a/modules/moderation/warn.py b/modules/moderation/warn.py deleted file mode 100644 index 6b50f57..0000000 --- a/modules/moderation/warn.py +++ /dev/null @@ -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, - ) diff --git a/modules/triggers/__init__.py b/modules/triggers/__init__.py deleted file mode 100644 index 7a664d5..0000000 --- a/modules/triggers/__init__.py +++ /dev/null @@ -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)) diff --git a/modules/triggers/add.py b/modules/triggers/add.py deleted file mode 100644 index 81a9819..0000000 --- a/modules/triggers/add.py +++ /dev/null @@ -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 diff --git a/modules/triggers/delete.py b/modules/triggers/delete.py deleted file mode 100644 index 91fe0c5..0000000 --- a/modules/triggers/delete.py +++ /dev/null @@ -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) diff --git a/modules/triggers/list.py b/modules/triggers/list.py deleted file mode 100644 index 6dcf064..0000000 --- a/modules/triggers/list.py +++ /dev/null @@ -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) diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index dabad4b..0000000 --- a/poetry.lock +++ /dev/null @@ -1,1142 +0,0 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.4.0" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, - {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, -] - -[[package]] -name = "aiohttp" -version = "3.10.5" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.8" -files = [ - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, - {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, - {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, - {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, - {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, - {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, - {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, - {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, - {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, - {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, - {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, - {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, - {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, - {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, - {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, - {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, - {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, - {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, - {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, - {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, - {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, - {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, - {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, - {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, - {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, - {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, - {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, - {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, - {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, - {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, - {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, - {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.3.0" -aiosignal = ">=1.1.2" -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -yarl = ">=1.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.3.1" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.7" -files = [ - {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, - {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" - -[[package]] -name = "anyio" -version = "4.4.0" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -optional = false -python-versions = ">=3.8" -files = [ - {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, - {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" - -[package.extras] -doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] -test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (>=0.23)"] - -[[package]] -name = "attrs" -version = "24.2.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.7" -files = [ - {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, - {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[package]] -name = "certifi" -version = "2024.7.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.6" -files = [ - {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, - {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, -] - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.3.2" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7.0" -files = [ - {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, - {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, - {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, - {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, - {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, - {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, - {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, - {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "distlib" -version = "0.3.8" -description = "Distribution utilities" -optional = false -python-versions = "*" -files = [ - {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, - {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, -] - -[[package]] -name = "dropbox" -version = "12.0.2" -description = "Official Dropbox API Client" -optional = false -python-versions = "*" -files = [ - {file = "dropbox-12.0.2-py2-none-any.whl", hash = "sha256:4b8207a9f4afd33726ec886c0d223f4bbc42fe649b87718690a24704f5e24c0c"}, - {file = "dropbox-12.0.2-py3-none-any.whl", hash = "sha256:c5b7e9c2668adb6b12dcecd84342565dc50f7d35ab6a748d155cb79040979d1c"}, - {file = "dropbox-12.0.2.tar.gz", hash = "sha256:50057fd5ad5fcf047f542dfc6747a896e7ef982f1b5f8500daf51f3abd609962"}, -] - -[package.dependencies] -requests = ">=2.16.2" -six = ">=1.12.0" -stone = ">=2,<3.3.3" - -[[package]] -name = "filelock" -version = "3.15.4" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.8" -files = [ - {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, - {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] -typing = ["typing-extensions (>=4.8)"] - -[[package]] -name = "frozenlist" -version = "1.4.1" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.8" -files = [ - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, - {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, - {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, - {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, - {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, - {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, - {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, - {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, - {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, - {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, - {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, - {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, - {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, - {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, - {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, - {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, - {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, - {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, - {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, - {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, - {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, - {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, - {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, - {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, - {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, - {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, - {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, - {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, -] - -[[package]] -name = "h11" -version = "0.14.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.7" -files = [ - {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, - {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, -] - -[[package]] -name = "httpcore" -version = "1.0.5" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, - {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.13,<0.15" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<0.26.0)"] - -[[package]] -name = "httpx" -version = "0.27.0" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" -sniffio = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] - -[[package]] -name = "identify" -version = "2.6.0" -description = "File identification library for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, - {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.8" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -files = [ - {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, - {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, -] - -[[package]] -name = "loguru" -version = "0.7.2" -description = "Python logging made (stupidly) simple" -optional = false -python-versions = ">=3.5" -files = [ - {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, - {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} -win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} - -[package.extras] -dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] - -[[package]] -name = "multidict" -version = "6.0.5" -description = "multidict implementation" -optional = false -python-versions = ">=3.7" -files = [ - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, - {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, - {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, - {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, - {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, - {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, - {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, - {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, - {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, - {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, - {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, - {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, - {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, - {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, - {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, - {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, - {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, - {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, - {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, - {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, - {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, - {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, - {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, - {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, - {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, - {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, - {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, - {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, - {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, - {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, - {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, - {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, - {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, -] - -[[package]] -name = "mysql-connector-python" -version = "9.0.0" -description = "MySQL driver written in Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "mysql-connector-python-9.0.0.tar.gz", hash = "sha256:8a404db37864acca43fd76222d1fbc7ff8d17d4ce02d803289c2141c2693ce9e"}, - {file = "mysql_connector_python-9.0.0-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:72bfd0213364c2bea0244f6432ababb2f204cff43f4f886c65dca2be11f536ee"}, - {file = "mysql_connector_python-9.0.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:052058cf3dc0bf183ab522132f3b18a614a26f3e392ae886efcdab38d4f4fc42"}, - {file = "mysql_connector_python-9.0.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f41cb8da8bb487ed60329ac31789c50621f0e6d2c26abc7d4ae2383838fb1b93"}, - {file = "mysql_connector_python-9.0.0-cp310-cp310-manylinux_2_17_x86_64.whl", hash = "sha256:67fc2b2e67a63963c633fc884f285a8de5a626967a3cc5f5d48ac3e8d15b122d"}, - {file = "mysql_connector_python-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:933c3e39d30cc6f9ff636d27d18aa3f1341b23d803ade4b57a76f91c26d14066"}, - {file = "mysql_connector_python-9.0.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:7af7f68198f2aca3a520e1201fe2b329331e0ca19a481f3b3451cb0746f56c01"}, - {file = "mysql_connector_python-9.0.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:38c229d76cd1dea8465357855f2b2842b7a9b201f17dea13b0eab7d3b9d6ad74"}, - {file = "mysql_connector_python-9.0.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c01aad36f0c34ca3f642018be37fd0d55c546f088837cba88f1a1aff408c63dd"}, - {file = "mysql_connector_python-9.0.0-cp311-cp311-manylinux_2_17_x86_64.whl", hash = "sha256:853c5916d188ef2c357a474e15ac81cafae6085e599ceb9b2b0bcb9104118e63"}, - {file = "mysql_connector_python-9.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:134b71e439e2eafaee4c550365221ae2890dd54fb76227c64a87a94a07fe79b4"}, - {file = "mysql_connector_python-9.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:9199d6ecc81576602990178f0c2fb71737c53a598c8a2f51e1097a53fcfaee40"}, - {file = "mysql_connector_python-9.0.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:b267a6c000b7f98e6436a9acefa5582a9662e503b0632a2562e3093a677f6845"}, - {file = "mysql_connector_python-9.0.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:ac92b2f2a9307ac0c4aafdfcf7ecf01ec92dfebd9140f8c95353adfbf5822cd4"}, - {file = "mysql_connector_python-9.0.0-cp312-cp312-manylinux_2_17_x86_64.whl", hash = "sha256:ced1fa55e653d28f66c4f3569ed524d4d92098119dcd80c2fa026872a30eba55"}, - {file = "mysql_connector_python-9.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca8349fe56ce39498d9b5ca8eabba744774e94d85775259f26a43a03e8825429"}, - {file = "mysql_connector_python-9.0.0-cp38-cp38-macosx_13_0_x86_64.whl", hash = "sha256:a48534b881c176557ddc78527c8c75b4c9402511e972670ad33c5e49d31eddfe"}, - {file = "mysql_connector_python-9.0.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e90a7b96ce2c6a60f6e2609b0c83f45bd55e144cc7c2a9714e344938827da363"}, - {file = "mysql_connector_python-9.0.0-cp38-cp38-manylinux_2_17_x86_64.whl", hash = "sha256:2a8f451c4d700802fdfe515890c14974766c322213df2ceed3b27752929dc70f"}, - {file = "mysql_connector_python-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:2dcf05355315e5c7c81e9eca34395d78f29c4da3662e869e42dd7b16380f92ce"}, - {file = "mysql_connector_python-9.0.0-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:823190e7f2a9b4bcc574ab6bb72a33802933e1a8c171594faad90162d2d27758"}, - {file = "mysql_connector_python-9.0.0-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:b8639d8aa381a7d19b92ca1a32448f09baaf80787e50187d1f7d072191430768"}, - {file = "mysql_connector_python-9.0.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:a688ea65b2ea771b9b69dc409377240a7cab7c1aafef46cd75219d5a94ba49e0"}, - {file = "mysql_connector_python-9.0.0-cp39-cp39-manylinux_2_17_x86_64.whl", hash = "sha256:6d92c58f71c691f86ad35bb2f3e13d7a9cc1c84ce0b04c146e5980e450faeff1"}, - {file = "mysql_connector_python-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:eacc353dcf6f39665d4ca3311ded5ddae0f5a117f03107991d4185ffa59fd890"}, - {file = "mysql_connector_python-9.0.0-py2.py3-none-any.whl", hash = "sha256:016d81bb1499dee8b77c82464244e98f10d3671ceefb4023adc559267d1fad50"}, -] - -[package.extras] -dns-srv = ["dnspython (==2.6.1)"] -fido2 = ["fido2 (==1.1.2)"] -gssapi = ["gssapi (>=1.6.9,<=1.8.2)"] -telemetry = ["opentelemetry-api (==1.18.0)", "opentelemetry-exporter-otlp-proto-http (==1.18.0)", "opentelemetry-sdk (==1.18.0)"] - -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, - {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] -type = ["mypy (>=1.8)"] - -[[package]] -name = "ply" -version = "3.11" -description = "Python Lex & Yacc" -optional = false -python-versions = "*" -files = [ - {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, - {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, -] - -[[package]] -name = "pre-commit" -version = "3.8.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -files = [ - {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, - {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "psutil" -version = "6.0.0" -description = "Cross-platform lib for process and system monitoring in Python." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -files = [ - {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, - {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, - {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, - {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, - {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, - {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, - {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, - {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, - {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, - {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, - {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, - {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, - {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, -] - -[package.extras] -test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] - -[[package]] -name = "py-cord" -version = "2.6.0" -description = "A Python wrapper for the Discord API" -optional = false -python-versions = ">=3.8" -files = [ - {file = "py_cord-2.6.0-py3-none-any.whl", hash = "sha256:906cc077904a4d478af0264ae4374b0412ed9f9ab950d0162c4f31239a906d26"}, - {file = "py_cord-2.6.0.tar.gz", hash = "sha256:bbc0349542965d05e4b18cc4424136206430a8cc911fda12a0a57df6fdf9cd9c"}, -] - -[package.dependencies] -aiohttp = ">=3.6.0,<4.0" - -[package.extras] -docs = ["furo (==2023.3.23)", "myst-parser (==1.0.0)", "sphinx (==5.3.0)", "sphinx-autodoc-typehints (==1.23.0)", "sphinx-copybutton (==0.5.2)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "sphinxext-opengraph (==0.9.1)"] -speed = ["aiohttp[speedups]", "msgspec (>=0.18.6,<0.19.0)"] -voice = ["PyNaCl (>=1.3.0,<1.6)"] - -[[package]] -name = "pyright" -version = "1.1.377" -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"}, -] - -[package.dependencies] -nodeenv = ">=1.6.0" - -[package.extras] -all = ["twine (>=3.4.1)"] -dev = ["twine (>=3.4.1)"] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.8" -files = [ - {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, - {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "pytimeparse" -version = "1.1.8" -description = "Time expression parser" -optional = false -python-versions = "*" -files = [ - {file = "pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd"}, - {file = "pytimeparse-1.1.8.tar.gz", hash = "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a"}, -] - -[[package]] -name = "pytz" -version = "2024.1" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -files = [ - {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, - {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "requests" -version = "2.32.3" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "ruff" -version = "0.5.7" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, -] - -[[package]] -name = "six" -version = "1.16.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, - {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "stone" -version = "3.3.1" -description = "Stone is an interface description language (IDL) for APIs." -optional = false -python-versions = "*" -files = [ - {file = "stone-3.3.1-py2-none-any.whl", hash = "sha256:cd2f7f9056fc39b16c8fd46a26971dc5ccd30b5c2c246566cd2c0dd27ff96609"}, - {file = "stone-3.3.1-py3-none-any.whl", hash = "sha256:e15866fad249c11a963cce3bdbed37758f2e88c8ff4898616bc0caeb1e216047"}, - {file = "stone-3.3.1.tar.gz", hash = "sha256:4ef0397512f609757975f7ec09b35639d72ba7e3e17ce4ddf399578346b4cb50"}, -] - -[package.dependencies] -ply = ">=3.4" -six = ">=1.12.0" - -[[package]] -name = "urllib3" -version = "2.2.2" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.8" -files = [ - {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, - {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "virtualenv" -version = "20.26.3" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.7" -files = [ - {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, - {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "win32-setctime" -version = "1.1.0" -description = "A small Python utility to set file creation time on Windows" -optional = false -python-versions = ">=3.5" -files = [ - {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, - {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, -] - -[package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] - -[[package]] -name = "yarl" -version = "1.9.4" -description = "Yet another URL library" -optional = false -python-versions = ">=3.7" -files = [ - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, - {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, - {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, - {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, - {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, - {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, - {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, - {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, - {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, - {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, - {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, - {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, - {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, - {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, - {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, - {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, - {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, - {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, - {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, - {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, - {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, - {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, - {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, - {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, - {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, - {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, - {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, - {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, - {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, - {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, - {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, - {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, - {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" - -[metadata] -lock-version = "2.0" -python-versions = "^3.12" -content-hash = "541857707095fb0b5c439aedbfacd91ca3582f110f12d786dc29e7c70f989b3e" diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index aa73806..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,27 +0,0 @@ -[tool.poetry] -authors = ["wlinator "] -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"] diff --git a/services/birthday_service.py b/services/birthday_service.py deleted file mode 100644 index 5ec2c66..0000000 --- a/services/birthday_service.py +++ /dev/null @@ -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] diff --git a/services/blacklist_service.py b/services/blacklist_service.py deleted file mode 100644 index 7ca40ac..0000000 --- a/services/blacklist_service.py +++ /dev/null @@ -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) diff --git a/services/config_service.py b/services/config_service.py deleted file mode 100644 index b996c92..0000000 --- a/services/config_service.py +++ /dev/null @@ -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)) diff --git a/services/currency_service.py b/services/currency_service.py deleted file mode 100644 index 314fd5d..0000000 --- a/services/currency_service.py +++ /dev/null @@ -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 diff --git a/services/daily_service.py b/services/daily_service.py deleted file mode 100644 index 9e5e119..0000000 --- a/services/daily_service.py +++ /dev/null @@ -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 diff --git a/services/help_service.py b/services/help_service.py deleted file mode 100644 index 757c231..0000000 --- a/services/help_service.py +++ /dev/null @@ -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) diff --git a/services/inventory_service.py b/services/inventory_service.py deleted file mode 100644 index c842486..0000000 --- a/services/inventory_service.py +++ /dev/null @@ -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 [] diff --git a/services/item_service.py b/services/item_service.py deleted file mode 100644 index 5afb20a..0000000 --- a/services/item_service.py +++ /dev/null @@ -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) diff --git a/services/moderation/case_service.py b/services/moderation/case_service.py deleted file mode 100644 index aaaf25b..0000000 --- a/services/moderation/case_service.py +++ /dev/null @@ -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 diff --git a/services/moderation/modlog_service.py b/services/moderation/modlog_service.py deleted file mode 100644 index 558efd9..0000000 --- a/services/moderation/modlog_service.py +++ /dev/null @@ -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 diff --git a/services/reactions_service.py b/services/reactions_service.py deleted file mode 100644 index 7f672ec..0000000 --- a/services/reactions_service.py +++ /dev/null @@ -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 diff --git a/services/stats_service.py b/services/stats_service.py deleted file mode 100644 index 7906fae..0000000 --- a/services/stats_service.py +++ /dev/null @@ -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, - } diff --git a/services/xkcd_service.py b/services/xkcd_service.py deleted file mode 100644 index e7f5b30..0000000 --- a/services/xkcd_service.py +++ /dev/null @@ -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()" diff --git a/services/xp_service.py b/services/xp_service.py deleted file mode 100644 index 8cf7b31..0000000 --- a/services/xp_service.py +++ /dev/null @@ -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 diff --git a/settings/responses/bdays.en-US.json b/settings/responses/bdays.en-US.json deleted file mode 100644 index b0626a9..0000000 --- a/settings/responses/bdays.en-US.json +++ /dev/null @@ -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! 🎉" - ] -} \ No newline at end of file diff --git a/settings/responses/levels.en-US.json b/settings/responses/levels.en-US.json deleted file mode 100644 index c9fe5ca..0000000 --- a/settings/responses/levels.en-US.json +++ /dev/null @@ -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 {}**." - ] -} \ No newline at end of file diff --git a/settings/responses/strings.en-US.json b/settings/responses/strings.en-US.json deleted file mode 100644 index eb8bc85..0000000 --- a/settings/responses/strings.en-US.json +++ /dev/null @@ -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 ` - View a specific case\n`/editcase ` - 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 .", - "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:** \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 .", - "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!" -} \ No newline at end of file diff --git a/settings/settings.yaml b/settings/settings.yaml deleted file mode 100644 index 250b4e6..0000000 --- a/settings/settings.yaml +++ /dev/null @@ -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!" From bb1bae2959340b43c9795f792a5f67ba4cb53aeb Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 05:00:23 -0400 Subject: [PATCH 002/102] Update README --- README.md | 44 ++++---------------------------------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 71eca41..74123ee 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,11 @@ -# You can invite me with [this link](https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot). Thanks for using Luminara! +## Version 3 (Early Development) -![Lumi art](https://git.wlinator.org/assets/img/logo.png) +Version 3 of Lumi is currently in early development. This new version will make the switch to `discord.py`. -## Self-Hosting +### Self-Hosting -Self-hosting refers to running Luminara on your own server or computer, rather than using the publicly hosted version. -This approach offers the ability to manage your own instance of the bot and give it a custom name and avatar. +While v3 is still in development, you can self-host the bot using the provided `docker-compose.dev.yml` file. Please note that this version is not yet stable and may contain bugs or incomplete features. -**Note:** From `v2.9.0` and onward, Lumi now utilizes a [settings.yaml](settings/settings.yaml) file to manage configuration settings. This allows you to customize your bot's behavior without needing to modify the source code itself. - -### Requirements - -Before you begin, make sure you have the following installed on your system: - -- [Docker](https://docs.docker.com/get-docker/) -- [Docker Compose](https://docs.docker.com/compose/install/) - -Additionally, you'll need to create a Discord bot application and obtain a token: - -1. Go to the [Discord Developer Portal](https://discord.com/developers/applications). -2. Click on "New Application" and give it a name. -3. Navigate to the "Bot" tab and click "Add Bot". -4. Under the bot's username, click "Reset Token" to reveal your bot token. -5. Copy this token; you'll need it for the `.env` file later. - -*Note: remember to keep your bot token secret and never share it publicly.* - -### Running Luminara: - -1. Copy the contents from [`docker-compose.prod.yml`](docker-compose.prod.yml) to a new file named `docker-compose.yml` - in an empty directory. - -2. Copy the contents from [`.env.example`](.env.example) to a new file named `.env` in the same directory. - -3. Fill out the `.env` file with your specific configuration details. - -4. Run the following command in your terminal: - - ``` - docker compose up -d --build - ``` - -This will build and start Luminara in detached mode. --- From a6862bd085de7137dbbc522595b7aabb3178d827 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:29:16 -0400 Subject: [PATCH 003/102] v3 official init: working DPY bot with a cog loader and tree sync command. --- Dockerfile | 2 +- Luminara.py | 1 - client.py | 58 +++ lib/const.py | 40 ++ loader.py | 55 +++ lumi.py | 41 ++ modules/admin/sync.py | 32 ++ modules/misc/ping.py | 17 + poetry.lock | 936 ++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 27 ++ settings.yaml | 6 + 11 files changed, 1213 insertions(+), 2 deletions(-) delete mode 100644 Luminara.py create mode 100644 client.py create mode 100644 lib/const.py create mode 100644 loader.py create mode 100644 lumi.py create mode 100644 modules/admin/sync.py create mode 100644 modules/misc/ping.py create mode 100644 poetry.lock create mode 100644 pyproject.toml create mode 100644 settings.yaml diff --git a/Dockerfile b/Dockerfile index ba2bc4b..cc3bbd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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", "./Luminara.py" ] \ No newline at end of file +CMD [ "poetry", "run", "python", "-OO", "./lumi.py" ] \ No newline at end of file diff --git a/Luminara.py b/Luminara.py deleted file mode 100644 index 445fe2e..0000000 --- a/Luminara.py +++ /dev/null @@ -1 +0,0 @@ -# TODO: switch to DPY diff --git a/client.py b/client.py new file mode 100644 index 0000000..53292ca --- /dev/null +++ b/client.py @@ -0,0 +1,58 @@ +from discord.ext import commands +from loguru import logger +import asyncio +from loader import CogLoader + + +class Luminara(commands.Bot): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.is_shutting_down: bool = False + self.setup_task: asyncio.Task = asyncio.create_task(self.setup()) + self.strip_after_prefix = True + self.case_insensitive = True + + async def setup(self) -> None: + try: + pass + except Exception as e: + logger.error(f"Failed to setup: {e}") + await self.load_cogs() + + async def load_cogs(self) -> None: + logger.info("Loading cogs...") + await CogLoader.setup(bot=self) + + @commands.Cog.listener() + async def on_ready(self) -> None: + logger.success(f"Logged in as {self.user}.") + + if not self.setup_task.done(): + await self.setup_task + + @commands.Cog.listener() + async def on_disconnect(self) -> None: + logger.warning("Disconnected from Discord.") + + async def shutdown(self) -> None: + if self.is_shutting_down: + logger.info("Shutdown already in progress. Exiting.") + return + + self.is_shutting_down = True + logger.info("Shutting down...") + + await self.close() + + 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: + task.cancel() + + await asyncio.gather(*tasks, return_exceptions=True) + logger.debug("All tasks cancelled.") + + logger.info("Shutdown complete.") diff --git a/lib/const.py b/lib/const.py new file mode 100644 index 0000000..76d46b6 --- /dev/null +++ b/lib/const.py @@ -0,0 +1,40 @@ +import os +import yaml +from functools import lru_cache +from typing import Optional, Callable, Set + + +class _parser: + """Internal parses class. Not intended to be used outside of this module.""" + + @lru_cache(maxsize=1024) + def read_settings(self) -> dict: + return self._read_file("settings.yaml", yaml.safe_load) + + def _read_file(self, file_path: str, load_func: Callable) -> dict: + with open(file_path) as file: + return load_func(file) + + +class _constants: + _p = _parser() + _s = _parser().read_settings() + + # bot credentials + TOKEN: Optional[str] = os.getenv("TOKEN") + + 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 + ) + + # settings + LOG_LEVEL: str = _s["logs"]["level"] or "DEBUG" + LOG_FORMAT: str = _s["logs"]["format"] + COG_IGNORE_LIST: Set[str] = ( + set(_s["cogs"]["ignore"]) if _s["cogs"]["ignore"] else set() + ) + + +CONST: _constants = _constants() diff --git a/loader.py b/loader.py new file mode 100644 index 0000000..70d176d --- /dev/null +++ b/loader.py @@ -0,0 +1,55 @@ +from loguru import logger +from discord.ext import commands +from lib.const import CONST +from pathlib import Path +import aiofiles.os + + +class CogLoader(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + self.cog_ignore_list: set[str] = CONST.COG_IGNORE_LIST + + async def is_cog(self, path: Path) -> bool: + cog_name: str = path.stem + + return ( + path.suffix == ".py" + and cog_name not in self.cog_ignore_list + and not path.name.startswith("_") + and await aiofiles.os.path.isfile(path) + ) + + async def load_cogs(self, path: Path) -> None: + try: + if await aiofiles.os.path.isdir(path): + for item in path.iterdir(): + try: + await self.load_cogs(path=item) + except Exception as e: + logger.exception(f"Error loading cog from {item}: {e}") + + elif await self.is_cog(path): + relative_path: Path = path.relative_to(Path(__file__).parent) + module: str = ( + str(relative_path).replace("/", ".").replace("\\", ".")[:-3] + ) + try: + await self.bot.load_extension(name=module) + logger.debug(f"Loaded cog: {module}") + + except Exception as e: + logger.exception(f"Error loading cog: {module}. Error: {e}") + + except Exception as e: + logger.exception(f"Error loading cogs from {path}: {e}") + + async def load_cog_from_dir(self, dir_name: str) -> None: + path: Path = Path(__file__).parent / dir_name + await self.load_cogs(path) + + @classmethod + async def setup(cls, bot: commands.Bot) -> None: + cog_loader = cls(bot) + await cog_loader.load_cog_from_dir(dir_name="modules") + await bot.add_cog(cog_loader) diff --git a/lumi.py b/lumi.py new file mode 100644 index 0000000..2089053 --- /dev/null +++ b/lumi.py @@ -0,0 +1,41 @@ +import sys +import asyncio +import discord +from discord.ext import commands +from loguru import logger +from lib.const import CONST +from client import Luminara + +logger.remove() +logger.add(sys.stdout, format=CONST.LOG_FORMAT, colorize=True, level=CONST.LOG_LEVEL) + + +async def get_prefix(bot, message): + return commands.when_mentioned_or(".")(bot, message) + + +async def main() -> None: + if not CONST.TOKEN: + logger.error("No token provided") + return + + lumi: Luminara = Luminara( + owner_ids=CONST.OWNER_IDS, + intents=discord.Intents.all(), + command_prefix=get_prefix, + allowed_mentions=discord.AllowedMentions(everyone=False), + ) + + try: + await lumi.start(CONST.TOKEN, reconnect=True) + + except KeyboardInterrupt: + logger.info("Keyboard interrupt detected. Shutting down...") + + finally: + logger.info("Closing resources...") + await lumi.shutdown() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/modules/admin/sync.py b/modules/admin/sync.py new file mode 100644 index 0000000..d13c111 --- /dev/null +++ b/modules/admin/sync.py @@ -0,0 +1,32 @@ +from discord.ext import commands +import discord +from typing import Optional + + +class Sync(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.command( + name="sync", + usage="sync [guild]", + ) + @commands.guild_only() + @commands.is_owner() + async def sync( + self, + ctx: commands.Context[commands.Bot], + guild: Optional[discord.Guild] = None, + ) -> None: + if not guild: + guild = ctx.guild + + assert guild + + self.bot.tree.copy_global_to(guild=guild) + await self.bot.tree.sync(guild=guild) + await ctx.send("Application command tree synced.") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Sync(bot)) diff --git a/modules/misc/ping.py b/modules/misc/ping.py new file mode 100644 index 0000000..7cfe18e --- /dev/null +++ b/modules/misc/ping.py @@ -0,0 +1,17 @@ +from discord.ext import commands + + +class Ping(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command( + name="ping", + usage="ping", + ) + async def ping(self, ctx: commands.Context[commands.Bot]) -> None: + await ctx.send(f"Pong! Latency: {self.bot.latency * 1000:.2f}ms") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Ping(bot)) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..98a689d --- /dev/null +++ b/poetry.lock @@ -0,0 +1,936 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "aiocache" +version = "0.12.2" +description = "multi backend asyncio cache" +optional = false +python-versions = "*" +files = [ + {file = "aiocache-0.12.2-py2.py3-none-any.whl", hash = "sha256:9b6fa30634ab0bfc3ecc44928a91ff07c6ea16d27d55469636b296ebc6eb5918"}, + {file = "aiocache-0.12.2.tar.gz", hash = "sha256:b41c9a145b050a5dcbae1599f847db6dd445193b1f3bd172d8e0fe0cb9e96684"}, +] + +[package.extras] +memcached = ["aiomcache (>=0.5.2)"] +msgpack = ["msgpack (>=0.5.5)"] +redis = ["redis (>=4.2.0)"] + +[[package]] +name = "aioconsole" +version = "0.7.1" +description = "Asynchronous console and interfaces for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aioconsole-0.7.1-py3-none-any.whl", hash = "sha256:1867a7cc86897a87398e6e6fba302738548f1cf76cbc6c76e06338e091113bdc"}, + {file = "aioconsole-0.7.1.tar.gz", hash = "sha256:a3e52428d32623c96746ec3862d97483c61c12a2f2dfba618886b709415d4533"}, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +description = "File support for asyncio." +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, + {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.4.0" +description = "Happy Eyeballs for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohappyeyeballs-2.4.0-py3-none-any.whl", hash = "sha256:7ce92076e249169a13c2f49320d1967425eaf1f407522d707d59cac7628d62bd"}, + {file = "aiohappyeyeballs-2.4.0.tar.gz", hash = "sha256:55a1714f084e63d49639800f95716da97a1f173d46a16dfcfda0016abb93b6b2"}, +] + +[[package]] +name = "aiohttp" +version = "3.10.5" +description = "Async http client/server framework (asyncio)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:18a01eba2574fb9edd5f6e5fb25f66e6ce061da5dab5db75e13fe1558142e0a3"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:94fac7c6e77ccb1ca91e9eb4cb0ac0270b9fb9b289738654120ba8cebb1189c6"}, + {file = "aiohttp-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f1f1c75c395991ce9c94d3e4aa96e5c59c8356a15b1c9231e783865e2772699"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7acae3cf1a2a2361ec4c8e787eaaa86a94171d2417aae53c0cca6ca3118ff6"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:94c4381ffba9cc508b37d2e536b418d5ea9cfdc2848b9a7fea6aebad4ec6aac1"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c31ad0c0c507894e3eaa843415841995bf8de4d6b2d24c6e33099f4bc9fc0d4f"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0912b8a8fadeb32ff67a3ed44249448c20148397c1ed905d5dac185b4ca547bb"}, + {file = "aiohttp-3.10.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0d93400c18596b7dc4794d48a63fb361b01a0d8eb39f28800dc900c8fbdaca91"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3c5e0d764a5c9aa5a62d99728c56d455310bcc288a79cab10157b3af426f"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d742c36ed44f2798c8d3f4bc511f479b9ceef2b93f348671184139e7d708042c"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:814375093edae5f1cb31e3407997cf3eacefb9010f96df10d64829362ae2df69"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8224f98be68a84b19f48e0bdc14224b5a71339aff3a27df69989fa47d01296f3"}, + {file = "aiohttp-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d9a487ef090aea982d748b1b0d74fe7c3950b109df967630a20584f9a99c0683"}, + {file = "aiohttp-3.10.5-cp310-cp310-win32.whl", hash = "sha256:d9ef084e3dc690ad50137cc05831c52b6ca428096e6deb3c43e95827f531d5ef"}, + {file = "aiohttp-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:66bf9234e08fe561dccd62083bf67400bdbf1c67ba9efdc3dac03650e97c6088"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8c6a4e5e40156d72a40241a25cc226051c0a8d816610097a8e8f517aeacd59a2"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2c634a3207a5445be65536d38c13791904fda0748b9eabf908d3fe86a52941cf"}, + {file = "aiohttp-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4aff049b5e629ef9b3e9e617fa6e2dfeda1bf87e01bcfecaf3949af9e210105e"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1942244f00baaacaa8155eca94dbd9e8cc7017deb69b75ef67c78e89fdad3c77"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e04a1f2a65ad2f93aa20f9ff9f1b672bf912413e5547f60749fa2ef8a644e061"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f2bfc0032a00405d4af2ba27f3c429e851d04fad1e5ceee4080a1c570476697"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:424ae21498790e12eb759040bbb504e5e280cab64693d14775c54269fd1d2bb7"}, + {file = "aiohttp-3.10.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:975218eee0e6d24eb336d0328c768ebc5d617609affaca5dbbd6dd1984f16ed0"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4120d7fefa1e2d8fb6f650b11489710091788de554e2b6f8347c7a20ceb003f5"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b90078989ef3fc45cf9221d3859acd1108af7560c52397ff4ace8ad7052a132e"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ba5a8b74c2a8af7d862399cdedce1533642fa727def0b8c3e3e02fcb52dca1b1"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:02594361128f780eecc2a29939d9dfc870e17b45178a867bf61a11b2a4367277"}, + {file = "aiohttp-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8fb4fc029e135859f533025bc82047334e24b0d489e75513144f25408ecaf058"}, + {file = "aiohttp-3.10.5-cp311-cp311-win32.whl", hash = "sha256:e1ca1ef5ba129718a8fc827b0867f6aa4e893c56eb00003b7367f8a733a9b072"}, + {file = "aiohttp-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:349ef8a73a7c5665cca65c88ab24abe75447e28aa3bc4c93ea5093474dfdf0ff"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:305be5ff2081fa1d283a76113b8df7a14c10d75602a38d9f012935df20731487"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a1c32a19ee6bbde02f1cb189e13a71b321256cc1d431196a9f824050b160d5a"}, + {file = "aiohttp-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:61645818edd40cc6f455b851277a21bf420ce347baa0b86eaa41d51ef58ba23d"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c225286f2b13bab5987425558baa5cbdb2bc925b2998038fa028245ef421e75"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8ba01ebc6175e1e6b7275c907a3a36be48a2d487549b656aa90c8a910d9f3178"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8eaf44ccbc4e35762683078b72bf293f476561d8b68ec8a64f98cf32811c323e"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1c43eb1ab7cbf411b8e387dc169acb31f0ca0d8c09ba63f9eac67829585b44f"}, + {file = "aiohttp-3.10.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:de7a5299827253023c55ea549444e058c0eb496931fa05d693b95140a947cb73"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4790f0e15f00058f7599dab2b206d3049d7ac464dc2e5eae0e93fa18aee9e7bf"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b324a6b8376a23e6ba25d368726ee3bc281e6ab306db80b5819999c737d820"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0d277cfb304118079e7044aad0b76685d30ecb86f83a0711fc5fb257ffe832ca"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:54d9ddea424cd19d3ff6128601a4a4d23d54a421f9b4c0fff740505813739a91"}, + {file = "aiohttp-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4f1c9866ccf48a6df2b06823e6ae80573529f2af3a0992ec4fe75b1a510df8a6"}, + {file = "aiohttp-3.10.5-cp312-cp312-win32.whl", hash = "sha256:dc4826823121783dccc0871e3f405417ac116055bf184ac04c36f98b75aacd12"}, + {file = "aiohttp-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:22c0a23a3b3138a6bf76fc553789cb1a703836da86b0f306b6f0dc1617398abc"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:7f6b639c36734eaa80a6c152a238242bedcee9b953f23bb887e9102976343092"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29930bc2921cef955ba39a3ff87d2c4398a0394ae217f41cb02d5c26c8b1b77"}, + {file = "aiohttp-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f489a2c9e6455d87eabf907ac0b7d230a9786be43fbe884ad184ddf9e9c1e385"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:123dd5b16b75b2962d0fff566effb7a065e33cd4538c1692fb31c3bda2bfb972"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b98e698dc34966e5976e10bbca6d26d6724e6bdea853c7c10162a3235aba6e16"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3b9162bab7e42f21243effc822652dc5bb5e8ff42a4eb62fe7782bcbcdfacf6"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1923a5c44061bffd5eebeef58cecf68096e35003907d8201a4d0d6f6e387ccaa"}, + {file = "aiohttp-3.10.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d55f011da0a843c3d3df2c2cf4e537b8070a419f891c930245f05d329c4b0689"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:afe16a84498441d05e9189a15900640a2d2b5e76cf4efe8cbb088ab4f112ee57"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f8112fb501b1e0567a1251a2fd0747baae60a4ab325a871e975b7bb67e59221f"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1e72589da4c90337837fdfe2026ae1952c0f4a6e793adbbfbdd40efed7c63599"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:4d46c7b4173415d8e583045fbc4daa48b40e31b19ce595b8d92cf639396c15d5"}, + {file = "aiohttp-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33e6bc4bab477c772a541f76cd91e11ccb6d2efa2b8d7d7883591dfb523e5987"}, + {file = "aiohttp-3.10.5-cp313-cp313-win32.whl", hash = "sha256:c58c6837a2c2a7cf3133983e64173aec11f9c2cd8e87ec2fdc16ce727bcf1a04"}, + {file = "aiohttp-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:38172a70005252b6893088c0f5e8a47d173df7cc2b2bd88650957eb84fcf5022"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f6f18898ace4bcd2d41a122916475344a87f1dfdec626ecde9ee802a711bc569"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5ede29d91a40ba22ac1b922ef510aab871652f6c88ef60b9dcdf773c6d32ad7a"}, + {file = "aiohttp-3.10.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:673f988370f5954df96cc31fd99c7312a3af0a97f09e407399f61583f30da9bc"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58718e181c56a3c02d25b09d4115eb02aafe1a732ce5714ab70326d9776457c3"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b38b1570242fbab8d86a84128fb5b5234a2f70c2e32f3070143a6d94bc854cf"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:074d1bff0163e107e97bd48cad9f928fa5a3eb4b9d33366137ffce08a63e37fe"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd31f176429cecbc1ba499d4aba31aaccfea488f418d60376b911269d3b883c5"}, + {file = "aiohttp-3.10.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7384d0b87d4635ec38db9263e6a3f1eb609e2e06087f0aa7f63b76833737b471"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8989f46f3d7ef79585e98fa991e6ded55d2f48ae56d2c9fa5e491a6e4effb589"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:c83f7a107abb89a227d6c454c613e7606c12a42b9a4ca9c5d7dad25d47c776ae"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cde98f323d6bf161041e7627a5fd763f9fd829bcfcd089804a5fdce7bb6e1b7d"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:676f94c5480d8eefd97c0c7e3953315e4d8c2b71f3b49539beb2aa676c58272f"}, + {file = "aiohttp-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2d21ac12dc943c68135ff858c3a989f2194a709e6e10b4c8977d7fcd67dfd511"}, + {file = "aiohttp-3.10.5-cp38-cp38-win32.whl", hash = "sha256:17e997105bd1a260850272bfb50e2a328e029c941c2708170d9d978d5a30ad9a"}, + {file = "aiohttp-3.10.5-cp38-cp38-win_amd64.whl", hash = "sha256:1c19de68896747a2aa6257ae4cf6ef59d73917a36a35ee9d0a6f48cff0f94db8"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7e2fe37ac654032db1f3499fe56e77190282534810e2a8e833141a021faaab0e"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f5bf3ead3cb66ab990ee2561373b009db5bc0e857549b6c9ba84b20bc462e172"}, + {file = "aiohttp-3.10.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1b2c16a919d936ca87a3c5f0e43af12a89a3ce7ccbce59a2d6784caba945b68b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad146dae5977c4dd435eb31373b3fe9b0b1bf26858c6fc452bf6af394067e10b"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c5c6fa16412b35999320f5c9690c0f554392dc222c04e559217e0f9ae244b92"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:95c4dc6f61d610bc0ee1edc6f29d993f10febfe5b76bb470b486d90bbece6b22"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da452c2c322e9ce0cfef392e469a26d63d42860f829026a63374fde6b5c5876f"}, + {file = "aiohttp-3.10.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:898715cf566ec2869d5cb4d5fb4be408964704c46c96b4be267442d265390f32"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:391cc3a9c1527e424c6865e087897e766a917f15dddb360174a70467572ac6ce"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:380f926b51b92d02a34119d072f178d80bbda334d1a7e10fa22d467a66e494db"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce91db90dbf37bb6fa0997f26574107e1b9d5ff939315247b7e615baa8ec313b"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:9093a81e18c45227eebe4c16124ebf3e0d893830c6aca7cc310bfca8fe59d857"}, + {file = "aiohttp-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ee40b40aa753d844162dcc80d0fe256b87cba48ca0054f64e68000453caead11"}, + {file = "aiohttp-3.10.5-cp39-cp39-win32.whl", hash = "sha256:03f2645adbe17f274444953bdea69f8327e9d278d961d85657cb0d06864814c1"}, + {file = "aiohttp-3.10.5-cp39-cp39-win_amd64.whl", hash = "sha256:d17920f18e6ee090bdd3d0bfffd769d9f2cb4c8ffde3eb203777a3895c128862"}, + {file = "aiohttp-3.10.5.tar.gz", hash = "sha256:f071854b47d39591ce9a17981c46790acb30518e2f83dfca8db2dfa091178691"}, +] + +[package.dependencies] +aiohappyeyeballs = ">=2.3.0" +aiosignal = ">=1.1.2" +attrs = ">=17.3.0" +frozenlist = ">=1.1.1" +multidict = ">=4.5,<7.0" +yarl = ">=1.0,<2.0" + +[package.extras] +speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"] + +[[package]] +name = "aiosignal" +version = "1.3.1" +description = "aiosignal: a list of registered asynchronous callbacks" +optional = false +python-versions = ">=3.7" +files = [ + {file = "aiosignal-1.3.1-py3-none-any.whl", hash = "sha256:f8376fb07dd1e86a584e4fcdec80b36b7f81aac666ebc724e2c090300dd83b17"}, + {file = "aiosignal-1.3.1.tar.gz", hash = "sha256:54cd96e15e1649b75d6c87526a6ff0b6c1b0dd3459f43d9ca11d48c339b68cfc"}, +] + +[package.dependencies] +frozenlist = ">=1.1.0" + +[[package]] +name = "anyio" +version = "4.4.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, + {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, +] + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[package]] +name = "attrs" +version = "24.2.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.7" +files = [ + {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"}, + {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "certifi" +version = "2024.7.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, + {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "discord-py" +version = "2.4.0" +description = "A Python wrapper for the Discord API" +optional = false +python-versions = ">=3.8" +files = [ + {file = "discord.py-2.4.0-py3-none-any.whl", hash = "sha256:b8af6711c70f7e62160bfbecb55be699b5cb69d007426759ab8ab06b1bd77d1d"}, + {file = "discord_py-2.4.0.tar.gz", hash = "sha256:d07cb2a223a185873a1d0ee78b9faa9597e45b3f6186df21a95cec1e9bcdc9a5"}, +] + +[package.dependencies] +aiohttp = ">=3.7.4,<4" + +[package.extras] +docs = ["sphinx (==4.4.0)", "sphinx-inline-tabs (==2023.4.21)", "sphinxcontrib-applehelp (==1.0.4)", "sphinxcontrib-devhelp (==1.0.2)", "sphinxcontrib-htmlhelp (==2.0.1)", "sphinxcontrib-jsmath (==1.0.1)", "sphinxcontrib-qthelp (==1.0.3)", "sphinxcontrib-serializinghtml (==1.1.5)", "sphinxcontrib-trio (==1.1.2)", "sphinxcontrib-websupport (==1.2.4)", "typing-extensions (>=4.3,<5)"] +speed = ["Brotli", "aiodns (>=1.1)", "cchardet (==2.1.7)", "orjson (>=3.5.4)"] +test = ["coverage[toml]", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", "typing-extensions (>=4.3,<5)", "tzdata"] +voice = ["PyNaCl (>=1.3.0,<1.6)"] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + +[[package]] +name = "filelock" +version = "3.15.4" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"}, + {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "frozenlist" +version = "1.4.1" +description = "A list-like structure which implements collections.abc.MutableSequence" +optional = false +python-versions = ">=3.8" +files = [ + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f9aa1878d1083b276b0196f2dfbe00c9b7e752475ed3b682025ff20c1c1f51ac"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:29acab3f66f0f24674b7dc4736477bcd4bc3ad4b896f5f45379a67bce8b96868"}, + {file = "frozenlist-1.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:74fb4bee6880b529a0c6560885fce4dc95936920f9f20f53d99a213f7bf66776"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:590344787a90ae57d62511dd7c736ed56b428f04cd8c161fcc5e7232c130c69a"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:068b63f23b17df8569b7fdca5517edef76171cf3897eb68beb01341131fbd2ad"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c849d495bf5154cd8da18a9eb15db127d4dba2968d88831aff6f0331ea9bd4c"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9750cc7fe1ae3b1611bb8cfc3f9ec11d532244235d75901fb6b8e42ce9229dfe"}, + {file = "frozenlist-1.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9b2de4cf0cdd5bd2dee4c4f63a653c61d2408055ab77b151c1957f221cabf2a"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0633c8d5337cb5c77acbccc6357ac49a1770b8c487e5b3505c57b949b4b82e98"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:27657df69e8801be6c3638054e202a135c7f299267f1a55ed3a598934f6c0d75"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f9a3ea26252bd92f570600098783d1371354d89d5f6b7dfd87359d669f2109b5"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4f57dab5fe3407b6c0c1cc907ac98e8a189f9e418f3b6e54d65a718aaafe3950"}, + {file = "frozenlist-1.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e02a0e11cf6597299b9f3bbd3f93d79217cb90cfd1411aec33848b13f5c656cc"}, + {file = "frozenlist-1.4.1-cp310-cp310-win32.whl", hash = "sha256:a828c57f00f729620a442881cc60e57cfcec6842ba38e1b19fd3e47ac0ff8dc1"}, + {file = "frozenlist-1.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:f56e2333dda1fe0f909e7cc59f021eba0d2307bc6f012a1ccf2beca6ba362439"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a0cb6f11204443f27a1628b0e460f37fb30f624be6051d490fa7d7e26d4af3d0"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b46c8ae3a8f1f41a0d2ef350c0b6e65822d80772fe46b653ab6b6274f61d4a49"}, + {file = "frozenlist-1.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fde5bd59ab5357e3853313127f4d3565fc7dad314a74d7b5d43c22c6a5ed2ced"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:722e1124aec435320ae01ee3ac7bec11a5d47f25d0ed6328f2273d287bc3abb0"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2471c201b70d58a0f0c1f91261542a03d9a5e088ed3dc6c160d614c01649c106"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c757a9dd70d72b076d6f68efdbb9bc943665ae954dad2801b874c8c69e185068"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f146e0911cb2f1da549fc58fc7bcd2b836a44b79ef871980d605ec392ff6b0d2"}, + {file = "frozenlist-1.4.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9c515e7914626b2a2e1e311794b4c35720a0be87af52b79ff8e1429fc25f19"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c302220494f5c1ebeb0912ea782bcd5e2f8308037b3c7553fad0e48ebad6ad82"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:442acde1e068288a4ba7acfe05f5f343e19fac87bfc96d89eb886b0363e977ec"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:1b280e6507ea8a4fa0c0a7150b4e526a8d113989e28eaaef946cc77ffd7efc0a"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:fe1a06da377e3a1062ae5fe0926e12b84eceb8a50b350ddca72dc85015873f74"}, + {file = "frozenlist-1.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db9e724bebd621d9beca794f2a4ff1d26eed5965b004a97f1f1685a173b869c2"}, + {file = "frozenlist-1.4.1-cp311-cp311-win32.whl", hash = "sha256:e774d53b1a477a67838a904131c4b0eef6b3d8a651f8b138b04f748fccfefe17"}, + {file = "frozenlist-1.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:fb3c2db03683b5767dedb5769b8a40ebb47d6f7f45b1b3e3b4b51ec8ad9d9825"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1979bc0aeb89b33b588c51c54ab0161791149f2461ea7c7c946d95d5f93b56ae"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cc7b01b3754ea68a62bd77ce6020afaffb44a590c2289089289363472d13aedb"}, + {file = "frozenlist-1.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9c92be9fd329ac801cc420e08452b70e7aeab94ea4233a4804f0915c14eba9b"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3894db91f5a489fc8fa6a9991820f368f0b3cbdb9cd8849547ccfab3392d86"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba60bb19387e13597fb059f32cd4d59445d7b18b69a745b8f8e5db0346f33480"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8aefbba5f69d42246543407ed2461db31006b0f76c4e32dfd6f42215a2c41d09"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:780d3a35680ced9ce682fbcf4cb9c2bad3136eeff760ab33707b71db84664e3a"}, + {file = "frozenlist-1.4.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9acbb16f06fe7f52f441bb6f413ebae6c37baa6ef9edd49cdd567216da8600cd"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:23b701e65c7b36e4bf15546a89279bd4d8675faabc287d06bbcfac7d3c33e1e6"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:3e0153a805a98f5ada7e09826255ba99fb4f7524bb81bf6b47fb702666484ae1"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:dd9b1baec094d91bf36ec729445f7769d0d0cf6b64d04d86e45baf89e2b9059b"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:1a4471094e146b6790f61b98616ab8e44f72661879cc63fa1049d13ef711e71e"}, + {file = "frozenlist-1.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5667ed53d68d91920defdf4035d1cdaa3c3121dc0b113255124bcfada1cfa1b8"}, + {file = "frozenlist-1.4.1-cp312-cp312-win32.whl", hash = "sha256:beee944ae828747fd7cb216a70f120767fc9f4f00bacae8543c14a6831673f89"}, + {file = "frozenlist-1.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:64536573d0a2cb6e625cf309984e2d873979709f2cf22839bf2d61790b448ad5"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:20b51fa3f588ff2fe658663db52a41a4f7aa6c04f6201449c6c7c476bd255c0d"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:410478a0c562d1a5bcc2f7ea448359fcb050ed48b3c6f6f4f18c313a9bdb1826"}, + {file = "frozenlist-1.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6321c9efe29975232da3bd0af0ad216800a47e93d763ce64f291917a381b8eb"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48f6a4533887e189dae092f1cf981f2e3885175f7a0f33c91fb5b7b682b6bab6"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6eb73fa5426ea69ee0e012fb59cdc76a15b1283d6e32e4f8dc4482ec67d1194d"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fbeb989b5cc29e8daf7f976b421c220f1b8c731cbf22b9130d8815418ea45887"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:32453c1de775c889eb4e22f1197fe3bdfe457d16476ea407472b9442e6295f7a"}, + {file = "frozenlist-1.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693945278a31f2086d9bf3df0fe8254bbeaef1fe71e1351c3bd730aa7d31c41b"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:1d0ce09d36d53bbbe566fe296965b23b961764c0bcf3ce2fa45f463745c04701"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3a670dc61eb0d0eb7080890c13de3066790f9049b47b0de04007090807c776b0"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:dca69045298ce5c11fd539682cff879cc1e664c245d1c64da929813e54241d11"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a06339f38e9ed3a64e4c4e43aec7f59084033647f908e4259d279a52d3757d09"}, + {file = "frozenlist-1.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b7f2f9f912dca3934c1baec2e4585a674ef16fe00218d833856408c48d5beee7"}, + {file = "frozenlist-1.4.1-cp38-cp38-win32.whl", hash = "sha256:e7004be74cbb7d9f34553a5ce5fb08be14fb33bc86f332fb71cbe5216362a497"}, + {file = "frozenlist-1.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:5a7d70357e7cee13f470c7883a063aae5fe209a493c57d86eb7f5a6f910fae09"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:bfa4a17e17ce9abf47a74ae02f32d014c5e9404b6d9ac7f729e01562bbee601e"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b7e3ed87d4138356775346e6845cccbe66cd9e207f3cd11d2f0b9fd13681359d"}, + {file = "frozenlist-1.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c99169d4ff810155ca50b4da3b075cbde79752443117d89429595c2e8e37fed8"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edb678da49d9f72c9f6c609fbe41a5dfb9a9282f9e6a2253d5a91e0fc382d7c0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6db4667b187a6742b33afbbaf05a7bc551ffcf1ced0000a571aedbb4aa42fc7b"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55fdc093b5a3cb41d420884cdaf37a1e74c3c37a31f46e66286d9145d2063bd0"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82e8211d69a4f4bc360ea22cd6555f8e61a1bd211d1d5d39d3d228b48c83a897"}, + {file = "frozenlist-1.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89aa2c2eeb20957be2d950b85974b30a01a762f3308cd02bb15e1ad632e22dc7"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d3e0c25a2350080e9319724dede4f31f43a6c9779be48021a7f4ebde8b2d742"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7268252af60904bf52c26173cbadc3a071cece75f873705419c8681f24d3edea"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:0c250a29735d4f15321007fb02865f0e6b6a41a6b88f1f523ca1596ab5f50bd5"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:96ec70beabbd3b10e8bfe52616a13561e58fe84c0101dd031dc78f250d5128b9"}, + {file = "frozenlist-1.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:23b2d7679b73fe0e5a4560b672a39f98dfc6f60df63823b0a9970525325b95f6"}, + {file = "frozenlist-1.4.1-cp39-cp39-win32.whl", hash = "sha256:a7496bfe1da7fb1a4e1cc23bb67c58fab69311cc7d32b5a99c2007b4b2a0e932"}, + {file = "frozenlist-1.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:e6a20a581f9ce92d389a8c7d7c3dd47c81fd5d6e655c8dddf341e14aa48659d0"}, + {file = "frozenlist-1.4.1-py3-none-any.whl", hash = "sha256:04ced3e6a46b4cfffe20f9ae482818e34eba9b5fb0ce4056e4cc9b6e212d09b7"}, + {file = "frozenlist-1.4.1.tar.gz", hash = "sha256:c037a86e8513059a2613aaba4d817bb90b9d9b6b69aace3ce9c877e8c8ed402b"}, +] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.5" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, + {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.26.0)"] + +[[package]] +name = "httpx" +version = "0.27.2" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "identify" +version = "2.6.0" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"}, + {file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.8" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.8-py3-none-any.whl", hash = "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac"}, + {file = "idna-3.8.tar.gz", hash = "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603"}, +] + +[[package]] +name = "loguru" +version = "0.7.2" +description = "Python logging made (stupidly) simple" +optional = false +python-versions = ">=3.5" +files = [ + {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, + {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, +] + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] + +[[package]] +name = "multidict" +version = "6.0.5" +description = "multidict implementation" +optional = false +python-versions = ">=3.7" +files = [ + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604"}, + {file = "multidict-6.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae"}, + {file = "multidict-6.0.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef"}, + {file = "multidict-6.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc"}, + {file = "multidict-6.0.5-cp310-cp310-win32.whl", hash = "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319"}, + {file = "multidict-6.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e"}, + {file = "multidict-6.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed"}, + {file = "multidict-6.0.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc"}, + {file = "multidict-6.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e"}, + {file = "multidict-6.0.5-cp311-cp311-win32.whl", hash = "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c"}, + {file = "multidict-6.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b"}, + {file = "multidict-6.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226"}, + {file = "multidict-6.0.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6"}, + {file = "multidict-6.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda"}, + {file = "multidict-6.0.5-cp312-cp312-win32.whl", hash = "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5"}, + {file = "multidict-6.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556"}, + {file = "multidict-6.0.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626"}, + {file = "multidict-6.0.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3"}, + {file = "multidict-6.0.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc"}, + {file = "multidict-6.0.5-cp37-cp37m-win32.whl", hash = "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee"}, + {file = "multidict-6.0.5-cp37-cp37m-win_amd64.whl", hash = "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d"}, + {file = "multidict-6.0.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50"}, + {file = "multidict-6.0.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461"}, + {file = "multidict-6.0.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44"}, + {file = "multidict-6.0.5-cp38-cp38-win32.whl", hash = "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241"}, + {file = "multidict-6.0.5-cp38-cp38-win_amd64.whl", hash = "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9"}, + {file = "multidict-6.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c"}, + {file = "multidict-6.0.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479"}, + {file = "multidict-6.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c"}, + {file = "multidict-6.0.5-cp39-cp39-win32.whl", hash = "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b"}, + {file = "multidict-6.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755"}, + {file = "multidict-6.0.5-py3-none-any.whl", hash = "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7"}, + {file = "multidict-6.0.5.tar.gz", hash = "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da"}, +] + +[[package]] +name = "mysql-connector-python" +version = "9.0.0" +description = "MySQL driver written in Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mysql-connector-python-9.0.0.tar.gz", hash = "sha256:8a404db37864acca43fd76222d1fbc7ff8d17d4ce02d803289c2141c2693ce9e"}, + {file = "mysql_connector_python-9.0.0-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:72bfd0213364c2bea0244f6432ababb2f204cff43f4f886c65dca2be11f536ee"}, + {file = "mysql_connector_python-9.0.0-cp310-cp310-macosx_13_0_x86_64.whl", hash = "sha256:052058cf3dc0bf183ab522132f3b18a614a26f3e392ae886efcdab38d4f4fc42"}, + {file = "mysql_connector_python-9.0.0-cp310-cp310-manylinux_2_17_aarch64.whl", hash = "sha256:f41cb8da8bb487ed60329ac31789c50621f0e6d2c26abc7d4ae2383838fb1b93"}, + {file = "mysql_connector_python-9.0.0-cp310-cp310-manylinux_2_17_x86_64.whl", hash = "sha256:67fc2b2e67a63963c633fc884f285a8de5a626967a3cc5f5d48ac3e8d15b122d"}, + {file = "mysql_connector_python-9.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:933c3e39d30cc6f9ff636d27d18aa3f1341b23d803ade4b57a76f91c26d14066"}, + {file = "mysql_connector_python-9.0.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:7af7f68198f2aca3a520e1201fe2b329331e0ca19a481f3b3451cb0746f56c01"}, + {file = "mysql_connector_python-9.0.0-cp311-cp311-macosx_13_0_x86_64.whl", hash = "sha256:38c229d76cd1dea8465357855f2b2842b7a9b201f17dea13b0eab7d3b9d6ad74"}, + {file = "mysql_connector_python-9.0.0-cp311-cp311-manylinux_2_17_aarch64.whl", hash = "sha256:c01aad36f0c34ca3f642018be37fd0d55c546f088837cba88f1a1aff408c63dd"}, + {file = "mysql_connector_python-9.0.0-cp311-cp311-manylinux_2_17_x86_64.whl", hash = "sha256:853c5916d188ef2c357a474e15ac81cafae6085e599ceb9b2b0bcb9104118e63"}, + {file = "mysql_connector_python-9.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:134b71e439e2eafaee4c550365221ae2890dd54fb76227c64a87a94a07fe79b4"}, + {file = "mysql_connector_python-9.0.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:9199d6ecc81576602990178f0c2fb71737c53a598c8a2f51e1097a53fcfaee40"}, + {file = "mysql_connector_python-9.0.0-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:b267a6c000b7f98e6436a9acefa5582a9662e503b0632a2562e3093a677f6845"}, + {file = "mysql_connector_python-9.0.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:ac92b2f2a9307ac0c4aafdfcf7ecf01ec92dfebd9140f8c95353adfbf5822cd4"}, + {file = "mysql_connector_python-9.0.0-cp312-cp312-manylinux_2_17_x86_64.whl", hash = "sha256:ced1fa55e653d28f66c4f3569ed524d4d92098119dcd80c2fa026872a30eba55"}, + {file = "mysql_connector_python-9.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:ca8349fe56ce39498d9b5ca8eabba744774e94d85775259f26a43a03e8825429"}, + {file = "mysql_connector_python-9.0.0-cp38-cp38-macosx_13_0_x86_64.whl", hash = "sha256:a48534b881c176557ddc78527c8c75b4c9402511e972670ad33c5e49d31eddfe"}, + {file = "mysql_connector_python-9.0.0-cp38-cp38-manylinux_2_17_aarch64.whl", hash = "sha256:e90a7b96ce2c6a60f6e2609b0c83f45bd55e144cc7c2a9714e344938827da363"}, + {file = "mysql_connector_python-9.0.0-cp38-cp38-manylinux_2_17_x86_64.whl", hash = "sha256:2a8f451c4d700802fdfe515890c14974766c322213df2ceed3b27752929dc70f"}, + {file = "mysql_connector_python-9.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:2dcf05355315e5c7c81e9eca34395d78f29c4da3662e869e42dd7b16380f92ce"}, + {file = "mysql_connector_python-9.0.0-cp39-cp39-macosx_13_0_arm64.whl", hash = "sha256:823190e7f2a9b4bcc574ab6bb72a33802933e1a8c171594faad90162d2d27758"}, + {file = "mysql_connector_python-9.0.0-cp39-cp39-macosx_13_0_x86_64.whl", hash = "sha256:b8639d8aa381a7d19b92ca1a32448f09baaf80787e50187d1f7d072191430768"}, + {file = "mysql_connector_python-9.0.0-cp39-cp39-manylinux_2_17_aarch64.whl", hash = "sha256:a688ea65b2ea771b9b69dc409377240a7cab7c1aafef46cd75219d5a94ba49e0"}, + {file = "mysql_connector_python-9.0.0-cp39-cp39-manylinux_2_17_x86_64.whl", hash = "sha256:6d92c58f71c691f86ad35bb2f3e13d7a9cc1c84ce0b04c146e5980e450faeff1"}, + {file = "mysql_connector_python-9.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:eacc353dcf6f39665d4ca3311ded5ddae0f5a117f03107991d4185ffa59fd890"}, + {file = "mysql_connector_python-9.0.0-py2.py3-none-any.whl", hash = "sha256:016d81bb1499dee8b77c82464244e98f10d3671ceefb4023adc559267d1fad50"}, +] + +[package.extras] +dns-srv = ["dnspython (==2.6.1)"] +fido2 = ["fido2 (==1.1.2)"] +gssapi = ["gssapi (>=1.6.9,<=1.8.2)"] +telemetry = ["opentelemetry-api (==1.18.0)", "opentelemetry-exporter-otlp-proto-http (==1.18.0)", "opentelemetry-sdk (==1.18.0)"] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + +[[package]] +name = "pre-commit" +version = "3.8.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, + {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "pyright" +version = "1.1.377" +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"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "ruff" +version = "0.6.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "virtualenv" +version = "20.26.3" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"}, + {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +optional = false +python-versions = ">=3.5" +files = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + +[[package]] +name = "yarl" +version = "1.9.4" +description = "Yet another URL library" +optional = false +python-versions = ">=3.7" +files = [ + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2"}, + {file = "yarl-1.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455"}, + {file = "yarl-1.9.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b"}, + {file = "yarl-1.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541"}, + {file = "yarl-1.9.4-cp310-cp310-win32.whl", hash = "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d"}, + {file = "yarl-1.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c"}, + {file = "yarl-1.9.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42"}, + {file = "yarl-1.9.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958"}, + {file = "yarl-1.9.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98"}, + {file = "yarl-1.9.4-cp311-cp311-win32.whl", hash = "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31"}, + {file = "yarl-1.9.4-cp311-cp311-win_amd64.whl", hash = "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142"}, + {file = "yarl-1.9.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4"}, + {file = "yarl-1.9.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc"}, + {file = "yarl-1.9.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10"}, + {file = "yarl-1.9.4-cp312-cp312-win32.whl", hash = "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7"}, + {file = "yarl-1.9.4-cp312-cp312-win_amd64.whl", hash = "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984"}, + {file = "yarl-1.9.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd"}, + {file = "yarl-1.9.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead"}, + {file = "yarl-1.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434"}, + {file = "yarl-1.9.4-cp37-cp37m-win32.whl", hash = "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749"}, + {file = "yarl-1.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f"}, + {file = "yarl-1.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130"}, + {file = "yarl-1.9.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be"}, + {file = "yarl-1.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3"}, + {file = "yarl-1.9.4-cp38-cp38-win32.whl", hash = "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece"}, + {file = "yarl-1.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1"}, + {file = "yarl-1.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136"}, + {file = "yarl-1.9.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c"}, + {file = "yarl-1.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0"}, + {file = "yarl-1.9.4-cp39-cp39-win32.whl", hash = "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575"}, + {file = "yarl-1.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15"}, + {file = "yarl-1.9.4-py3-none-any.whl", hash = "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad"}, + {file = "yarl-1.9.4.tar.gz", hash = "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf"}, +] + +[package.dependencies] +idna = ">=2.0" +multidict = ">=4.0" + +[metadata] +lock-version = "2.0" +python-versions = "^3.12" +content-hash = "78b6fb1bf2b5060cbb9ea5cbb840431d96e582c2cecb50b234ddb38d70c17494" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..ec93ced --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,27 @@ +[tool.poetry] +authors = ["wlinator "] +description = "A Discord application, can serve as a template for your own bot." +license = "GNU General Public License v3.0" +name = "luminara" +package-mode = false +readme = "README.md" +version = "3" + +[tool.poetry.dependencies] +discord-py = "^2.4.0" +httpx = "^0.27.2" +loguru = "^0.7.2" +mysql-connector-python = "^9.0.0" +pre-commit = "^3.8.0" +pyright = "^1.1.377" +python = "^3.12" +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" + +[build-system] +build-backend = "poetry.core.masonry.api" +requires = ["poetry-core"] diff --git a/settings.yaml b/settings.yaml new file mode 100644 index 0000000..93a1781 --- /dev/null +++ b/settings.yaml @@ -0,0 +1,6 @@ +logs: + level: DEBUG + format: "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}" + +cogs: + ignore: # add cogs to ignore here From 3aea9a4f8c35758ac6dab61a90f13ae128cd3312 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:32:17 -0400 Subject: [PATCH 004/102] chore: Add strings from v2 --- localization/strings.en-US.json | 296 ++++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 localization/strings.en-US.json diff --git a/localization/strings.en-US.json b/localization/strings.en-US.json new file mode 100644 index 0000000..2bc6bee --- /dev/null +++ b/localization/strings.en-US.json @@ -0,0 +1,296 @@ +{ + "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 ` - View a specific case\n`/editcase ` - 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 .", + "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:** \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 .", + "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!" +} \ No newline at end of file From f9109b2a5e211c1550156d8a7c34d52864edad9c Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:32:37 -0400 Subject: [PATCH 005/102] chore: Add level strings from v2 --- localization/levels.en-US.json | 79 ++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 localization/levels.en-US.json diff --git a/localization/levels.en-US.json b/localization/levels.en-US.json new file mode 100644 index 0000000..d6c8424 --- /dev/null +++ b/localization/levels.en-US.json @@ -0,0 +1,79 @@ +{ + "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 {}**." + ] +} \ No newline at end of file From 98168261222854fcdb880b5614c378279825a2f3 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:33:02 -0400 Subject: [PATCH 006/102] chore: Add bday strings from v2 --- localization/bdays.en-US.json | 76 +++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 localization/bdays.en-US.json diff --git a/localization/bdays.en-US.json b/localization/bdays.en-US.json new file mode 100644 index 0000000..972f264 --- /dev/null +++ b/localization/bdays.en-US.json @@ -0,0 +1,76 @@ +{ + "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! 🎉" + ] +} \ No newline at end of file From 1fd136e2eff72e63d6feb25c219697d6a96dc149 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:35:27 -0400 Subject: [PATCH 007/102] Add all localization strings to CONST --- lib/const.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/const.py b/lib/const.py index 76d46b6..2255c79 100644 --- a/lib/const.py +++ b/lib/const.py @@ -1,4 +1,5 @@ import os +import json import yaml from functools import lru_cache from typing import Optional, Callable, Set @@ -11,6 +12,9 @@ class _parser: def read_settings(self) -> dict: return self._read_file("settings.yaml", yaml.safe_load) + def read_json(self, path: str) -> dict: + return self._read_file(f"localization/{path}.json", json.load) + def _read_file(self, file_path: str, load_func: Callable) -> dict: with open(file_path) as file: return load_func(file) @@ -36,5 +40,14 @@ class _constants: set(_s["cogs"]["ignore"]) if _s["cogs"]["ignore"] else set() ) + # Reponse strings + # TODO: Implement switching between languages + STRINGS = _p.read_json("strings.en-US") + LEVEL_MESSAGES = _p.read_json("levels.en-US") + + _bday = _p.read_json("bdays.en-US") + BIRTHDAY_MESSAGES = _bday["birthday_messages"] + BIRTHDAY_MONTHS = _bday["months"] + CONST: _constants = _constants() From 67adfb0df7104b6323e6f45a56e95584df35958c Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:42:03 -0400 Subject: [PATCH 008/102] Add embed builder, all settings from v2 and update the ping command --- lib/const.py | 83 +++++++++++++++-- modules/misc/ping.py | 13 ++- settings.yaml | 104 ++++++++++++++++++++++ ui/embeds.py | 206 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 400 insertions(+), 6 deletions(-) create mode 100644 ui/embeds.py diff --git a/lib/const.py b/lib/const.py index 2255c79..318a974 100644 --- a/lib/const.py +++ b/lib/const.py @@ -2,14 +2,14 @@ import os import json import yaml from functools import lru_cache -from typing import Optional, Callable, Set +from typing import Optional, Callable, Set, List, Dict class _parser: """Internal parses class. Not intended to be used outside of this module.""" @lru_cache(maxsize=1024) - def read_settings(self) -> dict: + def read_s(self) -> dict: return self._read_file("settings.yaml", yaml.safe_load) def read_json(self, path: str) -> dict: @@ -22,10 +22,20 @@ class _parser: class _constants: _p = _parser() - _s = _parser().read_settings() + _s = _parser().read_s() # bot credentials - TOKEN: Optional[str] = os.getenv("TOKEN") + 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} @@ -33,13 +43,76 @@ class _constants: else None ) - # settings + # 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"] + + # loguru LOG_LEVEL: str = _s["logs"]["level"] or "DEBUG" LOG_FORMAT: str = _s["logs"]["format"] + + # cogs COG_IGNORE_LIST: 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"] + + # colors + COLOR_DEFAULT: int = _s["colors"]["color_default"] + COLOR_WARNING: int = _s["colors"]["color_warning"] + COLOR_ERROR: 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"] + + # art from git repository + _fetch_url: 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"] + 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"] + + # 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"] + + # emotes + EMOTES_SERVER_ID: int = _s["emotes"]["guild_id"] + EMOTE_IDS: 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" + ] + # Reponse strings # TODO: Implement switching between languages STRINGS = _p.read_json("strings.en-US") diff --git a/modules/misc/ping.py b/modules/misc/ping.py index 7cfe18e..8ba78e0 100644 --- a/modules/misc/ping.py +++ b/modules/misc/ping.py @@ -1,4 +1,6 @@ from discord.ext import commands +from lib.const import CONST +from ui.embeds import builder class Ping(commands.Cog): @@ -10,7 +12,16 @@ class Ping(commands.Cog): usage="ping", ) async def ping(self, ctx: commands.Context[commands.Bot]) -> None: - await ctx.send(f"Pong! Latency: {self.bot.latency * 1000:.2f}ms") + embed = builder.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.bot.latency), + ), + ) + + await ctx.send(embed=embed) async def setup(bot: commands.Bot) -> None: diff --git a/settings.yaml b/settings.yaml index 93a1781..11e567d 100644 --- a/settings.yaml +++ b/settings.yaml @@ -1,6 +1,110 @@ +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 + invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot + logs: level: DEBUG format: "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}" cogs: ignore: # add cogs to ignore here + +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? (nickname) + 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!" diff --git a/ui/embeds.py b/ui/embeds.py new file mode 100644 index 0000000..da8cb9b --- /dev/null +++ b/ui/embeds.py @@ -0,0 +1,206 @@ +import datetime + +import discord + +from lib.const import CONST + + +class builder: + @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, + ) -> discord.Embed: + 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, + ) -> discord.Embed: + return builder.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, + ) -> discord.Embed: + return builder.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, + ) -> discord.Embed: + return builder.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, + ) -> discord.Embed: + return builder.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, + ) From 17bb34c5af00d87657f9995add3b27ac3caae515 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:47:20 -0400 Subject: [PATCH 009/102] chore: Update strings and embed for the sync command --- localization/strings.en-US.json | 6 ++++-- modules/admin/sync.py | 11 ++++++++++- settings.yaml | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/localization/strings.en-US.json b/localization/strings.en-US.json index 2bc6bee..1368788 100644 --- a/localization/strings.en-US.json +++ b/localization/strings.en-US.json @@ -239,7 +239,7 @@ "mod_warned_user": "user `{0}` has been warned.", "ping_author": "I'm online!", "ping_footer": "Latency: {0}ms", - "ping_pong": "Pong!", + "ping_pong": "pong!", "ping_uptime": "I've been online since .", "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}**.", @@ -292,5 +292,7 @@ "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!" + "error_cant_use_buttons": "You can't use these buttons, they're someone else's!", + "sync_author": "Synced Commands", + "sync_description": "the application command tree has been synced." } \ No newline at end of file diff --git a/modules/admin/sync.py b/modules/admin/sync.py index d13c111..447a5b9 100644 --- a/modules/admin/sync.py +++ b/modules/admin/sync.py @@ -1,6 +1,8 @@ from discord.ext import commands import discord from typing import Optional +from ui.embeds import builder +from lib.const import CONST class Sync(commands.Cog): @@ -25,7 +27,14 @@ class Sync(commands.Cog): self.bot.tree.copy_global_to(guild=guild) await self.bot.tree.sync(guild=guild) - await ctx.send("Application command tree synced.") + + embed = builder.create_success_embed( + ctx, + author_text=CONST.STRINGS["sync_author"], + description=CONST.STRINGS["sync_description"], + ) + + await ctx.send(embed=embed) async def setup(bot: commands.Bot) -> None: diff --git a/settings.yaml b/settings.yaml index 11e567d..2a5c2b5 100644 --- a/settings.yaml +++ b/settings.yaml @@ -2,7 +2,7 @@ info: title: Luminara author: wlinator license: GNU General Public License v3.0 - version: "2.9.0" # "Settings & Customizability" update + version: "v3.0.1" repository_url: https://git.wlinator.org/Luminara/Lumi invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot From 8c9860b527cf7250d783a830596c918a2f0f9c07 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:48:21 -0400 Subject: [PATCH 010/102] Bring version down to 3.0.0-alpha --- settings.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/settings.yaml b/settings.yaml index 2a5c2b5..32138db 100644 --- a/settings.yaml +++ b/settings.yaml @@ -2,7 +2,7 @@ info: title: Luminara author: wlinator license: GNU General Public License v3.0 - version: "v3.0.1" + version: "v3.0.0-alpha" repository_url: https://git.wlinator.org/Luminara/Lumi invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot From 877b7171b0726fe4d101f736253b575b5f3a8d3d Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 06:51:36 -0400 Subject: [PATCH 011/102] Add uptime command --- modules/misc/uptime.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 modules/misc/uptime.py diff --git a/modules/misc/uptime.py b/modules/misc/uptime.py new file mode 100644 index 0000000..21dab90 --- /dev/null +++ b/modules/misc/uptime.py @@ -0,0 +1,32 @@ +from discord.ext import commands +from discord import Embed +from lib.const import CONST +from ui.embeds import builder +from datetime import datetime + + +class Uptime(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot: commands.Bot = bot + self.start_time: datetime = datetime.now() + + @commands.hybrid_command( + name="uptime", + usage="uptime", + ) + async def uptime(self, ctx: commands.Context[commands.Bot]) -> None: + unix_timestamp: int = int(self.start_time.timestamp()) + + embed: Embed = builder.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( + int(self.bot.latency * 1000), + ), + ) + await ctx.send(embed=embed) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Uptime(bot)) From fbaa326c1e5f50ab32d174d91fb0cdaf879e6ba5 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 07:10:13 -0400 Subject: [PATCH 012/102] Add info command --- localization/strings.en-US.json | 2 +- modules/misc/info.py | 49 +++++++++++++++++++++++++++++++++ poetry.lock | 31 ++++++++++++++++++++- pyproject.toml | 1 + settings.yaml | 2 +- 5 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 modules/misc/info.py diff --git a/localization/strings.en-US.json b/localization/strings.en-US.json index 1368788..c4eea1b 100644 --- a/localization/strings.en-US.json +++ b/localization/strings.en-US.json @@ -178,7 +178,7 @@ "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_api_version": "**discord.py:** v{0}\n", "info_database_records": "**Database:** {0} records", "info_latency": "**Latency:** {0}ms\n", "info_memory": "**Memory:** {0:.2f} MB\n", diff --git a/modules/misc/info.py b/modules/misc/info.py new file mode 100644 index 0000000..ffadf88 --- /dev/null +++ b/modules/misc/info.py @@ -0,0 +1,49 @@ +from discord.ext import commands +from lib.const import CONST +from ui.embeds import builder +import discord +import os +import platform +import psutil + + +class Info(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command( + name="info", + usage="info", + ) + async def info(self, ctx: commands.Context[commands.Bot]) -> 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_latency"].format(round(1000 * self.bot.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 = builder.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.send(embed=embed) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Info(bot)) diff --git a/poetry.lock b/poetry.lock index 98a689d..ac0753e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -664,6 +664,35 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" +[[package]] +name = "psutil" +version = "6.0.0" +description = "Cross-platform lib for process and system monitoring in Python." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "psutil-6.0.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:a021da3e881cd935e64a3d0a20983bda0bb4cf80e4f74fa9bfcb1bc5785360c6"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:1287c2b95f1c0a364d23bc6f2ea2365a8d4d9b726a3be7294296ff7ba97c17f0"}, + {file = "psutil-6.0.0-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:a9a3dbfb4de4f18174528d87cc352d1f788b7496991cca33c6996f40c9e3c92c"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:6ec7588fb3ddaec7344a825afe298db83fe01bfaaab39155fa84cf1c0d6b13c3"}, + {file = "psutil-6.0.0-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:1e7c870afcb7d91fdea2b37c24aeb08f98b6d67257a5cb0a8bc3ac68d0f1a68c"}, + {file = "psutil-6.0.0-cp27-none-win32.whl", hash = "sha256:02b69001f44cc73c1c5279d02b30a817e339ceb258ad75997325e0e6169d8b35"}, + {file = "psutil-6.0.0-cp27-none-win_amd64.whl", hash = "sha256:21f1fb635deccd510f69f485b87433460a603919b45e2a324ad65b0cc74f8fb1"}, + {file = "psutil-6.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:c588a7e9b1173b6e866756dde596fd4cad94f9399daf99ad8c3258b3cb2b47a0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ed2440ada7ef7d0d608f20ad89a04ec47d2d3ab7190896cd62ca5fc4fe08bf0"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd9a97c8e94059b0ef54a7d4baf13b405011176c3b6ff257c247cae0d560ecd"}, + {file = "psutil-6.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e8d0054fc88153ca0544f5c4d554d42e33df2e009c4ff42284ac9ebdef4132"}, + {file = "psutil-6.0.0-cp36-cp36m-win32.whl", hash = "sha256:fc8c9510cde0146432bbdb433322861ee8c3efbf8589865c8bf8d21cb30c4d14"}, + {file = "psutil-6.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:34859b8d8f423b86e4385ff3665d3f4d94be3cdf48221fbe476e883514fdb71c"}, + {file = "psutil-6.0.0-cp37-abi3-win32.whl", hash = "sha256:a495580d6bae27291324fe60cea0b5a7c23fa36a7cd35035a16d93bdcf076b9d"}, + {file = "psutil-6.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:33ea5e1c975250a720b3a6609c490db40dae5d83a4eb315170c4fe0d8b1f34b3"}, + {file = "psutil-6.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:ffe7fc9b6b36beadc8c322f84e1caff51e8703b88eee1da46d1e3a6ae11b4fd0"}, + {file = "psutil-6.0.0.tar.gz", hash = "sha256:8faae4f310b6d969fa26ca0545338b21f73c6b15db7c4a8d934a5482faa818f2"}, +] + +[package.extras] +test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] + [[package]] name = "pyright" version = "1.1.377" @@ -933,4 +962,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "78b6fb1bf2b5060cbb9ea5cbb840431d96e582c2cecb50b234ddb38d70c17494" +content-hash = "a93d83011121cab87e99209358e145ad73b8da37720436992597d707b0e2e607" diff --git a/pyproject.toml b/pyproject.toml index ec93ced..8e8aad0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,7 @@ typing-extensions = "^4.12.2" aiofiles = "^24.1.0" aiocache = "^0.12.2" aioconsole = "^0.7.1" +psutil = "^6.0.0" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/settings.yaml b/settings.yaml index 32138db..1674d01 100644 --- a/settings.yaml +++ b/settings.yaml @@ -2,7 +2,7 @@ info: title: Luminara author: wlinator license: GNU General Public License v3.0 - version: "v3.0.0-alpha" + version: "3.0.0-alpha" repository_url: https://git.wlinator.org/Luminara/Lumi invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot From 118c56c44a92aed58dd9c7d1f56263a799de47d5 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 07:23:28 -0400 Subject: [PATCH 013/102] Add avatar command --- modules/misc/avatar.py | 78 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 modules/misc/avatar.py diff --git a/modules/misc/avatar.py b/modules/misc/avatar.py new file mode 100644 index 0000000..160e52a --- /dev/null +++ b/modules/misc/avatar.py @@ -0,0 +1,78 @@ +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 + + +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. + """ + client: httpx.AsyncClient = httpx.AsyncClient() + 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") + + +class Avatar(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command( + name="avatar", + aliases=["av"], + usage="avatar [user]", + ) + async def avatar( + self, + ctx: commands.Context[commands.Bot], + member: Optional[discord.Member] = None, + ) -> None: + """ + Get the avatar of a member. + + Parameters: + ----------- + ctx : ApplicationContext + The discord context object. + member : Member + The member to get the avatar of. + """ + if member is None: + member = await 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 + + files: list[File] = [ + await create_avatar_file(avatar) + for avatar in [guild_avatar, profile_avatar] + if avatar + ] + + if files: + await ctx.send(files=files) + else: + await ctx.send(content="member has no avatar.") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Avatar(bot)) From 47e7d7ad71a00ee6793f21525a710f5c65e99a63 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 07:38:42 -0400 Subject: [PATCH 014/102] Add xkcd command (not functional) --- modules/misc/xkcd.py | 73 ++++++++++ wrappers/xkcd.py | 321 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 394 insertions(+) create mode 100644 modules/misc/xkcd.py create mode 100644 wrappers/xkcd.py diff --git a/modules/misc/xkcd.py b/modules/misc/xkcd.py new file mode 100644 index 0000000..ce7309d --- /dev/null +++ b/modules/misc/xkcd.py @@ -0,0 +1,73 @@ +from discord.ext import commands +from lib.const import CONST +from ui.embeds import builder +from discord import app_commands +import discord +from wrappers.xkcd import Client, HttpError +from typing import Optional + +_xkcd = Client() + + +async def print_comic( + interaction: discord.Interaction, + 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 interaction.followup.send( + embed=builder.create_success_embed( + interaction, + 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 interaction.followup.send( + embed=builder.create_error_embed( + interaction, + author_text=CONST.STRINGS["xkcd_not_found_author"], + description=CONST.STRINGS["xkcd_not_found"], + footer_text=CONST.STRINGS["xkcd_footer"], + ), + ) + + +class Xkcd(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + xkcd: app_commands.Group = app_commands.Group( + name="xkcd", + description="Xkcd commands", + ) + + @xkcd.command(name="latest", description="Get the latest xkcd comic") + async def xkcd_latest(self, interaction: discord.Interaction) -> None: + await print_comic(interaction, latest=True) + + @xkcd.command(name="random", description="Get a random xkcd comic") + async def xkcd_random(self, interaction: discord.Interaction) -> None: + 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 setup(bot: commands.Bot) -> None: + await bot.add_cog(Xkcd(bot)) diff --git a/wrappers/xkcd.py b/wrappers/xkcd.py new file mode 100644 index 0000000..e7f5b30 --- /dev/null +++ b/wrappers/xkcd.py @@ -0,0 +1,321 @@ +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()" From 24b84c11f2788aa28db536f57bba2a13a0d4e4c9 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 08:59:54 -0400 Subject: [PATCH 015/102] Working error handler and xkcd commands --- handlers/error.py | 140 +++++++++++++++++++++++++++++++++++++++++++ lib/exceptions.py | 31 ++++++++++ loader.py | 1 + modules/misc/xkcd.py | 9 +-- ui/embeds.py | 3 +- 5 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 handlers/error.py create mode 100644 lib/exceptions.py diff --git a/handlers/error.py b/handlers/error.py new file mode 100644 index 0000000..0a105a2 --- /dev/null +++ b/handlers/error.py @@ -0,0 +1,140 @@ +import sys +import traceback + +from discord.ext import commands +from discord.ext.commands import Cog +from loguru import logger + +from lib.const import CONST +from ui.embeds import builder +from lib import exceptions as LumiExceptions + + +async def handle_error( + ctx: commands.Context[commands.Bot], + error: commands.CommandError | commands.CheckFailure, +) -> None: + 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.send( + embed=builder.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 ErrorHandler(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}" + ) + + log_msg += " in DMs" if ctx.guild is None else f" | guild: {ctx.guild.name} " + logger.exception(f"{log_msg} | FAILED: {error}") + + @Cog.listener() + async def on_command_error( + self, + ctx: commands.Context[commands.Bot], + error: commands.CommandError | commands.CheckFailure, + ) -> None: + try: + await handle_error(ctx, error) + await self.log_command_error(ctx, error, ".") + except Exception as e: + logger.exception(f"Error in on_command_error: {e}") + traceback.print_exc() + + @Cog.listener() + async def on_app_command_error( + self, + ctx: commands.Context[commands.Bot], + error: commands.CommandError | commands.CheckFailure, + ) -> None: + try: + await handle_error(ctx, error) + await self.log_command_error(ctx, error, "/") + except Exception as e: + 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: + await on_error(event, *args, **kwargs) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(ErrorHandler(bot)) diff --git a/lib/exceptions.py b/lib/exceptions.py new file mode 100644 index 0000000..47d5b1a --- /dev/null +++ b/lib/exceptions.py @@ -0,0 +1,31 @@ +from discord.ext import commands + +from lib.const 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) diff --git a/loader.py b/loader.py index 70d176d..bd7a68b 100644 --- a/loader.py +++ b/loader.py @@ -52,4 +52,5 @@ class CogLoader(commands.Cog): async def setup(cls, bot: commands.Bot) -> None: cog_loader = cls(bot) await cog_loader.load_cog_from_dir(dir_name="modules") + await cog_loader.load_cog_from_dir(dir_name="handlers") await bot.add_cog(cog_loader) diff --git a/modules/misc/xkcd.py b/modules/misc/xkcd.py index ce7309d..e7831b0 100644 --- a/modules/misc/xkcd.py +++ b/modules/misc/xkcd.py @@ -22,7 +22,7 @@ async def print_comic( else: comic = _xkcd.get_random_comic(raw_comic_image=True) - await interaction.followup.send( + await interaction.response.send_message( embed=builder.create_success_embed( interaction, author_text=CONST.STRINGS["xkcd_title"].format(comic.id, comic.title), @@ -37,7 +37,7 @@ async def print_comic( ) except HttpError: - await interaction.followup.send( + await interaction.response.send_message( embed=builder.create_error_embed( interaction, author_text=CONST.STRINGS["xkcd_not_found_author"], @@ -51,10 +51,7 @@ class Xkcd(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot - xkcd: app_commands.Group = app_commands.Group( - name="xkcd", - description="Xkcd commands", - ) + xkcd = app_commands.Group(name="xkcd", description="Get the latest xkcd comic") @xkcd.command(name="latest", description="Get the latest xkcd comic") async def xkcd_latest(self, interaction: discord.Interaction) -> None: diff --git a/ui/embeds.py b/ui/embeds.py index da8cb9b..5b83b18 100644 --- a/ui/embeds.py +++ b/ui/embeds.py @@ -1,5 +1,6 @@ import datetime +from discord.ext import commands import discord from lib.const import CONST @@ -8,7 +9,7 @@ from lib.const import CONST class builder: @staticmethod def create_embed( - ctx, + ctx: commands.Context[commands.Bot], title=None, author_text=None, author_icon_url=None, From 7fd1b6657d3a95498688271640de369e993452aa Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 09:53:21 -0400 Subject: [PATCH 016/102] Refactor embed_builder to not require ctx --- handlers/error.py | 5 +- lib/const.py | 1 + modules/admin/sync.py | 5 +- modules/misc/info.py | 15 ++- modules/misc/ping.py | 5 +- modules/misc/uptime.py | 5 +- modules/misc/xkcd.py | 9 +- settings.yaml | 1 + ui/embeds.py | 227 ++++++++--------------------------------- 9 files changed, 68 insertions(+), 205 deletions(-) diff --git a/handlers/error.py b/handlers/error.py index 0a105a2..237857a 100644 --- a/handlers/error.py +++ b/handlers/error.py @@ -74,8 +74,9 @@ async def handle_error( description = CONST.STRINGS["error_unknown_error_description"] await ctx.send( - embed=builder.create_error_embed( - ctx, + embed=builder.create_embed( + theme="error", + user_name=ctx.author.name, author_text=author_text, description=description, footer_text=footer_text, diff --git a/lib/const.py b/lib/const.py index 318a974..1a8fc63 100644 --- a/lib/const.py +++ b/lib/const.py @@ -85,6 +85,7 @@ class _constants: 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"] diff --git a/modules/admin/sync.py b/modules/admin/sync.py index 447a5b9..a994565 100644 --- a/modules/admin/sync.py +++ b/modules/admin/sync.py @@ -28,8 +28,9 @@ class Sync(commands.Cog): self.bot.tree.copy_global_to(guild=guild) await self.bot.tree.sync(guild=guild) - embed = builder.create_success_embed( - ctx, + embed = builder.create_embed( + theme="success", + user_name=ctx.author.name, author_text=CONST.STRINGS["sync_author"], description=CONST.STRINGS["sync_description"], ) diff --git a/modules/misc/info.py b/modules/misc/info.py index ffadf88..28323e5 100644 --- a/modules/misc/info.py +++ b/modules/misc/info.py @@ -29,18 +29,15 @@ class Info(commands.Cog): ], ) - embed: discord.Embed = builder.create_success_embed( - ctx, + embed: discord.Embed = builder.create_embed( + theme="info", + user_name=ctx.author.name, + author_text=f"{CONST.TITLE} v{CONST.VERSION}", + author_url=CONST.REPO_URL, description=description, footer_text=CONST.STRINGS["info_service_footer"], - show_name=False, + thumbnail_url=CONST.LUMI_LOGO_OPAQUE, ) - 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.send(embed=embed) diff --git a/modules/misc/ping.py b/modules/misc/ping.py index 8ba78e0..e89e2f1 100644 --- a/modules/misc/ping.py +++ b/modules/misc/ping.py @@ -12,8 +12,9 @@ class Ping(commands.Cog): usage="ping", ) async def ping(self, ctx: commands.Context[commands.Bot]) -> None: - embed = builder.create_success_embed( - ctx, + embed = builder.create_embed( + theme="success", + user_name=ctx.author.name, author_text=CONST.STRINGS["ping_author"], description=CONST.STRINGS["ping_pong"], footer_text=CONST.STRINGS["ping_footer"].format( diff --git a/modules/misc/uptime.py b/modules/misc/uptime.py index 21dab90..0923d63 100644 --- a/modules/misc/uptime.py +++ b/modules/misc/uptime.py @@ -17,8 +17,9 @@ 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_success_embed( - ctx, + embed: Embed = builder.create_embed( + theme="info", + user_name=ctx.author.name, author_text=CONST.STRINGS["ping_author"], description=CONST.STRINGS["ping_uptime"].format(unix_timestamp), footer_text=CONST.STRINGS["ping_footer"].format( diff --git a/modules/misc/xkcd.py b/modules/misc/xkcd.py index e7831b0..24095b5 100644 --- a/modules/misc/xkcd.py +++ b/modules/misc/xkcd.py @@ -23,8 +23,8 @@ async def print_comic( comic = _xkcd.get_random_comic(raw_comic_image=True) await interaction.response.send_message( - embed=builder.create_success_embed( - interaction, + embed=builder.create_embed( + theme="info", author_text=CONST.STRINGS["xkcd_title"].format(comic.id, comic.title), description=CONST.STRINGS["xkcd_description"].format( comic.explanation_url, @@ -32,14 +32,13 @@ async def print_comic( ), footer_text=CONST.STRINGS["xkcd_footer"], image_url=comic.image_url, - show_name=False, ), ) except HttpError: await interaction.response.send_message( - embed=builder.create_error_embed( - interaction, + embed=builder.create_embed( + theme="error", author_text=CONST.STRINGS["xkcd_not_found_author"], description=CONST.STRINGS["xkcd_not_found"], footer_text=CONST.STRINGS["xkcd_footer"], diff --git a/settings.yaml b/settings.yaml index 1674d01..3e25f81 100644 --- a/settings.yaml +++ b/settings.yaml @@ -45,6 +45,7 @@ art: check: lumi_check.png cross: lumi_cross.png exclaim: lumi_exclaim.png + info: lumi_info.png?_=2 hammer: lumi_hammer.png money_bag: lumi_money_bag.png money_coins: lumi_money_coins.png diff --git a/ui/embeds.py b/ui/embeds.py index 5b83b18..bd632c8 100644 --- a/ui/embeds.py +++ b/ui/embeds.py @@ -1,6 +1,6 @@ -import datetime +from datetime import datetime +from typing import Optional, Literal -from discord.ext import commands import discord from lib.const import CONST @@ -9,199 +9,60 @@ from lib.const import CONST class builder: @staticmethod def create_embed( - ctx: commands.Context[commands.Bot], - 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, + 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, + hide_name_in_description: bool = False, + hide_time: bool = False, ) -> discord.Embed: - if not hide_author: - if not author_text: - author_text = ctx.author.name - elif show_name: - description = f"**{ctx.author.name}** {description}" + """ + Create a standard Lumi embed with the given parameters. + """ - if not hide_author_icon and not author_icon_url: - author_icon_url = ctx.author.display_avatar.url + theme_settings = { + "error": (CONST.COLOR_ERROR, CONST.CROSS_ICON), + "success": (CONST.COLOR_DEFAULT, CONST.CHECK_ICON), + "info": (CONST.COLOR_DEFAULT, CONST.INFO_ICON), + "warning": (CONST.COLOR_WARNING, CONST.WARNING_ICON), + } + if theme in theme_settings: + color, author_icon_url = theme_settings[theme] - if not footer_text: - footer_text = "Luminara" - if not footer_icon_url: - footer_icon_url = CONST.LUMI_LOGO_TRANSPARENT + if user_name and not hide_name_in_description: + description = f"**{user_name}** {description}" - embed = discord.Embed( + embed: discord.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() + embed.set_author( + name=author_text or user_name or None, + icon_url=author_icon_url or user_display_avatar_url or None, + url=author_url, + ) + + embed.set_footer( + text=footer_text or CONST.TITLE, + icon_url=footer_icon_url or CONST.LUMI_LOGO_TRANSPARENT, + ) + + embed.timestamp = None if hide_time else (timestamp or 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, - ) -> discord.Embed: - return builder.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, - ) -> discord.Embed: - return builder.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, - ) -> discord.Embed: - return builder.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, - ) -> discord.Embed: - return builder.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, - ) From 35a212ea3a8aa75c48857a3b8cd44a31b33b82de Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 09:54:41 -0400 Subject: [PATCH 017/102] Rename locales dir --- lib/const.py | 2 +- {localization => locales}/bdays.en-US.json | 0 {localization => locales}/levels.en-US.json | 0 {localization => locales}/strings.en-US.json | 0 4 files changed, 1 insertion(+), 1 deletion(-) rename {localization => locales}/bdays.en-US.json (100%) rename {localization => locales}/levels.en-US.json (100%) rename {localization => locales}/strings.en-US.json (100%) diff --git a/lib/const.py b/lib/const.py index 1a8fc63..d56cca6 100644 --- a/lib/const.py +++ b/lib/const.py @@ -13,7 +13,7 @@ class _parser: return self._read_file("settings.yaml", yaml.safe_load) def read_json(self, path: str) -> dict: - return self._read_file(f"localization/{path}.json", json.load) + return self._read_file(f"locales/{path}.json", json.load) def _read_file(self, file_path: str, load_func: Callable) -> dict: with open(file_path) as file: diff --git a/localization/bdays.en-US.json b/locales/bdays.en-US.json similarity index 100% rename from localization/bdays.en-US.json rename to locales/bdays.en-US.json diff --git a/localization/levels.en-US.json b/locales/levels.en-US.json similarity index 100% rename from localization/levels.en-US.json rename to locales/levels.en-US.json diff --git a/localization/strings.en-US.json b/locales/strings.en-US.json similarity index 100% rename from localization/strings.en-US.json rename to locales/strings.en-US.json From 48f50415b104f5663e369d477fe1ebe67facd20f Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 09:56:23 -0400 Subject: [PATCH 018/102] feat: Add hide_name_in_description option to info command --- modules/misc/info.py | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/misc/info.py b/modules/misc/info.py index 28323e5..e84980b 100644 --- a/modules/misc/info.py +++ b/modules/misc/info.py @@ -37,6 +37,7 @@ class Info(commands.Cog): description=description, footer_text=CONST.STRINGS["info_service_footer"], thumbnail_url=CONST.LUMI_LOGO_OPAQUE, + hide_name_in_description=True, ) await ctx.send(embed=embed) From aa2a4c21a63ceb2a04f8fb2895aaa51d00b153eb Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:18:50 -0400 Subject: [PATCH 019/102] Add introduction command --- locales/strings.en-US.json | 1 + modules/misc/introduction.py | 193 +++++++++++++++++++++++++++++++++++ ui/views/introduction.py | 83 +++++++++++++++ 3 files changed, 277 insertions(+) create mode 100644 modules/misc/introduction.py create mode 100644 ui/views/introduction.py diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index c4eea1b..60cddef 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -191,6 +191,7 @@ "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_author": "Introduction Posted", "intro_post_confirmation": "your introduction has been posted in {0}!", "intro_preview_field": "**{0}:** {1}\n\n", "intro_question_footer": "Type your answer below.", diff --git a/modules/misc/introduction.py b/modules/misc/introduction.py new file mode 100644 index 0000000..b1aa928 --- /dev/null +++ b/modules/misc/introduction.py @@ -0,0 +1,193 @@ +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.views.introduction import ( + IntroductionFinishButtons, + IntroductionStartButtons, +) + + +class Introduction(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command( + name="introduction", + aliases=["intro"], + usage="introduction", + ) + async def introduction(self, ctx: commands.Context[commands.Bot]) -> None: + guild: Optional[discord.Guild] = self.bot.get_guild( + CONST.INTRODUCTIONS_GUILD_ID, + ) + member: Optional[discord.Member] = ( + guild.get_member(ctx.author.id) if guild else None + ) + + if not guild or not member: + await ctx.send( + embed=builder.create_embed( + theme="error", + user_name=ctx.author.name, + 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.INTRODUCTIONS_QUESTION_MAPPING + channel: Optional[discord.abc.GuildChannel] = guild.get_channel( + CONST.INTRODUCTIONS_CHANNEL_ID, + ) + + if not channel or isinstance( + channel, + (discord.ForumChannel, discord.CategoryChannel), + ): + await ctx.send( + embed=builder.create_embed( + theme="error", + user_name=ctx.author.name, + 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.send( + embed=builder.create_embed( + theme="info", + user_name=ctx.author.name, + 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.clicked_stop: + 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"], + ), + ) + return + + if view.clicked_start: + + 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=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, + ) + 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 asyncio.TimeoutError: + 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"], + 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: + await bot.add_cog(Introduction(bot)) diff --git a/ui/views/introduction.py b/ui/views/introduction.py new file mode 100644 index 0000000..3fa27a0 --- /dev/null +++ b/ui/views/introduction.py @@ -0,0 +1,83 @@ +from typing import Optional + +import discord +from discord.ui import Button, View + + +class IntroductionStartButtons(View): + def __init__(self, ctx) -> None: + super().__init__(timeout=60) + self.ctx = ctx + self.clicked_start: bool = False + self.clicked_stop: bool = False + self.message: Optional[discord.Message] = None + + async def on_timeout(self) -> None: + for child in self.children: + if isinstance(child, Button): + child.disabled = True + if self.message: + await self.message.edit(view=None) + + @discord.ui.button(label="Start", style=discord.ButtonStyle.primary) + async def start_button_callback( + self, + interaction: discord.Interaction, + button: Button, + ) -> None: + await interaction.response.edit_message(view=None) + self.clicked_start = True + self.stop() + + @discord.ui.button(label="Stop", style=discord.ButtonStyle.red) + async def stop_button_callback( + self, + interaction: discord.Interaction, + button: Button, + ) -> None: + await interaction.response.edit_message(view=None) + self.clicked_stop = True + self.stop() + + +class IntroductionFinishButtons(View): + def __init__(self, ctx) -> None: + super().__init__(timeout=60) + self.ctx = ctx + self.clicked_confirm: bool = False + self.message: Optional[discord.Message] = None + + async def on_timeout(self) -> None: + for child in self.children: + if isinstance(child, 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 confirm_button_callback( + self, + interaction: discord.Interaction, + button: Button, + ) -> None: + await interaction.response.edit_message(view=None) + self.clicked_confirm = True + self.stop() + + @discord.ui.button(label="Stop", style=discord.ButtonStyle.red) + async def stop_button_callback( + self, + interaction: discord.Interaction, + button: Button, + ) -> 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 From 84ae737a3368a3c24b2765441e47869134e99620 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:19:42 -0400 Subject: [PATCH 020/102] Add invite view buttons --- ui/views/invite.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 ui/views/invite.py diff --git a/ui/views/invite.py b/ui/views/invite.py new file mode 100644 index 0000000..7c70095 --- /dev/null +++ b/ui/views/invite.py @@ -0,0 +1,14 @@ +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( + label=CONST.STRINGS["invite_button_text"], + style=ButtonStyle.url, + url=CONST.INVITE_URL, + ) + self.add_item(invite_button) From 5149fa172c0a6b525f78f8b8cc74dbeaa4b9f3ff Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:24:41 -0400 Subject: [PATCH 021/102] Add blackjack view and refactor other views --- ui/views/blackjack.py | 59 ++++++++++++++++++++++++++++++++++++++++ ui/views/introduction.py | 9 +++--- 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 ui/views/blackjack.py diff --git a/ui/views/blackjack.py b/ui/views/blackjack.py new file mode 100644 index 0000000..756a6ce --- /dev/null +++ b/ui/views/blackjack.py @@ -0,0 +1,59 @@ +import discord +from discord.ext import commands +from discord.ui import View, Button +from lib.const import CONST +from typing import List, Optional + + +class BlackJackButtons(View): + def __init__(self, ctx: commands.Context[commands.Bot]) -> None: + super().__init__(timeout=180) + self.ctx: commands.Context[commands.Bot] = ctx + self.clickedHit: bool = False + self.clickedStand: bool = False + self.clickedDoubleDown: bool = False + self.message: Optional[discord.Message] = None + + async def on_timeout(self) -> None: + self.children: List[discord.ui.Button] = [] + + for child in self.children: + if isinstance(child, Button): + child.disabled = True + + @discord.ui.button( + label=CONST.STRINGS["blackjack_hit"], + style=discord.ButtonStyle.gray, + emoji=CONST.BLACKJACK_HIT_EMOJI, + ) + async def hit_button_callback( + self, + interaction: discord.Interaction, + button: Button, + ) -> None: + 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, + interaction: discord.Interaction, + button: Button, + ) -> None: + self.clickedStand = True + await interaction.response.defer() + self.stop() + + async def interaction_check(self, interaction: discord.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 diff --git a/ui/views/introduction.py b/ui/views/introduction.py index 3fa27a0..22cb10d 100644 --- a/ui/views/introduction.py +++ b/ui/views/introduction.py @@ -1,13 +1,14 @@ from typing import Optional import discord +from discord.ext import commands from discord.ui import Button, View class IntroductionStartButtons(View): - def __init__(self, ctx) -> None: + def __init__(self, ctx: commands.Context[commands.Bot]) -> None: super().__init__(timeout=60) - self.ctx = ctx + self.ctx: commands.Context[commands.Bot] = ctx self.clicked_start: bool = False self.clicked_stop: bool = False self.message: Optional[discord.Message] = None @@ -41,9 +42,9 @@ class IntroductionStartButtons(View): class IntroductionFinishButtons(View): - def __init__(self, ctx) -> None: + def __init__(self, ctx: commands.Context[commands.Bot]) -> None: super().__init__(timeout=60) - self.ctx = ctx + self.ctx: commands.Context[commands.Bot] = ctx self.clicked_confirm: bool = False self.message: Optional[discord.Message] = None From 8f542865ed19bea5ea95c4aac3449565679d9121 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:27:46 -0400 Subject: [PATCH 022/102] Add invite author to invite description --- locales/strings.en-US.json | 3 ++- modules/misc/invite.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 modules/misc/invite.py diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index 60cddef..3d33ee4 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -205,7 +205,8 @@ "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!", + "invite_author": "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", diff --git a/modules/misc/invite.py b/modules/misc/invite.py new file mode 100644 index 0000000..8f3b4e8 --- /dev/null +++ b/modules/misc/invite.py @@ -0,0 +1,29 @@ +from discord.ext import commands +from lib.const import CONST +from ui.embeds import builder +from ui.views.invite import InviteButton + + +class Invite(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command( + name="invite", + aliases=["inv"], + usage="invite", + ) + async def invite(self, ctx: commands.Context[commands.Bot]) -> None: + await ctx.send( + embed=builder.create_embed( + theme="success", + user_name=ctx.author.name, + author_text=CONST.STRINGS["invite_author"], + description=CONST.STRINGS["invite_description"], + ), + view=InviteButton(), + ) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Invite(bot)) From fc2d06a944b3910d93a27b292b66d8acd8aeb853 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:33:31 -0400 Subject: [PATCH 023/102] Add backup module --- modules/misc/backup.py | 87 ++++++++++++++++++ poetry.lock | 194 ++++++++++++++++++++++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 modules/misc/backup.py diff --git a/modules/misc/backup.py b/modules/misc/backup.py new file mode 100644 index 0000000..a80ff56 --- /dev/null +++ b/modules/misc/backup.py @@ -0,0 +1,87 @@ +import subprocess +from datetime import datetime +from typing import List, Optional + +from discord.ext import commands, tasks +import dropbox +from dropbox.files import FileMetadata +from loguru import logger + +from lib.const 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.now().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 # type: ignore + if isinstance(entry, FileMetadata) + ] + + for file in sorted(all_backup_files)[:-48]: + _dbx.files_delete_v2(f"/{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".') + + +class Backup(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot: commands.Bot = bot + self.do_backup.start() + + @tasks.loop(hours=1) + async def do_backup(self) -> None: + await backup() + + @commands.command() + async def backup(self, ctx: commands.Context[commands.Bot]) -> None: + await backup() + await ctx.send("Backup successful.") + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Backup(bot)) diff --git a/poetry.lock b/poetry.lock index ac0753e..dc96b1a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -235,6 +235,105 @@ files = [ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "colorama" version = "0.4.6" @@ -277,6 +376,23 @@ files = [ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, ] +[[package]] +name = "dropbox" +version = "12.0.2" +description = "Official Dropbox API Client" +optional = false +python-versions = "*" +files = [ + {file = "dropbox-12.0.2-py2-none-any.whl", hash = "sha256:4b8207a9f4afd33726ec886c0d223f4bbc42fe649b87718690a24704f5e24c0c"}, + {file = "dropbox-12.0.2-py3-none-any.whl", hash = "sha256:c5b7e9c2668adb6b12dcecd84342565dc50f7d35ab6a748d155cb79040979d1c"}, + {file = "dropbox-12.0.2.tar.gz", hash = "sha256:50057fd5ad5fcf047f542dfc6747a896e7ef982f1b5f8500daf51f3abd609962"}, +] + +[package.dependencies] +requests = ">=2.16.2" +six = ">=1.12.0" +stone = ">=2,<3.3.3" + [[package]] name = "filelock" version = "3.15.4" @@ -646,6 +762,17 @@ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx- test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] type = ["mypy (>=1.8)"] +[[package]] +name = "ply" +version = "3.11" +description = "Python Lex & Yacc" +optional = false +python-versions = "*" +files = [ + {file = "ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce"}, + {file = "ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3"}, +] + [[package]] name = "pre-commit" version = "3.8.0" @@ -773,6 +900,27 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "ruff" version = "0.6.2" @@ -800,6 +948,17 @@ files = [ {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -811,6 +970,22 @@ files = [ {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] +[[package]] +name = "stone" +version = "3.3.1" +description = "Stone is an interface description language (IDL) for APIs." +optional = false +python-versions = "*" +files = [ + {file = "stone-3.3.1-py2-none-any.whl", hash = "sha256:cd2f7f9056fc39b16c8fd46a26971dc5ccd30b5c2c246566cd2c0dd27ff96609"}, + {file = "stone-3.3.1-py3-none-any.whl", hash = "sha256:e15866fad249c11a963cce3bdbed37758f2e88c8ff4898616bc0caeb1e216047"}, + {file = "stone-3.3.1.tar.gz", hash = "sha256:4ef0397512f609757975f7ec09b35639d72ba7e3e17ce4ddf399578346b4cb50"}, +] + +[package.dependencies] +ply = ">=3.4" +six = ">=1.12.0" + [[package]] name = "typing-extensions" version = "4.12.2" @@ -822,6 +997,23 @@ files = [ {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] +[[package]] +name = "urllib3" +version = "2.2.2" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, + {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "virtualenv" version = "20.26.3" @@ -962,4 +1154,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "a93d83011121cab87e99209358e145ad73b8da37720436992597d707b0e2e607" +content-hash = "189f79c9e4eaaae2cfdde9b29509d1dd15030bdf8c82f972f5883d87c74365ae" diff --git a/pyproject.toml b/pyproject.toml index 8e8aad0..92ebed9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ aiofiles = "^24.1.0" aiocache = "^0.12.2" aioconsole = "^0.7.1" psutil = "^6.0.0" +dropbox = "^12.0.2" [build-system] build-backend = "poetry.core.masonry.api" From 0b3c05ddc9550fa1f4468d875afe6c4dfc240119 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:44:07 -0400 Subject: [PATCH 024/102] Add database and migrations from v2 --- client.py | 7 +- db/database.py | 115 +++++++++++++++++++++++++++++ db/migrations/v2_5_8_init.sql | 109 +++++++++++++++++++++++++++ db/migrations/v2_5_9_reactions.sql | 21 ++++++ db/migrations/v2_6_0_cases.sql | 44 +++++++++++ 5 files changed, 294 insertions(+), 2 deletions(-) create mode 100644 db/database.py create mode 100644 db/migrations/v2_5_8_init.sql create mode 100644 db/migrations/v2_5_9_reactions.sql create mode 100644 db/migrations/v2_6_0_cases.sql diff --git a/client.py b/client.py index 53292ca..1994d0b 100644 --- a/client.py +++ b/client.py @@ -2,6 +2,7 @@ from discord.ext import commands from loguru import logger import asyncio from loader import CogLoader +from db.database import run_migrations class Luminara(commands.Bot): @@ -14,13 +15,15 @@ class Luminara(commands.Bot): async def setup(self) -> None: try: - pass + run_migrations() except Exception as e: logger.error(f"Failed to setup: {e}") + await self.shutdown() + await self.load_cogs() async def load_cogs(self) -> None: - logger.info("Loading cogs...") + logger.debug("Loading cogs...") await CogLoader.setup(bot=self) @commands.Cog.listener() diff --git a/db/database.py b/db/database.py new file mode 100644 index 0000000..ba8f1e1 --- /dev/null +++ b/db/database.py @@ -0,0 +1,115 @@ +import os +import pathlib +import re + +import mysql.connector +from loguru import logger +from mysql.connector import pooling + +from lib.const 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.success("All database migrations completed.") diff --git a/db/migrations/v2_5_8_init.sql b/db/migrations/v2_5_8_init.sql new file mode 100644 index 0000000..9dfb09f --- /dev/null +++ b/db/migrations/v2_5_8_init.sql @@ -0,0 +1,109 @@ +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) +); \ No newline at end of file diff --git a/db/migrations/v2_5_9_reactions.sql b/db/migrations/v2_5_9_reactions.sql new file mode 100644 index 0000000..ab0cba3 --- /dev/null +++ b/db/migrations/v2_5_9_reactions.sql @@ -0,0 +1,21 @@ +-- 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); \ No newline at end of file diff --git a/db/migrations/v2_6_0_cases.sql b/db/migrations/v2_6_0_cases.sql new file mode 100644 index 0000000..f6fcd60 --- /dev/null +++ b/db/migrations/v2_6_0_cases.sql @@ -0,0 +1,44 @@ +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); \ No newline at end of file From 92d5aafda1bd3bfd7bd9ec058aff26661f3ff665 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:47:30 -0400 Subject: [PATCH 025/102] Rename main files --- client.py => lib/client.py | 2 +- loader.py => lib/loader.py | 0 lumi.py => main.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename client.py => lib/client.py (98%) rename loader.py => lib/loader.py (100%) rename lumi.py => main.py (96%) diff --git a/client.py b/lib/client.py similarity index 98% rename from client.py rename to lib/client.py index 1994d0b..fedbe66 100644 --- a/client.py +++ b/lib/client.py @@ -1,7 +1,7 @@ from discord.ext import commands from loguru import logger import asyncio -from loader import CogLoader +from lib.loader import CogLoader from db.database import run_migrations diff --git a/loader.py b/lib/loader.py similarity index 100% rename from loader.py rename to lib/loader.py diff --git a/lumi.py b/main.py similarity index 96% rename from lumi.py rename to main.py index 2089053..a1714a0 100644 --- a/lumi.py +++ b/main.py @@ -4,7 +4,7 @@ import discord from discord.ext import commands from loguru import logger from lib.const import CONST -from client import Luminara +from lib.client import Luminara logger.remove() logger.add(sys.stdout, format=CONST.LOG_FORMAT, colorize=True, level=CONST.LOG_LEVEL) From f42139a0143c99cdfa92192b4409f49bff180740 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:48:24 -0400 Subject: [PATCH 026/102] Fix Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cc3bbd7..8dc36ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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", "./lumi.py" ] \ No newline at end of file +CMD [ "poetry", "run", "python", "-OO", "./main.py" ] \ No newline at end of file From afd2abfe35c7592fd7107393f07ab62ea4ddb33d Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 10:50:04 -0400 Subject: [PATCH 027/102] Add xp_service.py --- services/xp_service.py | 295 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 295 insertions(+) create mode 100644 services/xp_service.py diff --git a/services/xp_service.py b/services/xp_service.py new file mode 100644 index 0000000..8f5f10d --- /dev/null +++ b/services/xp_service.py @@ -0,0 +1,295 @@ +import time +from typing import Callable, Dict, List, Optional, Tuple + +from discord.ext import commands + +from db import database +from lib.const 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 From c9c56e20f8ff78aaa61b04e4418476d4b462c4cb Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 12:26:02 -0400 Subject: [PATCH 028/102] Add level command --- lib/loader.py | 9 +++++--- modules/levels/level.py | 46 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 modules/levels/level.py diff --git a/lib/loader.py b/lib/loader.py index bd7a68b..fdbed86 100644 --- a/lib/loader.py +++ b/lib/loader.py @@ -13,9 +13,12 @@ class CogLoader(commands.Cog): async def is_cog(self, path: Path) -> bool: cog_name: str = path.stem + if cog_name in self.cog_ignore_list: + logger.debug(f"Ignoring cog: {cog_name} because it is in the ignore list") + return False + return ( path.suffix == ".py" - and cog_name not in self.cog_ignore_list and not path.name.startswith("_") and await aiofiles.os.path.isfile(path) ) @@ -30,7 +33,7 @@ class CogLoader(commands.Cog): logger.exception(f"Error loading cog from {item}: {e}") elif await self.is_cog(path): - relative_path: Path = path.relative_to(Path(__file__).parent) + relative_path: Path = path.relative_to(Path(__file__).parent.parent) module: str = ( str(relative_path).replace("/", ".").replace("\\", ".")[:-3] ) @@ -45,7 +48,7 @@ class CogLoader(commands.Cog): logger.exception(f"Error loading cogs from {path}: {e}") async def load_cog_from_dir(self, dir_name: str) -> None: - path: Path = Path(__file__).parent / dir_name + path: Path = Path(__file__).parent.parent / dir_name await self.load_cogs(path) @classmethod diff --git a/modules/levels/level.py b/modules/levels/level.py new file mode 100644 index 0000000..6937f7c --- /dev/null +++ b/modules/levels/level.py @@ -0,0 +1,46 @@ +from discord.ext import commands +from discord import Embed +from lib.const import CONST +from ui.embeds import builder +from services.xp_service import XpService + + +class Level(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command( + name="level", + aliases=["rank", "lvl", "xp"], + usage="level", + ) + async def ping(self, ctx: commands.Context[commands.Bot]) -> 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 = builder.create_embed( + theme="success", + user_name=ctx.author.name, + title=CONST.STRINGS["xp_level"].format(xp_data.level), + footer_text=CONST.STRINGS["xp_server_rank"].format(rank or "NaN"), + thumbnail_url=ctx.author.display_avatar.url, + hide_name_in_description=True, + ) + 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.send(embed=embed) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Level(bot)) From e988cfa3e43f9a06f3f4ca8fd1d3e1eac8e06640 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 12:35:06 -0400 Subject: [PATCH 029/102] Add the "pretty-format-json" pre-commit hook --- .pre-commit-config.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 97be63e..c7f94b2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ repos: hooks: - id: check-yaml - id: check-json + - id: pretty-format-json - id: check-toml - repo: https://github.com/asottile/add-trailing-comma @@ -16,7 +17,7 @@ repos: hooks: # Run the linter. - id: ruff - args: [ --fix ] + args: [--fix] # Run the formatter. - id: ruff-format From a5481ea66e0336f4d2f67ec220f0cd2109f27050 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 12:37:48 -0400 Subject: [PATCH 030/102] Add "--autofix" arg to pretty-format-json --- .pre-commit-config.yaml | 1 + locales/bdays.en-US.json | 150 +++++----- locales/levels.en-US.json | 156 +++++----- locales/strings.en-US.json | 598 ++++++++++++++++++------------------- 4 files changed, 453 insertions(+), 452 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7f94b2..9a45b67 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ repos: - id: check-yaml - id: check-json - id: pretty-format-json + args: [--autofix] - id: check-toml - repo: https://github.com/asottile/add-trailing-comma diff --git a/locales/bdays.en-US.json b/locales/bdays.en-US.json index 972f264..605ae48 100644 --- a/locales/bdays.en-US.json +++ b/locales/bdays.en-US.json @@ -1,76 +1,76 @@ { - "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! 🎉" - ] -} \ No newline at end of file + "birthday_messages": [ + "\ud83c\udf82 Happy Birthday, **{0}**! \ud83c\udf89 Wishing you a day filled with joy and laughter.", + "\ud83c\udf88 It's party time! Happy Birthday, **{0}**! \ud83c\udf89", + "\ud83c\udf89 Another year older, another year wiser! Happy Birthday, **{0}**! \ud83c\udf82", + "\ud83c\udf1f Today's the day you shine brighter than ever! Happy Birthday, **{0}**! \ud83c\udf1f", + "\ud83c\udf81 Special day alert! It's **{0}**'s birthday! \ud83c\udf81", + "\ud83c\udf8a Hip, hip, hooray! It's **{0}**'s birthday today! \ud83c\udf8a", + "\ud83c\udf82 Cake and confetti time! Happy Birthday, **{0}**! \ud83c\udf89", + "\ud83c\udf08 Sending you a rainbow of happiness on your birthday, **{0}**! \ud83c\udf88", + "\ud83c\udf89 Let's raise a toast to **{0}** on their birthday! Cheers to another fantastic year! \ud83e\udd42", + "\ud83c\udf88 Birthdays are like sprinkles on the cupcake of life! Happy Birthday, **{0}**! \ud83e\uddc1", + "\ud83c\udf81 Gift-wrapped wishes for a wonderful birthday and an amazing year ahead, **{0}**! \ud83c\udf81", + "\ud83c\udf8a Time to blow out the candles and make a wish! Happy Birthday, **{0}**! \ud83c\udf82", + "\ud83c\udf1f It's your day to sparkle and shine, **{0}**! Happy Birthday! \u2728", + "\ud83c\udf88 May your birthday be as fabulous as you are, **{0}**! \ud83c\udf89", + "\ud83c\udf89 Here's to a year filled with success, happiness, and endless opportunities for **{0}**! Happy Birthday! \ud83e\udd73", + "\ud83c\udf81 Wishing **{0}** all the best on their special day! Happy Birthday! \ud83c\udf81", + "\ud83c\udf8a Another year of unforgettable memories begins today for **{0}**! Happy Birthday! \ud83c\udf8a", + "\ud83c\udf1f Your birthday is the perfect excuse to pamper yourself, **{0}**! Enjoy your special day! \ud83c\udf88", + "\ud83c\udf82 Age is just a number, and you're looking more fabulous with each passing year, **{0}**! Happy Birthday! \ud83d\udc95", + "\ud83c\udf89 Today, we celebrate the amazing person you are, **{0}**! Happy Birthday! \ud83c\udf82", + "\ud83c\udf88 Life's journey gets even more exciting as **{0}** celebrates another year of it! Happy Birthday! \ud83c\udf89", + "\ud83c\udf1f Happy Birthday to someone who makes every day brighter with their presence, **{0}**! \ud83c\udf1e", + "\ud83c\udf81 May this birthday be the beginning of the most extraordinary year yet for **{0}**! \ud83d\ude80", + "\ud83c\udf8a Birthdays are nature's way of telling us to eat more cake! Enjoy your special treat, **{0}**! \ud83c\udf70", + "\ud83c\udf89 Time to pop the confetti and make some fabulous birthday memories, **{0}**! \ud83c\udf82", + "\ud83c\udf88 Today, the world received a gift in the form of **{0}**! Happy Birthday! \ud83c\udf89", + "\ud83c\udf1f Wishing **{0}** health, happiness, and all the things they desire on their birthday! \ud83c\udf81", + "\ud83c\udf82 Cheers to **{0}** on another year of being amazing! Happy Birthday! \ud83c\udf89", + "\ud83c\udf89 It's **{0}**'s big day, so let loose and enjoy every moment! Happy Birthday! \ud83c\udf8a", + "\ud83c\udf88 Sending virtual hugs and lots of love to **{0}** on their special day! \ud83e\udd17\u2764\ufe0f", + "\ud83c\udf1f On your birthday, the world becomes a better place because of your presence, **{0}**! \ud83c\udf89", + "\ud83c\udf81 As you blow out the candles, know that your wishes are heard and your dreams matter. Happy Birthday, **{0}**! \ud83c\udf20", + "\ud83c\udf8a Here's to a birthday filled with laughter, love, and all the things that make you happy, **{0}**! Cheers! \ud83e\udd42", + "\ud83c\udf82 May your birthday be filled with delightful surprises and sweet moments, **{0}**! Enjoy your special day! \ud83c\udf88", + "\ud83c\udf89 It's time for the world to celebrate the incredible person that is **{0}**! Happy Birthday! \ud83c\udf8a", + "\ud83c\udf88 Another year, another chapter in the adventure of life for **{0}**! May this year be full of excitement and joy! \ud83c\udf1f", + "\ud83c\udf1f May this birthday mark the beginning of extraordinary achievements and unforgettable memories for **{0}**! \ud83c\udf81", + "\ud83c\udf82 Here's to a birthday filled with love, happiness, and all the good things you deserve, **{0}**! \ud83c\udf89", + "\ud83c\udf89 Sending virtual confetti and a big smile to **{0}** on their birthday! Let's celebrate! \ud83c\udf88", + "\ud83c\udf88 Time to indulge in cake and celebrate the wonderful human that is **{0}**! Happy Birthday! \ud83c\udf82", + "\ud83c\udf8a Today, we honor the amazing journey of **{0}**'s life! Happy Birthday! \ud83c\udf1f", + "\ud83c\udf1f Birthdays are a time for reflection, growth, and gratitude. Wishing **{0}** a wonderful birthday and year ahead! \ud83c\udf81", + "\ud83c\udf81 Another trip around the sun calls for a big celebration! Happy Birthday, **{0}**! \ud83c\udf89", + "\ud83c\udf82 As you blow out the candles, know that you are loved and cherished, **{0}**! Happy Birthday! \ud83c\udf88", + "\ud83c\udf89 It's a new age and a new opportunity to shine, **{0}**! May this year be your best yet! \ud83c\udf1f", + "\ud83c\udf88 On your special day, may you be surrounded by love, happiness, and everything that brings you joy, **{0}**! \ud83c\udf82", + "\ud83c\udf1f Today, we celebrate **{0}** and the unique light they bring into the world! Happy Birthday! \ud83c\udf89", + "\ud83c\udf81 Wishing a fantastic birthday to the one and only **{0}**! May this day be filled with laughter and love! \ud83c\udf88", + "\ud83c\udf8a May your birthday be as extraordinary as the person you are, **{0}**! Cheers to you! \ud83e\udd73", + "\ud83c\udf82 You're not just a year older; you're a year more incredible! Happy Birthday, **{0}**! \ud83c\udf1f", + "\ud83c\udf89 It's time to embrace the joy, love, and happiness that come with birthdays! Enjoy every moment, **{0}**! \ud83c\udf88", + "\ud83c\udf88 Another year, another chance to create beautiful memories. Happy Birthday, **{0}**! \ud83c\udf82", + "\ud83c\udf1f Your birthday is a reminder of how much you mean to all of us! Wishing you the best day ever, **{0}**! \ud83c\udf81", + "\ud83c\udf81 Sending warm birthday wishes and virtual hugs to **{0}** on their special day! \ud83e\udd17\u2764\ufe0f", + "\ud83c\udf89 Today, we celebrate the unique and wonderful person that is **{0}**! Happy Birthday! \ud83c\udf82", + "\ud83c\udf88 Here's to a birthday filled with laughter, love, and all the things that make you smile, **{0}**! \ud83c\udf1f", + "\ud83c\udf8a It's a day to be spoiled and celebrated, **{0}**! Wishing you the happiest of birthdays! \ud83c\udf81", + "\ud83c\udf82 As you turn another year older, know that you are loved and cherished, **{0}**! Happy Birthday! \ud83c\udf89" + ], + "months": [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + ] +} diff --git a/locales/levels.en-US.json b/locales/levels.en-US.json index d6c8424..2d7cee7 100644 --- a/locales/levels.en-US.json +++ b/locales/levels.en-US.json @@ -1,79 +1,79 @@ { - "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 {}**." - ] -} \ No newline at end of file + "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 {}** \ud83d\udc4d", + "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 {}**." + ] +} diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index 3d33ee4..874839a 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -1,300 +1,300 @@ { - "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 ` - View a specific case\n`/editcase ` - 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 .", - "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": "**discord.py:** 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:** \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_author": "Introduction Posted", - "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_author": "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 .", - "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!", - "sync_author": "Synced Commands", - "sync_description": "the application command tree has been synced." -} \ No newline at end of file + "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", + "balance_author": "{0}'s wallet", + "balance_cash": "**Cash**: ${0}", + "balance_footer": "check out /daily", + "bet_limit": "\u274c | **{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": "\ud83c\udf82 {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_hit": "hit", + "blackjack_lost": "You lost **${0}**.", + "blackjack_lost_generic": "You lost..", + "blackjack_player_hand": "**You**\nScore: {0}\n*Hand: {1}*", + "blackjack_stand": "stand", + "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}**.", + "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}` \ud83c\udfaf", + "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": "\ud83d\udcc8 | **lucas** you have reached **Level 15**.", + "config_level_type_whimsical": "level announcements will be **sarcastic comments**.", + "config_level_type_whimsical_example": "\ud83d\udcc8 | **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": "\ud83d\udcd6 Case commands", + "config_modlog_info_commands_value": "`/cases` - View all cases in this server\n`/case ` - View a specific case\n`/editcase ` - 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": "\u26a0\ufe0f 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": "\u2705 Enabled (default)", + "config_show_disabled": "\u274c Disabled", + "config_show_enabled": "\u2705 Enabled", + "config_show_guide": "Guide: {0}", + "config_show_level_announcements": "Level announcements", + "config_show_moderation_log": "Moderation Log", + "config_show_moderation_log_channel_deleted": "\u26a0\ufe0f **Not configured** (channel deleted?)", + "config_show_moderation_log_enabled": "\u2705 {0}", + "config_show_moderation_log_not_configured": "\u26a0\ufe0f **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 .", + "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_cant_use_buttons": "You can't use these buttons, they're someone else's!", + "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.", + "give_error_bot": "you can't give money to a bot.", + "give_error_insufficient_funds": "you don't have enough cash.", + "give_error_invalid_amount": "invalid amount.", + "give_error_self": "you can't give money to yourself.", + "give_success": "**{0}** gave **${1}** to {2}.", + "greet_default_description": "_ _\n**Welcome** to **{0}**", + "greet_template_description": "\u2193\u2193\u2193\n{0}", + "help_use_prefix": "Please use Lumi's prefix to get help. Type `{0}help`", + "info_api_version": "**discord.py:** 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:** \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_post_confirmation_author": "Introduction Posted", + "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_author": "Invite Lumi", + "invite_button_text": "Invite Lumi", + "invite_description": "thanks for inviting me to your server!", + "level_up": "\ud83d\udcc8 | **{0}** you have reached **Level {1}**.", + "level_up_prefix": "\ud83d\udcc8 | **{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 .", + "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", + "sync_description": "the application command tree has been synced.", + "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}" +} From b8b6880169b6e57596d479828cc702a0d71a2794 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 12:42:31 -0400 Subject: [PATCH 031/102] Add "sort-simple-yaml" pre-commit hook for settings.yaml --- .pre-commit-config.yaml | 2 ++ settings.yaml | 74 ++++++++++++++++++++--------------------- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a45b67..e3faa22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,8 @@ repos: rev: v4.6.0 hooks: - id: check-yaml + - id: sort-simple-yaml + files: settings.yaml - id: check-json - id: pretty-format-json args: [--autofix] diff --git a/settings.yaml b/settings.yaml index 3e25f81..e94fbd2 100644 --- a/settings.yaml +++ b/settings.yaml @@ -1,40 +1,3 @@ -info: - title: Luminara - author: wlinator - license: GNU General Public License v3.0 - version: "3.0.0-alpha" - repository_url: https://git.wlinator.org/Luminara/Lumi - invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot - -logs: - level: DEBUG - format: "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}" - -cogs: - ignore: # add cogs to ignore here - -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: @@ -63,6 +26,25 @@ art: cloud: https://i.imgur.com/rc68c43.png trophy: https://i.imgur.com/dvIIr2G.png +cogs: + ignore: # add cogs to ignore here + +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 + emotes: guild_id: 1038051105642401812 emote_ids: @@ -95,6 +77,20 @@ emotes: Blank: 1119287267001905283 lost: 1119288454212243607 +images: + allowed_image_extensions: + - .jpg + - .png + birthday_gif_url: https://media1.tenor.com/m/NXvU9jbBUGMAAAAC/fireworks.gif + +info: + title: Luminara + author: wlinator + license: GNU General Public License v3.0 + version: "3.0.0-alpha" + repository_url: https://git.wlinator.org/Luminara/Lumi + invite_url: https://discord.com/oauth2/authorize?client_id=1038050427272429588&permissions=8&scope=bot + introductions: intro_guild_id: 719227135151046700 intro_channel_id: 973619250507972600 @@ -109,3 +105,7 @@ introductions: Likes & interests: likes & interests Dislikes: dislikes EXTRAS: "EXTRAS: job status, zodiac sign, hobbies, etc. Tell us about yourself!" + +logs: + level: DEBUG + format: "{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}" From 09ab8db099e7d765f97e1fb0f440df09d27010c5 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 15:29:30 -0400 Subject: [PATCH 032/102] Add currency service --- services/currency_service.py | 91 ++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 services/currency_service.py diff --git a/services/currency_service.py b/services/currency_service.py new file mode 100644 index 0000000..7b824c8 --- /dev/null +++ b/services/currency_service.py @@ -0,0 +1,91 @@ +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) + + self.balance = max(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 From bdbc5d0e7328a1bc393bbb96df30cef64e780fe0 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 15:29:41 -0400 Subject: [PATCH 033/102] Add daily service --- services/daily_service.py | 117 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 services/daily_service.py diff --git a/services/daily_service.py b/services/daily_service.py new file mode 100644 index 0000000..133c549 --- /dev/null +++ b/services/daily_service.py @@ -0,0 +1,117 @@ +from datetime import datetime, timedelta +from typing import List, Optional, Tuple + +import pytz + +from db import database +from lib.const 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 From 3c6bc711d637f4a2613942cfceea0501c0a11800 Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 28 Aug 2024 15:39:55 -0400 Subject: [PATCH 034/102] Add leaderboard command --- modules/levels/leaderboard.py | 40 ++++++++ poetry.lock | 13 ++- pyproject.toml | 1 + settings.yaml | 2 +- ui/views/leaderboard.py | 185 ++++++++++++++++++++++++++++++++++ 5 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 modules/levels/leaderboard.py create mode 100644 ui/views/leaderboard.py diff --git a/modules/levels/leaderboard.py b/modules/levels/leaderboard.py new file mode 100644 index 0000000..015cefd --- /dev/null +++ b/modules/levels/leaderboard.py @@ -0,0 +1,40 @@ +from typing import Optional +from discord.ext import commands +from discord import Embed, Guild +from lib.const import CONST +from ui.embeds import builder +from ui.views.leaderboard import LeaderboardCommandOptions, LeaderboardCommandView + + +class Leaderboard(commands.Cog): + def __init__(self, bot: commands.Bot) -> None: + self.bot: commands.Bot = bot + + @commands.hybrid_command( + name="leaderboard", + aliases=["lb"], + usage="leaderboard", + ) + async def leaderboard(self, ctx: commands.Context[commands.Bot]) -> None: + guild: Optional[Guild] = ctx.guild + if not guild: + return + + options: LeaderboardCommandOptions = LeaderboardCommandOptions() + view: LeaderboardCommandView = LeaderboardCommandView(ctx, options) + + embed: Embed = builder.create_embed( + theme="info", + user_name=ctx.author.name, + thumbnail_url=ctx.author.display_avatar.url, + hide_name_in_description=True, + ) + + icon: str = guild.icon.url if guild.icon else CONST.FLOWERS_ART + await view.populate_leaderboard("xp", embed, icon) + + await ctx.send(embed=embed, view=view) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Leaderboard(bot)) diff --git a/poetry.lock b/poetry.lock index dc96b1a..142432f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -838,6 +838,17 @@ nodeenv = ">=1.6.0" all = ["twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +[[package]] +name = "pytz" +version = "2024.1" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"}, + {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"}, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -1154,4 +1165,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "189f79c9e4eaaae2cfdde9b29509d1dd15030bdf8c82f972f5883d87c74365ae" +content-hash = "1bec4428d16328dd4054cda20654e446c54aa0463b79ef32ae4cfa10de7c0dfd" diff --git a/pyproject.toml b/pyproject.toml index 92ebed9..c86138b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ aiocache = "^0.12.2" aioconsole = "^0.7.1" psutil = "^6.0.0" dropbox = "^12.0.2" +pytz = "^2024.1" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/settings.yaml b/settings.yaml index e94fbd2..bff4566 100644 --- a/settings.yaml +++ b/settings.yaml @@ -14,7 +14,7 @@ art: money_coins: lumi_money_coins.png question: lumi_question.png streak: lumi_streak.png - streak_bronze: lumi_streak_bronze.png + streak_bronze: lumi_streak_bronze.png\ streak_gold: lumi_streak_gold.png streak_silver: lumi_streak_silver.png warning: lumi_warning.png diff --git a/ui/views/leaderboard.py b/ui/views/leaderboard.py new file mode 100644 index 0000000..2e9b261 --- /dev/null +++ b/ui/views/leaderboard.py @@ -0,0 +1,185 @@ +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 + + +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: commands.Context[commands.Bot], + options: LeaderboardCommandOptions, + ) -> None: + self.ctx = ctx + self.options = options + + super().__init__(timeout=180) + self.add_item(self.options) + + async def on_timeout(self) -> None: + self.stop() + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + if interaction.user and interaction.user != self.ctx.author: + embed = builder.create_embed( + theme="error", + user_name=interaction.user.name, + description=CONST.STRINGS["xp_lb_cant_use_dropdown"], + hide_name_in_description=True, + ) + 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 = builder.create_embed( + theme="success", + user_name=interaction.user.name, + thumbnail_url=CONST.FLOWERS_ART, + hide_name_in_description=True, + ) + + 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, + ) From f87f8a0b398bfdbc26291e6e512601fe04015955 Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 04:48:14 -0400 Subject: [PATCH 035/102] Huge refactor --- Dockerfile | 2 +- db/__init__.py | 0 db/database.py | 111 ++++++++++----------- db/migrations/__init__.py | 0 handlers/__init__.py | 0 handlers/error.py | 45 ++++----- lib/__init__.py | 0 lib/client.py | 15 +-- lib/const.py | 171 ++++++++++++++++---------------- lib/exceptions.py | 6 +- lib/loader.py | 18 ++-- locales/strings.en-US.json | 7 ++ main.py | 6 +- modules/__init__.py | 0 modules/admin/__init__.py | 0 modules/admin/sync.py | 10 +- modules/levels/__init__.py | 0 modules/levels/leaderboard.py | 17 ++-- modules/levels/level.py | 7 +- modules/misc/__init__.py | 0 modules/misc/avatar.py | 23 ++--- modules/misc/backup.py | 56 ++++++----- modules/misc/info.py | 12 ++- modules/misc/introduction.py | 172 ++++++++++++++++----------------- modules/misc/invite.py | 5 +- modules/misc/ping.py | 5 +- modules/misc/uptime.py | 15 +-- modules/misc/xkcd.py | 20 ++-- modules/moderation/__init__.py | 0 modules/moderation/slowmode.py | 75 ++++++++++++++ poetry.lock | 142 ++++++++++++++++++++++++++- pyproject.toml | 137 ++++++++++++++++++++++++-- services/__init__.py | 0 services/currency_service.py | 47 ++++----- services/daily_service.py | 29 ++---- services/xp_service.py | 37 ++++--- settings.yaml | 4 +- ui/__init__.py | 0 ui/embeds.py | 34 +++---- ui/views/__init__.py | 0 ui/views/blackjack.py | 15 ++- ui/views/introduction.py | 14 ++- ui/views/invite.py | 3 +- ui/views/leaderboard.py | 105 +++++++++++--------- wrappers/__init__.py | 0 wrappers/xkcd.py | 4 +- 46 files changed, 853 insertions(+), 516 deletions(-) create mode 100644 db/__init__.py create mode 100644 db/migrations/__init__.py create mode 100644 handlers/__init__.py create mode 100644 lib/__init__.py create mode 100644 modules/__init__.py create mode 100644 modules/admin/__init__.py create mode 100644 modules/levels/__init__.py create mode 100644 modules/misc/__init__.py create mode 100644 modules/moderation/__init__.py create mode 100644 modules/moderation/slowmode.py create mode 100644 services/__init__.py create mode 100644 ui/__init__.py create mode 100644 ui/views/__init__.py create mode 100644 wrappers/__init__.py diff --git a/Dockerfile b/Dockerfile index 8dc36ee..3d6ba34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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" ] \ No newline at end of file +CMD [ "poetry", "run", "python", "-O", "./main.py" ] \ No newline at end of file diff --git a/db/__init__.py b/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/db/database.py b/db/database.py index ba8f1e1..14868b1 100644 --- a/db/database.py +++ b/db/database.py @@ -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.") diff --git a/db/migrations/__init__.py b/db/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/handlers/__init__.py b/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/handlers/error.py b/handlers/error.py index 237857a..7494a28 100644 --- a/handlers/error.py +++ b/handlers/error.py @@ -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) diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/client.py b/lib/client.py index fedbe66..e4a1154 100644 --- a/lib/client.py +++ b/lib/client.py @@ -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: diff --git a/lib/const.py b/lib/const.py index d56cca6..d87ed42 100644 --- a/lib/const.py +++ b/lib/const.py @@ -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() diff --git a/lib/exceptions.py b/lib/exceptions.py index 47d5b1a..4dda849 100644 --- a/lib/exceptions.py +++ b/lib/exceptions.py @@ -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) diff --git a/lib/loader.py b/lib/loader.py index fdbed86..21d7e4f 100644 --- a/lib/loader.py +++ b/lib/loader.py @@ -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}") diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index 874839a..168a2c2 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -252,6 +252,13 @@ "ping_footer": "Latency: {0}ms", "ping_pong": "pong!", "ping_uptime": "I've been online since .", + "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", diff --git a/main.py b/main.py index a1714a0..ce8bc98 100644 --- a/main.py +++ b/main.py @@ -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) diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/admin/__init__.py b/modules/admin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/admin/sync.py b/modules/admin/sync.py index a994565..91ec177 100644 --- a/modules/admin/sync.py +++ b/modules/admin/sync.py @@ -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"], diff --git a/modules/levels/__init__.py b/modules/levels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/levels/leaderboard.py b/modules/levels/leaderboard.py index 015cefd..eec0f3f 100644 --- a/modules/levels/leaderboard.py +++ b/modules/levels/leaderboard.py @@ -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, ) diff --git a/modules/levels/level.py b/modules/levels/level.py index 6937f7c..8eb19be 100644 --- a/modules/levels/level.py +++ b/modules/levels/level.py @@ -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), diff --git a/modules/misc/__init__.py b/modules/misc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/misc/avatar.py b/modules/misc/avatar.py index 160e52a..6c94f94 100644 --- a/modules/misc/avatar.py +++ b/modules/misc/avatar.py @@ -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) diff --git a/modules/misc/backup.py b/modules/misc/backup.py index a80ff56..fa3545f 100644 --- a/modules/misc/backup.py +++ b/modules/misc/backup.py @@ -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: diff --git a/modules/misc/info.py b/modules/misc/info.py index e84980b..1d210ea 100644 --- a/modules/misc/info.py +++ b/modules/misc/info.py @@ -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}", diff --git a/modules/misc/introduction.py b/modules/misc/introduction.py index b1aa928..53254a6 100644 --- a/modules/misc/introduction.py +++ b/modules/misc/introduction.py @@ -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: diff --git a/modules/misc/invite.py b/modules/misc/invite.py index 8f3b4e8..c7fe509 100644 --- a/modules/misc/invite.py +++ b/modules/misc/invite.py @@ -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"], diff --git a/modules/misc/ping.py b/modules/misc/ping.py index e89e2f1..8b51b30 100644 --- a/modules/misc/ping.py +++ b/modules/misc/ping.py @@ -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"], diff --git a/modules/misc/uptime.py b/modules/misc/uptime.py index 0923d63..acf84e9 100644 --- a/modules/misc/uptime.py +++ b/modules/misc/uptime.py @@ -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"], diff --git a/modules/misc/xkcd.py b/modules/misc/xkcd.py index 24095b5..282d947 100644 --- a/modules/misc/xkcd.py +++ b/modules/misc/xkcd.py @@ -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: diff --git a/modules/moderation/__init__.py b/modules/moderation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/moderation/slowmode.py b/modules/moderation/slowmode.py new file mode 100644 index 0000000..e213e30 --- /dev/null +++ b/modules/moderation/slowmode.py @@ -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 ", + ) + @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)) diff --git a/poetry.lock b/poetry.lock index 142432f..c61b3d2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index c86138b..810a6b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = "." diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/currency_service.py b/services/currency_service.py index 7b824c8..de71790 100644 --- a/services/currency_service.py +++ b/services/currency_service.py @@ -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 diff --git a/services/daily_service.py b/services/daily_service.py index 133c549..1420a21 100644 --- a/services/daily_service.py +++ b/services/daily_service.py @@ -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 diff --git a/services/xp_service.py b/services/xp_service.py index 8f5f10d..14aa7c0 100644 --- a/services/xp_service.py +++ b/services/xp_service.py @@ -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. diff --git a/settings.yaml b/settings.yaml index bff4566..d7bcf20 100644 --- a/settings.yaml +++ b/settings.yaml @@ -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? diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/embeds.py b/ui/embeds.py index bd632c8..e6be2d4 100644 --- a/ui/embeds.py +++ b/ui/embeds.py @@ -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: diff --git a/ui/views/__init__.py b/ui/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/views/blackjack.py b/ui/views/blackjack.py index 756a6ce..394f948 100644 --- a/ui/views/blackjack.py +++ b/ui/views/blackjack.py @@ -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() diff --git a/ui/views/introduction.py b/ui/views/introduction.py index 22cb10d..7e75c68 100644 --- a/ui/views/introduction.py +++ b/ui/views/introduction.py @@ -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() diff --git a/ui/views/invite.py b/ui/views/invite.py index 7c70095..a5d1219 100644 --- a/ui/views/invite.py +++ b/ui/views/invite.py @@ -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, diff --git a/ui/views/leaderboard.py b/ui/views/leaderboard.py index 2e9b261..bb131f1 100644 --- a/ui/views/leaderboard.py +++ b/ui/views/leaderboard.py @@ -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, ) diff --git a/wrappers/__init__.py b/wrappers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/wrappers/xkcd.py b/wrappers/xkcd.py index e7f5b30..b8cfc63 100644 --- a/wrappers/xkcd.py +++ b/wrappers/xkcd.py @@ -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) From bf05ae118cfa91ac504bced45edc322ac51b2366 Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 05:19:09 -0400 Subject: [PATCH 036/102] Add format.py and format slowmode durations --- lib/format.py | 139 +++++++++++++++++++++++++++++++++ main.py | 6 +- modules/moderation/slowmode.py | 10 ++- poetry.lock | 13 ++- pyproject.toml | 1 + services/config_service.py | 135 ++++++++++++++++++++++++++++++++ 6 files changed, 297 insertions(+), 7 deletions(-) create mode 100644 lib/format.py create mode 100644 services/config_service.py diff --git a/lib/format.py b/lib/format.py new file mode 100644 index 0000000..c6b8d7e --- /dev/null +++ b/lib/format.py @@ -0,0 +1,139 @@ +import textwrap + +import discord +from discord.ext import commands +from pytimeparse import parse # type: ignore + +from lib import exceptions +from lib.const import CONST +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[commands.Bot]) -> 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[commands.Bot]) -> 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.app_commands.CommandInvokeError, 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: int = parse(duration) # type: ignore + + if isinstance(parsed_duration, int): + return parsed_duration + + raise exceptions.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" + if hours > 0: + return f"{hours}h{minutes}m" if minutes > 0 else f"{hours}h" + + return f"{minutes}m" diff --git a/main.py b/main.py index ce8bc98..ed4d50f 100644 --- a/main.py +++ b/main.py @@ -7,13 +7,15 @@ from loguru import logger from lib.client import Luminara from lib.const import CONST +from services.config_service import GuildConfig logger.remove() logger.add(sys.stdout, format=CONST.LOG_FORMAT, colorize=True, level=CONST.LOG_LEVEL) -async def get_prefix(bot, message): - return commands.when_mentioned_or(".")(bot, message) +async def get_prefix(bot: Luminara, message: discord.Message) -> list[str]: + extras = GuildConfig.get_prefix(message) + return commands.when_mentioned_or(*extras)(bot, message) async def main() -> None: diff --git a/modules/moderation/slowmode.py b/modules/moderation/slowmode.py index e213e30..a257b82 100644 --- a/modules/moderation/slowmode.py +++ b/modules/moderation/slowmode.py @@ -4,6 +4,8 @@ import discord from discord.ext import commands from lib.const import CONST +from lib.exceptions import LumiException +from lib.format import format_duration_to_seconds from ui.embeds import Builder @@ -32,11 +34,11 @@ class Slowmode(commands.Cog): try: channel = await commands.TextChannelConverter().convert(ctx, arg) except commands.BadArgument: - with contextlib.suppress(ValueError): - duration = int(arg) + with contextlib.suppress(LumiException): + duration = format_duration_to_seconds(arg) else: - with contextlib.suppress(ValueError): - duration = int(arg) + with contextlib.suppress(LumiException): + duration = format_duration_to_seconds(arg) if not channel: await ctx.send(CONST.STRINGS["slowmode_channel_not_found"]) diff --git a/poetry.lock b/poetry.lock index c61b3d2..40b5a73 100644 --- a/poetry.lock +++ b/poetry.lock @@ -972,6 +972,17 @@ nodeenv = ">=1.6.0" all = ["twine (>=3.4.1)"] dev = ["twine (>=3.4.1)"] +[[package]] +name = "pytimeparse" +version = "1.1.8" +description = "Time expression parser" +optional = false +python-versions = "*" +files = [ + {file = "pytimeparse-1.1.8-py2.py3-none-any.whl", hash = "sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd"}, + {file = "pytimeparse-1.1.8.tar.gz", hash = "sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a"}, +] + [[package]] name = "pytz" version = "2024.1" @@ -1299,4 +1310,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "e6ab702cf6efc2ec25a9c033029869f9c4c3631e2e6063c4241153ea8c1f8e79" +content-hash = "4a7a75036f4de7e0126a8f6b058eb3deb52f710becf44e4da6bac4a0ea0a1a2f" diff --git a/pyproject.toml b/pyproject.toml index 810a6b9..eab4b62 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ pyyaml = "^6.0.2" ruff = "^0.6.2" typing-extensions = "^4.12.2" pydantic = "^2.8.2" +pytimeparse = "^1.1.8" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/services/config_service.py b/services/config_service.py new file mode 100644 index 0000000..172224a --- /dev/null +++ b/services/config_service.py @@ -0,0 +1,135 @@ +from typing import Any + +from db import database + + +class GuildConfig: + def __init__(self, guild_id: int) -> None: + self.guild_id: int = guild_id + self.birthday_channel_id: int | None = None + self.command_channel_id: int | None = None + self.intro_channel_id: int | None = None + self.welcome_channel_id: int | None = None + self.welcome_message: str | None = None + self.boost_channel_id: int | None = None + self.boost_message: str | None = None + self.boost_image_url: str | None = None + self.level_channel_id: int | None = None + self.level_message: str | None = None + self.level_message_type: int = 1 + + self.fetch_or_create_config() + + def fetch_or_create_config(self) -> None: + """ + Gets a Guild Config from the database or inserts a new row if it doesn't exist yet. + """ + query: str = """ + 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: str) -> None: + result: tuple[Any, ...] = database.select_query(query, (self.guild_id,))[0] + ( + 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, + ) = result + + def push(self) -> None: + query: str = """ + 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: Any) -> str: + """ + Gets the prefix from a given guild. + This function is done as static method to make the prefix fetch process faster. + """ + query: str = """ + SELECT prefix + FROM guild_config + WHERE guild_id = %s + """ + + prefix: str | None = 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: int) -> str: + query: str = """ + SELECT prefix + FROM guild_config + WHERE guild_id = %s + """ + + return database.select_query_one(query, (guild_id,)) or "." + + @staticmethod + def set_prefix(guild_id: int, prefix: str) -> None: + """ + Sets the prefix for a given guild. + """ + query: str = """ + UPDATE guild_config + SET prefix = %s + WHERE guild_id = %s; + """ + + database.execute_query(query, (prefix, guild_id)) From 7edf3600a86c51b3670aa62f7249b0e2b76d61e4 Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 05:31:38 -0400 Subject: [PATCH 037/102] Make slowmode command more flexible --- lib/format.py | 5 +++- locales/strings.en-US.json | 6 ++--- modules/moderation/slowmode.py | 45 ++++++++++------------------------ 3 files changed, 19 insertions(+), 37 deletions(-) diff --git a/lib/format.py b/lib/format.py index c6b8d7e..c505c1b 100644 --- a/lib/format.py +++ b/lib/format.py @@ -109,8 +109,11 @@ def get_invoked_name(ctx: commands.Context[commands.Bot]) -> str | None: def format_duration_to_seconds(duration: str) -> int: """ - Formats a duration in seconds to a human-readable string. + Converts a duration string to seconds. If the input is just an integer, it returns that integer as seconds. """ + if duration.isdigit(): + return int(duration) + parsed_duration: int = parse(duration) # type: ignore if isinstance(parsed_duration, int): diff --git a/locales/strings.en-US.json b/locales/strings.en-US.json index 168a2c2..7d0c2c7 100644 --- a/locales/strings.en-US.json +++ b/locales/strings.en-US.json @@ -252,13 +252,11 @@ "ping_footer": "Latency: {0}ms", "ping_pong": "pong!", "ping_uptime": "I've been online since .", - "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_current_value": "The current slowmode for {0} is **{1}s**.", "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}.", + "slowmode_success": "Slowmode set to **{0}s** 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", diff --git a/modules/moderation/slowmode.py b/modules/moderation/slowmode.py index a257b82..4610378 100644 --- a/modules/moderation/slowmode.py +++ b/modules/moderation/slowmode.py @@ -6,7 +6,6 @@ from discord.ext import commands from lib.const import CONST from lib.exceptions import LumiException from lib.format import format_duration_to_seconds -from ui.embeds import Builder class Slowmode(commands.Cog): @@ -22,55 +21,37 @@ class Slowmode(commands.Cog): async def slowmode( self, ctx: commands.Context[commands.Bot], - arg1: str, - arg2: str, + arg1: str | None = None, + arg2: str | None = None, ) -> None: - # define actual channel & duration - channel: discord.TextChannel | None = None - duration: int | None = None + channel, duration = None, None for arg in (arg1, arg2): - if not channel: - try: + if not channel and arg: + with contextlib.suppress(commands.BadArgument): channel = await commands.TextChannelConverter().convert(ctx, arg) - except commands.BadArgument: - with contextlib.suppress(LumiException): - duration = format_duration_to_seconds(arg) - else: + continue + if arg: with contextlib.suppress(LumiException): duration = format_duration_to_seconds(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"]) + current_slowmode = channel.slowmode_delay + await ctx.send(CONST.STRINGS["slowmode_current_value"].format(channel.mention, current_slowmode)) 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.") + await ctx.send(CONST.STRINGS["slowmode_duration_error"]) 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) + await ctx.send(CONST.STRINGS["slowmode_success"].format(duration, channel.mention)) + except discord.Forbidden as error: + raise LumiException(CONST.STRINGS["slowmode_forbidden"]) from error async def setup(bot: commands.Bot) -> None: From 5266877eb7d49cf567d40b7c5306372e985ef1e7 Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 05:40:35 -0400 Subject: [PATCH 038/102] Add slowmode app command --- modules/moderation/slowmode.py | 37 +++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/modules/moderation/slowmode.py b/modules/moderation/slowmode.py index 4610378..f7b7511 100644 --- a/modules/moderation/slowmode.py +++ b/modules/moderation/slowmode.py @@ -1,6 +1,7 @@ import contextlib import discord +from discord import app_commands from discord.ext import commands from lib.const import CONST @@ -44,7 +45,7 @@ class Slowmode(commands.Cog): return if duration < 0 or duration > 21600: # 21600 seconds = 6 hours (Discord's max slowmode) - await ctx.send(CONST.STRINGS["slowmode_duration_error"]) + await ctx.send(CONST.STRINGS["slowmode_invalid_duration"]) return try: @@ -53,6 +54,40 @@ class Slowmode(commands.Cog): except discord.Forbidden as error: raise LumiException(CONST.STRINGS["slowmode_forbidden"]) from error + @app_commands.command( + name="slowmode", + description="Set or view the slowmode for a channel", + ) + @app_commands.checks.has_permissions(manage_channels=True) + async def slowmode_slash( + self, + interaction: discord.Interaction, + channel: discord.TextChannel, + duration: str | None = None, + ) -> None: + if duration is None: + current_slowmode = channel.slowmode_delay + await interaction.response.send_message( + CONST.STRINGS["slowmode_current_value"].format(channel.mention, current_slowmode), + ) + return + + try: + seconds = format_duration_to_seconds(duration) + except LumiException: + await interaction.response.send_message(CONST.STRINGS["slowmode_invalid_duration"], ephemeral=True) + return + + if seconds < 0 or seconds > 21600: # 21600 seconds = 6 hours (Discord's max slowmode) + await interaction.response.send_message(CONST.STRINGS["slowmode_invalid_duration"], ephemeral=True) + return + + try: + await channel.edit(slowmode_delay=seconds) + await interaction.response.send_message(CONST.STRINGS["slowmode_success"].format(seconds, channel.mention)) + except discord.Forbidden: + await interaction.response.send_message(CONST.STRINGS["slowmode_forbidden"], ephemeral=True) + async def setup(bot: commands.Bot) -> None: await bot.add_cog(Slowmode(bot)) From 31c80b9b000c117fe029a1fb8c18e58f8ec25773 Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 05:43:33 -0400 Subject: [PATCH 039/102] chore: Refactor slowmode command for flexibility --- modules/moderation/slowmode.py | 82 ++++++++++++++++++---------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/modules/moderation/slowmode.py b/modules/moderation/slowmode.py index f7b7511..2e56469 100644 --- a/modules/moderation/slowmode.py +++ b/modules/moderation/slowmode.py @@ -13,6 +13,46 @@ class Slowmode(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot + async def _set_slowmode( + self, + ctx: commands.Context[commands.Bot] | discord.Interaction, + channel: discord.TextChannel, + duration: str | None, + ) -> None: + if duration is None: + await self._send_response( + ctx, + CONST.STRINGS["slowmode_current_value"].format(channel.mention, channel.slowmode_delay), + ) + return + + try: + seconds = format_duration_to_seconds(duration) + except LumiException: + await self._send_response(ctx, CONST.STRINGS["slowmode_invalid_duration"], ephemeral=True) + return + + if not 0 <= seconds <= 21600: # 21600 seconds = 6 hours (Discord's max slowmode) + await self._send_response(ctx, CONST.STRINGS["slowmode_invalid_duration"], ephemeral=True) + return + + try: + await channel.edit(slowmode_delay=seconds) + await self._send_response(ctx, CONST.STRINGS["slowmode_success"].format(seconds, channel.mention)) + except discord.Forbidden: + await self._send_response(ctx, CONST.STRINGS["slowmode_forbidden"], ephemeral=True) + + async def _send_response( + self, + ctx: commands.Context[commands.Bot] | discord.Interaction, + content: str, + ephemeral: bool = False, + ) -> None: + if isinstance(ctx, commands.Context): + await ctx.send(content) + else: + await ctx.response.send_message(content, ephemeral=ephemeral) + @commands.command( name="slowmode", aliases=["sm"], @@ -33,26 +73,13 @@ class Slowmode(commands.Cog): channel = await commands.TextChannelConverter().convert(ctx, arg) continue if arg: - with contextlib.suppress(LumiException): - duration = format_duration_to_seconds(arg) + duration = arg + if not channel: await ctx.send(CONST.STRINGS["slowmode_channel_not_found"]) return - if duration is None: - current_slowmode = channel.slowmode_delay - await ctx.send(CONST.STRINGS["slowmode_current_value"].format(channel.mention, current_slowmode)) - return - - if duration < 0 or duration > 21600: # 21600 seconds = 6 hours (Discord's max slowmode) - await ctx.send(CONST.STRINGS["slowmode_invalid_duration"]) - return - - try: - await channel.edit(slowmode_delay=duration) - await ctx.send(CONST.STRINGS["slowmode_success"].format(duration, channel.mention)) - except discord.Forbidden as error: - raise LumiException(CONST.STRINGS["slowmode_forbidden"]) from error + await self._set_slowmode(ctx, channel, duration) @app_commands.command( name="slowmode", @@ -65,28 +92,7 @@ class Slowmode(commands.Cog): channel: discord.TextChannel, duration: str | None = None, ) -> None: - if duration is None: - current_slowmode = channel.slowmode_delay - await interaction.response.send_message( - CONST.STRINGS["slowmode_current_value"].format(channel.mention, current_slowmode), - ) - return - - try: - seconds = format_duration_to_seconds(duration) - except LumiException: - await interaction.response.send_message(CONST.STRINGS["slowmode_invalid_duration"], ephemeral=True) - return - - if seconds < 0 or seconds > 21600: # 21600 seconds = 6 hours (Discord's max slowmode) - await interaction.response.send_message(CONST.STRINGS["slowmode_invalid_duration"], ephemeral=True) - return - - try: - await channel.edit(slowmode_delay=seconds) - await interaction.response.send_message(CONST.STRINGS["slowmode_success"].format(seconds, channel.mention)) - except discord.Forbidden: - await interaction.response.send_message(CONST.STRINGS["slowmode_forbidden"], ephemeral=True) + await self._set_slowmode(interaction, channel, duration) async def setup(bot: commands.Bot) -> None: From d68368a30e97159e927a9267f30109b2704de857 Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 06:21:55 -0400 Subject: [PATCH 040/102] Add mod module and warn command --- lib/actionable.py | 30 +++++++ lib/case_handler.py | 152 +++++++++++++++++++++++++++++++++ lib/format.py | 10 +-- modules/moderation/warn.py | 61 ++++++++++++++ services/case_service.py | 168 +++++++++++++++++++++++++++++++++++++ services/modlog_service.py | 30 +++++++ ui/cases.py | 107 +++++++++++++++++++++++ 7 files changed, 553 insertions(+), 5 deletions(-) create mode 100644 lib/actionable.py create mode 100644 lib/case_handler.py create mode 100644 modules/moderation/warn.py create mode 100644 services/case_service.py create mode 100644 services/modlog_service.py create mode 100644 ui/cases.py diff --git a/lib/actionable.py b/lib/actionable.py new file mode 100644 index 0000000..2a563b2 --- /dev/null +++ b/lib/actionable.py @@ -0,0 +1,30 @@ +import discord + +from lib.const import CONST +from lib.exceptions 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"]) diff --git a/lib/case_handler.py b/lib/case_handler.py new file mode 100644 index 0000000..4f39b54 --- /dev/null +++ b/lib/case_handler.py @@ -0,0 +1,152 @@ +import discord +from discord.ext import commands +from loguru import logger + +from lib.exceptions import LumiException +from services.case_service import CaseService +from services.modlog_service import ModLogService +from ui.cases import create_case_embed + +case_service = CaseService() +modlog_service = ModLogService() + + +async def create_case( + ctx: commands.Context[commands.Bot], + target: discord.User, + action_type: str, + reason: str | None = None, + duration: int | None = None, + expires_at: str | None = 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. + """ + + if not ctx.guild: + raise LumiException + + 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 commands.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: commands.Context[commands.Bot], + 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: + msg = f"Case {case_number} not found in guild {guild_id}" + raise ValueError(msg) + + 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 commands.TextChannelConverter().convert( + ctx, + str(mod_log_channel_id), + ) + message = await mod_log_channel.fetch_message(modlog_message_id) + target = await commands.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 diff --git a/lib/format.py b/lib/format.py index c505c1b..153f30a 100644 --- a/lib/format.py +++ b/lib/format.py @@ -114,12 +114,12 @@ def format_duration_to_seconds(duration: str) -> int: if duration.isdigit(): return int(duration) - parsed_duration: int = parse(duration) # type: ignore + try: + parsed_duration: int = parse(duration) # type: ignore + return max(0, parsed_duration) - if isinstance(parsed_duration, int): - return parsed_duration - - raise exceptions.LumiException(CONST.STRINGS["error_invalid_duration"].format(duration)) + except Exception as e: + raise exceptions.LumiException(CONST.STRINGS["error_invalid_duration"].format(duration)) from e def format_seconds_to_duration_string(seconds: int) -> str: diff --git a/modules/moderation/warn.py b/modules/moderation/warn.py new file mode 100644 index 0000000..1d864e1 --- /dev/null +++ b/modules/moderation/warn.py @@ -0,0 +1,61 @@ +import asyncio +from typing import cast + +import discord +from discord.ext import commands + +from lib.actionable import async_actionable +from lib.case_handler import create_case +from lib.const import CONST +from lib.exceptions import LumiException +from ui.embeds import Builder + + +class Warn(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command(name="warn", description="Warn a user") + @commands.has_permissions(manage_messages=True) + async def warn(self, ctx: commands.Context[commands.Bot], target: discord.Member, *, reason: str | None = None): + if not ctx.guild or not ctx.author or not ctx.bot.user: + raise LumiException + + bot_member = await commands.MemberConverter().convert(ctx, str(ctx.bot.user)) + await async_actionable(target, cast(discord.Member, ctx.author), bot_member) + + output_reason = reason or CONST.STRINGS["mod_no_reason"] + + dm_task = target.send( + embed=Builder.create_embed( + user_name=target.name, + author_text=CONST.STRINGS["mod_warned_author"], + description=CONST.STRINGS["mod_warn_dm"].format( + target.name, + ctx.guild.name, + output_reason, + ), + hide_name_in_description=True, + ), + ) + + respond_task = ctx.send( + embed=Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["mod_warned_author"], + description=CONST.STRINGS["mod_warned_user"].format(target.name), + ), + ) + + create_case_task = create_case(ctx, cast(discord.User, target), "WARN", reason) + + await asyncio.gather( + dm_task, + respond_task, + create_case_task, + return_exceptions=True, + ) + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Warn(bot)) diff --git a/services/case_service.py b/services/case_service.py new file mode 100644 index 0000000..2a8caf7 --- /dev/null +++ b/services/case_service.py @@ -0,0 +1,168 @@ +from typing import Any + +from db.database import execute_query, select_query_dict, select_query_one + + +class CaseService: + def __init__(self) -> None: + pass + + def create_case( + self, + guild_id: int, + target_id: int, + moderator_id: int, + action_type: str, + reason: str | None = None, + duration: int | None = None, + expires_at: str | None = None, + modlog_message_id: int | None = 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: int | None = select_query_one(query, (guild_id,)) + + if case_number is None: + msg: str = "Failed to retrieve the next case number." + raise ValueError(msg) + + # 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: int, case_number: int) -> None: + query: str = """ + 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: str | None = None, + ) -> bool: + query: str = """ + 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: int, case_number: int, changes: dict[str, Any]) -> None: + set_clause: str = ", ".join([f"{key} = %s" for key in changes]) + query: str = 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_cases(self, query: str, params: tuple[Any, ...]) -> list[dict[str, Any]]: + results: list[dict[str, Any]] = select_query_dict(query, params) + return results + + def _fetch_single_case(self, query: str, params: tuple[Any, ...]) -> dict[str, Any] | None: + result = self._fetch_cases(query, params) + return result[0] if result else None + + def fetch_case_by_id(self, case_id: int) -> dict[str, Any] | None: + query: str = """ + SELECT * FROM cases + WHERE id = %s + LIMIT 1 + """ + return self._fetch_single_case(query, (case_id,)) + + def fetch_case_by_guild_and_number( + self, + guild_id: int, + case_number: int, + ) -> dict[str, Any] | None: + query: str = """ + SELECT * FROM cases + WHERE guild_id = %s AND case_number = %s + ORDER BY case_number DESC + LIMIT 1 + """ + return self._fetch_single_case(query, (guild_id, case_number)) + + 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 + """ + return self._fetch_cases(query, (guild_id,)) + + 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 + """ + return self._fetch_cases(query, (guild_id, target_id)) + + 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 + """ + return self._fetch_cases(query, (guild_id, moderator_id)) + + 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 + """ + return self._fetch_cases(query, (guild_id, action_type.upper())) diff --git a/services/modlog_service.py b/services/modlog_service.py new file mode 100644 index 0000000..00e956f --- /dev/null +++ b/services/modlog_service.py @@ -0,0 +1,30 @@ +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) -> int | None: + 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 diff --git a/ui/cases.py b/ui/cases.py new file mode 100644 index 0000000..e3fe63a --- /dev/null +++ b/ui/cases.py @@ -0,0 +1,107 @@ +import datetime +from typing import Any + +import discord +from discord.ext import commands + +from lib.const import CONST +from lib.format import format_case_number, format_seconds_to_duration_string +from ui.embeds import Builder + + +def create_case_embed( + ctx: commands.Context[commands.Bot], + target: discord.User, + case_number: int, + action_type: str, + reason: str | None, + timestamp: datetime.datetime | None = None, + duration: int | None = None, +) -> discord.Embed: + embed: discord.Embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["case_new_case_author"], + thumbnail_url=target.display_avatar.url, + hide_name_in_description=True, + 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: commands.Context[commands.Bot], + cases: list[dict[str, Any]], + author_text: str, +) -> discord.Embed: + embed: discord.Embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=author_text, + hide_name_in_description=True, + ) + + 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"" + 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 From 84759ab29407d84b8f4190441eb84ef8db08e3fd Mon Sep 17 00:00:00 2001 From: wlinator Date: Thu, 29 Aug 2024 07:03:25 -0400 Subject: [PATCH 041/102] Add case commands --- modules/moderation/cases.py | 168 ++++++ poetry.lock | 16 +- pyproject.toml | 1 + stubs/reactionmenu/__init__.pyi | 21 + stubs/reactionmenu/abc.pyi | 816 ++++++++++++++++++++++++++++++ stubs/reactionmenu/buttons.pyi | 549 ++++++++++++++++++++ stubs/reactionmenu/core.pyi | 332 ++++++++++++ stubs/reactionmenu/decorators.pyi | 31 ++ stubs/reactionmenu/errors.pyi | 122 +++++ stubs/reactionmenu/views_menu.pyi | 714 ++++++++++++++++++++++++++ 10 files changed, 2769 insertions(+), 1 deletion(-) create mode 100644 modules/moderation/cases.py create mode 100644 stubs/reactionmenu/__init__.pyi create mode 100644 stubs/reactionmenu/abc.pyi create mode 100644 stubs/reactionmenu/buttons.pyi create mode 100644 stubs/reactionmenu/core.pyi create mode 100644 stubs/reactionmenu/decorators.pyi create mode 100644 stubs/reactionmenu/errors.pyi create mode 100644 stubs/reactionmenu/views_menu.pyi diff --git a/modules/moderation/cases.py b/modules/moderation/cases.py new file mode 100644 index 0000000..7d79946 --- /dev/null +++ b/modules/moderation/cases.py @@ -0,0 +1,168 @@ +import asyncio + +import discord +from discord.ext import commands +from reactionmenu import ViewButton, ViewMenu + +from lib.case_handler import edit_case_modlog +from lib.const import CONST +from lib.exceptions import LumiException +from lib.format import format_case_number +from services.case_service import CaseService +from ui.cases import ( + create_case_embed, + create_case_list_embed, +) +from ui.embeds import Builder + +case_service = CaseService() + + +class Cases(commands.Cog): + def __init__(self, bot: commands.Bot): + self.bot = bot + + @commands.hybrid_command(name="case", aliases=["c", "ca"], description="View a specific case by number") + @commands.has_permissions(manage_messages=True) + async def view_case_by_number(self, ctx: commands.Context[commands.Bot], case_number: int) -> None: + guild_id = ctx.guild.id if ctx.guild else 0 + case = case_service.fetch_case_by_guild_and_number(guild_id, case_number) + + if not case: + embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["error_no_case_found_author"], + description=CONST.STRINGS["error_no_case_found_description"], + ) + await ctx.send(embed=embed) + return + + target = await commands.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.send(embed=embed) + + @commands.hybrid_command(name="cases", description="View all cases in the guild") + @commands.has_permissions(manage_messages=True) + async def view_all_cases_in_guild(self, ctx: commands.Context[commands.Bot]) -> None: + if not ctx.guild: + raise LumiException(CONST.STRINGS["error_not_in_guild"]) + + guild_id = ctx.guild.id + cases = case_service.fetch_cases_by_guild(guild_id) + + if not cases: + embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["case_guild_no_cases_author"], + description=CONST.STRINGS["case_guild_no_cases"], + ) + await ctx.send(embed=embed) + return + + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed, all_can_click=True, delete_on_timeout=True) + + 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"], + ) + menu.add_page(embed) + + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_FIRST_PAGE, emoji="⏮️"), + ) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_PREVIOUS_PAGE, emoji="⏪"), + ) + menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_NEXT_PAGE, emoji="⏩")) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_LAST_PAGE, emoji="⏭️"), + ) + + await menu.start() + + @commands.hybrid_command(name="modcases", aliases=["mc", "modc"], description="View all cases in the guild") + @commands.has_permissions(manage_messages=True) + async def view_all_cases_by_mod(self, ctx: commands.Context[commands.Bot], moderator: discord.Member) -> None: + if not ctx.guild: + raise LumiException(CONST.STRINGS["error_not_in_guild"]) + + guild_id = ctx.guild.id + cases = case_service.fetch_cases_by_moderator(guild_id, moderator.id) + + menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed, all_can_click=True, delete_on_timeout=True) + + if not cases: + embed = Builder.create_embed( + user_name=ctx.author.name, + author_text=CONST.STRINGS["case_mod_no_cases_author"], + description=CONST.STRINGS["case_mod_no_cases"], + ) + await ctx.send(embed=embed) + return + + 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), + ) + menu.add_page(embed) + + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_FIRST_PAGE, emoji="⏮️"), + ) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_PREVIOUS_PAGE, emoji="⏪"), + ) + menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_NEXT_PAGE, emoji="⏩")) + menu.add_button( + ViewButton(style=discord.ButtonStyle.secondary, custom_id=ViewButton.ID_GO_TO_LAST_PAGE, emoji="⏭️"), + ) + + await menu.start() + + @commands.hybrid_command(name="editcase", aliases=["ec"], description="Edit the reason for a case") + @commands.has_permissions(manage_messages=True) + async def edit_case_reason(self, ctx: commands.Context[commands.Bot], case_number: int, *, new_reason: str): + if not ctx.guild: + raise LumiException(CONST.STRINGS["error_not_in_guild"]) + + guild_id = ctx.guild.id + + case_service.edit_case_reason( + guild_id, + case_number, + new_reason, + ) + + embed = Builder.create_embed( + user_name=ctx.author.name, + 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.send(embed=embed), + edit_case_modlog(ctx, guild_id, case_number, new_reason), + ) + + await update_tasks() + + +async def setup(bot: commands.Bot) -> None: + await bot.add_cog(Cases(bot)) diff --git a/poetry.lock b/poetry.lock index 40b5a73..14b513e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1056,6 +1056,20 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "reactionmenu" +version = "3.1.7" +description = "A library to create a discord.py 2.0+ paginator. Supports pagination with buttons, reactions, and category selection using selects." +optional = false +python-versions = ">=3.8" +files = [ + {file = "reactionmenu-3.1.7-py3-none-any.whl", hash = "sha256:51a217c920382dfecbb2f05d60bd20b79ed9895e9f5663f6c0edb75e806f863a"}, + {file = "reactionmenu-3.1.7.tar.gz", hash = "sha256:10da3c1966de2b6264fcdf72537348923c5e151501644375c25f430bfd870463"}, +] + +[package.dependencies] +"discord.py" = ">=2.0.0" + [[package]] name = "requests" version = "2.32.3" @@ -1310,4 +1324,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "4a7a75036f4de7e0126a8f6b058eb3deb52f710becf44e4da6bac4a0ea0a1a2f" +content-hash = "122c3bd137956c87143ead4ae71a7800669a9f379a62c93da21a7ef61231c0f5" diff --git a/pyproject.toml b/pyproject.toml index eab4b62..4ff0adc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ ruff = "^0.6.2" typing-extensions = "^4.12.2" pydantic = "^2.8.2" pytimeparse = "^1.1.8" +reactionmenu = "^3.1.7" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/stubs/reactionmenu/__init__.pyi b/stubs/reactionmenu/__init__.pyi new file mode 100644 index 0000000..f7a090f --- /dev/null +++ b/stubs/reactionmenu/__init__.pyi @@ -0,0 +1,21 @@ +""" +This type stub file was generated by pyright. +""" + +from .buttons import ReactionButton, ViewButton +from .core import ReactionMenu +from .views_menu import ViewMenu, ViewSelect +from .abc import Page + +""" +reactionmenu • discord pagination +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A library to create a discord.py 2.0+ paginator. Supports pagination with buttons, reactions, and category selection using selects. + +:copyright: (c) 2021-present @defxult +:license: MIT + +""" +__source__ = ... +__all__ = ('ReactionMenu', 'ReactionButton', 'ViewMenu', 'ViewButton', 'ViewSelect', 'Page') diff --git a/stubs/reactionmenu/abc.pyi b/stubs/reactionmenu/abc.pyi new file mode 100644 index 0000000..69bdd1a --- /dev/null +++ b/stubs/reactionmenu/abc.pyi @@ -0,0 +1,816 @@ +""" +This type stub file was generated by pyright. +""" + +import abc +import discord +from typing import Any, Callable, ClassVar, Final, Generic, List, Literal, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, TypeVar, Union, overload +from datetime import datetime +from typing_extensions import Self +from collections.abc import Sequence +from enum import Enum +from discord.ext.commands import Context +from discord.utils import MISSING +from .decorators import ensure_not_primed +from .errors import * + +""" +MIT License + +Copyright (c) 2021-present @defxult + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +if TYPE_CHECKING: + ... +_DYNAMIC_EMBED_LIMIT: Final[int] = ... +_DEFAULT_STYLE: Final[str] = ... +DEFAULT_BUTTONS = ... +DEFAULT = MISSING +GB = TypeVar('GB', bound='_BaseButton') +M = TypeVar('M', bound='_BaseMenu') +class Page: + """Represents a single "page" in the pagination process + + .. added:: v3.1.0 + """ + __slots__ = ... + def __init__(self, *, content: Optional[str] = ..., embed: Optional[discord.Embed] = ..., files: Optional[List[discord.File]] = ...) -> None: + ... + + def __repr__(self) -> str: + ... + + @staticmethod + def from_embeds(embeds: Sequence[discord.Embed]) -> List[Page]: + """|static method| + + Converts a sequence of embeds into a list of :class:`Page` + """ + ... + + + +class PaginationEmojis: + """A set of basic emojis for your convenience to use for your buttons emoji + - ◀️ as `BACK_BUTTON` + - ▶️ as `NEXT_BUTTON` + - ⏪ as `FIRST_PAGE` + - ⏩ as `LAST_PAGE` + - 🔢 as `GO_TO_PAGE` + - ⏹️ as `END_SESSION` + """ + BACK_BUTTON: ClassVar[str] = ... + NEXT_BUTTON: ClassVar[str] = ... + FIRST_PAGE: ClassVar[str] = ... + LAST_PAGE: ClassVar[str] = ... + GO_TO_PAGE: ClassVar[str] = ... + END_SESSION: ClassVar[str] = ... + + +class _PageController: + def __init__(self, pages: List[Page]) -> None: + ... + + @property + def current_page(self) -> Page: + ... + + @property + def total_pages(self) -> int: + """Return the total amount of pages registered to the menu""" + ... + + def validate_index(self) -> Page: + """If the index is out of bounds, assign the appropriate values so the pagination process can continue and return the associated page""" + ... + + def skip_loop(self, action: str, amount: int) -> None: + """Using `self.index += amount` does not work because this library is used to operating on a +-1 basis. This loop + provides a simple way to still operate on the +-1 standard. + """ + ... + + def skip(self, skip: _BaseButton.Skip) -> Page: + """Return the page that the skip value was set to""" + ... + + def next(self) -> Page: + """Return the next page in the pagination process""" + ... + + def prev(self) -> Page: + """Return the previous page in the pagination process""" + ... + + def first_page(self) -> Page: + """Return the first page in the pagination process""" + ... + + def last_page(self) -> Page: + """Return the last page in the pagination process""" + ... + + + +class _MenuType(Enum): + TypeEmbed = ... + TypeEmbedDynamic = ... + TypeText = ... + + +MenuType = _MenuType +class _LimitDetails(NamedTuple): + limit: int + per: str + message: str + set_by_user: bool = ... + @classmethod + def default(cls) -> Self: + ... + + + +class _BaseButton(Generic[GB], metaclass=abc.ABCMeta): + Emojis: ClassVar[PaginationEmojis] = ... + def __init__(self, name: str, event: Optional[_BaseButton.Event], skip: _BaseButton.Skip) -> None: + ... + + @property + @abc.abstractmethod + def menu(self): + ... + + @property + def clicked_by(self) -> Set[discord.Member]: + """ + Returns + ------- + Set[:class:`discord.Member`]: The members who clicked the button + """ + ... + + @property + def total_clicks(self) -> int: + """ + Returns + ------- + :class:`int`: The amount of clicks on the button + """ + ... + + @property + def last_clicked(self) -> Optional[datetime]: + """ + Returns + ------- + Optional[:class:`datetime.datetime`]: The time in UTC for when the button was last clicked. Can be :class:`None` if the button has not been clicked + """ + ... + + class Skip: + """Initialize a skip button with the appropriate values + + Parameters + ---------- + action: :class:`str` + Whether to go forward in the pagination process ("+") or backwards ("-") + + amount: :class:`int` + The amount of pages to skip. Must be >= 1. If value is <= 0, it is implicitly set to 2 + """ + def __init__(self, action: Literal['+', '-'], amount: int) -> None: + ... + + + + class Event: + """Set a button to be disabled or removed when it has been pressed a certain amount of times. If the button is a :class:`ReactionButton`, only the "remove" event is available + + Parameters + ---------- + event: :class:`str` + The action to take. Can either be "disable" or "remove" + + value: :class:`int` + The amount set for the specified event. Must be >= 1. If value is <= 0, it is implicitly set to 1""" + _DISABLE = ... + _REMOVE = ... + def __init__(self, event_type: Literal['disable', 'remove'], value: int) -> None: + ... + + + + + +class _BaseMenu(metaclass=abc.ABCMeta): + TypeEmbed: Final[_MenuType] = ... + TypeEmbedDynamic: Final[_MenuType] = ... + TypeText: Final[_MenuType] = ... + _sessions_limit_details = ... + _active_sessions: List[Self] + def __init__(self, method: Union[Context, discord.Interaction], /, menu_type: _MenuType, **kwargs) -> None: + ... + + @abc.abstractmethod + def remove_all_buttons(self): + ... + + @abc.abstractmethod + def get_button(self): + ... + + @abc.abstractmethod + def remove_button(self): + ... + + @abc.abstractmethod + def add_button(self): + ... + + @abc.abstractmethod + def add_buttons(self): + ... + + @abc.abstractmethod + def stop(self): + ... + + @abc.abstractmethod + async def start(self): + ... + + @abc.abstractmethod + async def quick_start(cls): + ... + + @staticmethod + def separate(values: Sequence[Any]) -> Tuple[List[discord.Embed], List[str]]: + """|static method| + + Sorts all embeds and strings into a single tuple + + Parameters + ---------- + values: Sequence[`Any`] + The values to separate + + Returns + ------- + Tuple[List[:class:`discord.Embed`], List[:class:`str`]] + + Example + ------- + >>> embeds, strings = .separate([...]) + """ + ... + + @staticmethod + def all_embeds(values: Sequence[Any]) -> bool: + """|static method| + + Tests to see if all items in the sequence are of type :class:`discord.Embed` + + Parameters + ---------- + values: Sequence[`Any`] + The values to test + + Returns + ------- + :class:`bool`: Can return `False` if the sequence is empty + """ + ... + + @staticmethod + def all_strings(values: Sequence[Any]) -> bool: + """|static method| + + Tests to see if all items in the sequence are of type :class:`str` + + Parameters + ---------- + values: Sequence[`Any`] + The values to test + + Returns + ------- + :class:`bool`: Can return `False` if the sequence is empty + """ + ... + + @classmethod + def remove_limit(cls) -> None: + """|class method| + + Remove the limits currently set for menu's + """ + ... + + @classmethod + def get_all_dm_sessions(cls) -> List[Self]: + """|class method| + + Retrieve all active DM menu sessions + + Returns + ------- + A :class:`list` of active DM menu sessions that are currently running. Can be an empty list if there are no active DM sessions + """ + ... + + @classmethod + def get_all_sessions(cls) -> List[Self]: + """|class method| + + Retrieve all active menu sessions + + Returns + ------- + A :class:`list` of menu sessions that are currently running. Can be an empty list if there are no active sessions + """ + ... + + @classmethod + def get_session(cls, name: str) -> List[Self]: + """|class method| + + Get a menu instance by it's name + + Parameters + ---------- + name: :class:`str` + The name of the menu to return + + Returns + ------- + A :class:`list` of menu sessions that are currently running that match the supplied :param:`name`. Can be an empty list if there are no active sessions that matched the :param:`name` + """ + ... + + @classmethod + def get_sessions_count(cls) -> int: + """|class method| + + Returns the number of active sessions + + Returns + ------- + :class:`int`: The amount of menu sessions that are active + """ + ... + + @classmethod + def set_sessions_limit(cls, limit: int, per: Literal['channel', 'guild', 'member'] = ..., message: str = ...) -> None: + """|class method| + + Sets the amount of menu sessions that can be active at the same time per guild, channel, or member. This applies to both :class:`ReactionMenu` & :class:`ViewMenu` + + Parameters + ---------- + limit: :class:`int` + The amount of menu sessions allowed + + per: :class:`str` + How menu sessions should be limited. Options: "channel", "guild", or "member" + + message: :class:`str` + Message that will be sent informing users about the menu limit when the limit is reached. Can be :class:`None` for no message + + Raises + ------ + - `IncorrectType`: The :param:`limit` parameter was not of type :class:`int` + - `MenuException`: The value of :param:`per` was not valid or the limit was not greater than or equal to one + """ + ... + + @classmethod + async def stop_session(cls, name: str, include_all: bool = ...) -> None: + """|coro class method| + + Stop a specific menu with the supplied name + + Parameters + ---------- + name: :class:`str` + The menus name + + include_all: :class:`bool` + If set to `True`, it stops all menu sessions with the supplied :param:`name`. If `False`, stops only the most recently started menu with the supplied :param:`name` + + Raises + ------ + - `MenuException`: The session with the supplied name was not found + """ + ... + + @classmethod + async def stop_all_sessions(cls) -> None: + """|coro class method| + + Stops all menu sessions that are currently running + """ + ... + + @classmethod + def get_menu_from_message(cls, message_id: int, /) -> Optional[Self]: + """|class method| + + Return the menu object associated with the message with the given ID + + Parameters + ---------- + message_id: :class:`int` + The `discord.Message.id` from the menu message + + Returns + ------- + The menu object. Can be :class:`None` if the menu was not found in the list of active menu sessions + """ + ... + + @property + def rows(self) -> Optional[List[str]]: + """ + Returns + ------- + Optional[List[:class:`str`]]: All rows that's been added to the menu. Can return `None` if the menu has not started or the `menu_type` is not `TypeEmbedDynamic` + + .. added: v3.1.0 + """ + ... + + @property + def menu_type(self) -> str: + """ + Returns + ------- + :class:`str`: The `menu_type` you set via the constructor. This will either be `TypeEmbed`, `TypeEmbedDynamic`, or `TypeText` + + .. added:: v3.1.0 + """ + ... + + @property + def last_viewed(self) -> Optional[Page]: + """ + Returns + ------- + Optional[:class:`Page`]: The last page that was viewed in the pagination process. Can be :class:`None` if the menu has not been started + """ + ... + + @property + def owner(self) -> Union[discord.Member, discord.User]: + """ + Returns + ------- + Union[:class:`discord.Member`, :class:`discord.User`]: The owner of the menu (the person that started the menu). If the menu was started in a DM, this will return :class:`discord.User` + """ + ... + + @property + def total_pages(self) -> int: + """ + Returns + ------- + :class:`int`: The amount of pages that have been added to the menu. If the `menu_type` is :attr:`TypeEmbedDynamic`, the amount of pages is not known until AFTER the menu has started. + If attempted to retrieve the value before a dynamic menu has started, this will return a value of -1 + """ + ... + + @property + def pages(self) -> Optional[List[Page]]: + """ + Returns + ------- + Optional[List[:class:`Page`]]: The pages currently applied to the menu. Can return :class:`None` if there are no pages + + Note: If the `menu_type` is :attr:`TypeEmbedDynamic`, the pages aren't known until after the menu has started + """ + ... + + @property + def message(self) -> Optional[Union[discord.Message, discord.InteractionMessage]]: + """ + Returns + ------- + Optional[Union[:class:`discord.Message`, :class:`discord.InteractionMessage`]]: The menu's message object. Can be :class:`None` if the menu has not been started + """ + ... + + @property + def is_running(self) -> bool: + """ + Returns + ------- + :class:`bool`: `True` if the menu is currently running, `False` otherwise + """ + ... + + @property + def in_dms(self) -> bool: + """ + Returns + ------- + :class:`bool`: If the menu was started in a DM + """ + ... + + def randomize_embed_colors(self) -> None: + """Randomize the color of all the embeds that have been added to the menu + + Raises + ------ + - `MenuException`: The `menu_type` was not of `TypeEmbed` + + .. added:: v3.1.0 + """ + ... + + def set_page_director_style(self, style_id: int, separator: str = ...) -> None: + """Set how the page numbers dictating what page you are on (in the footer of an embed/regular message) are displayed + + Parameters + ---------- + style_id: :class:`int` + Varying formats of how the page director can be presented. The following ID's are available: + + - `1` = Page 1/10 + - `2` = Page 1 out of 10 + - `3` = 1 out of 10 + - `4` = 1 • 10 + - `5` = 1 » 10 + - `6` = 1 | 10 + - `7` = 1 : 10 + - `8` = 1 - 10 + - `9` = 1 / 10 + - `10` = 1 🔹 10 + - `11` = 1 🔸 10 + + separator: :class:`str` + The separator between the page director and any text you may have in the embed footer. The default separator is ":". It should be noted that whichever separator you assign, + if you wish to have spacing between the page director and the separator, you must place the space inside the string yourself as such: " :" + + Raises + ------ + - `MenuException`: The :param:`style_id` value was not valid + """ + ... + + async def wait_until_closed(self) -> None: + """|coro| + + Waits until the menu session ends using `.stop()` or when the menu times out. This should not be used inside relays + + .. added:: v3.0.1 + """ + ... + + @ensure_not_primed + def add_from_messages(self, messages: Sequence[discord.Message]) -> None: + """Add pages to the menu using the message object itself + + Parameters + ---------- + messages: Sequence[:class:`discord.Message`] + A sequence of discord message objects + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The messages provided did not have the correct values. For example, the `menu_type` was set to `TypeEmbed`, but the messages you've provided only contains text. If the `menu_type` is `TypeEmbed`, only messages with embeds should be provided + - `IncorrectType`: All messages were not of type :class:`discord.Message` + """ + ... + + @ensure_not_primed + async def add_from_ids(self, messageable: discord.abc.Messageable, message_ids: Sequence[int]) -> None: + """|coro| + + Add pages to the menu using the IDs of messages. This only grabs embeds (if the `menu_type` is :attr:`TypeEmbed`) or the content (if the `menu_type` is :attr:`TypeText`) from the message + + Parameters + ---------- + messageable: :class:`discord.abc.Messageable` + A discord :class:`Messageable` object (:class:`discord.TextChannel`, :class:`commands.Context`, etc.) + + message_ids: Sequence[:class:`int`] + The messages to fetch + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The message IDs provided did not have the correct values when fetched. For example, the `menu_type` was set to `TypeEmbed`, but the messages you've provided for the library to fetch only contains text. If the `menu_type` is `TypeEmbed`, only messages with embeds should be provided + - `MenuException`: An error occurred when attempting to fetch a message or not all :param:`message_ids` were of type int + """ + ... + + @ensure_not_primed + def clear_all_row_data(self) -> None: + """Delete all the data thats been added using :meth:`add_row()` + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: This method was called but the menus `menu_type` was not :attr:`TypeEmbedDynamic` + """ + ... + + @ensure_not_primed + def add_row(self, data: str) -> None: + """Add text to the embed page by rows of data + + Parameters + ---------- + data: :class:`str` + The data to add + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: This method was called but the menus `menu_type` was not :attr:`TypeEmbedDynamic` + - `MissingSetting`: The kwarg "rows_requested" (int) has not been set for the menu + """ + ... + + @ensure_not_primed + def set_main_pages(self, *embeds: discord.Embed) -> None: + """On a menu with a `menu_type` of :attr:`TypeEmbedDynamic`, set the pages you would like to show first. These embeds will be shown before the embeds that contain your data + + Parameters + ---------- + *embeds: :class:`discord.Embed` + An argument list of :class:`discord.Embed` objects + + Raises + ------ + - `MenuSettingsMismatch`: Tried to use method on a menu that was not of `menu_type` :attr:`TypeEmbedDynamic` + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuException`: The "embeds" parameter was empty. At least one value is needed + - `IncorrectType`: All values in the argument list were not of type :class:`discord.Embed` + """ + ... + + @ensure_not_primed + def set_last_pages(self, *embeds: discord.Embed) -> None: + """On a menu with a `menu_type` of :attr:`TypeEmbedDynamic`, set the pages you would like to show last. These embeds will be shown after the embeds that contain your data + + Parameters + ---------- + *embeds: :class:`discord.Embed` + An argument list of :class:`discord.Embed` objects + + Raises + ------ + - `MenuSettingsMismatch`: Tried to use method on a menu that was not of `menu_type` :attr:`TypeEmbedDynamic` + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuException`: The "embeds" parameter was empty. At least one value is needed + - `IncorrectType`: All values in the argument list were not of type :class:`discord.Embed` + """ + ... + + @ensure_not_primed + def add_page(self, embed: Optional[discord.Embed] = ..., content: Optional[str] = ..., files: Optional[List[discord.File]] = ...) -> None: + """Add a page to the menu + + Parameters + ---------- + embed: Optional[:class:`discord.Embed`] + The embed of the page + + content: Optional[:class:`str`] + The text that appears above an embed in a message + + files: Optional[Sequence[:class:`discord.File`]] + Files you'd like to attach to the page + + Raises + ------ + - `MenuException`: Attempted to add a page with no parameters + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The page being added does not match the menus `menu_type` + + .. changes:: + v3.1.0 + Added parameter content + Added parameter embed + Added parameter files + Removed parameter "page" + """ + ... + + @overload + def add_pages(self, pages: Sequence[discord.Embed]) -> None: + ... + + @overload + def add_pages(self, pages: Sequence[str]) -> None: + ... + + @ensure_not_primed + def add_pages(self, pages: Sequence[Union[discord.Embed, str]]) -> None: + """Add multiple pages to the menu at once + + Parameters + ---------- + pages: Sequence[Union[:class:`discord.Embed`, :class:`str`]] + The pages to add. Can only be used when the menus `menu_type` is :attr:`TypeEmbed` (adding a :class:`discord.Embed`) + or :attr:`TypeText` (adding a :class:`str`) + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `MenuSettingsMismatch`: The page being added does not match the menus `menu_type` + """ + ... + + @ensure_not_primed + def remove_all_pages(self) -> None: + """Remove all pages from the menu + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + """ + ... + + @ensure_not_primed + def remove_page(self, page_number: int) -> None: + """Remove a page from the menu + + Parameters + ---------- + page_number: :class:`int` + The page to remove + + Raises + ------ + - `MenuAlreadyRunning`: Attempted to call method after the menu has already started + - `InvalidPage`: The page associated with the given page number was not valid + """ + ... + + def set_on_timeout(self, func: Callable[[M], None]) -> None: + """Set the function to be called when the menu times out + + Parameters + ---------- + func: Callable[[:type:`M`]], :class:`None`] + The function object that will be called when the menu times out. The function should contain a single positional argument + and should not return anything. The argument passed to that function is an instance of the menu. + + Raises + ------ + - `IncorrectType`: Parameter "func" was not a callable object + """ + ... + + def remove_on_timeout(self) -> None: + """Remove the timeout call to the function you have set when the menu times out""" + ... + + def set_relay(self, func: Callable[[NamedTuple], None], *, only: Optional[List[GB]] = ...) -> None: + """Set a function to be called with a given set of information when a button is pressed on the menu. The information passed is `RelayPayload`, a named tuple. + The named tuple contains the following attributes: + + - `member`: The :class:`discord.Member` object of the person who pressed the button. Could be :class:`discord.User` if the menu was started in a DM + - `button`: Depending on the menu instance, the :class:`ReactionButton` or :class:`ViewButton` object of the button that was pressed + + Parameters + ---------- + func: Callable[[:class:`NamedTuple`], :class:`None`] + The function should only contain a single positional argument. Command functions (`@bot.command()`) not supported + + only: Optional[List[:generic:`GB`]] + A list of buttons (`GB`) associated with the current menu instance. If the menu instance is :class:`ReactionMenu`, this should be a list of :class:`ReactionButton` + and vice-versa for :class:`ViewMenu` instances. If this is :class:`None`, all buttons on the menu will be relayed. If set, only button presses from those specified buttons will be relayed + + Raises + ------ + - `IncorrectType`: The :param:`func` argument provided was not callable + """ + ... + + def remove_relay(self) -> None: + """Remove the relay that's been set""" + ... + + + diff --git a/stubs/reactionmenu/buttons.pyi b/stubs/reactionmenu/buttons.pyi new file mode 100644 index 0000000..a713cbb --- /dev/null +++ b/stubs/reactionmenu/buttons.pyi @@ -0,0 +1,549 @@ +""" +This type stub file was generated by pyright. +""" + +import discord +from typing import Any, Callable, Dict, Final, Iterable, List, Literal, NamedTuple, Optional, TYPE_CHECKING, Union +from . import ReactionButton, ReactionMenu, ViewMenu +from enum import Enum +from .abc import _BaseButton + +""" +MIT License + +Copyright (c) 2021-present @defxult + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +""" +if TYPE_CHECKING: + ... +class _Details(NamedTuple): + """Used for buttons with a `custom_id` of `ID_CALLER`""" + func: Callable[..., None] + args: Iterable[Any] + kwargs: Dict[str, Any] + ... + + +Details = _Details +class ViewButton(discord.ui.Button, _BaseButton): + """A helper class for :class:`ViewMenu`. Represents a UI button. + + Parameters + ---------- + style: :class:`discord.ButtonStyle` + The style of the button + + label: Optional[:class:`str`] + The button label, if any + + disabled: :class:`bool` + Whether the button is disabled or not + + custom_id: Optional[:class:`str`] + The ID of the button that gets received during an interaction. If this button is for a URL, it does not have a custom ID + + url: Optional[:class:`str`] + The URL this button sends you to + + emoji: Optional[Union[:class:`str`, :class:`discord.PartialEmoji`]] + The emoji of the button, if available + + followup: Optional[:class:`ViewButton.Follow`] + Used with buttons with custom_id :attr:`ViewButton.ID_CALLER`, :attr:`ViewButton.ID_SEND_MESSAGE`, :attr:`ViewButton.ID_CUSTOM_EMBED` + + event: Optional[:class:`ViewButton.Event`] + Set the button to be disabled or removed when it has been pressed a certain amount of times + + Kwargs + ------ + name: :class:`str` + An optional name for the button. Can be set to retrieve it later via :meth:`ViewMenu.get_button()` + + skip: :class:`ViewButton.Skip` + Set the action and the amount of pages to skip when using a `custom_id` of `ViewButton.ID_SKIP` + + persist: :class:`bool` + Available only when using link buttons. This prevents link buttons from being disabled/removed when the menu times out or is stopped so they can remain clickable + + .. added v3.1.0 + :param:`persist` + """ + ID_NEXT_PAGE: Final[str] = ... + ID_PREVIOUS_PAGE: Final[str] = ... + ID_GO_TO_FIRST_PAGE: Final[str] = ... + ID_GO_TO_LAST_PAGE: Final[str] = ... + ID_GO_TO_PAGE: Final[str] = ... + ID_END_SESSION: Final[str] = ... + ID_CALLER: Final[str] = ... + ID_SEND_MESSAGE: Final[str] = ... + ID_CUSTOM_EMBED: Final[str] = ... + ID_SKIP: Final[str] = ... + _RE_IDs = ... + _RE_UNIQUE_ID_SET = ... + def __init__(self, *, style: discord.ButtonStyle = ..., label: Optional[str] = ..., disabled: bool = ..., custom_id: Optional[str] = ..., url: Optional[str] = ..., emoji: Optional[Union[str, discord.PartialEmoji]] = ..., followup: Optional[ViewButton.Followup] = ..., event: Optional[ViewButton.Event] = ..., **kwargs) -> None: + ... + + def __repr__(self): # -> str: + ... + + async def callback(self, interaction: discord.Interaction) -> None: + """*INTERNAL USE ONLY* - The callback function from the button interaction. This should not be manually called""" + ... + + class Followup: + """A class that represents the message sent using a :class:`ViewButton`. Contains parameters similar to method `discord.abc.Messageable.send`. Only to be used with :class:`ViewButton` kwarg "followup". + It is to be noted that this should not be used with :class:`ViewButton` with a "style" of `discord.ButtonStyle.link` because link buttons do not send interaction events. + + Parameters + ---------- + content: Optional[:class:`str`] + Message to send + + embed: Optional[:class:`discord.Embed`] + Embed to send. Can also bet set for buttons with a custom_id of :attr:`ViewButton.ID_CUSTOM_EMBED` + + file: Optional[:class:`discord.File`] + File to send. If the :class:`ViewButton` custom_id is :attr:`ViewButton.ID_SEND_MESSAGE`, the file will be ignored because of discord API limitations + + tts: :class:`bool` + If discord should read the message aloud. Not valid for `ephemeral` messages + + allowed_mentions: Optional[:class:`discord.AllowedMentions`] + Controls the mentions being processed in the menu message. Not valid for `ephemeral` messages + + delete_after: Optional[Union[:class:`int`, :class:`float`]] + Amount of time to wait before the message is deleted. Not valid for `ephemeral` messages + + ephemeral: :class:`bool` + If the message will be hidden from everyone except the person that pressed the button. This is only valid for a :class:`ViewButton` with custom_id :attr:`ViewButton.ID_SEND_MESSAGE` + + Kwargs + ------ + details: :meth:`ViewButton.Followup.set_caller_details()` + The information that will be used when a `ViewButton.ID_CALLER` button is pressed (defaults to :class:`None`) + """ + __slots__ = ... + def __repr__(self): # -> LiteralString: + ... + + def __init__(self, content: Optional[str] = ..., *, embed: Optional[discord.Embed] = ..., file: Optional[discord.File] = ..., tts: bool = ..., allowed_mentions: Optional[discord.AllowedMentions] = ..., delete_after: Optional[Union[int, float]] = ..., ephemeral: bool = ..., **kwargs) -> None: + ... + + @staticmethod + def set_caller_details(func: Callable[..., None], *args, **kwargs) -> Details: + """|static method| + + Set the parameters for the function you set for a :class:`ViewButton` with the custom_id :attr:`ViewButton.ID_CALLER` + + Parameters + ---------- + func: Callable[..., :class:`None`] + The function object that will be called when the associated button is pressed + + *args: `Any` + An argument list that represents the parameters of that function + + **kwargs: `Any` + An argument list that represents the kwarg parameters of that function + + Returns + ------- + :class:`Details`: The :class:`NamedTuple` containing the values needed to internally call the function you have set + + Raises + ------ + - `IncorrectType`: Parameter "func" was not a callable object + """ + ... + + + + @property + def menu(self) -> Optional[ViewMenu]: + """ + Returns + ------- + Optional[:class:`ViewMenu`]: The menu instance this button is attached to. Could be :class:`None` if the button is not attached to a menu + """ + ... + + @classmethod + def generate_skip(cls, label: str, action: Literal['+', '-'], amount: int) -> ViewButton: + """|class method| + + A factory method that returns a :class:`ViewButton` with the following parameters set: + + - style: `discord.ButtonStyle.gray` + - label: `