mirror of
https://github.com/wlinator/luminara.git
synced 2024-10-02 18:03:12 +00:00
Merge pull request #41 from wlinator/v3
[v3] Rewrite entire codebase to port to discord.py
This commit is contained in:
commit
14111b15e0
131 changed files with 8495 additions and 5277 deletions
|
@ -1,11 +1,14 @@
|
|||
TOKEN=
|
||||
INSTANCE=
|
||||
OWNER_IDS=
|
||||
|
||||
XP_GAIN_PER_MESSAGE=
|
||||
XP_GAIN_COOLDOWN=
|
||||
|
||||
DBX_OAUTH2_REFRESH_TOKEN=
|
||||
DBX_APP_KEY=
|
||||
DBX_APP_SECRET=
|
||||
|
||||
MARIADB_USER=
|
||||
MARIADB_PASSWORD=
|
||||
MARIADB_ROOT_PASSWORD=
|
||||
|
|
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,23 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
|
@ -1,20 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
55
.github/workflows/docker-image.yml
vendored
55
.github/workflows/docker-image.yml
vendored
|
@ -1,55 +0,0 @@
|
|||
name: Create and Publish Docker Image CI
|
||||
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
tags: [ "v*.*.*" ]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
docker:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
wlinator/luminara
|
||||
ghcr.io/wlinator/luminara
|
||||
tags: |
|
||||
type=raw,value=latest,enable={{is_default_branch}}
|
||||
type=ref,event=pr
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Login to GHCR
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
||||
|
|
@ -3,7 +3,11 @@ 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]
|
||||
- id: check-toml
|
||||
|
||||
- repo: https://github.com/asottile/add-trailing-comma
|
||||
|
@ -16,7 +20,7 @@ repos:
|
|||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [ --fix ]
|
||||
args: [--fix]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
|
||||
|
@ -25,11 +29,6 @@ repos:
|
|||
hooks:
|
||||
- id: gitleaks
|
||||
|
||||
- repo: https://github.com/hija/clean-dotenv
|
||||
rev: v0.0.7
|
||||
hooks:
|
||||
- id: clean-dotenv
|
||||
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.16.0
|
||||
hooks:
|
||||
|
|
43
Client.py
43
Client.py
|
@ -1,43 +0,0 @@
|
|||
import os
|
||||
import platform
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
|
||||
|
||||
class LumiBot(bridge.Bot):
|
||||
async def on_ready(self):
|
||||
"""
|
||||
Called when the bot is ready.
|
||||
|
||||
Logs various information about the bot and the environment it is running on.
|
||||
Note: This function isn't guaranteed to only be called once. The event is called when a RESUME request fails.
|
||||
"""
|
||||
logger.info(f"{CONST.TITLE} v{CONST.VERSION}")
|
||||
logger.info(f"Logged in with ID {self.user.id if self.user else 'Unknown'}")
|
||||
logger.info(f"discord.py API version: {discord.__version__}")
|
||||
logger.info(f"Python version: {platform.python_version()}")
|
||||
logger.info(f"Running on: {platform.system()} {platform.release()} ({os.name})")
|
||||
|
||||
if self.owner_ids:
|
||||
for owner_id in self.owner_ids:
|
||||
logger.info(f"Added bot admin: {owner_id}")
|
||||
|
||||
async def process_commands(self, message: discord.Message):
|
||||
"""
|
||||
Processes commands sent by users.
|
||||
|
||||
Args:
|
||||
message (discord.Message): The message object containing the command.
|
||||
"""
|
||||
if message.author.bot:
|
||||
return
|
||||
|
||||
ctx = await self.get_context(message)
|
||||
|
||||
if ctx.command:
|
||||
# await ctx.trigger_typing()
|
||||
await self.invoke(ctx)
|
|
@ -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" ]
|
||||
CMD [ "poetry", "run", "python", "-O", "./main.py" ]
|
100
Luminara.py
100
Luminara.py
|
@ -1,100 +0,0 @@
|
|||
import os
|
||||
import sys
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
import Client
|
||||
import services.config_service
|
||||
import services.help_service
|
||||
from db.database import run_migrations
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import Blacklisted
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
|
||||
# Remove the default logger configuration
|
||||
logger.remove()
|
||||
|
||||
# Add a new logger configuration with colors and a short datetime format
|
||||
log_format = (
|
||||
"<green>{time:YY-MM-DD HH:mm:ss}</green> | "
|
||||
"<level>{level: <8}</level> | "
|
||||
# "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - "
|
||||
"<level>{message}</level>"
|
||||
)
|
||||
logger.add(sys.stdout, format=log_format, colorize=True, level="DEBUG")
|
||||
|
||||
|
||||
async def get_prefix(bot, message):
|
||||
extras = services.config_service.GuildConfig.get_prefix(message)
|
||||
return commands.when_mentioned_or(*extras)(bot, message)
|
||||
|
||||
|
||||
client = Client.LumiBot(
|
||||
owner_ids=CONST.OWNER_IDS,
|
||||
command_prefix=get_prefix,
|
||||
intents=discord.Intents.all(),
|
||||
status=discord.Status.online,
|
||||
help_command=services.help_service.LumiHelp(),
|
||||
)
|
||||
|
||||
|
||||
@client.check
|
||||
async def blacklist_check(ctx):
|
||||
if BlacklistUserService.is_user_blacklisted(ctx.author.id):
|
||||
raise Blacklisted
|
||||
return True
|
||||
|
||||
|
||||
def load_modules():
|
||||
loaded = set()
|
||||
|
||||
# Load event listeners (handlers) and command cogs (modules)
|
||||
for directory in ["handlers", "modules"]:
|
||||
directory_path = os.path.join(os.getcwd(), directory)
|
||||
if not os.path.isdir(directory_path):
|
||||
continue
|
||||
|
||||
items = (
|
||||
[
|
||||
d
|
||||
for d in os.listdir(directory_path)
|
||||
if os.path.isdir(os.path.join(directory_path, d))
|
||||
]
|
||||
if directory == "modules"
|
||||
else [f[:-3] for f in os.listdir(directory_path) if f.endswith(".py")]
|
||||
)
|
||||
|
||||
for item in items:
|
||||
if item in loaded:
|
||||
continue
|
||||
|
||||
try:
|
||||
client.load_extension(f"{directory}.{item}")
|
||||
loaded.add(item)
|
||||
logger.debug(f"{item.upper()} loaded.")
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Failed to load {item.upper()}: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
This code is only ran when Lumi.py is the primary module,
|
||||
so NOT when main is imported from a cog. (sys.modules)
|
||||
"""
|
||||
|
||||
logger.info("LUMI IS BOOTING")
|
||||
|
||||
# Run database migrations
|
||||
run_migrations()
|
||||
|
||||
# load command and listener cogs
|
||||
load_modules()
|
||||
|
||||
if not CONST.TOKEN:
|
||||
logger.error("token is not set in .env")
|
||||
exit(1)
|
||||
|
||||
client.run(CONST.TOKEN)
|
|
@ -7,7 +7,7 @@
|
|||
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.
|
||||
|
||||
**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.
|
||||
**Note:** From `v2.9.0` and onward, Lumi now utilizes a [settings.yaml](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
|
||||
|
||||
|
|
0
db/__init__.py
Normal file
0
db/__init__.py
Normal file
115
db/database.py
115
db/database.py
|
@ -1,12 +1,13 @@
|
|||
import os
|
||||
import pathlib
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import mysql.connector
|
||||
from loguru import logger
|
||||
from mysql.connector import pooling
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.const import CONST
|
||||
|
||||
|
||||
def create_connection_pool(name: str, size: int) -> pooling.MySQLConnectionPool:
|
||||
|
@ -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
|
||||
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
|
||||
|
||||
# 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.")
|
||||
logger.success("All database migrations completed.")
|
||||
|
|
0
db/migrations/__init__.py
Normal file
0
db/migrations/__init__.py
Normal file
|
@ -1,6 +1,6 @@
|
|||
services:
|
||||
core:
|
||||
image: ghcr.io/wlinator/luminara:2 # Remove "ghcr.io/" if you want to use the Docker Hub image.
|
||||
image: ghcr.io/wlinator/luminara:3 # Remove "ghcr.io/" if you want to use the Docker Hub image.
|
||||
container_name: lumi-core
|
||||
restart: always
|
||||
env_file:
|
||||
|
|
0
handlers/__init__.py
Normal file
0
handlers/__init__.py
Normal file
111
handlers/error.py
Normal file
111
handlers/error.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
import sys
|
||||
import traceback
|
||||
from typing import Any
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from lib import exceptions
|
||||
from lib.const import CONST
|
||||
|
||||
error_map: dict[type[Exception], str] = {
|
||||
commands.BotMissingPermissions: CONST.STRINGS["error_bot_missing_permissions_description"],
|
||||
commands.MissingPermissions: CONST.STRINGS["error_missing_permissions_description"],
|
||||
commands.NoPrivateMessage: CONST.STRINGS["error_no_private_message_description"],
|
||||
commands.NotOwner: CONST.STRINGS["error_not_owner_unknown"],
|
||||
commands.PrivateMessageOnly: CONST.STRINGS["error_private_message_only_description"],
|
||||
exceptions.BirthdaysDisabled: CONST.STRINGS["error_birthdays_disabled_description"],
|
||||
}
|
||||
|
||||
|
||||
async def on_error(event: str, *args: Any, **kwargs: Any) -> 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()
|
||||
|
||||
|
||||
async def log_command_error(
|
||||
user_name: str,
|
||||
command_name: str | None,
|
||||
guild_id: int | None,
|
||||
error: commands.CommandError | commands.CheckFailure | app_commands.AppCommandError,
|
||||
command_type: str,
|
||||
) -> None:
|
||||
if isinstance(error, commands.NotOwner | exceptions.Blacklisted):
|
||||
return
|
||||
|
||||
log_msg = f"{user_name} executed {command_type}{command_name or 'Unknown'}"
|
||||
|
||||
log_msg += " in DMs" if guild_id is None else f" | guild: {guild_id}"
|
||||
|
||||
if CONST.INSTANCE == "dev":
|
||||
logger.exception(
|
||||
f"{log_msg} | {error.__module__}.{error.__class__.__name__} | {''.join(traceback.format_exception(type(error), error, error.__traceback__))}",
|
||||
)
|
||||
else:
|
||||
logger.error(f"{log_msg} | {error.__module__}.{error.__class__.__name__}")
|
||||
|
||||
|
||||
class ErrorHandler(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot = bot
|
||||
|
||||
async def cog_load(self):
|
||||
tree = self.bot.tree
|
||||
self._old_tree_error = tree.on_error
|
||||
tree.on_error = self.on_app_command_error
|
||||
|
||||
async def cog_unload(self):
|
||||
tree = self.bot.tree
|
||||
tree.on_error = self._old_tree_error
|
||||
|
||||
async def on_app_command_error(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
error: app_commands.AppCommandError,
|
||||
) -> None:
|
||||
if isinstance(error, commands.CommandNotFound | exceptions.Blacklisted):
|
||||
return
|
||||
|
||||
await log_command_error(
|
||||
user_name=interaction.user.name,
|
||||
command_name=interaction.command.qualified_name if interaction.command else None,
|
||||
guild_id=interaction.guild.id if interaction.guild else None,
|
||||
error=error,
|
||||
command_type="/",
|
||||
)
|
||||
|
||||
error_msg = error_map.get(type(error), str(error))
|
||||
await interaction.response.send_message(content=f"❌ **{interaction.user.name}** {error_msg}", ephemeral=True)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_command_error(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
error: commands.CommandError | commands.CheckFailure,
|
||||
) -> None:
|
||||
if isinstance(error, commands.CommandNotFound | exceptions.Blacklisted):
|
||||
return
|
||||
|
||||
await log_command_error(
|
||||
user_name=ctx.author.name,
|
||||
command_name=ctx.command.qualified_name if ctx.command else None,
|
||||
guild_id=ctx.guild.id if ctx.guild else None,
|
||||
error=error,
|
||||
command_type=".",
|
||||
)
|
||||
|
||||
error_msg = error_map.get(type(error), str(error))
|
||||
await ctx.send(content=f"❌ **{ctx.author.name}** {error_msg}")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_error(self, event: str, *args: Any, **kwargs: Any) -> None:
|
||||
await on_error(event, *args, **kwargs)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(ErrorHandler(bot))
|
|
@ -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))
|
98
handlers/event.py
Normal file
98
handlers/event.py
Normal file
|
@ -0,0 +1,98 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
from services.config_service import GuildConfig
|
||||
from ui.config import create_boost_embed, create_greet_embed
|
||||
|
||||
|
||||
class EventHandler(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_join(self, member: discord.Member):
|
||||
if BlacklistUserService.is_user_blacklisted(member.id):
|
||||
return
|
||||
|
||||
config = GuildConfig(member.guild.id)
|
||||
|
||||
if not config.welcome_channel_id:
|
||||
return
|
||||
|
||||
embed = create_greet_embed(
|
||||
user_name=member.name,
|
||||
user_avatar_url=member.display_avatar.url,
|
||||
guild_name=member.guild.name,
|
||||
template=config.welcome_message,
|
||||
)
|
||||
|
||||
try:
|
||||
channel = member.guild.get_channel(config.welcome_channel_id)
|
||||
if isinstance(channel, discord.TextChannel):
|
||||
await channel.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}",
|
||||
)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_member_update(self, before: discord.Member, after: discord.Member):
|
||||
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: discord.Member):
|
||||
config = GuildConfig(member.guild.id)
|
||||
|
||||
if not config.boost_channel_id:
|
||||
return
|
||||
|
||||
embed = create_boost_embed(
|
||||
user_name=member.name,
|
||||
user_avatar_url=member.display_avatar.url,
|
||||
boost_count=member.guild.premium_subscription_count,
|
||||
template=config.boost_message,
|
||||
image_url=config.boost_image_url,
|
||||
)
|
||||
|
||||
try:
|
||||
channel = member.guild.get_channel(config.boost_channel_id)
|
||||
if isinstance(channel, discord.TextChannel):
|
||||
await channel.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}",
|
||||
)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_command_completion(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
log_msg = f"{ctx.author.name} executed .{ctx.command.qualified_name if ctx.command else 'Unknown'}"
|
||||
|
||||
if ctx.guild is not None:
|
||||
logger.debug(f"{log_msg} | guild: {ctx.guild.name} ")
|
||||
else:
|
||||
logger.debug(f"{log_msg} in DMs")
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_application_command_completion(self, ctx: discord.Interaction) -> None:
|
||||
log_msg = f"{ctx.user.name} executed /{ctx.command.qualified_name if ctx.command else 'Unknown'}"
|
||||
|
||||
if ctx.guild is not None:
|
||||
logger.debug(f"{log_msg} | guild: {ctx.guild.name} ")
|
||||
else:
|
||||
logger.debug(f"{log_msg} in DMs")
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot):
|
||||
await bot.add_cog(EventHandler(bot))
|
|
@ -1,86 +0,0 @@
|
|||
from discord.ext.commands import Cog
|
||||
from loguru import logger
|
||||
|
||||
from modules.config import c_boost, c_greet
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
class EventHandler(Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@Cog.listener()
|
||||
async def on_member_join(self, member):
|
||||
if BlacklistUserService.is_user_blacklisted(member.id):
|
||||
return
|
||||
|
||||
config = GuildConfig(member.guild.id)
|
||||
|
||||
if not config.welcome_channel_id:
|
||||
return
|
||||
|
||||
embed = c_greet.create_greet_embed(member, config.welcome_message)
|
||||
|
||||
try:
|
||||
await member.guild.get_channel(config.welcome_channel_id).send(
|
||||
embed=embed,
|
||||
content=member.mention,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Greet message not sent in '{member.guild.name}'. Channel ID may be invalid. {e}",
|
||||
)
|
||||
|
||||
@Cog.listener()
|
||||
async def on_member_update(self, before, after):
|
||||
if BlacklistUserService.is_user_blacklisted(after.id):
|
||||
return
|
||||
|
||||
if before.premium_since is None and after.premium_since is not None:
|
||||
await self.on_nitro_boost(after)
|
||||
|
||||
@staticmethod
|
||||
async def on_nitro_boost(member):
|
||||
config = GuildConfig(member.guild.id)
|
||||
|
||||
if not config.boost_channel_id:
|
||||
return
|
||||
|
||||
embed = c_boost.create_boost_embed(
|
||||
member,
|
||||
config.boost_message,
|
||||
config.boost_image_url,
|
||||
)
|
||||
|
||||
try:
|
||||
await member.guild.get_channel(config.boost_channel_id).send(
|
||||
embed=embed,
|
||||
content=member.mention,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Boost message not sent in '{member.guild.name}'. Channel ID may be invalid. {e}",
|
||||
)
|
||||
|
||||
@Cog.listener()
|
||||
async def on_command_completion(self, ctx) -> None:
|
||||
log_msg = f"{ctx.author.name} executed .{ctx.command.qualified_name}"
|
||||
|
||||
if ctx.guild is not None:
|
||||
logger.debug(f"{log_msg} | guild: {ctx.guild.name} ")
|
||||
else:
|
||||
logger.debug(f"{log_msg} in DMs")
|
||||
|
||||
@Cog.listener()
|
||||
async def on_application_command_completion(self, ctx) -> None:
|
||||
log_msg = f"{ctx.author.name} executed /{ctx.command.qualified_name}"
|
||||
|
||||
if ctx.guild is not None:
|
||||
logger.debug(f"{log_msg} | guild: {ctx.guild.name} ")
|
||||
else:
|
||||
logger.debug(f"{log_msg} in DMs")
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(EventHandler(client))
|
|
@ -1,9 +1,11 @@
|
|||
import contextlib
|
||||
from typing import Any
|
||||
|
||||
from discord import Message
|
||||
from discord.ext.commands import Cog
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from lib.client import Luminara
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
@ -13,8 +15,8 @@ class ReactionHandler:
|
|||
Handles reactions to messages based on predefined triggers and responses.
|
||||
"""
|
||||
|
||||
def __init__(self, client, message: Message) -> None:
|
||||
self.client = client
|
||||
def __init__(self, bot: Luminara, message: Message) -> None:
|
||||
self.bot = bot
|
||||
self.message: Message = message
|
||||
self.content: str = self.message.content.lower()
|
||||
self.reaction_service = CustomReactionsService()
|
||||
|
@ -43,7 +45,7 @@ class ReactionHandler:
|
|||
int(data["id"]),
|
||||
)
|
||||
|
||||
async def try_respond(self, data) -> bool:
|
||||
async def try_respond(self, data: dict[str, Any]) -> bool:
|
||||
"""
|
||||
Tries to respond to the message.
|
||||
"""
|
||||
|
@ -53,23 +55,23 @@ class ReactionHandler:
|
|||
return True
|
||||
return False
|
||||
|
||||
async def try_react(self, data) -> bool:
|
||||
async def try_react(self, data: dict[str, Any]) -> 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):
|
||||
if emoji := self.bot.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
|
||||
class ReactionListener(commands.Cog):
|
||||
def __init__(self, bot: Luminara) -> None:
|
||||
self.bot = bot
|
||||
|
||||
@Cog.listener("on_message")
|
||||
@commands.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.
|
||||
|
@ -79,8 +81,8 @@ class ReactionListener(Cog):
|
|||
if not message.author.bot and not BlacklistUserService.is_user_blacklisted(
|
||||
message.author.id,
|
||||
):
|
||||
await ReactionHandler(self.client, message).run_checks()
|
||||
await ReactionHandler(self.bot, message).run_checks()
|
||||
|
||||
|
||||
def setup(client) -> None:
|
||||
client.add_cog(ReactionListener(client))
|
||||
async def setup(bot: Luminara) -> None:
|
||||
await bot.add_cog(ReactionListener(bot))
|
|
@ -2,27 +2,26 @@ 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 loguru import logger
|
||||
|
||||
from Client import LumiBot
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
import lib.format
|
||||
from lib.client import Luminara
|
||||
from lib.const 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:
|
||||
def __init__(self, client: Luminara, message: discord.Message) -> None:
|
||||
"""
|
||||
Initializes the XPHandler with the given client and message.
|
||||
|
||||
Args:
|
||||
client (LumiBot): The bot client.
|
||||
client (Luminara): The bot client.
|
||||
message (discord.Message): The message object.
|
||||
"""
|
||||
self.client = client
|
||||
|
@ -33,7 +32,7 @@ class XPHandler:
|
|||
self.author.id,
|
||||
self.guild.id if self.guild else 0,
|
||||
)
|
||||
self.guild_conf: Optional[GuildConfig] = None
|
||||
self.guild_conf: GuildConfig | None = None
|
||||
|
||||
def process(self) -> bool:
|
||||
"""
|
||||
|
@ -72,13 +71,13 @@ class XPHandler:
|
|||
_xp: XpService = self.xp_conf
|
||||
_gd: GuildConfig = GuildConfig(self.guild.id)
|
||||
|
||||
level_message: Optional[str] = None # Initialize level_message
|
||||
level_message: str | None = 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(
|
||||
level_channel: discord.TextChannel | None = await self.get_level_channel(
|
||||
self.message,
|
||||
_gd,
|
||||
)
|
||||
|
@ -102,29 +101,29 @@ class XPHandler:
|
|||
reason: str = "Automated Level Reward"
|
||||
|
||||
if role := self.guild.get_role(role_id):
|
||||
with contextlib.suppress(
|
||||
discord.Forbidden,
|
||||
discord.NotFound,
|
||||
discord.HTTPException,
|
||||
):
|
||||
try:
|
||||
if isinstance(self.author, discord.Member):
|
||||
await self.author.add_roles(role, reason=reason)
|
||||
except (discord.Forbidden, discord.NotFound, discord.HTTPException) as e:
|
||||
logger.error(f"Failed to add role {role_id} to {self.author.id}: {e}")
|
||||
|
||||
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)
|
||||
if (
|
||||
replace
|
||||
and isinstance(self.author, discord.Member)
|
||||
and (role := self.guild.get_role(previous or role_id))
|
||||
):
|
||||
try:
|
||||
await self.author.remove_roles(role, reason=reason)
|
||||
except (discord.Forbidden, discord.NotFound, discord.HTTPException) as e:
|
||||
logger.error(f"Failed to replace role {previous} with {role_id} from {self.author.id}: {e}")
|
||||
|
||||
async def get_level_channel(
|
||||
self,
|
||||
message: discord.Message,
|
||||
guild_config: GuildConfig,
|
||||
) -> Optional[discord.TextChannel]:
|
||||
) -> discord.TextChannel | None:
|
||||
"""
|
||||
Retrieves the level up notification channel for the guild.
|
||||
|
||||
|
@ -139,7 +138,7 @@ class XPHandler:
|
|||
context = await self.client.get_context(message)
|
||||
|
||||
with contextlib.suppress(commands.BadArgument, commands.CommandError):
|
||||
return await TextChannelConverter().convert(
|
||||
return await commands.TextChannelConverter().convert(
|
||||
context,
|
||||
str(guild_config.level_channel_id),
|
||||
)
|
||||
|
@ -150,7 +149,7 @@ class XPHandler:
|
|||
guild_config: GuildConfig,
|
||||
level_config: XpService,
|
||||
author: discord.Member,
|
||||
) -> Optional[str]:
|
||||
) -> str | None:
|
||||
"""
|
||||
Retrieves the level up message for the user.
|
||||
|
||||
|
@ -174,13 +173,14 @@ class XPHandler:
|
|||
author,
|
||||
)
|
||||
else:
|
||||
level_message = formatter.template(
|
||||
level_message = lib.format.template(
|
||||
guild_config.level_message,
|
||||
author.name,
|
||||
level_config.level,
|
||||
)
|
||||
case _:
|
||||
raise ValueError("Invalid level message type")
|
||||
msg = "Invalid level message type"
|
||||
raise ValueError(msg)
|
||||
|
||||
return level_message
|
||||
|
||||
|
@ -210,8 +210,8 @@ class XPHandler:
|
|||
Returns:
|
||||
str: The whimsical level up message.
|
||||
"""
|
||||
level_range: Optional[str] = None
|
||||
for key in CONST.LEVEL_MESSAGES.keys():
|
||||
level_range: str | None = None
|
||||
for key in CONST.LEVEL_MESSAGES:
|
||||
start, end = map(int, key.split("-"))
|
||||
if start <= level <= end:
|
||||
level_range = key
|
||||
|
@ -228,14 +228,14 @@ class XPHandler:
|
|||
|
||||
|
||||
class XpListener(commands.Cog):
|
||||
def __init__(self, client: LumiBot) -> None:
|
||||
def __init__(self, client: Luminara) -> None:
|
||||
"""
|
||||
Initializes the XpListener with the given client.
|
||||
|
||||
Args:
|
||||
client (LumiBot): The bot client.
|
||||
client (Luminara): The bot client.
|
||||
"""
|
||||
self.client: LumiBot = client
|
||||
self.client: Luminara = client
|
||||
|
||||
@commands.Cog.listener("on_message")
|
||||
async def xp_listener(self, message: discord.Message) -> None:
|
||||
|
@ -259,5 +259,5 @@ class XpListener(commands.Cog):
|
|||
)
|
||||
|
||||
|
||||
def setup(client: LumiBot) -> None:
|
||||
client.add_cog(XpListener(client))
|
||||
async def setup(client: Luminara) -> None:
|
||||
await client.add_cog(XpListener(client))
|
0
lib/__init__.py
Normal file
0
lib/__init__.py
Normal file
|
@ -1,7 +1,7 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from lib.const import CONST
|
||||
from lib.exceptions import LumiException
|
||||
|
||||
|
||||
async def async_actionable(
|
|
@ -1,24 +1,23 @@
|
|||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import TextChannelConverter, UserConverter
|
||||
from discord.ext import commands
|
||||
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
|
||||
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,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.User,
|
||||
action_type: str,
|
||||
reason: Optional[str] = None,
|
||||
duration: Optional[int] = None,
|
||||
expires_at: Optional[str] = None,
|
||||
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.
|
||||
|
@ -43,6 +42,10 @@ async def create_case(
|
|||
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
|
||||
|
@ -63,7 +66,7 @@ async def create_case(
|
|||
|
||||
if mod_log_channel_id := modlog_service.fetch_modlog_channel_id(guild_id):
|
||||
try:
|
||||
mod_log_channel = await TextChannelConverter().convert(
|
||||
mod_log_channel = await commands.TextChannelConverter().convert(
|
||||
ctx,
|
||||
str(mod_log_channel_id),
|
||||
)
|
||||
|
@ -90,7 +93,7 @@ async def create_case(
|
|||
|
||||
|
||||
async def edit_case_modlog(
|
||||
ctx,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
guild_id: int,
|
||||
case_number: int,
|
||||
new_reason: str,
|
||||
|
@ -110,7 +113,8 @@ async def edit_case_modlog(
|
|||
"""
|
||||
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}")
|
||||
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:
|
||||
|
@ -121,12 +125,12 @@ async def edit_case_modlog(
|
|||
return False
|
||||
|
||||
try:
|
||||
mod_log_channel = await TextChannelConverter().convert(
|
||||
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 UserConverter().convert(ctx, str(case["target_id"]))
|
||||
target = await commands.UserConverter().convert(ctx, str(case["target_id"]))
|
||||
|
||||
updated_embed: discord.Embed = create_case_embed(
|
||||
ctx=ctx,
|
|
@ -1,19 +1,19 @@
|
|||
from discord.ext import commands
|
||||
import discord
|
||||
from discord import app_commands
|
||||
|
||||
from lib.exceptions import LumiExceptions
|
||||
from lib.exceptions import BirthdaysDisabled
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
def birthdays_enabled():
|
||||
async def predicate(ctx):
|
||||
if ctx.guild is None:
|
||||
async def predicate(interaction: discord.Interaction) -> bool:
|
||||
if interaction.guild is None:
|
||||
return True
|
||||
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.birthday_channel_id:
|
||||
raise LumiExceptions.BirthdaysDisabled
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
if guild_config.birthday_channel_id is None:
|
||||
raise BirthdaysDisabled
|
||||
|
||||
return True
|
||||
|
||||
return commands.check(predicate)
|
||||
return app_commands.check(predicate)
|
||||
|
|
70
lib/client.py
Normal file
70
lib/client.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import asyncio
|
||||
import os
|
||||
import platform
|
||||
from typing import Any
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from db.database import run_migrations
|
||||
from lib.const import CONST
|
||||
from lib.loader import CogLoader
|
||||
|
||||
|
||||
class Luminara(commands.Bot):
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_shutting_down: bool = False
|
||||
self.setup_task: asyncio.Task[None] = asyncio.create_task(self.setup())
|
||||
|
||||
async def on_ready(self) -> None:
|
||||
logger.success(f"{CONST.TITLE} v{CONST.VERSION}")
|
||||
logger.success(f"Logged in with ID {self.user.id if self.user else 'Unknown'}")
|
||||
logger.success(f"discord.py API version: {discord.__version__}")
|
||||
logger.success(f"Python version: {platform.python_version()}")
|
||||
logger.success(f"Running on: {platform.system()} {platform.release()} ({os.name})")
|
||||
|
||||
if self.owner_ids:
|
||||
for owner in self.owner_ids:
|
||||
logger.info(f"Added bot administrator: {owner}")
|
||||
|
||||
if not self.setup_task.done():
|
||||
await self.setup_task
|
||||
|
||||
async def setup(self) -> None:
|
||||
try:
|
||||
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:
|
||||
await CogLoader.setup(bot=self)
|
||||
|
||||
@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.")
|
127
lib/const.py
Normal file
127
lib/const.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
import json
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, Final
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
class Parser:
|
||||
"""Internal parses class. Not intended to be used outside of this module."""
|
||||
|
||||
def __init__(self):
|
||||
self._cache: dict[str, Any] = {}
|
||||
|
||||
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_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: Final = Parser()
|
||||
_s: Final = Parser().read_s()
|
||||
|
||||
# bot credentials
|
||||
TOKEN: Final[str | None] = os.environ.get("TOKEN")
|
||||
INSTANCE: Final[str | None] = os.environ.get("INSTANCE")
|
||||
OWNER_IDS: Final[set[int]] = {int(oid) for oid in os.environ.get("OWNER_IDS", "").split(",") if oid.strip()}
|
||||
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")
|
||||
|
||||
# metadata
|
||||
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: Final[str] = _s["logs"]["level"] or "DEBUG"
|
||||
LOG_FORMAT: Final[str] = _s["logs"]["format"]
|
||||
|
||||
# cogs
|
||||
COG_IGNORE_LIST: Final[set[str]] = set(_s["cogs"]["ignore"]) if _s["cogs"]["ignore"] else set()
|
||||
|
||||
# images
|
||||
ALLOWED_IMAGE_EXTENSIONS: Final[list[str]] = _s["images"]["allowed_image_extensions"]
|
||||
BIRTHDAY_GIF_URL: Final[str] = _s["images"]["birthday_gif_url"]
|
||||
|
||||
# colors
|
||||
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: 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: Final[str] = _s["art"]["fetch_url"]
|
||||
|
||||
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: 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: 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: 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: Final = _p.read_json("strings.en-US")
|
||||
LEVEL_MESSAGES: Final = _p.read_json("levels.en-US")
|
||||
|
||||
_bday: Final = _p.read_json("bdays.en-US")
|
||||
BIRTHDAY_MESSAGES: Final[list[str]] = _bday["birthday_messages"]
|
||||
BIRTHDAY_MONTHS: Final[list[str]] = _bday["months"]
|
||||
|
||||
|
||||
CONST = Constants()
|
120
lib/constants.py
120
lib/constants.py
|
@ -1,120 +0,0 @@
|
|||
import os
|
||||
from typing import Optional, Set, List, Dict
|
||||
import yaml
|
||||
import json
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
class _parser:
|
||||
"""Internal parser class. Not intended for direct use outside this module."""
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def read_yaml(self, path):
|
||||
return self._read_file(f"settings/{path}.yaml", yaml.safe_load)
|
||||
|
||||
@lru_cache(maxsize=1024)
|
||||
def read_json(self, path):
|
||||
return self._read_file(f"settings/{path}.json", json.load)
|
||||
|
||||
def _read_file(self, file_path, load_func):
|
||||
with open(file_path) as file:
|
||||
return load_func(file)
|
||||
|
||||
|
||||
class Constants:
|
||||
_p = _parser()
|
||||
_settings = _p.read_yaml("settings")
|
||||
|
||||
# bot credentials (.env file)
|
||||
TOKEN: Optional[str] = os.environ.get("TOKEN")
|
||||
INSTANCE: Optional[str] = os.environ.get("INSTANCE")
|
||||
XP_GAIN_PER_MESSAGE: int = int(os.environ.get("XP_GAIN_PER_MESSAGE", 1))
|
||||
XP_GAIN_COOLDOWN: int = int(os.environ.get("XP_GAIN_COOLDOWN", 8))
|
||||
DBX_TOKEN: Optional[str] = os.environ.get("DBX_OAUTH2_REFRESH_TOKEN")
|
||||
DBX_APP_KEY: Optional[str] = os.environ.get("DBX_APP_KEY")
|
||||
DBX_APP_SECRET: Optional[str] = os.environ.get("DBX_APP_SECRET")
|
||||
MARIADB_USER: Optional[str] = os.environ.get("MARIADB_USER")
|
||||
MARIADB_PASSWORD: Optional[str] = os.environ.get("MARIADB_PASSWORD")
|
||||
MARIADB_ROOT_PASSWORD: Optional[str] = os.environ.get("MARIADB_ROOT_PASSWORD")
|
||||
MARIADB_DATABASE: Optional[str] = os.environ.get("MARIADB_DATABASE")
|
||||
|
||||
OWNER_IDS: Optional[Set[int]] = (
|
||||
{int(id.strip()) for id in os.environ.get("OWNER_IDS", "").split(",") if id}
|
||||
if "OWNER_IDS" in os.environ
|
||||
else None
|
||||
)
|
||||
|
||||
# metadata
|
||||
TITLE: str = _settings["info"]["title"]
|
||||
AUTHOR: str = _settings["info"]["author"]
|
||||
LICENSE: str = _settings["info"]["license"]
|
||||
VERSION: str = _settings["info"]["version"]
|
||||
REPO_URL: str = _settings["info"]["repository_url"]
|
||||
|
||||
# images
|
||||
ALLOWED_IMAGE_EXTENSIONS: List[str] = _settings["images"][
|
||||
"allowed_image_extensions"
|
||||
]
|
||||
BIRTHDAY_GIF_URL: str = _settings["images"]["birthday_gif_url"]
|
||||
|
||||
# colors
|
||||
COLOR_DEFAULT: int = _settings["colors"]["color_default"]
|
||||
COLOR_WARNING: int = _settings["colors"]["color_warning"]
|
||||
COLOR_ERROR: int = _settings["colors"]["color_error"]
|
||||
|
||||
# economy
|
||||
DAILY_REWARD: int = _settings["economy"]["daily_reward"]
|
||||
BLACKJACK_MULTIPLIER: float = _settings["economy"]["blackjack_multiplier"]
|
||||
BLACKJACK_HIT_EMOJI: str = _settings["economy"]["blackjack_hit_emoji"]
|
||||
BLACKJACK_STAND_EMOJI: str = _settings["economy"]["blackjack_stand_emoji"]
|
||||
SLOTS_MULTIPLIERS: Dict[str, float] = _settings["economy"]["slots_multipliers"]
|
||||
|
||||
# art from git repository
|
||||
_fetch_url: str = _settings["art"]["fetch_url"]
|
||||
|
||||
LUMI_LOGO_OPAQUE: str = _fetch_url + _settings["art"]["logo"]["opaque"]
|
||||
LUMI_LOGO_TRANSPARENT: str = _fetch_url + _settings["art"]["logo"]["transparent"]
|
||||
BOOST_ICON: str = _fetch_url + _settings["art"]["icons"]["boost"]
|
||||
CHECK_ICON: str = _fetch_url + _settings["art"]["icons"]["check"]
|
||||
CROSS_ICON: str = _fetch_url + _settings["art"]["icons"]["cross"]
|
||||
EXCLAIM_ICON: str = _fetch_url + _settings["art"]["icons"]["exclaim"]
|
||||
HAMMER_ICON: str = _fetch_url + _settings["art"]["icons"]["hammer"]
|
||||
MONEY_BAG_ICON: str = _fetch_url + _settings["art"]["icons"]["money_bag"]
|
||||
MONEY_COINS_ICON: str = _fetch_url + _settings["art"]["icons"]["money_coins"]
|
||||
QUESTION_ICON: str = _fetch_url + _settings["art"]["icons"]["question"]
|
||||
STREAK_ICON: str = _fetch_url + _settings["art"]["icons"]["streak"]
|
||||
STREAK_BRONZE_ICON: str = _fetch_url + _settings["art"]["icons"]["streak_bronze"]
|
||||
STREAK_GOLD_ICON: str = _fetch_url + _settings["art"]["icons"]["streak_gold"]
|
||||
STREAK_SILVER_ICON: str = _fetch_url + _settings["art"]["icons"]["streak_silver"]
|
||||
WARNING_ICON: str = _fetch_url + _settings["art"]["icons"]["warning"]
|
||||
|
||||
# art from imgur
|
||||
FLOWERS_ART: str = _settings["art"]["juicybblue"]["flowers"]
|
||||
TEAPOT_ART: str = _settings["art"]["juicybblue"]["teapot"]
|
||||
MUFFIN_ART: str = _settings["art"]["juicybblue"]["muffin"]
|
||||
CLOUD_ART: str = _settings["art"]["other"]["cloud"]
|
||||
TROPHY_ART: str = _settings["art"]["other"]["trophy"]
|
||||
|
||||
# emotes
|
||||
EMOTES_SERVER_ID: int = _settings["emotes"]["guild_id"]
|
||||
EMOTE_IDS: Dict[str, int] = _settings["emotes"]["emote_ids"]
|
||||
|
||||
# introductions (currently only usable in ONE guild)
|
||||
INTRODUCTIONS_GUILD_ID: int = _settings["introductions"]["intro_guild_id"]
|
||||
INTRODUCTIONS_CHANNEL_ID: int = _settings["introductions"]["intro_channel_id"]
|
||||
INTRODUCTIONS_QUESTION_MAPPING: Dict[str, str] = _settings["introductions"][
|
||||
"intro_question_mapping"
|
||||
]
|
||||
|
||||
# Response strings
|
||||
# TODO: Implement switching between languages
|
||||
STRINGS = _p.read_json("responses/strings.en-US")
|
||||
LEVEL_MESSAGES = _p.read_json("responses/levels.en-US")
|
||||
|
||||
# birthday messages
|
||||
_bday = _p.read_json("responses/bdays.en-US")
|
||||
BIRTHDAY_MESSAGES = _bday["birthday_messages"]
|
||||
BIRTHDAY_MONTHS = _bday["months"]
|
||||
|
||||
|
||||
CONST = Constants()
|
|
@ -1,206 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
|
||||
|
||||
class EmbedBuilder:
|
||||
@staticmethod
|
||||
def create_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
color=None,
|
||||
footer_text=None,
|
||||
footer_icon_url=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
if not hide_author:
|
||||
if not author_text:
|
||||
author_text = ctx.author.name
|
||||
elif show_name:
|
||||
description = f"**{ctx.author.name}** {description}"
|
||||
|
||||
if not hide_author_icon and not author_icon_url:
|
||||
author_icon_url = ctx.author.display_avatar.url
|
||||
|
||||
if not footer_text:
|
||||
footer_text = "Luminara"
|
||||
if not footer_icon_url:
|
||||
footer_icon_url = CONST.LUMI_LOGO_TRANSPARENT
|
||||
|
||||
embed = discord.Embed(
|
||||
title=title,
|
||||
description=description,
|
||||
color=color or CONST.COLOR_DEFAULT,
|
||||
)
|
||||
if not hide_author:
|
||||
embed.set_author(
|
||||
name=author_text,
|
||||
icon_url=None if hide_author_icon else author_icon_url,
|
||||
url=author_url,
|
||||
)
|
||||
embed.set_footer(text=footer_text, icon_url=footer_icon_url)
|
||||
if not hide_timestamp:
|
||||
embed.timestamp = timestamp or datetime.datetime.now()
|
||||
|
||||
if image_url:
|
||||
embed.set_image(url=image_url)
|
||||
if thumbnail_url:
|
||||
embed.set_thumbnail(url=thumbnail_url)
|
||||
|
||||
return embed
|
||||
|
||||
@staticmethod
|
||||
def create_error_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.CROSS_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_ERROR,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_success_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.CHECK_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_DEFAULT,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_info_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.EXCLAIM_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_DEFAULT,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create_warning_embed(
|
||||
ctx,
|
||||
title=None,
|
||||
author_text=None,
|
||||
author_icon_url=None,
|
||||
author_url=None,
|
||||
description=None,
|
||||
footer_text=None,
|
||||
show_name=True,
|
||||
image_url=None,
|
||||
thumbnail_url=None,
|
||||
timestamp=None,
|
||||
hide_author=False,
|
||||
hide_author_icon=False,
|
||||
hide_timestamp=False,
|
||||
):
|
||||
return EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
title=title,
|
||||
author_text=author_text,
|
||||
author_icon_url=author_icon_url or CONST.WARNING_ICON,
|
||||
author_url=author_url,
|
||||
description=description,
|
||||
color=CONST.COLOR_WARNING,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=show_name,
|
||||
image_url=image_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
timestamp=timestamp,
|
||||
hide_author=hide_author,
|
||||
hide_author_icon=hide_author_icon,
|
||||
hide_timestamp=hide_timestamp,
|
||||
)
|
36
lib/exceptions.py
Normal file
36
lib/exceptions.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.const import CONST
|
||||
|
||||
|
||||
class BirthdaysDisabled(commands.CheckFailure, app_commands.CheckFailure):
|
||||
"""
|
||||
Raised when the birthdays module is disabled in ctx.guild.
|
||||
"""
|
||||
|
||||
|
||||
class LumiException(commands.CommandError, app_commands.AppCommandError):
|
||||
"""
|
||||
A generic exception to raise for quick error handling.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = CONST.STRINGS["lumi_exception_generic"]):
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
||||
|
||||
|
||||
class Blacklisted(commands.CommandError, app_commands.AppCommandError):
|
||||
"""
|
||||
Raised when a user is blacklisted.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str = CONST.STRINGS["lumi_exception_blacklisted"]):
|
||||
self.message = message
|
||||
super().__init__(message)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.message
|
|
@ -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)
|
|
@ -1,11 +1,13 @@
|
|||
import inspect
|
||||
import textwrap
|
||||
from typing import Any
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from pytimeparse import parse
|
||||
from pytimeparse import parse # type: ignore
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from lib import exceptions
|
||||
from lib.const import CONST
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
|
@ -74,7 +76,7 @@ def format_case_number(case_number: int) -> str:
|
|||
return f"{case_number:03d}" if case_number < 1000 else str(case_number)
|
||||
|
||||
|
||||
def get_prefix(ctx: commands.Context) -> str:
|
||||
def get_prefix(ctx: commands.Context[commands.Bot]) -> str:
|
||||
"""
|
||||
Attempts to retrieve the prefix for the given guild context.
|
||||
|
||||
|
@ -90,7 +92,7 @@ def get_prefix(ctx: commands.Context) -> str:
|
|||
return "."
|
||||
|
||||
|
||||
def get_invoked_name(ctx: commands.Context) -> str | None:
|
||||
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.
|
||||
|
||||
|
@ -102,20 +104,24 @@ def get_invoked_name(ctx: commands.Context) -> str | None:
|
|||
"""
|
||||
try:
|
||||
return ctx.invoked_with
|
||||
except (discord.ApplicationCommandInvokeError, AttributeError):
|
||||
|
||||
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.
|
||||
Converts a duration string to seconds. If the input is just an integer, it returns that integer as seconds.
|
||||
"""
|
||||
parsed_duration = parse(duration)
|
||||
if duration.isdigit():
|
||||
return int(duration)
|
||||
|
||||
if isinstance(parsed_duration, int):
|
||||
return parsed_duration
|
||||
else:
|
||||
raise LumiException(CONST.STRINGS["error_invalid_duration"].format(duration))
|
||||
try:
|
||||
parsed_duration: int = parse(duration) # type: ignore
|
||||
return max(0, parsed_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:
|
||||
|
@ -132,7 +138,61 @@ def format_seconds_to_duration_string(seconds: int) -> str:
|
|||
|
||||
if days > 0:
|
||||
return f"{days}d{hours}h" if hours > 0 else f"{days}d"
|
||||
elif hours > 0:
|
||||
if hours > 0:
|
||||
return f"{hours}h{minutes}m" if minutes > 0 else f"{hours}h"
|
||||
else:
|
||||
return f"{minutes}m"
|
||||
|
||||
return f"{minutes}m"
|
||||
|
||||
|
||||
def generate_usage(
|
||||
command: commands.Command[Any, Any, Any],
|
||||
flag_converter: type[commands.FlagConverter] | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Generate a usage string for a command with flags.
|
||||
Credit to https://github.com/allthingslinux/tux (thanks kaizen ;p)
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command : commands.Command
|
||||
The command for which to generate the usage string.
|
||||
flag_converter : type[commands.FlagConverter]
|
||||
The flag converter class for the command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The usage string for the command. Example: "ban [target] -[reason] -<silent>"
|
||||
"""
|
||||
|
||||
# Get the name of the command
|
||||
command_name = command.qualified_name
|
||||
|
||||
# Start the usage string with the command name
|
||||
usage = f"{command_name}"
|
||||
|
||||
# Get the parameters of the command (excluding the `ctx` and `flags` parameters)
|
||||
parameters: dict[str, commands.Parameter] = command.clean_params
|
||||
|
||||
flag_prefix = getattr(flag_converter, "__commands_flag_prefix__", "-")
|
||||
flags: dict[str, commands.Flag] = flag_converter.get_flags() if flag_converter else {}
|
||||
|
||||
# Add non-flag arguments to the usage string
|
||||
for param_name, param in parameters.items():
|
||||
# Ignore these parameters
|
||||
if param_name in ["ctx", "flags"]:
|
||||
continue
|
||||
# Determine if the parameter is required
|
||||
is_required = param.default == inspect.Parameter.empty
|
||||
# Add the parameter to the usage string with required or optional wrapping
|
||||
usage += f" <{param_name}>" if is_required else f" [{param_name}]"
|
||||
|
||||
# Add flag arguments to the usage string
|
||||
for flag_name, flag_obj in flags.items():
|
||||
# Determine if the flag is required or optional
|
||||
if flag_obj.required:
|
||||
usage += f" {flag_prefix}<{flag_name}>"
|
||||
else:
|
||||
usage += f" {flag_prefix}[{flag_name}]"
|
||||
|
||||
return usage
|
180
lib/help.py
Normal file
180
lib/help.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
import os
|
||||
from collections.abc import Mapping
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
class LuminaraHelp(commands.HelpCommand):
|
||||
def __init__(self):
|
||||
"""Initializes the LuminaraHelp command with necessary attributes."""
|
||||
super().__init__(
|
||||
command_attrs={
|
||||
"help": "Lists all commands and sub-commands.",
|
||||
"aliases": ["h"],
|
||||
"usage": "$help <command> or <sub-command>",
|
||||
},
|
||||
)
|
||||
|
||||
async def _get_prefix(self) -> str:
|
||||
"""
|
||||
Dynamically fetches the prefix from the context or uses a default prefix constant.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
The prefix used to invoke the bot.
|
||||
"""
|
||||
return "."
|
||||
|
||||
def _embed_base(self, author: str, description: str | None = None) -> discord.Embed:
|
||||
"""
|
||||
Creates a base embed with uniform styling.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
title : str
|
||||
The title of the embed.
|
||||
description : str | None
|
||||
The description of the embed.
|
||||
|
||||
Returns
|
||||
-------
|
||||
discord.Embed
|
||||
The created embed.
|
||||
"""
|
||||
return Builder.create_embed(
|
||||
theme="info",
|
||||
author_text=author,
|
||||
description=description,
|
||||
footer_text=CONST.STRINGS["help_footer"],
|
||||
)
|
||||
|
||||
def _get_cog_groups(self) -> list[str]:
|
||||
"""
|
||||
Retrieves a list of cog groups from the 'modules' folder.
|
||||
|
||||
Returns
|
||||
-------
|
||||
list[str]
|
||||
A list of cog groups.
|
||||
"""
|
||||
cog_groups = sorted(
|
||||
[
|
||||
d
|
||||
for d in os.listdir("./modules")
|
||||
if Path(f"./modules/{d}").is_dir() and d not in ("__pycache__", "admin")
|
||||
],
|
||||
)
|
||||
if "moderation" in cog_groups:
|
||||
cog_groups.remove("moderation")
|
||||
cog_groups.insert(0, "moderation")
|
||||
return cog_groups
|
||||
|
||||
async def send_bot_help(
|
||||
self,
|
||||
mapping: Mapping[commands.Cog | None, list[commands.Command[Any, Any, Any]]],
|
||||
) -> None:
|
||||
"""
|
||||
Sends an overview of all commands in a single embed, grouped by module.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
mapping : Mapping[commands.Cog | None, list[commands.Command[Any, Any, Any]]]
|
||||
The mapping of cogs to commands.
|
||||
"""
|
||||
embed = self._embed_base("Luminara Help Overview")
|
||||
|
||||
cog_groups = self._get_cog_groups()
|
||||
for group in cog_groups:
|
||||
group_commands: list[commands.Command[Any, Any, Any]] = []
|
||||
for cog, commands_list in mapping.items():
|
||||
if cog and commands_list and cog.__module__.startswith(f"modules.{group}"):
|
||||
group_commands.extend(commands_list)
|
||||
if group_commands:
|
||||
command_list = ", ".join(f"`{c.name}`" for c in group_commands)
|
||||
embed.add_field(name=group.capitalize(), value=command_list, inline=False)
|
||||
|
||||
await self.get_destination().send(embed=embed)
|
||||
|
||||
async def _add_command_help_fields(self, embed: discord.Embed, command: commands.Command[Any, Any, Any]) -> None:
|
||||
"""
|
||||
Adds fields with usage and alias information for a command to an embed.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
embed : discord.Embed
|
||||
The embed to which the fields will be added.
|
||||
command : commands.Command[Any, Any, Any]
|
||||
The command whose details are to be added.
|
||||
"""
|
||||
prefix = await self._get_prefix()
|
||||
|
||||
embed.add_field(
|
||||
name="Usage",
|
||||
value=f"`{prefix}{command.usage or 'No usage.'}`",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
async def send_command_help(self, command: commands.Command[Any, Any, Any]) -> None:
|
||||
"""
|
||||
Sends a help message for a specific command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
command : commands.Command[Any, Any, Any]
|
||||
The command for which the help message is to be sent.
|
||||
"""
|
||||
prefix = await self._get_prefix()
|
||||
|
||||
author = f"{prefix}{command.qualified_name}"
|
||||
author += f" ({', '.join(command.aliases)})" if command.aliases else ""
|
||||
|
||||
embed = self._embed_base(
|
||||
author=author,
|
||||
description=f"> {command.help}" or "No description available.",
|
||||
)
|
||||
|
||||
await self._add_command_help_fields(embed, command)
|
||||
await self.get_destination().send(embed=embed)
|
||||
|
||||
async def send_group_help(self, group: commands.Group[Any, Any, Any]) -> None:
|
||||
"""
|
||||
Sends a help message for a specific command group.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
group : commands.Group[Any, Any, Any]
|
||||
The group for which the help message is to be sent.
|
||||
"""
|
||||
prefix = await self._get_prefix()
|
||||
embed = self._embed_base(
|
||||
author=f"{prefix}{group.qualified_name}",
|
||||
description=group.help or "No description available.",
|
||||
)
|
||||
|
||||
for command in group.commands:
|
||||
embed.add_field(name=command.name, value=command.short_doc or "No description available.", inline=False)
|
||||
|
||||
await self.get_destination().send(embed=embed)
|
||||
|
||||
async def send_error_message(self, error: str) -> None:
|
||||
"""
|
||||
Sends an error message.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
error : str
|
||||
The error message to be sent.
|
||||
"""
|
||||
embed = Builder.create_embed(
|
||||
theme="error",
|
||||
title="Error in help command",
|
||||
description=error,
|
||||
)
|
||||
await self.get_destination().send(embed=embed, delete_after=30)
|
|
@ -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
|
55
lib/loader.py
Normal file
55
lib/loader.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
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):
|
||||
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
|
||||
|
||||
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 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.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.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 cog_loader.load_cog_from_dir(dir_name="handlers")
|
||||
await bot.add_cog(cog_loader)
|
|
@ -1,36 +0,0 @@
|
|||
import random
|
||||
|
||||
|
||||
class ReactionHandler:
|
||||
def __init__(self):
|
||||
self.eightball = [
|
||||
"It is certain.",
|
||||
"It is decidedly so.",
|
||||
"Without a doubt.",
|
||||
"Yes - definitely.",
|
||||
"You may rely on it.",
|
||||
"As I see it, yes.",
|
||||
"Most likely.",
|
||||
"Outlook good.",
|
||||
"Yes.",
|
||||
"Signs point to yes.",
|
||||
"Reply hazy, try again.",
|
||||
"Ask again later.",
|
||||
"Better not tell you now.",
|
||||
"Cannot predict now.",
|
||||
"Concentrate and ask again.",
|
||||
"Don't count on it.",
|
||||
"My reply is no.",
|
||||
"My sources say no.",
|
||||
"Outlook not so good.",
|
||||
"Very doubtful.",
|
||||
]
|
||||
|
||||
async def handle_message(self, message):
|
||||
content = message.content.lower()
|
||||
|
||||
if (
|
||||
content.startswith("Lumi ") or content.startswith("Lumi, ")
|
||||
) and content.endswith("?"):
|
||||
response = random.choice(self.eightball)
|
||||
await message.reply(content=response)
|
19
lib/time.py
19
lib/time.py
|
@ -1,19 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import pytz
|
||||
|
||||
|
||||
def seconds_until(hours, minutes):
|
||||
eastern_timezone = pytz.timezone("US/Eastern")
|
||||
|
||||
now = datetime.datetime.now(eastern_timezone)
|
||||
|
||||
# Create a datetime object for the given time in the Eastern Timezone
|
||||
given_time = datetime.time(hours, minutes)
|
||||
future_exec = eastern_timezone.localize(datetime.datetime.combine(now, given_time))
|
||||
|
||||
# If the given time is before the current time, add one day to the future execution time
|
||||
if future_exec < now:
|
||||
future_exec += datetime.timedelta(days=1)
|
||||
|
||||
return (future_exec - now).total_seconds()
|
76
locales/bdays.en-US.json
Normal file
76
locales/bdays.en-US.json
Normal file
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
|
@ -14,7 +14,7 @@
|
|||
"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",
|
||||
"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.",
|
||||
|
@ -39,7 +39,7 @@
|
|||
"*elevator music* Welcome to **level {}**."
|
||||
],
|
||||
"21-40": [
|
||||
"**Level {}** 👍",
|
||||
"**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?",
|
||||
|
@ -68,7 +68,7 @@
|
|||
"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)",
|
||||
"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?",
|
|
@ -16,7 +16,10 @@
|
|||
"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}**.",
|
||||
"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}**.",
|
||||
|
@ -29,7 +32,7 @@
|
|||
"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_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}",
|
||||
|
@ -42,15 +45,15 @@
|
|||
"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}**.",
|
||||
"blackjack_hit": "hit",
|
||||
"blackjack_stand": "stand",
|
||||
"boost_default_description": "Thanks for boosting, **{0}**!!",
|
||||
"boost_default_title": "New Booster",
|
||||
"case_case_field": "Case:",
|
||||
|
@ -71,7 +74,7 @@
|
|||
"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_target_field_value": "`{0}` \ud83c\udfaf",
|
||||
"case_type_field": "Type:",
|
||||
"case_type_field_value": "`{0}`",
|
||||
"case_type_field_value_with_duration": "`{0} ({1})`",
|
||||
|
@ -87,6 +90,7 @@
|
|||
"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_boost_total_count": "Total server boosts: {0}",
|
||||
"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.",
|
||||
|
@ -98,15 +102,15 @@
|
|||
"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_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": "📈 | **lucas** Lol it took you this long to reach **Level 15**.",
|
||||
"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": "📖 Case commands",
|
||||
"config_modlog_info_commands_name": "\ud83d\udcd6 Case commands",
|
||||
"config_modlog_info_commands_value": "`/cases` - View all cases in this server\n`/case <case_id>` - View a specific case\n`/editcase <case_id> <new_reason>` - Update a case reason",
|
||||
"config_modlog_info_description": "This channel has been set as the moderation log channel for **{0}**. All moderation actions issued with Lumi will be logged here as cases.",
|
||||
"config_modlog_info_warning_name": "⚠️ Warning",
|
||||
"config_modlog_info_warning_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}`",
|
||||
|
@ -115,21 +119,24 @@
|
|||
"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_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": "⚠️ **Not configured** (channel deleted?)",
|
||||
"config_show_moderation_log_enabled": "✅ {0}",
|
||||
"config_show_moderation_log_not_configured": "⚠️ **Not configured yet**",
|
||||
"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.",
|
||||
"config_xpreward_added": "xp reward for **Level {0}** with role {1} has been added.",
|
||||
"config_xpreward_removed": "xp reward for **Level {0}** has been removed.",
|
||||
"config_xpreward_show_no_rewards": "**There are no XP rewards set up yet.**\n\nTo add a reward, use `/config xpreward add`.",
|
||||
"daily_already_claimed_author": "Already Claimed",
|
||||
"daily_already_claimed_description": "you can claim your daily reward again <t:{0}:R>.",
|
||||
"daily_already_claimed_footer": "Daily reset is at 7 AM EST",
|
||||
|
@ -137,12 +144,12 @@
|
|||
"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}**.",
|
||||
"dev_clear_tree": "The application command tree has been cleared.",
|
||||
"dev_sync_tree": "The application command tree has been synced.",
|
||||
"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.",
|
||||
|
@ -150,6 +157,7 @@
|
|||
"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.",
|
||||
|
@ -167,18 +175,24 @@
|
|||
"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_not_owner": "{0} tried to use a bot admin command ({1})",
|
||||
"error_not_owner_unknown": "Unknown",
|
||||
"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": "you gave **${1}** to {2}.",
|
||||
"greet_default_description": "_ _\n**Welcome** to **{0}**",
|
||||
"greet_template_description": "↓↓↓\n{0}",
|
||||
"greet_template_description": "\u2193\u2193\u2193\n{0}",
|
||||
"help_footer": "Help Service",
|
||||
"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",
|
||||
|
@ -192,6 +206,7 @@
|
|||
"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",
|
||||
|
@ -203,15 +218,16 @@
|
|||
"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": "📈 | **{0}** you have reached **Level {1}**.",
|
||||
"level_up_prefix": "📈 | **{0}** ",
|
||||
"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.",
|
||||
"lumi_exception_generic": "An error occurred. Please try again later.",
|
||||
"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_banned_user": "user `{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}`",
|
||||
|
@ -230,7 +246,8 @@
|
|||
"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_timeout_too_long": "you cannot timeout a user for longer than 27 days.",
|
||||
"mod_unbanned": "user `{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",
|
||||
|
@ -239,10 +256,15 @@
|
|||
"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 <t:{0}:R>.",
|
||||
"stats_blackjack": "🃏 | You've played **{0}** games of BlackJack, betting a total of **${1}**. You won **{2}** of those games with a total payout of **${3}**.",
|
||||
"stats_slots": "🎰 | You've played **{0}** games of Slots, betting a total of **${1}**. Your total payout was **${2}**.",
|
||||
"slowmode_channel_not_found": "Channel not found.",
|
||||
"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}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}**.",
|
||||
"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",
|
||||
|
@ -283,14 +305,5 @@
|
|||
"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!"
|
||||
"xp_server_rank": "Server Rank: #{0}"
|
||||
}
|
49
main.py
Normal file
49
main.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
import asyncio
|
||||
import sys
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from lib.client import Luminara
|
||||
from lib.const import CONST
|
||||
from lib.help import LuminaraHelp
|
||||
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: Luminara, message: discord.Message) -> list[str]:
|
||||
extras = GuildConfig.get_prefix(message)
|
||||
return commands.when_mentioned_or(*extras)(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),
|
||||
case_insensitive=True,
|
||||
strip_after_prefix=True,
|
||||
help_command=LuminaraHelp(),
|
||||
)
|
||||
|
||||
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())
|
0
modules/__init__.py
Normal file
0
modules/__init__.py
Normal file
|
@ -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))
|
109
modules/admin/admin.py
Normal file
109
modules/admin/admin.py
Normal file
|
@ -0,0 +1,109 @@
|
|||
import mysql.connector
|
||||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
from db import database
|
||||
from lib.const import CONST
|
||||
from lib.format import shorten
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
class Sql(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.select_cmd.usage = lib.format.generate_usage(self.select_cmd)
|
||||
self.inject_cmd.usage = lib.format.generate_usage(self.inject_cmd)
|
||||
|
||||
@commands.command(name="sqlselect", aliases=["sqls"])
|
||||
@commands.is_owner()
|
||||
async def select_cmd(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
query: str,
|
||||
) -> None:
|
||||
"""
|
||||
Execute a SQL SELECT query.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
query : str
|
||||
The SQL query to execute.
|
||||
"""
|
||||
if query.lower().startswith("select "):
|
||||
query = query[7:]
|
||||
|
||||
try:
|
||||
results = database.select_query(f"SELECT {query}")
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["admin_sql_select_title"],
|
||||
description=CONST.STRINGS["admin_sql_select_description"].format(
|
||||
shorten(query, 200),
|
||||
shorten(str(results), 200),
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
except mysql.connector.Error as error:
|
||||
embed = Builder.create_embed(
|
||||
theme="error",
|
||||
user_name=ctx.author.name,
|
||||
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),
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed, ephemeral=True)
|
||||
|
||||
@commands.command(name="sqlinject", aliases=["sqli"])
|
||||
@commands.is_owner()
|
||||
async def inject_cmd(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
*,
|
||||
query: str,
|
||||
) -> None:
|
||||
"""
|
||||
Execute a SQL INJECT query.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
query : str
|
||||
The SQL query to execute.
|
||||
"""
|
||||
try:
|
||||
database.execute_query(query)
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["admin_sql_inject_title"],
|
||||
description=CONST.STRINGS["admin_sql_inject_description"].format(
|
||||
shorten(query, 200),
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
except mysql.connector.Error as error:
|
||||
embed = Builder.create_embed(
|
||||
theme="error",
|
||||
user_name=ctx.author.name,
|
||||
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),
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed, ephemeral=True)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Sql(bot))
|
|
@ -1,23 +1,53 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from services.currency_service import Currency
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
async def cmd(ctx, user: discord.User, amount: int):
|
||||
# Currency handler
|
||||
curr = Currency(user.id)
|
||||
curr.add_balance(amount)
|
||||
curr.push()
|
||||
class Award(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.award_command.usage = lib.format.generate_usage(self.award_command)
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
@commands.command(name="award", aliases=["aw"])
|
||||
@commands.is_owner()
|
||||
async def award_command(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
user: discord.User,
|
||||
amount: int,
|
||||
) -> None:
|
||||
"""
|
||||
Award a user with a specified amount of currency.
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
user : discord.User
|
||||
The user to award.
|
||||
amount : int
|
||||
The amount of currency to award.
|
||||
"""
|
||||
curr = Currency(user.id)
|
||||
curr.add_balance(amount)
|
||||
curr.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["admin_award_title"],
|
||||
description=CONST.STRINGS["admin_award_description"].format(
|
||||
Currency.format(amount),
|
||||
user.name,
|
||||
),
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Award(bot))
|
||||
|
|
|
@ -1,26 +1,51 @@
|
|||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from services.blacklist_service import BlacklistUserService
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
async def blacklist_user(
|
||||
ctx,
|
||||
user: discord.User,
|
||||
reason: Optional[str] = None,
|
||||
) -> None:
|
||||
blacklist_service = BlacklistUserService(user.id)
|
||||
blacklist_service.add_to_blacklist(reason)
|
||||
class Blacklist(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.blacklist_command.usage = lib.format.generate_usage(self.blacklist_command)
|
||||
|
||||
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,
|
||||
)
|
||||
@commands.command(name="blacklist")
|
||||
@commands.is_owner()
|
||||
async def blacklist_command(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
user: discord.User,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Blacklist a user from the bot.
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
user : discord.User
|
||||
The user to blacklist.
|
||||
reason : str | None, optional
|
||||
"""
|
||||
blacklist_service = BlacklistUserService(user.id)
|
||||
blacklist_service.add_to_blacklist(reason)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["admin_blacklist_author"],
|
||||
description=CONST.STRINGS["admin_blacklist_description"].format(user.name),
|
||||
footer_text=CONST.STRINGS["admin_blacklist_footer"],
|
||||
hide_time=True,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Blacklist(bot))
|
||||
|
|
70
modules/admin/dev.py
Normal file
70
modules/admin/dev.py
Normal file
|
@ -0,0 +1,70 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
|
||||
|
||||
class Dev(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.sync.usage = lib.format.generate_usage(self.sync)
|
||||
self.clear.usage = lib.format.generate_usage(self.clear)
|
||||
|
||||
@commands.group(name="dev", description="Lumi developer commands")
|
||||
@commands.guild_only()
|
||||
@commands.is_owner()
|
||||
async def dev(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
pass
|
||||
|
||||
@dev.command(
|
||||
name="sync_tree",
|
||||
aliases=["sync"],
|
||||
)
|
||||
async def sync(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
guild: discord.Guild | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Sync the bot's tree to the specified guild.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
guild : discord.Guild | None, optional
|
||||
The guild to sync the tree to, by default None.
|
||||
"""
|
||||
if guild:
|
||||
self.bot.tree.copy_global_to(guild=guild)
|
||||
|
||||
await self.bot.tree.sync(guild=guild)
|
||||
|
||||
await ctx.send(content=CONST.STRINGS["dev_sync_tree"])
|
||||
|
||||
@dev.command(
|
||||
name="clear_tree",
|
||||
aliases=["clear"],
|
||||
)
|
||||
async def clear(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
guild: discord.Guild | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Clear the bot's tree for the specified guild.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
guild : discord.Guild | None, optional
|
||||
"""
|
||||
self.bot.tree.clear_commands(guild=guild)
|
||||
|
||||
await ctx.send(content=CONST.STRINGS["dev_clear_tree"])
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Dev(bot))
|
|
@ -1,60 +0,0 @@
|
|||
import mysql.connector
|
||||
|
||||
from db import database
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.formatter import shorten
|
||||
|
||||
|
||||
async def select_cmd(ctx, query: str):
|
||||
if query.lower().startswith("select "):
|
||||
query = query[7:]
|
||||
|
||||
try:
|
||||
results = database.select_query(f"SELECT {query}")
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_select_title"],
|
||||
description=CONST.STRINGS["admin_sql_select_description"].format(
|
||||
shorten(query, 200),
|
||||
shorten(str(results), 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
except mysql.connector.Error as error:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_select_error_title"],
|
||||
description=CONST.STRINGS["admin_sql_select_error_description"].format(
|
||||
shorten(query, 200),
|
||||
shorten(str(error), 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed, ephemeral=True)
|
||||
|
||||
|
||||
async def inject_cmd(ctx, query: str):
|
||||
try:
|
||||
database.execute_query(query)
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_inject_title"],
|
||||
description=CONST.STRINGS["admin_sql_inject_description"].format(
|
||||
shorten(query, 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
except mysql.connector.Error as error:
|
||||
embed = EmbedBuilder.create_error_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sql_inject_error_title"],
|
||||
description=CONST.STRINGS["admin_sql_inject_error_description"].format(
|
||||
shorten(query, 200),
|
||||
shorten(str(error), 200),
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed, ephemeral=True)
|
|
@ -1,20 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
|
||||
|
||||
async def sync_commands(client, ctx):
|
||||
try:
|
||||
await client.sync_commands()
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["admin_sync_title"],
|
||||
description=CONST.STRINGS["admin_sync_description"],
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
except discord.HTTPException as e:
|
||||
raise LumiException(
|
||||
CONST.STRINGS["admin_sync_error_description"].format(e),
|
||||
) from e
|
|
@ -1,46 +0,0 @@
|
|||
import datetime
|
||||
|
||||
import discord
|
||||
import pytz
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import commands, tasks
|
||||
|
||||
from lib import checks
|
||||
from lib.constants import CONST
|
||||
from modules.birthdays import birthday, daily_check
|
||||
|
||||
|
||||
class Birthdays(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
self.daily_birthday_check.start()
|
||||
|
||||
birthday = SlashCommandGroup(
|
||||
name="birthday",
|
||||
description="Birthday commands.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
|
||||
@birthday.command(name="set", description="Set your birthday in this server.")
|
||||
@checks.birthdays_enabled()
|
||||
@discord.commands.option(name="month", choices=CONST.BIRTHDAY_MONTHS)
|
||||
async def set_birthday(self, ctx, month, day: int):
|
||||
index = CONST.BIRTHDAY_MONTHS.index(month) + 1
|
||||
await birthday.add(ctx, month, index, day)
|
||||
|
||||
@birthday.command(name="delete", description="Delete your birthday in this server.")
|
||||
async def delete_birthday(self, ctx):
|
||||
await birthday.delete(ctx)
|
||||
|
||||
@birthday.command(name="upcoming", description="Shows the upcoming birthdays.")
|
||||
@checks.birthdays_enabled()
|
||||
async def upcoming_birthdays(self, ctx):
|
||||
await birthday.upcoming(ctx)
|
||||
|
||||
@tasks.loop(time=datetime.time(hour=12, minute=0, tzinfo=pytz.UTC)) # 12 PM UTC
|
||||
async def daily_birthday_check(self):
|
||||
await daily_check.daily_birthday_check(self.client)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Birthdays(client))
|
|
@ -1,85 +1,211 @@
|
|||
import asyncio
|
||||
import calendar
|
||||
import datetime
|
||||
import random
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from discord import app_commands
|
||||
from discord.ext import commands, tasks
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.birthday_service import Birthday
|
||||
from lib.checks import birthdays_enabled
|
||||
from lib.const import CONST
|
||||
from services.birthday_service import BirthdayService
|
||||
from services.config_service import GuildConfig
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
async def add(ctx, month, month_index, day):
|
||||
leap_year = 2020
|
||||
max_days = calendar.monthrange(leap_year, month_index)[1]
|
||||
@app_commands.guild_only()
|
||||
@app_commands.default_permissions(manage_guild=True)
|
||||
class Birthday(commands.GroupCog, group_name="birthday"):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.daily_birthday_check.start()
|
||||
|
||||
if not 1 <= day <= max_days:
|
||||
raise commands.BadArgument(CONST.STRINGS["birthday_add_invalid_date"])
|
||||
@tasks.loop(time=datetime.time(hour=12, minute=0, tzinfo=ZoneInfo("UTC")))
|
||||
async def daily_birthday_check(self):
|
||||
logger.info(CONST.STRINGS["birthday_check_started"])
|
||||
birthdays_today = BirthdayService.get_birthdays_today()
|
||||
processed_birthdays = 0
|
||||
failed_birthdays = 0
|
||||
|
||||
date_obj = datetime.datetime(leap_year, month_index, day)
|
||||
if birthdays_today:
|
||||
for user_id, guild_id in birthdays_today:
|
||||
try:
|
||||
guild = await self.bot.fetch_guild(guild_id)
|
||||
member = await guild.fetch_member(user_id)
|
||||
guild_config = GuildConfig(guild.id)
|
||||
|
||||
birthday = Birthday(ctx.author.id, ctx.guild.id)
|
||||
birthday.set(date_obj)
|
||||
if not guild_config.birthday_channel_id:
|
||||
logger.debug(
|
||||
CONST.STRINGS["birthday_check_skipped"].format(guild.id),
|
||||
)
|
||||
continue
|
||||
|
||||
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)
|
||||
message = random.choice(CONST.BIRTHDAY_MESSAGES)
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
author_text="Happy Birthday!",
|
||||
description=message.format(member.name),
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
embed.set_image(url=CONST.BIRTHDAY_GIF_URL)
|
||||
|
||||
channel = await guild.fetch_channel(guild_config.birthday_channel_id)
|
||||
assert isinstance(channel, discord.TextChannel)
|
||||
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
|
||||
|
||||
async def delete(ctx):
|
||||
Birthday(ctx.author.id, ctx.guild.id).delete()
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
CONST.STRINGS["birthday_check_error"].format(user_id, guild_id, e),
|
||||
)
|
||||
failed_birthdays += 1
|
||||
|
||||
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)
|
||||
# wait one second to avoid rate limits
|
||||
await asyncio.sleep(1)
|
||||
|
||||
|
||||
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,
|
||||
logger.info(
|
||||
CONST.STRINGS["birthday_check_finished"].format(
|
||||
processed_birthdays,
|
||||
failed_birthdays,
|
||||
),
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
return
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["birthday_upcoming_author"],
|
||||
description="",
|
||||
show_name=False,
|
||||
@app_commands.command(name="set")
|
||||
@birthdays_enabled()
|
||||
@app_commands.choices(
|
||||
month=[discord.app_commands.Choice(name=month_name, value=month_name) for month_name in CONST.BIRTHDAY_MONTHS],
|
||||
)
|
||||
embed.set_thumbnail(url=CONST.LUMI_LOGO_TRANSPARENT)
|
||||
async def set_birthday(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
month: str,
|
||||
day: int,
|
||||
) -> None:
|
||||
"""
|
||||
Set your birthday.
|
||||
|
||||
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,
|
||||
),
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction object.
|
||||
month : Month
|
||||
The month of your birthday.
|
||||
day : int
|
||||
The day of your birthday.
|
||||
"""
|
||||
assert interaction.guild
|
||||
leap_year = 2020
|
||||
month_index = CONST.BIRTHDAY_MONTHS.index(month) + 1
|
||||
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, tzinfo=ZoneInfo("US/Eastern"))
|
||||
|
||||
birthday = BirthdayService(interaction.user.id, interaction.guild.id)
|
||||
birthday.set(date_obj)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["birthday_add_success_author"],
|
||||
description=CONST.STRINGS["birthday_add_success_description"].format(
|
||||
month,
|
||||
day,
|
||||
),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="remove")
|
||||
async def remove_birthday(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
) -> None:
|
||||
"""
|
||||
Remove your birthday.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction object.
|
||||
"""
|
||||
assert interaction.guild
|
||||
BirthdayService(interaction.user.id, interaction.guild.id).delete()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["birthday_delete_success_author"],
|
||||
description=CONST.STRINGS["birthday_delete_success_description"],
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="upcoming")
|
||||
@birthdays_enabled()
|
||||
async def upcoming_birthdays(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
) -> None:
|
||||
"""
|
||||
View upcoming birthdays.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction object.
|
||||
"""
|
||||
assert interaction.guild
|
||||
upcoming_birthdays = BirthdayService.get_upcoming_birthdays(interaction.guild.id)
|
||||
|
||||
if not upcoming_birthdays:
|
||||
embed = Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["birthday_upcoming_no_birthdays_author"],
|
||||
description=CONST.STRINGS["birthday_upcoming_no_birthdays"],
|
||||
)
|
||||
except (discord.HTTPException, ValueError):
|
||||
continue
|
||||
await interaction.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
embed.description = "\n".join(birthday_lines)
|
||||
await ctx.respond(embed=embed)
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["birthday_upcoming_author"],
|
||||
description="",
|
||||
)
|
||||
embed.set_thumbnail(url=CONST.LUMI_LOGO_TRANSPARENT)
|
||||
|
||||
birthday_lines: list[str] = []
|
||||
for user_id, birthday in upcoming_birthdays[:10]:
|
||||
try:
|
||||
member = await interaction.guild.fetch_member(user_id)
|
||||
birthday_date = datetime.datetime.strptime(birthday, "%m-%d").replace(tzinfo=ZoneInfo("US/Eastern"))
|
||||
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 interaction.response.send_message(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Birthday(bot))
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
import asyncio
|
||||
import random
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.birthday_service import Birthday
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def daily_birthday_check(client):
|
||||
logger.info(CONST.STRINGS["birthday_check_started"])
|
||||
birthdays_today = Birthday.get_birthdays_today()
|
||||
processed_birthdays = 0
|
||||
failed_birthdays = 0
|
||||
|
||||
if birthdays_today:
|
||||
for user_id, guild_id in birthdays_today:
|
||||
try:
|
||||
guild = await client.fetch_guild(guild_id)
|
||||
member = await guild.fetch_member(user_id)
|
||||
guild_config = GuildConfig(guild.id)
|
||||
|
||||
if not guild_config.birthday_channel_id:
|
||||
logger.debug(
|
||||
CONST.STRINGS["birthday_check_skipped"].format(guild.id),
|
||||
)
|
||||
continue
|
||||
|
||||
message = random.choice(CONST.BIRTHDAY_MESSAGES)
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
None,
|
||||
author_text="Happy Birthday!",
|
||||
description=message.format(member.name),
|
||||
show_name=False,
|
||||
)
|
||||
embed.set_image(url=CONST.BIRTHDAY_GIF_URL)
|
||||
|
||||
channel = await guild.fetch_channel(guild_config.birthday_channel_id)
|
||||
await channel.send(embed=embed, content=member.mention)
|
||||
logger.debug(
|
||||
CONST.STRINGS["birthday_check_success"].format(
|
||||
member.id,
|
||||
guild.id,
|
||||
channel.id,
|
||||
),
|
||||
)
|
||||
processed_birthdays += 1
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
CONST.STRINGS["birthday_check_error"].format(user_id, guild_id, e),
|
||||
)
|
||||
failed_birthdays += 1
|
||||
|
||||
# wait one second to avoid rate limits
|
||||
await asyncio.sleep(1)
|
||||
|
||||
logger.info(
|
||||
CONST.STRINGS["birthday_check_finished"].format(
|
||||
processed_birthdays,
|
||||
failed_birthdays,
|
||||
),
|
||||
)
|
|
@ -1,174 +0,0 @@
|
|||
import discord
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.config import (
|
||||
c_birthday,
|
||||
c_boost,
|
||||
c_greet,
|
||||
c_level,
|
||||
c_moderation,
|
||||
c_prefix,
|
||||
c_show,
|
||||
xp_reward,
|
||||
)
|
||||
|
||||
|
||||
class Config(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="xprewards",
|
||||
aliases=["xpr"],
|
||||
description="Show your server's XP rewards list.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def xp_reward_command_show(self, ctx):
|
||||
await xp_reward.show(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="addxpreward",
|
||||
aliases=["axpr"],
|
||||
description="Add a Lumi XP reward.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def xp_reward_command_add(
|
||||
self,
|
||||
ctx,
|
||||
level: int,
|
||||
role: discord.Role,
|
||||
persistent: bool = False,
|
||||
):
|
||||
await xp_reward.add_reward(ctx, level, role.id, persistent)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="removexpreward",
|
||||
aliases=["rxpr"],
|
||||
description="Remove a Lumi XP reward.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.has_permissions(manage_roles=True)
|
||||
async def xp_reward_command_remove(self, ctx, level: int):
|
||||
await xp_reward.remove_reward(ctx, level)
|
||||
|
||||
"""
|
||||
CONFIG GROUPS
|
||||
The 'config' group consists of many different configuration types, each being guild-specific and guild-only.
|
||||
All commands in this group are exclusively available as slash-commands.
|
||||
Only administrators can access commands in this group.
|
||||
|
||||
- Birthdays
|
||||
- Welcome
|
||||
- Boosts
|
||||
- Levels
|
||||
- Prefix
|
||||
- Modlog channel
|
||||
- Permissions preset (coming soon)
|
||||
|
||||
Running '/config show' will show a list of all available configuration types.
|
||||
"""
|
||||
config = SlashCommandGroup(
|
||||
"config",
|
||||
"server config commands.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
default_member_permissions=discord.Permissions(administrator=True),
|
||||
)
|
||||
|
||||
@config.command(name="show")
|
||||
async def config_command(self, ctx):
|
||||
await c_show.cmd(ctx)
|
||||
|
||||
birthday_config = config.create_subgroup(name="birthdays")
|
||||
|
||||
@birthday_config.command(name="channel")
|
||||
async def config_birthdays_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_birthday.set_birthday_channel(ctx, channel)
|
||||
|
||||
@birthday_config.command(name="disable")
|
||||
async def config_birthdays_disable(self, ctx):
|
||||
await c_birthday.disable_birthday_module(ctx)
|
||||
|
||||
welcome_config = config.create_subgroup(name="greetings")
|
||||
|
||||
@welcome_config.command(name="channel")
|
||||
async def config_welcome_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_greet.set_welcome_channel(ctx, channel)
|
||||
|
||||
@welcome_config.command(name="disable")
|
||||
async def config_welcome_disable(self, ctx):
|
||||
await c_greet.disable_welcome_module(ctx)
|
||||
|
||||
@welcome_config.command(name="template")
|
||||
@discord.commands.option(name="text", type=str, max_length=2000)
|
||||
async def config_welcome_template(self, ctx, text):
|
||||
await c_greet.set_welcome_template(ctx, text)
|
||||
|
||||
boost_config = config.create_subgroup(name="boosts")
|
||||
|
||||
@boost_config.command(name="channel")
|
||||
async def config_boosts_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_boost.set_boost_channel(ctx, channel)
|
||||
|
||||
@boost_config.command(name="disable")
|
||||
async def config_boosts_disable(self, ctx):
|
||||
await c_boost.disable_boost_module(ctx)
|
||||
|
||||
@boost_config.command(name="template")
|
||||
@discord.commands.option(name="text", type=str, max_length=2000)
|
||||
async def config_boosts_template(self, ctx, text):
|
||||
await c_boost.set_boost_template(ctx, text)
|
||||
|
||||
@boost_config.command(name="image")
|
||||
@discord.commands.option(name="url", type=str, max_length=2000)
|
||||
async def config_boosts_image(self, ctx, url):
|
||||
await c_boost.set_boost_image(ctx, url)
|
||||
|
||||
level_config = config.create_subgroup(name="levels")
|
||||
|
||||
@level_config.command(name="channel")
|
||||
async def config_level_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_level.set_level_channel(ctx, channel)
|
||||
|
||||
@level_config.command(name="currentchannel")
|
||||
async def config_level_samechannel(self, ctx):
|
||||
await c_level.set_level_current_channel(ctx)
|
||||
|
||||
@level_config.command(name="disable")
|
||||
async def config_level_disable(self, ctx):
|
||||
await c_level.disable_level_module(ctx)
|
||||
|
||||
@level_config.command(name="enable")
|
||||
async def config_level_enable(self, ctx):
|
||||
await c_level.enable_level_module(ctx)
|
||||
|
||||
@level_config.command(name="type")
|
||||
@discord.commands.option(name="type", choices=["whimsical", "generic"])
|
||||
async def config_level_type(self, ctx, type):
|
||||
await c_level.set_level_type(ctx, type)
|
||||
|
||||
@level_config.command(name="template")
|
||||
async def config_level_template(self, ctx, text: str):
|
||||
await c_level.set_level_template(ctx, text)
|
||||
|
||||
prefix_config = config.create_subgroup(name="prefix")
|
||||
|
||||
@prefix_config.command(name="set")
|
||||
async def config_prefix_set(self, ctx, prefix: str):
|
||||
await c_prefix.set_prefix(ctx, prefix)
|
||||
|
||||
modlog = config.create_subgroup(name="moderation")
|
||||
|
||||
@modlog.command(name="log")
|
||||
async def config_moderation_log_channel(self, ctx, channel: discord.TextChannel):
|
||||
await c_moderation.set_mod_log_channel(ctx, channel)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Config(client))
|
|
@ -1,43 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_birthday_channel(ctx, channel: discord.TextChannel):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.birthday_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_channel_set"].format(
|
||||
channel.mention,
|
||||
),
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_birthday_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.birthday_channel_id:
|
||||
embed = EmbedBuilder().create_warning_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_module_already_disabled"],
|
||||
)
|
||||
|
||||
else:
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_module_disabled"],
|
||||
)
|
||||
guild_config.birthday_channel_id = None
|
||||
guild_config.push()
|
||||
|
||||
return await ctx.respond(embed=embed)
|
|
@ -1,125 +0,0 @@
|
|||
import discord
|
||||
|
||||
import lib.formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_boost_channel(ctx, channel: discord.TextChannel):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.boost_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_boost_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.boost_channel_id:
|
||||
embed = EmbedBuilder().create_warning_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_module_already_disabled"],
|
||||
)
|
||||
else:
|
||||
guild_config.boost_channel_id = None
|
||||
guild_config.boost_message = None
|
||||
guild_config.push()
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_module_disabled"],
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_boost_template(ctx, text: str):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.boost_message = text
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_template_updated"],
|
||||
footer_text=CONST.STRINGS["config_example_next_footer"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_boost_template_field"],
|
||||
value=f"```{text}```",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
example_embed = create_boost_embed(ctx.author, text, guild_config.boost_image_url)
|
||||
return await ctx.send(embed=example_embed, content=ctx.author.mention)
|
||||
|
||||
|
||||
async def set_boost_image(ctx, image_url: str | None):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if image_url is None or image_url.lower() == "original":
|
||||
guild_config.boost_image_url = None
|
||||
guild_config.push()
|
||||
image_url = None
|
||||
elif not image_url.endswith(CONST.ALLOWED_IMAGE_EXTENSIONS):
|
||||
raise LumiException(CONST.STRINGS["error_boost_image_url_invalid"])
|
||||
elif not image_url.startswith(("http://", "https://")):
|
||||
raise LumiException(CONST.STRINGS["error_image_url_invalid"])
|
||||
else:
|
||||
guild_config.boost_image_url = image_url
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_image_updated"],
|
||||
footer_text=CONST.STRINGS["config_example_next_footer"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_boost_image_field"],
|
||||
value=image_url or CONST.STRINGS["config_boost_image_original"],
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
example_embed = create_boost_embed(
|
||||
ctx.author,
|
||||
guild_config.boost_message,
|
||||
image_url,
|
||||
)
|
||||
return await ctx.send(embed=example_embed, content=ctx.author.mention)
|
||||
|
||||
|
||||
async def create_boost_embed(
|
||||
member: discord.Member,
|
||||
template: str | None = None,
|
||||
image_url: str | None = None,
|
||||
):
|
||||
embed = discord.Embed(
|
||||
color=discord.Color.nitro_pink(),
|
||||
title=CONST.STRINGS["boost_default_title"],
|
||||
description=CONST.STRINGS["boost_default_description"].format(member.name),
|
||||
)
|
||||
|
||||
if template:
|
||||
embed.description = lib.formatter.template(template, member.name)
|
||||
|
||||
embed.set_author(name=member.name, icon_url=member.display_avatar)
|
||||
embed.set_image(url=image_url or CONST.BOOST_ICON)
|
||||
embed.set_footer(
|
||||
text=f"Total server boosts: {member.guild.premium_subscription_count}",
|
||||
icon_url=CONST.EXCLAIM_ICON,
|
||||
)
|
|
@ -1,96 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import MemberConverter
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_welcome_channel(ctx, channel: discord.TextChannel) -> None:
|
||||
if not ctx.guild:
|
||||
raise LumiException()
|
||||
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
guild_config.welcome_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_welcome_module(ctx) -> None:
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
|
||||
if not guild_config.welcome_channel_id:
|
||||
embed: discord.Embed = EmbedBuilder().create_warning_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_module_already_disabled"],
|
||||
)
|
||||
else:
|
||||
guild_config.welcome_channel_id = None
|
||||
guild_config.welcome_message = None
|
||||
guild_config.push()
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_module_disabled"],
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_welcome_template(ctx, text: str) -> None:
|
||||
if not ctx.guild:
|
||||
raise LumiException()
|
||||
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
guild_config.welcome_message = text
|
||||
guild_config.push()
|
||||
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_template_updated"],
|
||||
footer_text=CONST.STRINGS["config_example_next_footer"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_welcome_template_field"],
|
||||
value=f"```{text}```",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
greet_member: discord.Member = await MemberConverter().convert(ctx, str(ctx.author))
|
||||
example_embed: discord.Embed = create_greet_embed(greet_member, text)
|
||||
await ctx.send(embed=example_embed, content=ctx.author.mention)
|
||||
|
||||
|
||||
def create_greet_embed(
|
||||
member: discord.Member,
|
||||
template: Optional[str] = None,
|
||||
) -> discord.Embed:
|
||||
embed: discord.Embed = discord.Embed(
|
||||
color=discord.Color.embed_background(),
|
||||
description=CONST.STRINGS["greet_default_description"].format(
|
||||
member.guild.name,
|
||||
),
|
||||
)
|
||||
if template and embed.description is not None:
|
||||
embed.description += CONST.STRINGS["greet_template_description"].format(
|
||||
formatter.template(template, member.name),
|
||||
)
|
||||
|
||||
embed.set_thumbnail(url=member.display_avatar.url)
|
||||
|
||||
return embed
|
|
@ -1,137 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_level_channel(ctx, channel: discord.TextChannel):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
if guild_config.level_message_type == 0:
|
||||
embed.set_footer(text=CONST.STRINGS["config_level_module_disabled_warning"])
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_level_current_channel(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_channel_id = None
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_current_channel_set"],
|
||||
)
|
||||
|
||||
if guild_config.level_message_type == 0:
|
||||
embed.set_footer(text=CONST.STRINGS["config_level_module_disabled_warning"])
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def disable_level_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_message_type = 0
|
||||
guild_config.push()
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_disabled"],
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def enable_level_module(ctx):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
if guild_config.level_message_type != 0:
|
||||
embed = EmbedBuilder().create_info_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_already_enabled"],
|
||||
)
|
||||
else:
|
||||
guild_config.level_message_type = 1
|
||||
guild_config.push()
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_enabled"],
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_level_type(ctx, type: str):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
)
|
||||
|
||||
guild_config.level_message = None
|
||||
if type == "whimsical":
|
||||
guild_config.level_message_type = 1
|
||||
guild_config.push()
|
||||
|
||||
embed.description = CONST.STRINGS["config_level_type_whimsical"]
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_type_example"],
|
||||
value=CONST.STRINGS["config_level_type_whimsical_example"],
|
||||
inline=False,
|
||||
)
|
||||
else:
|
||||
guild_config.level_message_type = 2
|
||||
guild_config.push()
|
||||
|
||||
embed.description = CONST.STRINGS["config_level_type_generic"]
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_type_example"],
|
||||
value=CONST.STRINGS["config_level_type_generic_example"],
|
||||
inline=False,
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def set_level_template(ctx, text: str):
|
||||
guild_config = GuildConfig(ctx.guild.id)
|
||||
guild_config.level_message = text
|
||||
guild_config.push()
|
||||
|
||||
preview = formatter.template(text, "Lucas", 15)
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_template_updated"],
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_template"],
|
||||
value=f"```{text}```",
|
||||
inline=False,
|
||||
)
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_level_type_example"],
|
||||
value=preview,
|
||||
inline=False,
|
||||
)
|
||||
|
||||
if guild_config.level_message_type == 0:
|
||||
embed.set_footer(text=CONST.STRINGS["config_level_module_disabled_warning"])
|
||||
|
||||
return await ctx.respond(embed=embed)
|
|
@ -1,44 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.moderation.modlog_service import ModLogService
|
||||
|
||||
|
||||
async def set_mod_log_channel(ctx, channel: discord.TextChannel):
|
||||
mod_log = ModLogService()
|
||||
|
||||
info_embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_modlog_info_author"],
|
||||
description=CONST.STRINGS["config_modlog_info_description"].format(
|
||||
ctx.guild.name,
|
||||
),
|
||||
show_name=False,
|
||||
)
|
||||
info_embed.add_field(
|
||||
name=CONST.STRINGS["config_modlog_info_commands_name"],
|
||||
value=CONST.STRINGS["config_modlog_info_commands_value"],
|
||||
inline=False,
|
||||
)
|
||||
info_embed.add_field(
|
||||
name=CONST.STRINGS["config_modlog_info_warning_name"],
|
||||
value=CONST.STRINGS["config_modlog_info_warning_value"],
|
||||
inline=False,
|
||||
)
|
||||
|
||||
try:
|
||||
await channel.send(embed=info_embed)
|
||||
except discord.errors.Forbidden as e:
|
||||
raise LumiException(CONST.STRINGS["config_modlog_permission_error"]) from e
|
||||
|
||||
mod_log.set_modlog_channel(ctx.guild.id, channel.id)
|
||||
|
||||
success_embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_modlog_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
return await ctx.respond(embed=success_embed)
|
|
@ -1,35 +0,0 @@
|
|||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
|
||||
|
||||
async def set_prefix(ctx, prefix):
|
||||
if len(prefix) > 25:
|
||||
embed = EmbedBuilder().create_error_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_too_long"],
|
||||
)
|
||||
return await ctx.respond(embed=embed)
|
||||
|
||||
guild_config = GuildConfig(
|
||||
ctx.guild.id,
|
||||
) # generate a guild_config for if it didn't already exist
|
||||
GuildConfig.set_prefix(guild_config.guild_id, prefix)
|
||||
|
||||
embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_set"].format(prefix),
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def get_prefix(ctx):
|
||||
prefix = GuildConfig.get_prefix_from_guild_id(ctx.guild.id) if ctx.guild else "."
|
||||
embed = EmbedBuilder().create_info_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_get"].format(prefix),
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,75 +0,0 @@
|
|||
from typing import List, Tuple, Optional
|
||||
|
||||
import discord
|
||||
from discord import Guild
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.config_service import GuildConfig
|
||||
from services.moderation.modlog_service import ModLogService
|
||||
|
||||
|
||||
async def cmd(ctx) -> None:
|
||||
guild_config: GuildConfig = GuildConfig(ctx.guild.id)
|
||||
guild: Guild = ctx.guild
|
||||
embed: discord.Embed = EmbedBuilder().create_success_embed(
|
||||
ctx=ctx,
|
||||
author_text=CONST.STRINGS["config_show_author"].format(guild.name),
|
||||
thumbnail_url=guild.icon.url if guild.icon else CONST.LUMI_LOGO_TRANSPARENT,
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
config_items: List[Tuple[str, bool, bool]] = [
|
||||
(
|
||||
CONST.STRINGS["config_show_birthdays"],
|
||||
bool(guild_config.birthday_channel_id),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CONST.STRINGS["config_show_new_member_greets"],
|
||||
bool(guild_config.welcome_channel_id),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CONST.STRINGS["config_show_boost_announcements"],
|
||||
bool(guild_config.boost_channel_id),
|
||||
False,
|
||||
),
|
||||
(
|
||||
CONST.STRINGS["config_show_level_announcements"],
|
||||
guild_config.level_message_type != 0,
|
||||
False,
|
||||
),
|
||||
]
|
||||
|
||||
for name, enabled, default_enabled in config_items:
|
||||
status: str = (
|
||||
CONST.STRINGS["config_show_enabled"]
|
||||
if enabled
|
||||
else CONST.STRINGS["config_show_disabled"]
|
||||
)
|
||||
if not enabled and default_enabled:
|
||||
status = CONST.STRINGS["config_show_default_enabled"]
|
||||
embed.add_field(name=name, value=status, inline=False)
|
||||
|
||||
modlog_service: ModLogService = ModLogService()
|
||||
modlog_channel_id: Optional[int] = modlog_service.fetch_modlog_channel_id(guild.id)
|
||||
modlog_channel = guild.get_channel(modlog_channel_id) if modlog_channel_id else None
|
||||
|
||||
modlog_status: str
|
||||
if modlog_channel:
|
||||
modlog_status = CONST.STRINGS["config_show_moderation_log_enabled"].format(
|
||||
modlog_channel.mention,
|
||||
)
|
||||
elif modlog_channel_id:
|
||||
modlog_status = CONST.STRINGS["config_show_moderation_log_channel_deleted"]
|
||||
else:
|
||||
modlog_status = CONST.STRINGS["config_show_moderation_log_not_configured"]
|
||||
|
||||
embed.add_field(
|
||||
name=CONST.STRINGS["config_show_moderation_log"],
|
||||
value=modlog_status,
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
797
modules/config/config.py
Normal file
797
modules/config/config.py
Normal file
|
@ -0,0 +1,797 @@
|
|||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from lib.exceptions import LumiException
|
||||
from services.config_service import GuildConfig
|
||||
from services.modlog_service import ModLogService
|
||||
from services.xp_service import XpRewardService
|
||||
from ui.config import create_boost_embed, create_greet_embed
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
@app_commands.guild_only()
|
||||
@app_commands.default_permissions(administrator=True)
|
||||
class Config(commands.GroupCog, group_name="config"):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
birthdays = app_commands.Group(name="birthdays", description="Configure the birthdays module")
|
||||
boosts = app_commands.Group(name="boosts", description="Configure the boosts module")
|
||||
greets = app_commands.Group(name="greets", description="Configure the greets module")
|
||||
levels = app_commands.Group(name="levels", description="Configure the levels module")
|
||||
moderation = app_commands.Group(name="moderation", description="Configure the moderation module")
|
||||
prefix = app_commands.Group(name="prefix", description="Configure the prefix for the bot")
|
||||
xpreward = app_commands.Group(name="xpreward", description="Configure the xp reward for the bot")
|
||||
|
||||
@app_commands.command(name="show")
|
||||
async def config_help(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Show the current configuration for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to show the config for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config: GuildConfig = GuildConfig(interaction.guild.id)
|
||||
guild: discord.Guild = interaction.guild
|
||||
embed: discord.Embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_show_author"].format(guild.name),
|
||||
thumbnail_url=guild.icon.url if guild.icon else CONST.LUMI_LOGO_TRANSPARENT,
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
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: int | None = 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 interaction.response.send_message(embed=embed)
|
||||
|
||||
@birthdays.command(name="channel")
|
||||
async def birthday_channel(self, interaction: discord.Interaction, channel: discord.TextChannel) -> None:
|
||||
"""
|
||||
Set the birthday channel for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the birthday channel for.
|
||||
channel : discord.TextChannel
|
||||
The channel to set as the birthday channel.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
guild_config.birthday_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_channel_set"].format(
|
||||
channel.mention,
|
||||
),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@birthdays.command(name="disable")
|
||||
async def birthday_disable(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Disable the birthday module for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to disable the birthday module for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
|
||||
if not guild_config.birthday_channel_id:
|
||||
embed = Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_module_already_disabled"],
|
||||
)
|
||||
|
||||
else:
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_birthday_module_disabled"],
|
||||
)
|
||||
guild_config.birthday_channel_id = None
|
||||
guild_config.push()
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@levels.command(name="channel")
|
||||
async def set_level_channel(self, interaction: discord.Interaction, channel: discord.TextChannel) -> None:
|
||||
"""
|
||||
Set the level-up announcement channel for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the level-up announcement channel for.
|
||||
channel : discord.TextChannel
|
||||
The channel to set as the level-up announcement channel.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
guild_config.level_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
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"])
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@boosts.command(name="channel")
|
||||
async def set_boost_channel(self, interaction: discord.Interaction, channel: discord.TextChannel) -> None:
|
||||
"""
|
||||
Set the boost announcement channel for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the boost announcement channel for.
|
||||
channel : discord.TextChannel
|
||||
The channel to set as the boost announcement channel.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
guild_config.boost_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@boosts.command(name="disable")
|
||||
async def disable_boost_module(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Disable the boost module for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to disable the boost module for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
|
||||
if not guild_config.boost_channel_id:
|
||||
embed = Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=interaction.user.name,
|
||||
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 = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_boost_module_disabled"],
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@boosts.command(name="template")
|
||||
async def set_boost_template(self, interaction: discord.Interaction, text: str) -> None:
|
||||
"""
|
||||
Set the boost message template for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the boost message template for.
|
||||
text : str
|
||||
The template text to set for boost messages.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
guild_config.boost_message = text
|
||||
guild_config.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
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 interaction.response.send_message(embed=embed)
|
||||
|
||||
example_embed = create_boost_embed(
|
||||
user_name=interaction.user.name,
|
||||
user_avatar_url=interaction.user.display_avatar.url,
|
||||
boost_count=interaction.guild.premium_subscription_count,
|
||||
template=text,
|
||||
image_url=guild_config.boost_image_url,
|
||||
)
|
||||
await interaction.followup.send(embed=example_embed, content=interaction.user.mention)
|
||||
|
||||
@boosts.command(name="image")
|
||||
async def set_boost_image(self, interaction: discord.Interaction, image_url: str | None) -> None:
|
||||
"""
|
||||
Set the boost message image for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the boost message image for.
|
||||
image_url : str | None
|
||||
The image URL to set for boost messages.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.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(tuple(CONST.ALLOWED_IMAGE_EXTENSIONS)):
|
||||
raise ValueError(CONST.STRINGS["error_boost_image_url_invalid"])
|
||||
elif not image_url.startswith(("http://", "https://")):
|
||||
raise ValueError(CONST.STRINGS["error_image_url_invalid"])
|
||||
else:
|
||||
guild_config.boost_image_url = image_url
|
||||
guild_config.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
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 interaction.response.send_message(embed=embed)
|
||||
|
||||
example_embed = create_boost_embed(
|
||||
user_name=interaction.user.name,
|
||||
user_avatar_url=interaction.user.display_avatar.url,
|
||||
boost_count=interaction.guild.premium_subscription_count,
|
||||
template=guild_config.boost_message,
|
||||
image_url=image_url,
|
||||
)
|
||||
await interaction.followup.send(embed=example_embed, content=interaction.user.mention)
|
||||
|
||||
@greets.command(name="channel")
|
||||
async def set_welcome_channel(self, interaction: discord.Interaction, channel: discord.TextChannel) -> None:
|
||||
"""
|
||||
Set the welcome channel for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the welcome channel for.
|
||||
channel : discord.TextChannel
|
||||
The channel to set as the welcome channel.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config: GuildConfig = GuildConfig(interaction.guild.id)
|
||||
guild_config.welcome_channel_id = channel.id
|
||||
guild_config.push()
|
||||
|
||||
embed: discord.Embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@greets.command(name="disable")
|
||||
async def disable_welcome_module(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Disable the welcome module for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to disable the welcome module for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config: GuildConfig = GuildConfig(interaction.guild.id)
|
||||
|
||||
if not guild_config.welcome_channel_id:
|
||||
embed: discord.Embed = Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=interaction.user.name,
|
||||
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 = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_welcome_module_disabled"],
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@greets.command(name="template")
|
||||
async def set_welcome_template(self, interaction: discord.Interaction, text: str) -> None:
|
||||
"""
|
||||
Set the welcome message template for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the welcome message template for.
|
||||
text : str
|
||||
The welcome message template.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config: GuildConfig = GuildConfig(interaction.guild.id)
|
||||
guild_config.welcome_message = text
|
||||
guild_config.push()
|
||||
|
||||
embed: discord.Embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
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 interaction.response.send_message(embed=embed)
|
||||
|
||||
example_embed: discord.Embed = create_greet_embed(
|
||||
user_name=interaction.user.name,
|
||||
user_avatar_url=interaction.user.display_avatar.url,
|
||||
guild_name=interaction.guild.name,
|
||||
template=text,
|
||||
)
|
||||
|
||||
await interaction.followup.send(embed=example_embed, content=interaction.user.mention)
|
||||
|
||||
@levels.command(name="current_channel")
|
||||
async def set_level_current_channel(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Set the current channel as the level-up announcement channel for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the current channel as the level-up announcement channel for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
guild_config.level_channel_id = None
|
||||
guild_config.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
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"])
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@levels.command(name="disable")
|
||||
async def disable_level_module(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Disable the level-up module for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to disable the level-up module for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
guild_config.level_message_type = 0
|
||||
guild_config.push()
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_disabled"],
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@levels.command(name="enable")
|
||||
async def enable_level_module(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Enable the level-up module for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to enable the level-up module for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
|
||||
if guild_config.level_message_type != 0:
|
||||
embed = Builder.create_embed(
|
||||
theme="info",
|
||||
user_name=interaction.user.name,
|
||||
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 = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_level_module_enabled"],
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@levels.command(name="template")
|
||||
async def set_level_template(self, interaction: discord.Interaction, text: str) -> None:
|
||||
"""
|
||||
Set the template for level-up messages for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the template for level-up messages for.
|
||||
text : str
|
||||
The template text to set for level-up messages.
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
guild_config.level_message = text
|
||||
guild_config.push()
|
||||
|
||||
preview = lib.format.template(text, "Lucas", 15)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
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"])
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@levels.command(name="type")
|
||||
async def set_level_type(self, interaction: discord.Interaction, level_type: str) -> None:
|
||||
"""
|
||||
Set the type of level-up messages for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the type of level-up messages for.
|
||||
level_type : str
|
||||
The type of level-up messages to set (e.g., "whimsical" or "generic").
|
||||
"""
|
||||
assert interaction.guild
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
)
|
||||
|
||||
guild_config.level_message = None
|
||||
if level_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,
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@moderation.command(name="log")
|
||||
async def set_mod_log_channel(self, interaction: discord.Interaction, channel: discord.TextChannel) -> None:
|
||||
"""
|
||||
Set the moderation log channel for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the moderation log channel for.
|
||||
channel : discord.TextChannel
|
||||
The channel to set as the moderation log channel.
|
||||
"""
|
||||
assert interaction.guild
|
||||
mod_log = ModLogService()
|
||||
|
||||
info_embed = Builder.create_embed(
|
||||
theme="info",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_modlog_info_author"],
|
||||
description=CONST.STRINGS["config_modlog_info_description"].format(
|
||||
interaction.guild.name,
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
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(interaction.guild.id, channel.id)
|
||||
|
||||
success_embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_modlog_channel_set"].format(channel.mention),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=success_embed)
|
||||
|
||||
@prefix.command(name="set")
|
||||
async def set_prefix(self, interaction: discord.Interaction, prefix: str) -> None:
|
||||
"""
|
||||
Set the prefix for the bot in the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to set the prefix for.
|
||||
prefix : str
|
||||
The prefix to set for the bot.
|
||||
"""
|
||||
assert interaction.guild
|
||||
if len(prefix) > 25:
|
||||
embed = Builder.create_embed(
|
||||
theme="error",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_too_long"],
|
||||
)
|
||||
await interaction.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
guild_config = GuildConfig(interaction.guild.id)
|
||||
GuildConfig.set_prefix(guild_config.guild_id, prefix)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_prefix_set"].format(prefix),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@xpreward.command(name="show")
|
||||
async def show_xpreward(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Show the current XP rewards for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to show the XP rewards for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
level_reward = XpRewardService(interaction.guild.id)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="info",
|
||||
user_name=interaction.user.name,
|
||||
author_text="Level Rewards",
|
||||
thumbnail_url=interaction.guild.icon.url if interaction.guild.icon else CONST.LUMI_LOGO_OPAQUE,
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
if not level_reward.rewards:
|
||||
embed.description = CONST.STRINGS["config_xpreward_show_no_rewards"]
|
||||
else:
|
||||
for level in sorted(level_reward.rewards.keys()):
|
||||
role_id, persistent = level_reward.rewards[level]
|
||||
role = interaction.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 interaction.response.send_message(embed=embed)
|
||||
|
||||
@xpreward.command(name="add")
|
||||
async def add_xpreward(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
level: int,
|
||||
role: discord.Role,
|
||||
persistent: bool,
|
||||
) -> None:
|
||||
"""
|
||||
Add an XP reward for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to add the XP reward for.
|
||||
level : int
|
||||
The level to add the reward for.
|
||||
role : discord.Role
|
||||
The role to assign as a reward.
|
||||
persistent : bool
|
||||
Whether the reward is persistent.
|
||||
"""
|
||||
assert interaction.guild
|
||||
level_reward = XpRewardService(interaction.guild.id)
|
||||
level_reward.add_reward(level, role.id, persistent)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_xpreward_added"].format(level, role.mention),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@xpreward.command(name="remove")
|
||||
async def remove_xpreward(self, interaction: discord.Interaction, level: int) -> None:
|
||||
"""
|
||||
Remove an XP reward for the server.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to remove the XP reward for.
|
||||
level : int
|
||||
The level to remove the reward for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
level_reward = XpRewardService(interaction.guild.id)
|
||||
level_reward.remove_reward(level)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["config_author"],
|
||||
description=CONST.STRINGS["config_xpreward_removed"].format(level),
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Config(bot))
|
|
@ -1,43 +0,0 @@
|
|||
import discord
|
||||
|
||||
from lib.constants import CONST
|
||||
from services.xp_service import XpRewardService
|
||||
|
||||
|
||||
async def show(ctx):
|
||||
level_reward = XpRewardService(ctx.guild.id)
|
||||
|
||||
embed = discord.Embed(
|
||||
color=discord.Color.embed_background(),
|
||||
description="",
|
||||
)
|
||||
|
||||
icon = ctx.guild.icon or CONST.LUMI_LOGO_OPAQUE
|
||||
embed.set_author(name="Level Rewards", icon_url=icon)
|
||||
for level in sorted(level_reward.rewards.keys()):
|
||||
role_id, persistent = level_reward.rewards[level]
|
||||
role = ctx.guild.get_role(role_id)
|
||||
|
||||
if embed.description is None:
|
||||
embed.description = ""
|
||||
|
||||
embed.description += (
|
||||
f"\n**Level {level}** -> {role.mention if role else 'Role not found'}"
|
||||
)
|
||||
|
||||
if persistent:
|
||||
embed.description += " (persistent)"
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def add_reward(ctx, level, role_id, persistent):
|
||||
level_reward = XpRewardService(ctx.guild.id)
|
||||
level_reward.add_reward(level, role_id, persistent)
|
||||
await show(ctx)
|
||||
|
||||
|
||||
async def remove_reward(ctx, level):
|
||||
level_reward = XpRewardService(ctx.guild.id)
|
||||
level_reward.remove_reward(level)
|
||||
await show(ctx)
|
|
@ -1,77 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.economy import balance, blackjack, daily, give, slots
|
||||
|
||||
|
||||
class Economy(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="balance",
|
||||
aliases=["bal", "$"],
|
||||
description="Shows your current Lumi balance.",
|
||||
help="Shows your current Lumi balance. The economy system is global, meaning your balance will be synced in "
|
||||
"all servers.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.cooldown(1, 10, commands.BucketType.user)
|
||||
async def balance_command(self, ctx):
|
||||
return await balance.cmd(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="blackjack",
|
||||
aliases=["bj"],
|
||||
description="Start a game of blackjack.",
|
||||
help="Start a game of blackjack.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def blackjack_command(self, ctx, *, bet: int):
|
||||
return await blackjack.cmd(ctx, bet)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="daily",
|
||||
aliases=["timely"],
|
||||
description="Claim your daily reward.",
|
||||
help="Claim your daily reward! Reset is at 7 AM EST.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def daily_command(self, ctx):
|
||||
return await daily.cmd(ctx)
|
||||
|
||||
@commands.slash_command(
|
||||
name="give",
|
||||
description="Give a server member some cash.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
async def give_command(self, ctx, *, user: discord.Member, amount: int):
|
||||
return await give.cmd(ctx, user, amount)
|
||||
|
||||
@commands.command(
|
||||
name="give",
|
||||
help="Give a server member some cash. You can use ID or mention them.",
|
||||
)
|
||||
@guild_only()
|
||||
async def give_command_prefixed(self, ctx, user: discord.User, *, amount: int):
|
||||
return await give.cmd(ctx, user, amount)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="slots",
|
||||
aliases=["slot"],
|
||||
description="Start a slots game.",
|
||||
help="Spin the slots for a chance to win the jackpot!",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
@commands.cooldown(1, 5, commands.BucketType.user)
|
||||
async def slots_command(self, ctx, *, bet: int):
|
||||
return await slots.cmd(self, ctx, bet)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Economy(client))
|
|
@ -1,22 +1,50 @@
|
|||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from services.currency_service import Currency
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
async def cmd(ctx: commands.Context[commands.Bot]) -> None:
|
||||
ctx_currency = Currency(ctx.author.id)
|
||||
balance = Currency.format(ctx_currency.balance)
|
||||
class Balance(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot: commands.Bot = bot
|
||||
self.balance.usage = lib.format.generate_usage(self.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,
|
||||
@commands.hybrid_command(
|
||||
name="balance",
|
||||
aliases=["bal", "$"],
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def balance(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
) -> None:
|
||||
"""
|
||||
Check your current balance.
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
|
||||
ctx_currency = Currency(ctx.author.id)
|
||||
balance = Currency.format(ctx_currency.balance)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
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"],
|
||||
hide_name_in_description=True,
|
||||
hide_time=True,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Balance(bot))
|
||||
|
|
|
@ -1,324 +1,303 @@
|
|||
import random
|
||||
from typing import List, Tuple
|
||||
from loguru import logger
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import discord
|
||||
from discord.ui import View
|
||||
import pytz
|
||||
from discord.ext import commands
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from lib.exceptions import LumiException
|
||||
from services.currency_service import Currency
|
||||
from services.stats_service import BlackJackStats
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from ui.embeds import Builder
|
||||
from ui.views.blackjack import BlackJackButtons
|
||||
|
||||
EST = pytz.timezone("US/Eastern")
|
||||
EST = ZoneInfo("US/Eastern")
|
||||
ACTIVE_BLACKJACK_GAMES: dict[int, bool] = {}
|
||||
|
||||
Card = str
|
||||
Hand = List[Card]
|
||||
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"])
|
||||
class Blackjack(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot: commands.Bot = bot
|
||||
self.blackjack.usage = lib.format.generate_usage(self.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"])
|
||||
@commands.hybrid_command(
|
||||
name="blackjack",
|
||||
aliases=["bj"],
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def blackjack(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
bet: int,
|
||||
) -> None:
|
||||
"""
|
||||
Play a game of blackjack.
|
||||
|
||||
ACTIVE_BLACKJACK_GAMES[ctx.author.id] = True
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
bet : int
|
||||
The amount to bet.
|
||||
"""
|
||||
if ctx.author.id in ACTIVE_BLACKJACK_GAMES:
|
||||
raise LumiException(CONST.STRINGS["error_already_playing_blackjack"])
|
||||
|
||||
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]
|
||||
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
|
||||
|
||||
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
|
||||
try:
|
||||
await self.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]
|
||||
|
||||
player_value = calculate_hand_value(player_hand)
|
||||
status = 5 if player_value == 21 else 0
|
||||
view = BlackJackButtons(ctx)
|
||||
playing_embed = False
|
||||
async def play_blackjack(self, ctx: commands.Context[commands.Bot], currency: Currency, bet: int) -> None:
|
||||
deck = self.get_new_deck()
|
||||
player_hand, dealer_hand = self.initial_deal(deck)
|
||||
multiplier = CONST.BLACKJACK_MULTIPLIER
|
||||
|
||||
while status == 0:
|
||||
dealer_value = calculate_hand_value(dealer_hand)
|
||||
player_value = self.calculate_hand_value(player_hand)
|
||||
status = 5 if player_value == 21 else 0
|
||||
view = BlackJackButtons(ctx)
|
||||
playing_embed = False
|
||||
response_message: discord.Message | None = None
|
||||
|
||||
embed = create_game_embed(
|
||||
while status == 0:
|
||||
dealer_value = self.calculate_hand_value(dealer_hand)
|
||||
|
||||
embed = self.create_game_embed(
|
||||
ctx,
|
||||
bet,
|
||||
player_hand,
|
||||
dealer_hand,
|
||||
player_value,
|
||||
dealer_value,
|
||||
)
|
||||
if not playing_embed:
|
||||
response_message = await ctx.reply(embed=embed, view=view)
|
||||
playing_embed = True
|
||||
else:
|
||||
assert response_message
|
||||
await response_message.edit(embed=embed, view=view)
|
||||
|
||||
await view.wait()
|
||||
|
||||
if view.clickedHit:
|
||||
player_hand.append(self.deal_card(deck))
|
||||
player_value = self.calculate_hand_value(player_hand)
|
||||
if player_value > 21:
|
||||
status = 1
|
||||
break
|
||||
if player_value == 21:
|
||||
status = 2
|
||||
break
|
||||
elif view.clickedStand:
|
||||
status = self.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 self.handle_game_end(
|
||||
ctx,
|
||||
response_message,
|
||||
currency,
|
||||
bet,
|
||||
player_hand,
|
||||
dealer_hand,
|
||||
player_value,
|
||||
dealer_value,
|
||||
status,
|
||||
multiplier,
|
||||
playing_embed,
|
||||
)
|
||||
if not playing_embed:
|
||||
await ctx.respond(embed=embed, view=view, content=ctx.author.mention)
|
||||
playing_embed = True
|
||||
|
||||
def initial_deal(self, deck: list[Card]) -> tuple[Hand, Hand]:
|
||||
return [self.deal_card(deck) for _ in range(2)], [self.deal_card(deck)]
|
||||
|
||||
def dealer_play(self, deck: list[Card], dealer_hand: Hand, player_value: int) -> int:
|
||||
while self.calculate_hand_value(dealer_hand) <= player_value:
|
||||
dealer_hand.append(self.deal_card(deck))
|
||||
return 3 if self.calculate_hand_value(dealer_hand) > 21 else 4
|
||||
|
||||
async def handle_game_end(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
response_message: discord.Message | None,
|
||||
currency: Currency,
|
||||
bet: int,
|
||||
player_hand: Hand,
|
||||
dealer_hand: Hand,
|
||||
status: int,
|
||||
multiplier: float,
|
||||
playing_embed: bool,
|
||||
) -> None:
|
||||
player_value = self.calculate_hand_value(player_hand)
|
||||
dealer_value = self.calculate_hand_value(dealer_hand)
|
||||
payout = bet * (2 if status == 5 else multiplier)
|
||||
is_won = status not in [1, 4]
|
||||
|
||||
embed = self.create_end_game_embed(ctx, bet, player_value, dealer_value, payout, status)
|
||||
if playing_embed:
|
||||
assert response_message
|
||||
await response_message.edit(embed=embed)
|
||||
else:
|
||||
await ctx.edit(embed=embed, view=view)
|
||||
await ctx.reply(embed=embed)
|
||||
|
||||
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
|
||||
if is_won:
|
||||
currency.add_balance(int(payout))
|
||||
else:
|
||||
currency.take_balance(bet)
|
||||
currency.push()
|
||||
raise LumiException(CONST.STRINGS["error_out_of_time_economy"])
|
||||
currency.push()
|
||||
|
||||
view = BlackJackButtons(ctx)
|
||||
BlackJackStats(
|
||||
user_id=ctx.author.id,
|
||||
is_won=is_won,
|
||||
bet=bet,
|
||||
payout=int(payout) if is_won else 0,
|
||||
hand_player=player_hand,
|
||||
hand_dealer=dealer_hand,
|
||||
).push()
|
||||
|
||||
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,
|
||||
def create_game_embed(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
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:])
|
||||
)
|
||||
return False
|
||||
|
||||
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 Builder.create_embed(
|
||||
theme="default",
|
||||
user_name=ctx.author.name,
|
||||
title=CONST.STRINGS["blackjack_title"],
|
||||
description=description,
|
||||
footer_text=footer_text,
|
||||
footer_icon_url=CONST.MUFFIN_ART,
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
def create_end_game_embed(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
bet: int,
|
||||
player_value: int,
|
||||
dealer_value: int,
|
||||
payout: int | float,
|
||||
status: int,
|
||||
) -> discord.Embed:
|
||||
embed = Builder.create_embed(
|
||||
theme="default",
|
||||
user_name=ctx.author.name,
|
||||
title=CONST.STRINGS["blackjack_title"],
|
||||
description=CONST.STRINGS["blackjack_description"].format(
|
||||
player_value,
|
||||
dealer_value,
|
||||
),
|
||||
footer_text=CONST.STRINGS["blackjack_footer"],
|
||||
footer_icon_url=CONST.MUFFIN_ART,
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
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(int(payout))),
|
||||
discord.Color.green(),
|
||||
CONST.TROPHY_ART,
|
||||
),
|
||||
3: (
|
||||
CONST.STRINGS["blackjack_dealer_busted"],
|
||||
CONST.STRINGS["blackjack_won_payout"].format(Currency.format_human(int(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(int(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(self) -> 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(self, deck: list[Card]) -> Card:
|
||||
return deck.pop()
|
||||
|
||||
def calculate_hand_value(self, 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
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Blackjack(bot))
|
||||
|
|
|
@ -1,43 +1,85 @@
|
|||
from datetime import datetime, timedelta
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import lib.time
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from discord import Embed
|
||||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from services.currency_service import Currency
|
||||
from services.daily_service import Dailies
|
||||
from ui.embeds import Builder
|
||||
|
||||
tz = ZoneInfo("US/Eastern")
|
||||
|
||||
|
||||
async def cmd(ctx) -> None:
|
||||
ctx_daily = Dailies(ctx.author.id)
|
||||
def seconds_until(hours: int, minutes: int) -> int:
|
||||
now = datetime.now(tz)
|
||||
given_time = now.replace(hour=hours, minute=minutes, second=0, microsecond=0)
|
||||
|
||||
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)
|
||||
if given_time < now:
|
||||
given_time += timedelta(days=1)
|
||||
|
||||
ctx_daily.refresh()
|
||||
return int((given_time - now).total_seconds())
|
||||
|
||||
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,
|
||||
|
||||
class Daily(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot: commands.Bot = bot
|
||||
self.daily.usage = lib.format.generate_usage(self.daily)
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="daily",
|
||||
aliases=["timely"],
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def daily(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
) -> None:
|
||||
"""
|
||||
Claim your daily reward.
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
ctx_daily: Dailies = Dailies(ctx.author.id)
|
||||
|
||||
if not ctx_daily.can_be_claimed():
|
||||
wait_time: datetime = datetime.now(tz) + timedelta(seconds=seconds_until(7, 0))
|
||||
unix_time: int = int(round(wait_time.timestamp()))
|
||||
error_embed: Embed = Builder.create_embed(
|
||||
theme="error",
|
||||
user_name=ctx.author.name,
|
||||
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.send(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: Embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
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.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Daily(bot))
|
||||
|
|
|
@ -1,39 +1,70 @@
|
|||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from lib.exceptions import LumiException
|
||||
from services.currency_service import Currency
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
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"])
|
||||
class Give(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot: commands.Bot = bot
|
||||
self.give.usage = lib.format.generate_usage(self.give)
|
||||
|
||||
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,
|
||||
),
|
||||
@commands.hybrid_command(
|
||||
name="give",
|
||||
)
|
||||
@commands.guild_only()
|
||||
async def give(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
user: discord.User,
|
||||
amount: int,
|
||||
) -> None:
|
||||
"""
|
||||
Give currency to another user.
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
user : discord.User
|
||||
The user to give currency to.
|
||||
amount : int
|
||||
The amount of currency to give.
|
||||
"""
|
||||
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 = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
description=CONST.STRINGS["give_success"].format(
|
||||
Currency.format(amount),
|
||||
user.name,
|
||||
),
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Give(bot))
|
||||
|
|
|
@ -2,213 +2,254 @@ import asyncio
|
|||
import datetime
|
||||
import random
|
||||
from collections import Counter
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import discord
|
||||
import pytz
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from lib.exceptions import LumiException
|
||||
from services.currency_service import Currency
|
||||
from services.stats_service import SlotsStats
|
||||
|
||||
est = pytz.timezone("US/Eastern")
|
||||
est = ZoneInfo("US/Eastern")
|
||||
|
||||
|
||||
async def cmd(self, ctx, bet):
|
||||
ctx_currency = Currency(ctx.author.id)
|
||||
class Slots(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot: commands.Bot = bot
|
||||
self.slots.usage = lib.format.generate_usage(self.slots)
|
||||
|
||||
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),
|
||||
@commands.hybrid_command(
|
||||
name="slots",
|
||||
aliases=["slot"],
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
@commands.guild_only()
|
||||
async def slots(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
bet: int,
|
||||
) -> None:
|
||||
"""
|
||||
Play the slots machine.
|
||||
|
||||
for i in range(2, 0, -1):
|
||||
await ctx.edit(
|
||||
embed=slots_spinning(ctx, i, Currency.format_human(bet), results, emojis),
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
bet : int
|
||||
The amount to bet.
|
||||
"""
|
||||
ctx_currency: Currency = Currency(ctx.author.id)
|
||||
|
||||
player_balance: int = ctx_currency.balance
|
||||
if bet > player_balance:
|
||||
raise LumiException(CONST.STRINGS["error_not_enough_cash"])
|
||||
if bet <= 0:
|
||||
raise LumiException(CONST.STRINGS["error_invalid_bet"])
|
||||
|
||||
results: list[int] = [random.randint(0, 6) for _ in range(3)]
|
||||
calculated_results: tuple[str, int, float] = self.calculate_slots_results(bet, results)
|
||||
|
||||
result_type, payout, _ = calculated_results
|
||||
is_won: bool = result_type != "lost"
|
||||
emojis: dict[str, discord.Emoji | None] = self.get_emotes()
|
||||
|
||||
await ctx.defer()
|
||||
|
||||
message: discord.Message = await ctx.reply(
|
||||
embed=self.slots_spinning(ctx, 3, Currency.format_human(bet), results, emojis),
|
||||
)
|
||||
|
||||
for i in range(2, 0, -1):
|
||||
await asyncio.sleep(1)
|
||||
await message.edit(
|
||||
embed=self.slots_spinning(ctx, i, Currency.format_human(bet), results, emojis),
|
||||
)
|
||||
|
||||
# output final result
|
||||
finished_output: discord.Embed = self.slots_finished(
|
||||
ctx,
|
||||
result_type,
|
||||
Currency.format_human(bet),
|
||||
Currency.format_human(payout),
|
||||
results,
|
||||
emojis,
|
||||
)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
await message.edit(embed=finished_output)
|
||||
|
||||
# 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"
|
||||
# user payout
|
||||
if payout > 0:
|
||||
ctx_currency.add_balance(payout)
|
||||
else:
|
||||
result_type = "three_of_a_kind"
|
||||
multiplier = rewards[result_type]
|
||||
ctx_currency.take_balance(bet)
|
||||
|
||||
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']}"
|
||||
stats: SlotsStats = SlotsStats(
|
||||
user_id=ctx.author.id,
|
||||
is_won=is_won,
|
||||
bet=bet,
|
||||
payout=payout,
|
||||
spin_type=result_type,
|
||||
icons=[str(icon) for icon in results],
|
||||
)
|
||||
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",
|
||||
)
|
||||
ctx_currency.push()
|
||||
stats.push()
|
||||
|
||||
return embed
|
||||
def get_emotes(self) -> dict[str, discord.Emoji | None]:
|
||||
emotes: dict[str, int] = CONST.EMOTE_IDS
|
||||
return {name: self.bot.get_emoji(emoji_id) for name, emoji_id in emotes.items()}
|
||||
|
||||
def calculate_slots_results(self, bet: int, results: list[int]) -> tuple[str, int, float]:
|
||||
result_type: str = "lost"
|
||||
multiplier: float = 0.0
|
||||
rewards: dict[str, float] = CONST.SLOTS_MULTIPLIERS
|
||||
|
||||
# count occurrences of each item in the list
|
||||
counts: Counter[int] = Counter(results)
|
||||
|
||||
# no icons match
|
||||
if len(counts) == 3:
|
||||
result_type = "lost"
|
||||
multiplier = 0.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: int = int(bet * multiplier)
|
||||
return result_type, payout, multiplier
|
||||
|
||||
def slots_spinning(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
spinning_icons_amount: int,
|
||||
bet: str,
|
||||
results: list[int],
|
||||
emojis: dict[str, discord.Emoji | None],
|
||||
) -> discord.Embed:
|
||||
first_slots_emote: discord.Emoji | None = emojis.get(f"slots_{results[0]}_id")
|
||||
second_slots_emote: discord.Emoji | None = emojis.get(f"slots_{results[1]}_id")
|
||||
slots_animated_emote: discord.Emoji | None = emojis.get("slots_animated_id")
|
||||
|
||||
current_time: str = datetime.datetime.now(est).strftime("%I:%M %p")
|
||||
one: discord.Emoji | None = slots_animated_emote
|
||||
two: discord.Emoji | None = slots_animated_emote
|
||||
three: discord.Emoji | None = slots_animated_emote
|
||||
|
||||
if spinning_icons_amount == 1:
|
||||
one = first_slots_emote
|
||||
two = second_slots_emote
|
||||
|
||||
elif spinning_icons_amount == 2:
|
||||
one = first_slots_emote
|
||||
description: str = (
|
||||
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 = discord.Embed(
|
||||
description=description,
|
||||
)
|
||||
embed.set_author(name=ctx.author.name, icon_url=ctx.author.avatar.url if ctx.author.avatar else None)
|
||||
embed.set_footer(
|
||||
text=f"Bet ${bet} • jackpot = x15 • {current_time}",
|
||||
icon_url="https://i.imgur.com/wFsgSnr.png",
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
def slots_finished(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
payout_type: str,
|
||||
bet: str,
|
||||
payout: str,
|
||||
results: list[int],
|
||||
emojis: dict[str, discord.Emoji | None],
|
||||
) -> discord.Embed:
|
||||
first_slots_emote: discord.Emoji | None = emojis.get(f"slots_{results[0]}_id")
|
||||
second_slots_emote: discord.Emoji | None = emojis.get(f"slots_{results[1]}_id")
|
||||
third_slots_emote: discord.Emoji | None = emojis.get(f"slots_{results[2]}_id")
|
||||
current_time: str = datetime.datetime.now(est).strftime("%I:%M %p")
|
||||
|
||||
field_name: str = "You lost."
|
||||
field_value: str = f"You lost **${bet}**."
|
||||
color: discord.Color = discord.Color.red()
|
||||
is_lost: bool = 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: str = (
|
||||
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 = 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 if ctx.author.avatar else None)
|
||||
embed.set_footer(
|
||||
text=f"Game finished • {current_time}",
|
||||
icon_url="https://i.imgur.com/wFsgSnr.png",
|
||||
)
|
||||
|
||||
return embed
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Slots(bot))
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
from discord.ext import commands
|
||||
|
||||
from lib import constants, embed_builder, formatter
|
||||
|
||||
|
||||
class Help(commands.Cog):
|
||||
def __init__(self, client: commands.Bot) -> None:
|
||||
self.client = client
|
||||
|
||||
@commands.slash_command(
|
||||
name="help",
|
||||
description="Get Lumi help.",
|
||||
)
|
||||
async def help_command(self, ctx) -> None:
|
||||
prefix = formatter.get_prefix(ctx)
|
||||
embed = embed_builder.EmbedBuilder.create_warning_embed(
|
||||
ctx=ctx,
|
||||
description=constants.CONST.STRINGS["help_use_prefix"].format(prefix),
|
||||
)
|
||||
await ctx.respond(embed=embed, ephemeral=True)
|
||||
|
||||
|
||||
def setup(client: commands.Bot) -> None:
|
||||
client.add_cog(Help(client))
|
|
@ -1,36 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.levels import leaderboard, level
|
||||
|
||||
|
||||
class Levels(commands.Cog):
|
||||
def __init__(self, client: commands.Bot) -> None:
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="level",
|
||||
aliases=["rank", "xp"],
|
||||
description="Displays your level and server rank.",
|
||||
help="Displays your level and server rank.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def level_command(self, ctx) -> None:
|
||||
await level.rank(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="leaderboard",
|
||||
aliases=["lb", "xplb"],
|
||||
description="See the Lumi leaderboards.",
|
||||
help="Shows three different leaderboards: levels, currency and daily streaks.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def leaderboard_command(self, ctx) -> None:
|
||||
await leaderboard.cmd(ctx)
|
||||
|
||||
|
||||
def setup(client: commands.Bot) -> None:
|
||||
client.add_cog(Levels(client))
|
|
@ -1,202 +1,52 @@
|
|||
from datetime import datetime
|
||||
from typing import cast
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge
|
||||
from discord import Embed, Guild, Member
|
||||
from discord.ext import commands
|
||||
|
||||
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
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
from ui.views.leaderboard import LeaderboardCommandOptions, LeaderboardCommandView
|
||||
|
||||
|
||||
async def cmd(ctx: bridge.Context) -> None:
|
||||
if not ctx.guild:
|
||||
return
|
||||
class Leaderboard(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot) -> None:
|
||||
self.bot: commands.Bot = bot
|
||||
self.leaderboard.usage = lib.format.generate_usage(self.leaderboard)
|
||||
|
||||
options = LeaderboardCommandOptions()
|
||||
view = LeaderboardCommandView(ctx, options)
|
||||
|
||||
# default leaderboard
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx=ctx,
|
||||
thumbnail_url=CONST.FLOWERS_ART,
|
||||
show_name=False,
|
||||
@commands.hybrid_command(
|
||||
name="leaderboard",
|
||||
aliases=["lb"],
|
||||
)
|
||||
async def leaderboard(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Get the leaderboard for the server.
|
||||
|
||||
icon = ctx.guild.icon.url if ctx.guild.icon else CONST.FLOWERS_ART
|
||||
await view.populate_leaderboard("xp", embed, icon)
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
guild: Guild | None = ctx.guild
|
||||
if not guild:
|
||||
return
|
||||
|
||||
await ctx.respond(embed=embed, view=view)
|
||||
options: LeaderboardCommandOptions = LeaderboardCommandOptions()
|
||||
view: LeaderboardCommandView = LeaderboardCommandView(ctx, options)
|
||||
|
||||
|
||||
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="📅",
|
||||
),
|
||||
],
|
||||
author: Member = cast(Member, ctx.author)
|
||||
embed: Embed = Builder.create_embed(
|
||||
theme="info",
|
||||
user_name=author.name,
|
||||
thumbnail_url=author.display_avatar.url,
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
if self.view:
|
||||
await self.view.on_select(self.values[0], interaction)
|
||||
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)
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Leaderboard(bot))
|
||||
|
|
|
@ -1,31 +1,56 @@
|
|||
from discord import Embed
|
||||
from discord.ext import bridge
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from services.xp_service import XpService
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
async def rank(ctx: bridge.Context) -> None:
|
||||
if not ctx.guild:
|
||||
return
|
||||
class Level(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.level.usage = lib.format.generate_usage(self.level)
|
||||
|
||||
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,
|
||||
@commands.hybrid_command(
|
||||
name="level",
|
||||
aliases=["rank", "lvl", "xp"],
|
||||
)
|
||||
async def level(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Get the level of the user.
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
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))
|
||||
|
|
|
@ -1,135 +0,0 @@
|
|||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import bridge, commands, tasks
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from Client import LumiBot
|
||||
from modules.config import c_prefix
|
||||
from modules.misc import avatar, backup, info, introduction, invite, ping, xkcd
|
||||
|
||||
|
||||
class Misc(commands.Cog):
|
||||
def __init__(self, client: LumiBot) -> None:
|
||||
self.client: LumiBot = client
|
||||
self.start_time: datetime = datetime.now()
|
||||
self.do_backup.start()
|
||||
|
||||
@tasks.loop(hours=1)
|
||||
async def do_backup(self) -> None:
|
||||
await backup.backup()
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="avatar",
|
||||
aliases=["av"],
|
||||
description="Get a user's avatar.",
|
||||
help="Get a user's avatar.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@guild_only()
|
||||
async def avatar(self, ctx, user: discord.Member) -> None:
|
||||
return await avatar.get_avatar(ctx, user)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="ping",
|
||||
aliases=["p", "status"],
|
||||
description="Simple status check.",
|
||||
help="Simple status check.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def ping(self, ctx) -> None:
|
||||
await ping.ping(self, ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="uptime",
|
||||
description="See Lumi's uptime since the last update.",
|
||||
help="See how long Lumi has been online since his last update.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def uptime(self, ctx) -> None:
|
||||
await ping.uptime(self, ctx, self.start_time)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="invite",
|
||||
description="Generate an invite link.",
|
||||
help="Generate a link to invite Lumi to your own server!",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def invite_command(self, ctx) -> None:
|
||||
await invite.cmd(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="prefix",
|
||||
description="See the server's current prefix.",
|
||||
help="See the server's current prefix.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def prefix_command(self, ctx) -> None:
|
||||
await c_prefix.get_prefix(ctx)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="info",
|
||||
aliases=["stats"],
|
||||
description="Shows basic Lumi stats.",
|
||||
help="Shows basic Lumi stats.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
async def info_command(self, ctx) -> None:
|
||||
unix_timestamp: int = int(round(self.start_time.timestamp()))
|
||||
await info.cmd(self, ctx, unix_timestamp)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="introduction",
|
||||
aliases=["intro", "introduce"],
|
||||
description="This command can only be used in DMs.",
|
||||
help="Introduce yourself. For now this command "
|
||||
"can only be done in ONE server and only in Lumi's DMs.",
|
||||
contexts={discord.InteractionContextType.bot_dm},
|
||||
)
|
||||
@commands.dm_only()
|
||||
async def intro_command(self, ctx) -> None:
|
||||
await introduction.cmd(self, ctx)
|
||||
|
||||
"""
|
||||
xkcd submodule - slash command only
|
||||
"""
|
||||
xkcd: SlashCommandGroup = SlashCommandGroup(
|
||||
"xkcd",
|
||||
"A web comic of romance, sarcasm, math, and language.",
|
||||
contexts={
|
||||
discord.InteractionContextType.guild,
|
||||
discord.InteractionContextType.bot_dm,
|
||||
},
|
||||
)
|
||||
|
||||
@xkcd.command(name="latest", description="Get the latest xkcd comic.")
|
||||
async def xkcd_latest(self, ctx) -> None:
|
||||
await xkcd.print_comic(ctx, latest=True)
|
||||
|
||||
@xkcd.command(name="random", description="Get a random xkcd comic.")
|
||||
async def xkcd_random(self, ctx) -> None:
|
||||
await xkcd.print_comic(ctx)
|
||||
|
||||
@xkcd.command(name="search", description="Search for a xkcd comic by ID.")
|
||||
async def xkcd_search(self, ctx, *, id: int) -> None:
|
||||
await xkcd.print_comic(ctx, number=id)
|
||||
|
||||
|
||||
def setup(client: LumiBot) -> None:
|
||||
client.add_cog(Misc(client))
|
|
@ -1,39 +1,11 @@
|
|||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
import httpx
|
||||
from discord import File, Member
|
||||
from discord.ext import bridge
|
||||
from discord import File
|
||||
from discord.ext import commands
|
||||
|
||||
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.")
|
||||
import lib.format
|
||||
|
||||
|
||||
async def create_avatar_file(url: str) -> File:
|
||||
|
@ -50,9 +22,52 @@ async def create_avatar_file(url: str) -> File:
|
|||
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
|
||||
self.avatar.usage = lib.format.generate_usage(self.avatar)
|
||||
|
||||
@commands.hybrid_command(
|
||||
name="avatar",
|
||||
aliases=["av"],
|
||||
)
|
||||
async def avatar(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
member: discord.Member | None = 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 commands.MemberConverter().convert(ctx, str(ctx.author.id))
|
||||
|
||||
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]
|
||||
|
||||
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))
|
||||
|
|
|
@ -1,19 +1,22 @@
|
|||
import asyncio
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from typing import List, Optional
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import dropbox
|
||||
from dropbox.files import FileMetadata
|
||||
import dropbox # type: ignore
|
||||
from discord.ext import commands, tasks
|
||||
from dropbox.files import FileMetadata # type: ignore
|
||||
from loguru import logger
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.const import CONST
|
||||
|
||||
# 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,
|
||||
|
@ -22,36 +25,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.today().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(ZoneInfo("US/Eastern")).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
|
||||
if isinstance(entry, FileMetadata) # type: ignore
|
||||
]
|
||||
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)
|
||||
_dbx.files_delete_v2(f"/{file}") # type: ignore
|
||||
|
||||
|
||||
async def backup() -> None:
|
||||
|
@ -65,3 +74,22 @@ async def backup() -> None:
|
|||
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()
|
||||
|
||||
@do_backup.before_loop
|
||||
async def before_do_backup(self) -> None:
|
||||
await self.bot.wait_until_ready()
|
||||
await asyncio.sleep(30)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Backup(bot))
|
||||
|
|
|
@ -3,40 +3,48 @@ import platform
|
|||
|
||||
import discord
|
||||
import psutil
|
||||
from discord.ext import bridge
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.currency_service import Currency
|
||||
from services.stats_service import BlackJackStats
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
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())
|
||||
class Info(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.info.usage = lib.format.generate_usage(self.info)
|
||||
|
||||
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),
|
||||
],
|
||||
@commands.hybrid_command(
|
||||
name="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())
|
||||
|
||||
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)
|
||||
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),
|
||||
],
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
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"],
|
||||
thumbnail_url=CONST.LUMI_LOGO_OPAQUE,
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Info(bot))
|
||||
|
|
|
@ -1,166 +1,191 @@
|
|||
import asyncio
|
||||
from typing import Dict, Optional
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.interactions.introduction import (
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
from ui.views.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
|
||||
)
|
||||
class Introduction(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.introduction.usage = lib.format.generate_usage(self.introduction)
|
||||
|
||||
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"],
|
||||
),
|
||||
@commands.hybrid_command(name="introduction", aliases=["intro"])
|
||||
async def introduction(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Introduction command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
guild: discord.Guild | None = self.bot.get_guild(
|
||||
CONST.INTRODUCTIONS_GUILD_ID,
|
||||
)
|
||||
return
|
||||
member: discord.Member | None = guild.get_member(ctx.author.id) if guild else None
|
||||
|
||||
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():
|
||||
if not guild or not member:
|
||||
await ctx.send(
|
||||
embed=EmbedBuilder.create_embed(
|
||||
ctx,
|
||||
author_text=key,
|
||||
description=question,
|
||||
footer_text=CONST.STRINGS["intro_question_footer"],
|
||||
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
|
||||
|
||||
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()
|
||||
question_mapping: dict[str, str] = CONST.INTRODUCTIONS_QUESTION_MAPPING
|
||||
channel: discord.abc.GuildChannel | None = guild.get_channel(
|
||||
CONST.INTRODUCTIONS_CHANNEL_ID,
|
||||
)
|
||||
|
||||
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)
|
||||
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 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),
|
||||
)
|
||||
if view.clicked_stop:
|
||||
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,
|
||||
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 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))
|
||||
|
|
|
@ -1,27 +1,36 @@
|
|||
from discord import ButtonStyle
|
||||
from discord.ext import bridge
|
||||
from discord.ui import Button, View
|
||||
from discord.ext import commands
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
from ui.views.invite import InviteButton
|
||||
|
||||
|
||||
async def cmd(ctx: bridge.BridgeContext) -> None:
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
description=CONST.STRINGS["invite_description"],
|
||||
),
|
||||
view=InviteButton(),
|
||||
)
|
||||
class Invite(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.invite.usage = lib.format.generate_usage(self.invite)
|
||||
|
||||
@commands.hybrid_command(name="invite", aliases=["inv"])
|
||||
async def invite(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Invite command.
|
||||
|
||||
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,
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
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(),
|
||||
)
|
||||
self.add_item(invite_button)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Invite(bot))
|
||||
|
|
|
@ -1,32 +1,37 @@
|
|||
from datetime import datetime
|
||||
from discord.ext import commands
|
||||
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
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)
|
||||
class Ping(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.ping.usage = lib.format.generate_usage(self.ping)
|
||||
|
||||
@commands.hybrid_command(name="ping")
|
||||
async def ping(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Ping command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
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(
|
||||
round(1000 * self.bot.latency),
|
||||
),
|
||||
)
|
||||
|
||||
await ctx.send(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)
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Ping(bot))
|
||||
|
|
43
modules/misc/uptime.py
Normal file
43
modules/misc/uptime.py
Normal file
|
@ -0,0 +1,43 @@
|
|||
from datetime import datetime
|
||||
|
||||
import discord
|
||||
from discord import Embed
|
||||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
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 = discord.utils.utcnow()
|
||||
self.uptime.usage = lib.format.generate_usage(self.uptime)
|
||||
|
||||
@commands.hybrid_command(name="uptime")
|
||||
async def uptime(self, ctx: commands.Context[commands.Bot]) -> None:
|
||||
"""
|
||||
Uptime command.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx : commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
unix_timestamp: int = int(self.start_time.timestamp())
|
||||
|
||||
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(
|
||||
int(self.bot.latency * 1000),
|
||||
),
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Uptime(bot))
|
|
@ -1,18 +1,18 @@
|
|||
from typing import Optional
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.xkcd_service import Client, HttpError
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
from wrappers.xkcd import Client, HttpError
|
||||
|
||||
_xkcd = Client()
|
||||
|
||||
|
||||
async def print_comic(
|
||||
ctx: bridge.Context,
|
||||
interaction: discord.Interaction,
|
||||
latest: bool = False,
|
||||
number: Optional[int] = None,
|
||||
number: int | None = None,
|
||||
) -> None:
|
||||
try:
|
||||
if latest:
|
||||
|
@ -22,9 +22,9 @@ async def print_comic(
|
|||
else:
|
||||
comic = _xkcd.get_random_comic(raw_comic_image=True)
|
||||
|
||||
await ctx.respond(
|
||||
embed=EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
await interaction.response.send_message(
|
||||
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,16 +32,64 @@ async def print_comic(
|
|||
),
|
||||
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,
|
||||
await interaction.response.send_message(
|
||||
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"],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class Xkcd(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
xkcd = app_commands.Group(name="xkcd", description="Get the latest xkcd comic")
|
||||
|
||||
@xkcd.command(name="latest")
|
||||
async def xkcd_latest(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Get the latest xkcd comic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to get the latest comic for.
|
||||
"""
|
||||
await print_comic(interaction, latest=True)
|
||||
|
||||
@xkcd.command(name="random")
|
||||
async def xkcd_random(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
Get a random xkcd comic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to get the random comic for.
|
||||
"""
|
||||
await print_comic(interaction)
|
||||
|
||||
@xkcd.command(name="search")
|
||||
async def xkcd_search(self, interaction: discord.Interaction, comic_id: int) -> None:
|
||||
"""
|
||||
Get a specific xkcd comic.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction : discord.Interaction
|
||||
The interaction to get the comic for.
|
||||
comic_id : int
|
||||
The ID of the comic to get.
|
||||
"""
|
||||
await print_comic(interaction, number=comic_id)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Xkcd(bot))
|
||||
|
|
|
@ -1,195 +0,0 @@
|
|||
import discord
|
||||
from discord.ext import bridge, commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from modules.moderation import ban, cases, kick, softban, timeout, warn
|
||||
|
||||
|
||||
class Moderation(commands.Cog):
|
||||
def __init__(self, client):
|
||||
self.client = client
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="ban",
|
||||
aliases=["b"],
|
||||
description="Ban a user from the server.",
|
||||
help="Bans a user from the server, you can use ID or mention them.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@guild_only()
|
||||
async def ban_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.User,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await ban.ban_user(self, ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="case",
|
||||
aliases=["c"],
|
||||
description="View a case by its number.",
|
||||
help="Views a case by its number in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def case_command(self, ctx, case_number: int):
|
||||
await cases.view_case_by_number(ctx, ctx.guild.id, case_number)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="cases",
|
||||
aliases=["caselist"],
|
||||
description="View all cases in the server.",
|
||||
help="Lists all moderation cases for the current server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def cases_command(self, ctx):
|
||||
await cases.view_all_cases_in_guild(ctx, ctx.guild.id)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="editcase",
|
||||
aliases=["uc", "ec"],
|
||||
description="Edit the reason for a case.",
|
||||
help="Updates the reason for a specific case in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def edit_case_command(self, ctx, case_number: int, *, new_reason: str):
|
||||
await cases.edit_case_reason(ctx, ctx.guild.id, case_number, new_reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="kick",
|
||||
aliases=["k"],
|
||||
description="Kick a user from the server.",
|
||||
help="Kicks a user from the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(kick_members=True)
|
||||
@commands.bot_has_permissions(kick_members=True)
|
||||
@guild_only()
|
||||
async def kick_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await kick.kick_user(self, ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="modcases",
|
||||
aliases=["moderatorcases", "mc"],
|
||||
description="View all cases by a specific moderator.",
|
||||
help="Lists all moderation cases handled by a specific moderator in the current server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(view_audit_log=True)
|
||||
@guild_only()
|
||||
async def moderator_cases_command(self, ctx, moderator: discord.Member):
|
||||
await cases.view_all_cases_by_mod(ctx, ctx.guild.id, moderator)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="softban",
|
||||
aliases=["sb"],
|
||||
description="Softban a user from the server.",
|
||||
help="Softbans a user from the server (ban and immediately unban to delete messages).",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@guild_only()
|
||||
async def softban_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await softban.softban_user(ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="timeout",
|
||||
aliases=["t", "to"],
|
||||
description="Timeout a user.",
|
||||
help="Timeouts a user in the server for a specified duration.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(moderate_members=True)
|
||||
@commands.bot_has_permissions(moderate_members=True)
|
||||
@guild_only()
|
||||
async def timeout_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
duration: str,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await timeout.timeout_user(self, ctx, target, duration, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="unban",
|
||||
aliases=["ub", "pardon"],
|
||||
description="Unbans a user from the server.",
|
||||
help="Unbans a user from the server, you can use ID or provide their username.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@guild_only()
|
||||
async def unban_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.User,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await ban.unban_user(ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="untimeout",
|
||||
aliases=["removetimeout", "rto", "uto"],
|
||||
description="Remove timeout from a user.",
|
||||
help="Removes the timeout from a user in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(moderate_members=True)
|
||||
@commands.bot_has_permissions(moderate_members=True)
|
||||
@guild_only()
|
||||
async def untimeout_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await timeout.untimeout_user(ctx, target, reason)
|
||||
|
||||
@bridge.bridge_command(
|
||||
name="warn",
|
||||
aliases=["w"],
|
||||
description="Warn a user.",
|
||||
help="Warns a user in the server.",
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
@bridge.has_permissions(kick_members=True)
|
||||
@guild_only()
|
||||
async def warn_command(
|
||||
self,
|
||||
ctx,
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
):
|
||||
await warn.warn_user(ctx, target, reason)
|
||||
|
||||
|
||||
def setup(client):
|
||||
client.add_cog(Moderation(client))
|
|
@ -1,114 +1,149 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
import contextlib
|
||||
from typing import cast
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import MemberConverter
|
||||
from discord.ext import commands
|
||||
|
||||
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
|
||||
import lib.format
|
||||
from lib.actionable import async_actionable
|
||||
from lib.case_handler import create_case
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
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))
|
||||
class Ban(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.ban.usage = lib.format.generate_usage(self.ban)
|
||||
self.unban.usage = lib.format.generate_usage(self.unban)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
@commands.hybrid_command(name="ban", aliases=["b"])
|
||||
@commands.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@commands.guild_only()
|
||||
async def ban(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member | discord.User,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Ban a user from the guild.
|
||||
|
||||
# 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)
|
||||
Parameters
|
||||
----------
|
||||
target: discord.Member | discord.User
|
||||
The user to ban.
|
||||
reason: str | None
|
||||
The reason for the ban.
|
||||
"""
|
||||
assert ctx.guild
|
||||
assert ctx.author
|
||||
assert ctx.bot.user
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
formatted_reason = CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
lib.format.shorten(output_reason, 200),
|
||||
)
|
||||
|
||||
dm_sent = False
|
||||
if isinstance(target, discord.Member):
|
||||
bot_member = await commands.MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, cast(discord.Member, ctx.author), bot_member)
|
||||
|
||||
with contextlib.suppress(discord.HTTPException, discord.Forbidden):
|
||||
await target.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=target.name,
|
||||
author_text=CONST.STRINGS["mod_banned_author"],
|
||||
description=CONST.STRINGS["mod_ban_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
output_reason,
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
),
|
||||
)
|
||||
dm_sent = True
|
||||
|
||||
await ctx.guild.ban(target, reason=formatted_reason)
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["mod_banned_author"],
|
||||
description=CONST.STRINGS["mod_banned_user"].format(target.name),
|
||||
)
|
||||
if isinstance(target, discord.Member):
|
||||
embed.set_footer(text=CONST.STRINGS["mod_dm_sent"] if dm_sent else CONST.STRINGS["mod_dm_not_sent"])
|
||||
|
||||
await asyncio.gather(
|
||||
ctx.send(embed=embed),
|
||||
create_case(ctx, cast(discord.User, target), "BAN", reason),
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
@commands.hybrid_command(name="unban")
|
||||
@commands.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@commands.guild_only()
|
||||
async def unban(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.User,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Unban a user from the guild.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target: discord.User
|
||||
The user to unban.
|
||||
reason: str | None
|
||||
The reason for the unban.
|
||||
"""
|
||||
assert ctx.guild
|
||||
assert ctx.author
|
||||
assert ctx.bot.user
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
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,
|
||||
await ctx.guild.unban(
|
||||
target,
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
lib.format.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
dm_sent = True
|
||||
|
||||
except (discord.HTTPException, discord.Forbidden):
|
||||
dm_sent = False
|
||||
respond_task = ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["mod_unbanned_author"],
|
||||
description=CONST.STRINGS["mod_unbanned"].format(target.name),
|
||||
),
|
||||
)
|
||||
create_case_task = create_case(ctx, target, "UNBAN", reason)
|
||||
await asyncio.gather(respond_task, create_case_task)
|
||||
|
||||
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)
|
||||
except (discord.NotFound, discord.HTTPException):
|
||||
await ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="error",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["mod_not_banned_author"],
|
||||
description=CONST.STRINGS["mod_not_banned"].format(target.id),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Ban(bot))
|
||||
|
|
|
@ -1,115 +1,236 @@
|
|||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import pages
|
||||
from discord.ext.commands import UserConverter
|
||||
from discord.ext import commands
|
||||
from reactionmenu import ViewButton, ViewMenu
|
||||
|
||||
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 (
|
||||
import lib.format
|
||||
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 modules.moderation.utils.case_handler import edit_case_modlog
|
||||
from services.moderation.case_service import CaseService
|
||||
from ui.embeds import Builder
|
||||
|
||||
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,
|
||||
def create_no_cases_embed(ctx: commands.Context[commands.Bot], author_text: str, description: str) -> discord.Embed:
|
||||
return Builder.create_embed(
|
||||
theme="info",
|
||||
user_name=ctx.author.name,
|
||||
author_text=author_text,
|
||||
description=description,
|
||||
)
|
||||
|
||||
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),
|
||||
def create_case_view_menu(ctx: commands.Context[commands.Bot]) -> ViewMenu:
|
||||
menu = ViewMenu(ctx, menu_type=ViewMenu.TypeEmbed, all_can_click=True, delete_on_timeout=True)
|
||||
|
||||
buttons = [
|
||||
(ViewButton.ID_GO_TO_FIRST_PAGE, "⏮️"),
|
||||
(ViewButton.ID_PREVIOUS_PAGE, "⏪"),
|
||||
(ViewButton.ID_NEXT_PAGE, "⏩"),
|
||||
(ViewButton.ID_GO_TO_LAST_PAGE, "⏭️"),
|
||||
]
|
||||
|
||||
for custom_id, emoji in buttons:
|
||||
menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=custom_id, emoji=emoji))
|
||||
|
||||
return menu
|
||||
|
||||
|
||||
class Cases(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.view_case_by_number.usage = lib.format.generate_usage(self.view_case_by_number)
|
||||
self.view_all_cases_in_guild.usage = lib.format.generate_usage(self.view_all_cases_in_guild)
|
||||
self.view_all_cases_by_mod.usage = lib.format.generate_usage(self.view_all_cases_by_mod)
|
||||
self.edit_case_reason.usage = lib.format.generate_usage(self.edit_case_reason)
|
||||
|
||||
@commands.hybrid_command(name="case", aliases=["c", "ca"])
|
||||
@commands.has_permissions(manage_messages=True)
|
||||
@commands.guild_only()
|
||||
async def view_case_by_number(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
case_number: int | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
View a specific case by number or all cases if no number is provided.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
case_number: int | None
|
||||
The case number to view. If None, view all cases.
|
||||
"""
|
||||
if case_number is None:
|
||||
await ctx.invoke(self.view_all_cases_in_guild)
|
||||
return
|
||||
|
||||
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 = create_no_cases_embed(
|
||||
ctx,
|
||||
CONST.STRINGS["error_no_case_found_author"],
|
||||
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")
|
||||
@commands.has_permissions(manage_messages=True)
|
||||
@commands.guild_only()
|
||||
async def view_all_cases_in_guild(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
) -> None:
|
||||
"""
|
||||
View all cases in the guild.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ctx: commands.Context[commands.Bot]
|
||||
The context of the command.
|
||||
"""
|
||||
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 = create_no_cases_embed(
|
||||
ctx,
|
||||
CONST.STRINGS["case_guild_no_cases_author"],
|
||||
CONST.STRINGS["case_guild_no_cases"],
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
menu = create_case_view_menu(ctx)
|
||||
|
||||
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)
|
||||
|
||||
await menu.start()
|
||||
|
||||
@commands.hybrid_command(name="modcases", aliases=["mc", "modc"])
|
||||
@commands.has_permissions(manage_messages=True)
|
||||
@commands.guild_only()
|
||||
async def view_all_cases_by_mod(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
moderator: discord.Member,
|
||||
) -> None:
|
||||
"""
|
||||
View all cases by a specific moderator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
moderator: discord.Member
|
||||
The moderator to view cases for.
|
||||
"""
|
||||
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)
|
||||
|
||||
if not cases:
|
||||
embed = create_no_cases_embed(
|
||||
ctx,
|
||||
CONST.STRINGS["case_mod_no_cases_author"],
|
||||
CONST.STRINGS["case_mod_no_cases"],
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
return
|
||||
|
||||
menu = create_case_view_menu(ctx)
|
||||
|
||||
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)
|
||||
|
||||
await menu.start()
|
||||
|
||||
@commands.hybrid_command(name="editcase", aliases=["ec", "uc"])
|
||||
@commands.has_permissions(manage_messages=True)
|
||||
@commands.guild_only()
|
||||
async def edit_case_reason(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
case_number: int,
|
||||
*,
|
||||
new_reason: str,
|
||||
) -> None:
|
||||
"""
|
||||
Edit the reason for a specific case.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
case_number: int
|
||||
The case number to edit.
|
||||
new_reason: str
|
||||
The new reason for the case.
|
||||
"""
|
||||
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,
|
||||
)
|
||||
|
||||
await update_tasks()
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
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))
|
||||
|
|
|
@ -1,58 +1,90 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
from typing import cast
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import UserConverter, MemberConverter
|
||||
from discord.ext import commands
|
||||
|
||||
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
|
||||
import lib.format
|
||||
from lib.actionable import async_actionable
|
||||
from lib.case_handler import create_case
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
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)
|
||||
class Kick(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.kick.usage = lib.format.generate_usage(self.kick)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
@commands.hybrid_command(name="kick", aliases=["k"])
|
||||
@commands.has_permissions(kick_members=True)
|
||||
@commands.bot_has_permissions(kick_members=True)
|
||||
@commands.guild_only()
|
||||
async def kick(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Kick a user from the guild.
|
||||
|
||||
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,
|
||||
Parameters
|
||||
----------
|
||||
target: discord.Member
|
||||
The user to kick.
|
||||
reason: str | None
|
||||
The reason for the kick. Defaults to None.
|
||||
"""
|
||||
assert ctx.guild
|
||||
assert ctx.author
|
||||
assert ctx.bot.user
|
||||
|
||||
bot_member = await commands.MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, cast(discord.Member, ctx.author), bot_member)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
try:
|
||||
await target.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=target.name,
|
||||
author_text=CONST.STRINGS["mod_kicked_author"],
|
||||
description=CONST.STRINGS["mod_kick_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
output_reason,
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
),
|
||||
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,
|
||||
lib.format.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
dm_sent = True
|
||||
|
||||
except (discord.HTTPException, discord.Forbidden):
|
||||
dm_sent = False
|
||||
respond_task = ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
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"],
|
||||
),
|
||||
)
|
||||
|
||||
await target.kick(
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
create_case_task = create_case(ctx, cast(discord.User, target), "KICK", reason)
|
||||
await asyncio.gather(respond_task, create_case_task, return_exceptions=True)
|
||||
|
||||
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)
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Kick(bot))
|
||||
|
|
123
modules/moderation/slowmode.py
Normal file
123
modules/moderation/slowmode.py
Normal file
|
@ -0,0 +1,123 @@
|
|||
import contextlib
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from lib.exceptions import LumiException
|
||||
from lib.format import format_duration_to_seconds
|
||||
|
||||
|
||||
class Slowmode(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.slowmode.usage = lib.format.generate_usage(self.slowmode)
|
||||
|
||||
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"],
|
||||
)
|
||||
@commands.has_permissions(manage_channels=True)
|
||||
@commands.bot_has_permissions(manage_channels=True)
|
||||
@commands.guild_only()
|
||||
async def slowmode(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
arg1: str | None = None,
|
||||
arg2: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set or view the slowmode for a channel.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
arg1: str | None
|
||||
The first argument. Defaults to None.
|
||||
arg2: str | None
|
||||
The second argument. Defaults to None.
|
||||
"""
|
||||
channel, duration = None, None
|
||||
|
||||
for arg in (arg1, arg2):
|
||||
if not channel and arg:
|
||||
with contextlib.suppress(commands.BadArgument):
|
||||
channel = await commands.TextChannelConverter().convert(ctx, arg)
|
||||
continue
|
||||
if arg:
|
||||
duration = arg
|
||||
|
||||
if not channel:
|
||||
await ctx.send(CONST.STRINGS["slowmode_channel_not_found"])
|
||||
return
|
||||
|
||||
await self._set_slowmode(ctx, channel, duration)
|
||||
|
||||
@app_commands.command(
|
||||
name="slowmode",
|
||||
)
|
||||
@app_commands.checks.has_permissions(manage_channels=True)
|
||||
@app_commands.checks.bot_has_permissions(manage_channels=True)
|
||||
@app_commands.guild_only()
|
||||
async def slowmode_slash(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
channel: discord.TextChannel,
|
||||
duration: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Set or view the slowmode for a channel.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
channel: discord.TextChannel
|
||||
The channel to set the slowmode for.
|
||||
duration: str | None
|
||||
The duration of the slowmode. Defaults to None.
|
||||
"""
|
||||
await self._set_slowmode(interaction, channel, duration)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Slowmode(bot))
|
|
@ -1,66 +1,98 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
from typing import cast
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import MemberConverter, UserConverter
|
||||
from discord.ext import commands
|
||||
|
||||
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
|
||||
import lib.format
|
||||
from lib.actionable import async_actionable
|
||||
from lib.case_handler import create_case
|
||||
from lib.const import CONST
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
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)
|
||||
class Softban(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.softban.usage = lib.format.generate_usage(self.softban)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
@commands.hybrid_command(name="softban", aliases=["sb"])
|
||||
@commands.has_permissions(ban_members=True)
|
||||
@commands.bot_has_permissions(ban_members=True)
|
||||
@commands.guild_only()
|
||||
async def softban(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Softban a user from the guild.
|
||||
|
||||
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,
|
||||
Parameters
|
||||
----------
|
||||
target: discord.Member
|
||||
The user to softban.
|
||||
reason: str | None
|
||||
The reason for the softban. Defaults to None.
|
||||
"""
|
||||
assert ctx.guild
|
||||
assert ctx.author
|
||||
assert ctx.bot.user
|
||||
|
||||
bot_member = await commands.MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, cast(discord.Member, ctx.author), bot_member)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
try:
|
||||
await target.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=target.name,
|
||||
author_text=CONST.STRINGS["mod_softbanned_author"],
|
||||
description=CONST.STRINGS["mod_softban_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
output_reason,
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
),
|
||||
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,
|
||||
lib.format.shorten(output_reason, 200),
|
||||
),
|
||||
delete_message_seconds=86400,
|
||||
)
|
||||
|
||||
await ctx.guild.unban(
|
||||
target,
|
||||
reason=CONST.STRINGS["mod_softban_unban_reason"].format(
|
||||
ctx.author.name,
|
||||
),
|
||||
)
|
||||
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,
|
||||
)
|
||||
respond_task = ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=target.name,
|
||||
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"],
|
||||
),
|
||||
)
|
||||
|
||||
await ctx.guild.unban(
|
||||
target,
|
||||
reason=CONST.STRINGS["mod_softban_unban_reason"].format(
|
||||
ctx.author.name,
|
||||
),
|
||||
)
|
||||
create_case_task = create_case(ctx, cast(discord.User, target), "SOFTBAN", reason)
|
||||
await asyncio.gather(respond_task, create_case_task, return_exceptions=True)
|
||||
|
||||
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)
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Softban(bot))
|
||||
|
|
|
@ -1,103 +1,163 @@
|
|||
import asyncio
|
||||
import datetime
|
||||
from typing import Optional
|
||||
from typing import cast
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import UserConverter, MemberConverter
|
||||
from discord.ext import commands
|
||||
|
||||
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
|
||||
import lib.format
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
class Timeout(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.timeout.usage = lib.format.generate_usage(self.timeout)
|
||||
self.untimeout.usage = lib.format.generate_usage(self.untimeout)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
@commands.hybrid_command(name="timeout", aliases=["t", "to"])
|
||||
@commands.has_permissions(moderate_members=True)
|
||||
@commands.bot_has_permissions(moderate_members=True)
|
||||
@commands.guild_only()
|
||||
async def timeout(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member,
|
||||
duration: str,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Timeout a user in the guild.
|
||||
|
||||
# Parse duration to minutes and validate
|
||||
duration_int = format_duration_to_seconds(duration)
|
||||
duration_str = format_seconds_to_duration_string(duration_int)
|
||||
Parameters
|
||||
----------
|
||||
target: discord.Member
|
||||
The member to timeout.
|
||||
duration: str
|
||||
The duration of the timeout. Can be in the format of "1d2h3m4s".
|
||||
reason: str | None
|
||||
The reason for the timeout. Defaults to None.
|
||||
"""
|
||||
assert ctx.guild
|
||||
assert ctx.author
|
||||
assert ctx.bot.user
|
||||
|
||||
await target.timeout_for(
|
||||
duration=datetime.timedelta(seconds=duration_int),
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
# Parse duration to minutes and validate
|
||||
duration_int = lib.format.format_duration_to_seconds(duration)
|
||||
duration_str = lib.format.format_seconds_to_duration_string(duration_int)
|
||||
|
||||
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,
|
||||
),
|
||||
)
|
||||
# if longer than 27 days, return LumiException
|
||||
if duration_int > 2332800:
|
||||
raise LumiException(CONST.STRINGS["mod_timeout_too_long"])
|
||||
|
||||
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),
|
||||
),
|
||||
)
|
||||
bot_member = await commands.MemberConverter().convert(ctx, str(ctx.bot.user.id))
|
||||
await async_actionable(target, cast(discord.Member, ctx.author), bot_member)
|
||||
|
||||
target_user = await UserConverter().convert(ctx, str(target.id))
|
||||
create_case_task = create_case(ctx, target_user, "TIMEOUT", reason, duration_int)
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
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(
|
||||
await target.timeout(
|
||||
datetime.timedelta(seconds=duration_int),
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
formatter.shorten(output_reason, 200),
|
||||
lib.format.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),
|
||||
dm_task = target.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=target.name,
|
||||
author_text=CONST.STRINGS["mod_timed_out_author"],
|
||||
description=CONST.STRINGS["mod_timeout_dm"].format(
|
||||
target.name,
|
||||
ctx.guild.name,
|
||||
duration_str,
|
||||
output_reason,
|
||||
),
|
||||
hide_name_in_description=True,
|
||||
),
|
||||
)
|
||||
|
||||
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),
|
||||
respond_task = ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=target.name,
|
||||
author_text=CONST.STRINGS["mod_timed_out_author"],
|
||||
description=CONST.STRINGS["mod_timed_out_user"].format(target.name),
|
||||
),
|
||||
)
|
||||
|
||||
create_case_task = create_case(ctx, cast(discord.User, target), "TIMEOUT", reason, duration_int)
|
||||
|
||||
await asyncio.gather(
|
||||
dm_task,
|
||||
respond_task,
|
||||
create_case_task,
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
@commands.hybrid_command(name="untimeout", aliases=["ut", "rto"])
|
||||
@commands.has_permissions(moderate_members=True)
|
||||
@commands.bot_has_permissions(moderate_members=True)
|
||||
@commands.guild_only()
|
||||
async def untimeout(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Untimeout a user in the guild.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
target: discord.Member
|
||||
The member to untimeout.
|
||||
reason: str | None
|
||||
The reason for the untimeout. Defaults to None.
|
||||
"""
|
||||
assert ctx.guild
|
||||
assert ctx.author
|
||||
assert ctx.bot.user
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
|
||||
try:
|
||||
await target.timeout(
|
||||
None,
|
||||
reason=CONST.STRINGS["mod_reason"].format(
|
||||
ctx.author.name,
|
||||
lib.format.shorten(output_reason, 200),
|
||||
),
|
||||
)
|
||||
|
||||
respond_task = ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["mod_untimed_out_author"],
|
||||
description=CONST.STRINGS["mod_untimed_out"].format(target.name),
|
||||
),
|
||||
)
|
||||
|
||||
target_user = await commands.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:
|
||||
await ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=ctx.author.name,
|
||||
author_text=CONST.STRINGS["mod_not_timed_out_author"],
|
||||
description=CONST.STRINGS["mod_not_timed_out"].format(target.name),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Timeout(bot))
|
||||
|
|
|
@ -1,48 +1,82 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
from typing import cast
|
||||
|
||||
import discord
|
||||
from discord.ext.commands import UserConverter, MemberConverter
|
||||
from discord.ext import commands
|
||||
|
||||
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
|
||||
import lib.format
|
||||
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
|
||||
|
||||
|
||||
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)
|
||||
class Warn(commands.Cog):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
self.warn.usage = lib.format.generate_usage(self.warn)
|
||||
|
||||
output_reason = reason or CONST.STRINGS["mod_no_reason"]
|
||||
@commands.hybrid_command(name="warn", aliases=["w"])
|
||||
@commands.has_permissions(manage_messages=True)
|
||||
@commands.guild_only()
|
||||
async def warn(
|
||||
self,
|
||||
ctx: commands.Context[commands.Bot],
|
||||
target: discord.Member,
|
||||
*,
|
||||
reason: str | None = None,
|
||||
) -> None:
|
||||
"""
|
||||
Warn a user.
|
||||
|
||||
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,
|
||||
Parameters
|
||||
----------
|
||||
target: discord.Member
|
||||
The user to warn.
|
||||
reason: str | None
|
||||
The reason for the warn. Defaults to 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(
|
||||
theme="info",
|
||||
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,
|
||||
),
|
||||
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),
|
||||
),
|
||||
)
|
||||
respond_task = ctx.send(
|
||||
embed=Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=ctx.author.name,
|
||||
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)
|
||||
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,
|
||||
)
|
||||
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))
|
||||
|
|
|
@ -1,81 +0,0 @@
|
|||
import discord
|
||||
from discord.commands import SlashCommandGroup
|
||||
from discord.ext import commands
|
||||
from discord.ext.commands import guild_only
|
||||
|
||||
from Client import LumiBot
|
||||
from modules.triggers.add import add_reaction
|
||||
from modules.triggers.delete import delete_reaction
|
||||
from modules.triggers.list import list_reactions
|
||||
|
||||
|
||||
class Triggers(commands.Cog):
|
||||
def __init__(self, client: LumiBot):
|
||||
self.client = client
|
||||
|
||||
trigger = SlashCommandGroup(
|
||||
"trigger",
|
||||
"Manage custom reactions.",
|
||||
default_member_permissions=discord.Permissions(manage_guild=True),
|
||||
contexts={discord.InteractionContextType.guild},
|
||||
)
|
||||
add = trigger.create_subgroup("add", "Add new custom reactions.")
|
||||
|
||||
@add.command(
|
||||
name="response",
|
||||
description="Add a new custom text reaction.",
|
||||
help="Add a new custom text reaction to the database.",
|
||||
)
|
||||
@guild_only()
|
||||
async def add_text_reaction_command(
|
||||
self,
|
||||
ctx,
|
||||
trigger_text: str,
|
||||
response: str,
|
||||
is_full_match: bool,
|
||||
):
|
||||
await add_reaction(ctx, trigger_text, response, None, False, is_full_match)
|
||||
|
||||
@add.command(
|
||||
name="emoji",
|
||||
description="Add a new custom emoji reaction.",
|
||||
help="Add a new custom emoji reaction to the database.",
|
||||
)
|
||||
@guild_only()
|
||||
async def add_emoji_reaction_command(
|
||||
self,
|
||||
ctx,
|
||||
trigger_text: str,
|
||||
emoji: discord.Emoji,
|
||||
is_full_match: bool,
|
||||
):
|
||||
await add_reaction(ctx, trigger_text, None, emoji.id, True, is_full_match)
|
||||
|
||||
@trigger.command(
|
||||
name="delete",
|
||||
description="Delete an existing custom reaction.",
|
||||
help="Delete an existing custom reaction from the database.",
|
||||
)
|
||||
@guild_only()
|
||||
async def delete_reaction_command(
|
||||
self,
|
||||
ctx,
|
||||
reaction_id: int,
|
||||
):
|
||||
await delete_reaction(ctx, reaction_id)
|
||||
|
||||
@trigger.command(
|
||||
name="list",
|
||||
description="List all custom reactions.",
|
||||
help="List all custom reactions for the current guild.",
|
||||
)
|
||||
@guild_only()
|
||||
async def list_reactions_command(
|
||||
self,
|
||||
ctx,
|
||||
):
|
||||
await list_reactions(ctx)
|
||||
|
||||
|
||||
def setup(client: LumiBot):
|
||||
client.add_cog(Triggers(client))
|
|
@ -1,107 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from discord.ext import bridge
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
||||
async def add_reaction(
|
||||
ctx: bridge.Context,
|
||||
trigger_text: str,
|
||||
response: Optional[str],
|
||||
emoji_id: Optional[int],
|
||||
is_emoji: bool,
|
||||
is_full_match: bool,
|
||||
) -> None:
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
reaction_service = CustomReactionsService()
|
||||
guild_id: int = ctx.guild.id
|
||||
creator_id: int = ctx.author.id
|
||||
|
||||
if not await check_reaction_limit(
|
||||
reaction_service,
|
||||
guild_id,
|
||||
):
|
||||
return
|
||||
|
||||
if not await check_existing_trigger(
|
||||
reaction_service,
|
||||
guild_id,
|
||||
trigger_text,
|
||||
):
|
||||
return
|
||||
|
||||
success: bool = await reaction_service.create_custom_reaction(
|
||||
guild_id=guild_id,
|
||||
creator_id=creator_id,
|
||||
trigger_text=trigger_text,
|
||||
response=response,
|
||||
emoji_id=emoji_id,
|
||||
is_emoji=is_emoji,
|
||||
is_full_match=is_full_match,
|
||||
is_global=False,
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise LumiException(CONST.STRINGS["triggers_not_added"])
|
||||
|
||||
trigger_text = formatter.shorten(trigger_text, 50)
|
||||
|
||||
if response:
|
||||
response = formatter.shorten(response, 50)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["triggers_add_author"],
|
||||
description="",
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
embed.description += CONST.STRINGS["triggers_add_description"].format(
|
||||
trigger_text,
|
||||
CONST.STRINGS["triggers_type_emoji"]
|
||||
if is_emoji
|
||||
else CONST.STRINGS["triggers_type_text"],
|
||||
is_full_match,
|
||||
)
|
||||
|
||||
if is_emoji:
|
||||
embed.description += CONST.STRINGS["triggers_add_emoji_details"].format(
|
||||
emoji_id,
|
||||
)
|
||||
else:
|
||||
embed.description += CONST.STRINGS["triggers_add_text_details"].format(response)
|
||||
|
||||
await ctx.respond(embed=embed)
|
||||
|
||||
|
||||
async def check_reaction_limit(
|
||||
reaction_service: CustomReactionsService,
|
||||
guild_id: int,
|
||||
) -> bool:
|
||||
limit_reached = await reaction_service.count_custom_reactions(guild_id) >= 100
|
||||
|
||||
if limit_reached:
|
||||
raise LumiException(CONST.STRINGS["trigger_limit_reached"])
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def check_existing_trigger(
|
||||
reaction_service: CustomReactionsService,
|
||||
guild_id: int,
|
||||
trigger_text: str,
|
||||
) -> bool:
|
||||
existing_trigger = await reaction_service.find_trigger(guild_id, trigger_text)
|
||||
|
||||
if existing_trigger:
|
||||
raise LumiException(CONST.STRINGS["trigger_already_exists"])
|
||||
|
||||
return True
|
|
@ -1,29 +0,0 @@
|
|||
from discord.ext import bridge
|
||||
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from lib.exceptions.LumiExceptions import LumiException
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
||||
async def delete_reaction(ctx: bridge.Context, reaction_id: int) -> None:
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
reaction_service = CustomReactionsService()
|
||||
guild_id: int = ctx.guild.id
|
||||
reaction = await reaction_service.find_id(reaction_id)
|
||||
|
||||
if reaction is None or reaction["guild_id"] != guild_id or reaction["is_global"]:
|
||||
raise LumiException(CONST.STRINGS["triggers_not_found"])
|
||||
|
||||
await reaction_service.delete_custom_reaction(reaction_id)
|
||||
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["triggers_delete_author"],
|
||||
description=CONST.STRINGS["triggers_delete_description"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
)
|
||||
|
||||
await ctx.respond(embed=embed)
|
|
@ -1,80 +0,0 @@
|
|||
from typing import Any, Dict, List
|
||||
|
||||
import discord
|
||||
from discord.ext import bridge, pages
|
||||
|
||||
from lib import formatter
|
||||
from lib.constants import CONST
|
||||
from lib.embed_builder import EmbedBuilder
|
||||
from services.reactions_service import CustomReactionsService
|
||||
|
||||
|
||||
async def list_reactions(ctx: bridge.Context) -> None:
|
||||
if ctx.guild is None:
|
||||
return
|
||||
|
||||
reaction_service: CustomReactionsService = CustomReactionsService()
|
||||
guild_id: int = ctx.guild.id
|
||||
|
||||
reactions: List[Dict[str, Any]] = await reaction_service.find_all_by_guild(guild_id)
|
||||
if not reactions:
|
||||
embed: discord.Embed = EmbedBuilder.create_warning_embed(
|
||||
ctx,
|
||||
author_text=CONST.STRINGS["triggers_no_reactions_title"],
|
||||
description=CONST.STRINGS["triggers_no_reactions_description"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
show_name=False,
|
||||
)
|
||||
await ctx.respond(embed=embed)
|
||||
return
|
||||
|
||||
pages_list = []
|
||||
for reaction in reactions:
|
||||
embed = EmbedBuilder.create_success_embed(
|
||||
ctx,
|
||||
title=CONST.STRINGS["triggers_list_custom_reaction_id"].format(
|
||||
reaction["id"],
|
||||
),
|
||||
author_text=CONST.STRINGS["triggers_list_custom_reactions_title"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
show_name=False,
|
||||
)
|
||||
|
||||
description_lines = [
|
||||
CONST.STRINGS["triggers_list_trigger_text"].format(
|
||||
formatter.shorten(reaction["trigger_text"], 50),
|
||||
),
|
||||
CONST.STRINGS["triggers_list_reaction_type"].format(
|
||||
CONST.STRINGS["triggers_type_emoji"]
|
||||
if reaction["is_emoji"]
|
||||
else CONST.STRINGS["triggers_type_text"],
|
||||
),
|
||||
]
|
||||
|
||||
if reaction["is_emoji"]:
|
||||
description_lines.append(
|
||||
CONST.STRINGS["triggers_list_emoji_id"].format(reaction["emoji_id"]),
|
||||
)
|
||||
else:
|
||||
description_lines.append(
|
||||
CONST.STRINGS["triggers_list_response"].format(
|
||||
formatter.shorten(reaction["response"], 50),
|
||||
),
|
||||
)
|
||||
|
||||
description_lines.extend(
|
||||
[
|
||||
CONST.STRINGS["triggers_list_full_match"].format(
|
||||
"True" if reaction["is_full_match"] else "False",
|
||||
),
|
||||
CONST.STRINGS["triggers_list_usage_count"].format(
|
||||
reaction["usage_count"],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
embed.description = "\n".join(description_lines)
|
||||
pages_list.append(embed)
|
||||
|
||||
paginator: pages.Paginator = pages.Paginator(pages=pages_list, timeout=180.0)
|
||||
await paginator.respond(ctx, ephemeral=False)
|
286
modules/triggers/triggers.py
Normal file
286
modules/triggers/triggers.py
Normal file
|
@ -0,0 +1,286 @@
|
|||
from typing import Any
|
||||
|
||||
import discord
|
||||
from discord import app_commands
|
||||
from discord.ext import commands
|
||||
from reactionmenu import ViewButton, ViewMenu
|
||||
|
||||
import lib.format
|
||||
from lib.const import CONST
|
||||
from lib.exceptions import LumiException
|
||||
from services.reactions_service import CustomReactionsService
|
||||
from ui.embeds import Builder
|
||||
|
||||
|
||||
@app_commands.guild_only()
|
||||
@app_commands.default_permissions(manage_guild=True)
|
||||
class Triggers(commands.GroupCog, group_name="trigger"):
|
||||
def __init__(self, bot: commands.Bot):
|
||||
self.bot = bot
|
||||
|
||||
add = app_commands.Group(
|
||||
name="add",
|
||||
description="Add a trigger",
|
||||
allowed_contexts=app_commands.AppCommandContext(
|
||||
guild=True,
|
||||
dm_channel=False,
|
||||
private_channel=False,
|
||||
),
|
||||
default_permissions=discord.Permissions(manage_guild=True),
|
||||
)
|
||||
|
||||
@add.command(name="response")
|
||||
async def add_text_response(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
trigger_text: str,
|
||||
response: str,
|
||||
is_full_match: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Add a custom reaction that uses text.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trigger_text: str
|
||||
The text that triggers the reaction.
|
||||
response: str
|
||||
The text to respond with.
|
||||
"""
|
||||
assert interaction.guild
|
||||
|
||||
reaction_service = CustomReactionsService()
|
||||
guild_id: int = interaction.guild.id
|
||||
creator_id: int = interaction.user.id
|
||||
|
||||
limit_reached = await reaction_service.count_custom_reactions(guild_id) >= 100
|
||||
if limit_reached:
|
||||
raise LumiException(CONST.STRINGS["trigger_limit_reached"])
|
||||
|
||||
existing_trigger = await reaction_service.find_trigger(guild_id, trigger_text)
|
||||
if existing_trigger:
|
||||
raise LumiException(CONST.STRINGS["trigger_already_exists"])
|
||||
|
||||
success: bool = await reaction_service.create_custom_reaction(
|
||||
guild_id=guild_id,
|
||||
creator_id=creator_id,
|
||||
trigger_text=trigger_text,
|
||||
response=response,
|
||||
emoji_id=None,
|
||||
is_emoji=False,
|
||||
is_full_match=is_full_match,
|
||||
is_global=False,
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise LumiException(CONST.STRINGS["triggers_not_added"])
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["triggers_add_author"],
|
||||
description="",
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
embed.description += CONST.STRINGS["triggers_add_description"].format(
|
||||
lib.format.shorten(trigger_text, 50),
|
||||
CONST.STRINGS["triggers_type_text"],
|
||||
is_full_match,
|
||||
)
|
||||
embed.description += CONST.STRINGS["triggers_add_text_details"].format(lib.format.shorten(response, 50))
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@add.command(name="emoji")
|
||||
async def add_emoji_response(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
trigger_text: str,
|
||||
emoji_id: int,
|
||||
is_full_match: bool = False,
|
||||
) -> None:
|
||||
"""
|
||||
Add a custom reaction that uses an emoji.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trigger_text: str
|
||||
The text that triggers the reaction.
|
||||
emoji_id: int
|
||||
The ID of the emoji to use.
|
||||
"""
|
||||
assert interaction.guild
|
||||
|
||||
reaction_service = CustomReactionsService()
|
||||
guild_id: int = interaction.guild.id
|
||||
creator_id: int = interaction.user.id
|
||||
|
||||
limit_reached = await reaction_service.count_custom_reactions(guild_id) >= 100
|
||||
if limit_reached:
|
||||
raise LumiException(CONST.STRINGS["trigger_limit_reached"])
|
||||
|
||||
existing_trigger = await reaction_service.find_trigger(guild_id, trigger_text)
|
||||
if existing_trigger:
|
||||
raise LumiException(CONST.STRINGS["trigger_already_exists"])
|
||||
|
||||
success: bool = await reaction_service.create_custom_reaction(
|
||||
guild_id=guild_id,
|
||||
creator_id=creator_id,
|
||||
trigger_text=trigger_text,
|
||||
response=None,
|
||||
emoji_id=emoji_id,
|
||||
is_emoji=True,
|
||||
is_full_match=is_full_match,
|
||||
is_global=False,
|
||||
)
|
||||
|
||||
if not success:
|
||||
raise LumiException(CONST.STRINGS["triggers_not_added"])
|
||||
|
||||
embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["triggers_add_author"],
|
||||
description="",
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
embed.description += CONST.STRINGS["triggers_add_description"].format(
|
||||
lib.format.shorten(trigger_text, 50),
|
||||
CONST.STRINGS["triggers_type_emoji"],
|
||||
is_full_match,
|
||||
)
|
||||
embed.description += CONST.STRINGS["triggers_add_emoji_details"].format(emoji_id)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="delete")
|
||||
async def remove_text_response(
|
||||
self,
|
||||
interaction: discord.Interaction,
|
||||
reaction_id: int,
|
||||
) -> None:
|
||||
"""
|
||||
Delete a custom reaction by its ID.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
reaction_id: int
|
||||
The ID of the reaction to delete.
|
||||
"""
|
||||
assert interaction.guild
|
||||
|
||||
reaction_service = CustomReactionsService()
|
||||
guild_id: int = interaction.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 = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["triggers_delete_author"],
|
||||
description=CONST.STRINGS["triggers_delete_description"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
)
|
||||
|
||||
await interaction.response.send_message(embed=embed)
|
||||
|
||||
@app_commands.command(name="list")
|
||||
async def list_reactions(self, interaction: discord.Interaction) -> None:
|
||||
"""
|
||||
List all custom reactions for the current guild.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
interaction: discord.Interaction
|
||||
The interaction to list the reactions for.
|
||||
"""
|
||||
assert interaction.guild
|
||||
reaction_service: CustomReactionsService = CustomReactionsService()
|
||||
guild_id: int = interaction.guild.id
|
||||
|
||||
reactions: list[dict[str, Any]] = await reaction_service.find_all_by_guild(guild_id)
|
||||
if not reactions:
|
||||
embed: discord.Embed = Builder.create_embed(
|
||||
theme="warning",
|
||||
user_name=interaction.user.name,
|
||||
author_text=CONST.STRINGS["triggers_no_reactions_title"],
|
||||
description=CONST.STRINGS["triggers_no_reactions_description"],
|
||||
footer_text=CONST.STRINGS["triggers_reaction_service_footer"],
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
await interaction.response.send_message(embed=embed)
|
||||
return
|
||||
|
||||
menu = ViewMenu(interaction, menu_type=ViewMenu.TypeEmbed, all_can_click=True, remove_items_on_timeout=True)
|
||||
|
||||
for reaction in reactions:
|
||||
embed: discord.Embed = Builder.create_embed(
|
||||
theme="success",
|
||||
user_name=interaction.user.name,
|
||||
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"],
|
||||
hide_name_in_description=True,
|
||||
)
|
||||
|
||||
description_lines = [
|
||||
CONST.STRINGS["triggers_list_trigger_text"].format(
|
||||
lib.format.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(
|
||||
lib.format.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)
|
||||
menu.add_page(embed)
|
||||
|
||||
buttons = [
|
||||
(ViewButton.ID_GO_TO_FIRST_PAGE, "⏮️"),
|
||||
(ViewButton.ID_PREVIOUS_PAGE, "⏪"),
|
||||
(ViewButton.ID_NEXT_PAGE, "⏩"),
|
||||
(ViewButton.ID_GO_TO_LAST_PAGE, "⏭️"),
|
||||
]
|
||||
|
||||
for custom_id, emoji in buttons:
|
||||
menu.add_button(ViewButton(style=discord.ButtonStyle.secondary, custom_id=custom_id, emoji=emoji))
|
||||
|
||||
await menu.start()
|
||||
|
||||
|
||||
async def setup(bot: commands.Bot) -> None:
|
||||
await bot.add_cog(Triggers(bot))
|
486
poetry.lock
generated
486
poetry.lock
generated
|
@ -1,5 +1,43 @@
|
|||
# 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"
|
||||
|
@ -136,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"
|
||||
|
@ -177,13 +226,13 @@ tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
|||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2024.7.4"
|
||||
version = "2024.8.30"
|
||||
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"},
|
||||
{file = "certifi-2024.8.30-py3-none-any.whl", hash = "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8"},
|
||||
{file = "certifi-2024.8.30.tar.gz", hash = "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -307,6 +356,26 @@ files = [
|
|||
{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"
|
||||
|
@ -471,13 +540,13 @@ trio = ["trio (>=0.22.0,<0.26.0)"]
|
|||
|
||||
[[package]]
|
||||
name = "httpx"
|
||||
version = "0.27.0"
|
||||
version = "0.27.2"
|
||||
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"},
|
||||
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -492,6 +561,7 @@ 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"
|
||||
|
@ -762,33 +832,137 @@ files = [
|
|||
test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
|
||||
|
||||
[[package]]
|
||||
name = "py-cord"
|
||||
version = "2.6.0"
|
||||
description = "A Python wrapper for the Discord API"
|
||||
name = "pydantic"
|
||||
version = "2.8.2"
|
||||
description = "Data validation using Python type hints"
|
||||
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"},
|
||||
{file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
|
||||
{file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.6.0,<4.0"
|
||||
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]
|
||||
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)"]
|
||||
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]
|
||||
|
@ -798,20 +972,6 @@ nodeenv = ">=1.6.0"
|
|||
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"
|
||||
|
@ -823,17 +983,6 @@ files = [
|
|||
{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"
|
||||
|
@ -896,6 +1045,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"
|
||||
|
@ -919,29 +1082,29 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.5.7"
|
||||
version = "0.6.3"
|
||||
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"},
|
||||
{file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"},
|
||||
{file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"},
|
||||
{file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"},
|
||||
{file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"},
|
||||
{file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"},
|
||||
{file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"},
|
||||
{file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"},
|
||||
{file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"},
|
||||
{file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -982,6 +1145,17 @@ files = [
|
|||
ply = ">=3.4"
|
||||
six = ">=1.12.0"
|
||||
|
||||
[[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 = "urllib3"
|
||||
version = "2.2.2"
|
||||
|
@ -1035,101 +1209,103 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"]
|
|||
|
||||
[[package]]
|
||||
name = "yarl"
|
||||
version = "1.9.4"
|
||||
version = "1.9.7"
|
||||
description = "Yet another URL library"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
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"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:60c04415b31a1611ef5989a6084dd6f6b95652c6a18378b58985667b65b2ecb6"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1787dcfdbe730207acb454548a6e19f80ae75e6d2d1f531c5a777bc1ab6f7952"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f5ddad20363f9f1bbedc95789c897da62f939e6bc855793c3060ef8b9f9407bf"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdb156a06208fc9645ae7cc0fca45c40dd40d7a8c4db626e542525489ca81a9"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:522fa3d300d898402ae4e0fa7c2c21311248ca43827dc362a667de87fdb4f1be"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e7f9cabfb8b980791b97a3ae3eab2e38b2ba5eab1af9b7495bdc44e1ce7c89e3"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fc728857df4087da6544fc68f62d7017fa68d74201d5b878e18ed4822c31fb3"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dba2ebac677184d56374fa3e452b461f5d6a03aa132745e648ae8859361eb6b"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a95167ae34667c5cc7d9206c024f793e8ffbadfb307d5c059de470345de58a21"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9d319ac113ca47352319cbea92d1925a37cb7bd61a8c2f3e3cd2e96eb33cccae"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:2d71a5d818d82586ac46265ae01466e0bda0638760f18b21f1174e0dd58a9d2f"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ff03f1c1ac474c66d474929ae7e4dd195592c1c7cc8c36418528ed81b1ca0a79"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78250f635f221dde97d02c57aade3313310469bc291888dfe32acd1012594441"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-win32.whl", hash = "sha256:f3aaf9fa960d55bd7876d55d7ea3cc046f3660df1ff73fc1b8c520a741ed1f21"},
|
||||
{file = "yarl-1.9.7-cp310-cp310-win_amd64.whl", hash = "sha256:e8362c941e07fbcde851597672a5e41b21dc292b7d5a1dc439b7a93c9a1af5d9"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:596069ddeaf72b5eb36cd714dcd2b5751d0090d05a8d65113b582ed9e1c801fb"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cb870907e8b86b2f32541403da9455afc1e535ce483e579bea0e6e79a0cc751c"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ca5e86be84492fa403c4dcd4dcaf8e1b1c4ffc747b5176f7c3d09878c45719b0"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a99cecfb51c84d00132db909e83ae388793ca86e48df7ae57f1be0beab0dcce5"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25508739e9b44d251172145f54c084b71747b09e4d237dc2abb045f46c36a66e"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60f3b5aec3146b6992640592856414870f5b20eb688c1f1d5f7ac010a7f86561"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b1557456afce5db3d655b5f8a31cdcaae1f47e57958760525c44b76e812b4987"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:71bb1435a84688ed831220c5305d96161beb65cac4a966374475348aa3de4575"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f87d8645a7a806ec8f66aac5e3b1dcb5014849ff53ffe2a1f0b86ca813f534c7"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:58e3f01673873b8573da3abe138debc63e4e68541b2104a55df4c10c129513a4"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8af0bbd4d84f8abdd9b11be9488e32c76b1501889b73c9e2292a15fb925b378b"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7fc441408ed0d9c6d2d627a02e281c21f5de43eb5209c16636a17fc704f7d0f8"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a9552367dc440870556da47bb289a806f08ad06fbc4054072d193d9e5dd619ba"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-win32.whl", hash = "sha256:628619008680a11d07243391271b46f07f13b75deb9fe92ef342305058c70722"},
|
||||
{file = "yarl-1.9.7-cp311-cp311-win_amd64.whl", hash = "sha256:bc23d870864971c8455cfba17498ccefa53a5719ea9f5fce5e7e9c1606b5755f"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0d8cf3d0b67996edc11957aece3fbce4c224d0451c7c3d6154ec3a35d0e55f6b"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3a7748cd66fef49c877e59503e0cc76179caf1158d1080228e67e1db14554f08"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a6fa3aeca8efabb0fbbb3b15e0956b0cb77f7d9db67c107503c30af07cd9e00"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf37dd0008e5ac5c3880198976063c491b6a15b288d150d12833248cf2003acb"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87aa5308482f248f8c3bd9311cd6c7dfd98ea1a8e57e35fb11e4adcac3066003"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:867b13c1b361f9ba5d2f84dc5408082f5d744c83f66de45edc2b96793a9c5e48"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48ce93947554c2c85fe97fc4866646ec90840bc1162e4db349b37d692a811755"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcd3d94b848cba132f39a5b40d80b0847d001a91a6f35a2204505cdd46afe1b2"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d06d6a8f98dd87646d98f0c468be14b201e47ec6092ad569adf835810ad0dffb"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:91567ff4fce73d2e7ac67ed5983ad26ba2343bc28cb22e1e1184a9677df98d7c"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1d5594512541e63188fea640b7f066c218d2176203d6e6f82abf702ae3dca3b2"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c2743e43183e4afbb07d5605693299b8756baff0b086c25236c761feb0e3c56"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daa69a3a2204355af39f4cfe7f3870d87c53d77a597b5100b97e3faa9460428b"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-win32.whl", hash = "sha256:36b16884336c15adf79a4bf1d592e0c1ffdb036a760e36a1361565b66785ec6c"},
|
||||
{file = "yarl-1.9.7-cp312-cp312-win_amd64.whl", hash = "sha256:2ead2f87a1174963cc406d18ac93d731fbb190633d3995fa052d10cefae69ed8"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:808eddabcb6f7b2cdb6929b3e021ac824a2c07dc7bc83f7618e18438b1b65781"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:395ab0d8ce6d104a988da429bcbfd445e03fb4c911148dfd523f69d13f772e47"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:49827dfccbd59c4499605c13805e947349295466e490860a855b7c7e82ec9c75"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f6b8bbdd425d0978311520ea99fb6c0e9e04e64aee84fac05f3157ace9f81b05"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:71d33fd1c219b5b28ee98cd76da0c9398a4ed4792fd75c94135237db05ba5ca8"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62440431741d0b7d410e5cbad800885e3289048140a43390ecab4f0b96dde3bb"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4db97210433366dfba55590e48285b89ad0146c52bf248dd0da492dd9f0f72cf"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:653597b615809f2e5f4dba6cd805608b6fd3597128361a22cc612cf7c7a4d1bf"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df47612129e66f7ce7c9994d4cd4e6852f6e3bf97699375d86991481796eeec8"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5e338b6febbae6c9fe86924bac3ea9c1944e33255c249543cd82a4af6df6047b"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:e649d37d04665dddb90994bbf0034331b6c14144cc6f3fbce400dc5f28dc05b7"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0a1b8fd849567be56342e988e72c9d28bd3c77b9296c38b9b42d2fe4813c9d3f"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9d715b2175dff9a49c6dafdc2ab3f04850ba2f3d4a77f69a5a1786b057a9d45"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-win32.whl", hash = "sha256:bc9233638b07c2e4a3a14bef70f53983389bffa9e8cb90a2da3f67ac9c5e1842"},
|
||||
{file = "yarl-1.9.7-cp313-cp313-win_amd64.whl", hash = "sha256:62e110772330d7116f91e79cd83fef92545cb2f36414c95881477aa01971f75f"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a564155cc2194ecd9c0d8f8dc57059b822a507de5f08120063675eb9540576aa"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:03e917cc44a01e1be60a83ee1a17550b929490aaa5df2a109adc02137bddf06b"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:eefda67ba0ba44ab781e34843c266a76f718772b348f7c5d798d8ea55b95517f"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:316c82b499b6df41444db5dea26ee23ece9356e38cea43a8b2af9e6d8a3558e4"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10452727843bc847596b75e30a7fe92d91829f60747301d1bd60363366776b0b"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:050f3e4d886be55728fef268587d061c5ce6f79a82baba71840801b63441c301"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0aabe557446aa615693a82b4d3803c102fd0e7a6a503bf93d744d182a510184"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23404842228e6fa8ace235024519df37f3f8e173620407644d40ddca571ff0f4"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:34736fcc9d6d7080ebbeb0998ecb91e4f14ad8f18648cf0b3099e2420a225d86"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:48f7a158f3ca67509d21cb02a96964e4798b6f133691cc0c86cf36e26e26ec8f"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:6639444d161c693cdabb073baaed1945c717d3982ecedf23a219bc55a242e728"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:1cd450e10cb53d63962757c3f6f7870be49a3e448c46621d6bd46f8088d532de"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:74d3ef5e81f81507cea04bf5ae22f18ef538607a7c754aac2b6e3029956a2842"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-win32.whl", hash = "sha256:4052dbd0c900bece330e3071c636f99dff06e4628461a29b38c6e222a427cf98"},
|
||||
{file = "yarl-1.9.7-cp38-cp38-win_amd64.whl", hash = "sha256:dd08da4f2d171e19bd02083c921f1bef89f8f5f87000d0ffc49aa257bc5a9802"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ab906a956d2109c6ea11e24c66592b06336e2743509290117f0f7f47d2c1dd3"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d8ad761493d5aaa7ab2a09736e62b8a220cb0b10ff8ccf6968c861cd8718b915"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d35f9cdab0ec5e20cf6d2bd46456cf599052cf49a1698ef06b9592238d1cf1b1"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a48d2b9f0ae29a456fb766ae461691378ecc6cf159dd9f938507d925607591c3"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cf85599c9336b89b92c313519bcaa223d92fa5d98feb4935a47cce2e8722b4b8"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e8916b1ff7680b1f2b1608c82dc15c569b9f2cb2da100c747c291f1acf18a14"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:29c80890e0a64fb0e5f71350d48da330995073881f8b8e623154aef631febfb0"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9163d21aa40ff8528db2aee2b0b6752efe098055b41ab8e5422b2098457199fe"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:65e3098969baf221bb45e3b2f60735fc2b154fc95902131ebc604bae4c629ea6"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cddebd096effe4be90fd378e4224cd575ac99e1c521598a6900e94959006e02e"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8525f955a2dcc281573b6aadeb8ab9c37e2d3428b64ca6a2feec2a794a69c1da"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:5d585c7d834c13f24c7e3e0efaf1a4b7678866940802e11bd6c4d1f99c935e6b"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78805148e780a9ca66f3123e04741e344b66cf06b4fb13223e3a209f39a6da55"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-win32.whl", hash = "sha256:3f53df493ec80b76969d6e1ae6e4411a55ab1360e02b80c84bd4b33d61a567ba"},
|
||||
{file = "yarl-1.9.7-cp39-cp39-win_amd64.whl", hash = "sha256:c81c28221a85add23a0922a6aeb2cdda7f9723e03e2dfae06fee5c57fe684262"},
|
||||
{file = "yarl-1.9.7-py3-none-any.whl", hash = "sha256:49935cc51d272264358962d050d726c3e5603a616f53e52ea88e9df1728aa2ee"},
|
||||
{file = "yarl-1.9.7.tar.gz", hash = "sha256:f28e602edeeec01fc96daf7728e8052bc2e12a672e2a138561a1ebaf30fd9df7"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -1139,4 +1315,4 @@ multidict = ">=4.0"
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "541857707095fb0b5c439aedbfacd91ca3582f110f12d786dc29e7c70f989b3e"
|
||||
content-hash = "70d489a46ab888e4ed82b7447d5a02cde51e9062b735715d98cc3e4f089aadb6"
|
||||
|
|
149
pyproject.toml
149
pyproject.toml
|
@ -1,27 +1,156 @@
|
|||
[tool.poetry]
|
||||
authors = ["wlinator <dokimakimaki@gmail.com>"]
|
||||
authors = ["wlinator <git@wlinator.org>"]
|
||||
description = "A Discord application, can serve as a template for your own bot."
|
||||
license = "GNU General Public License v3.0"
|
||||
name = "lumi"
|
||||
name = "luminara"
|
||||
package-mode = false
|
||||
readme = "README.md"
|
||||
version = "0.1.0"
|
||||
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.0"
|
||||
httpx = "^0.27.2"
|
||||
loguru = "^0.7.2"
|
||||
mysql-connector-python = "^9.0.0"
|
||||
pre-commit = "^3.7.1"
|
||||
pre-commit = "^3.8.0"
|
||||
psutil = "^6.0.0"
|
||||
py-cord = "^2.5.0"
|
||||
pyright = "^1.1.371"
|
||||
pydantic = "^2.8.2"
|
||||
pyright = "^1.1.377"
|
||||
python = "^3.12"
|
||||
python-dotenv = "^1.0.1"
|
||||
pytimeparse = "^1.1.8"
|
||||
pytz = "^2024.1"
|
||||
ruff = "^0.5.2"
|
||||
pyyaml = "^6.0.2"
|
||||
reactionmenu = "^3.1.7"
|
||||
ruff = "^0.6.2"
|
||||
typing-extensions = "^4.12.2"
|
||||
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["poetry-core"]
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
"examples",
|
||||
"tmp",
|
||||
"tests",
|
||||
".archive",
|
||||
"stubs",
|
||||
]
|
||||
|
||||
indent-width = 4
|
||||
line-length = 120
|
||||
target-version = "py312"
|
||||
|
||||
# Ruff Linting Configuration
|
||||
[tool.ruff.lint]
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
fixable = ["ALL"]
|
||||
ignore = ["E501", "N814", "PLR0913", "PLR2004"]
|
||||
select = [
|
||||
"I", # isort
|
||||
"E", # pycodestyle-error
|
||||
"F", # pyflakes
|
||||
"PERF", # perflint
|
||||
"N", # pep8-naming
|
||||
"TRY", # tryceratops
|
||||
"UP", # pyupgrade
|
||||
"FURB", # refurb
|
||||
"PL", # pylint
|
||||
"B", # flake8-bugbear
|
||||
"SIM", # flake8-simplify
|
||||
"ASYNC", # flake8-async
|
||||
"A", # flake8-builtins
|
||||
"C4", # flake8-comprehensions
|
||||
"DTZ", # flake8-datetimez
|
||||
"EM", # flake8-errmsg
|
||||
"PIE", # flake8-pie
|
||||
"T20", # flake8-print
|
||||
"Q", # flake8-quotes
|
||||
"RET", # flake8-return
|
||||
"PTH", # flake8-use-pathlib
|
||||
"INP", # flake8-no-pep420
|
||||
"RSE", # flake8-raise
|
||||
"ICN", # flake8-import-conventions
|
||||
"RUF", # ruff
|
||||
]
|
||||
unfixable = []
|
||||
|
||||
# Ruff Formatting Configuration
|
||||
[tool.ruff.format]
|
||||
docstring-code-format = true
|
||||
docstring-code-line-length = "dynamic"
|
||||
indent-style = "space"
|
||||
line-ending = "lf"
|
||||
quote-style = "double"
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Pyright Configuration
|
||||
[tool.pyright]
|
||||
defineConstant = {DEBUG = true}
|
||||
exclude = [
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".hg",
|
||||
".ipynb_checkpoints",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pyenv",
|
||||
".pytest_cache",
|
||||
".pytype",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
".vscode",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"site-packages",
|
||||
"venv",
|
||||
"examples",
|
||||
"tests",
|
||||
".archive",
|
||||
"stubs",
|
||||
]
|
||||
include = ["**/*.py"]
|
||||
pythonPlatform = "Linux"
|
||||
pythonVersion = "3.12"
|
||||
reportMissingTypeStubs = true
|
||||
reportShadowedImports = false
|
||||
stubPath = "./stubs"
|
||||
typeCheckingMode = "strict"
|
||||
venv = ".venv"
|
||||
venvPath = "."
|
||||
|
|
0
services/__init__.py
Normal file
0
services/__init__.py
Normal file
|
@ -1,17 +1,16 @@
|
|||
import datetime
|
||||
|
||||
import pytz
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
class Birthday:
|
||||
def __init__(self, user_id, guild_id):
|
||||
self.user_id = user_id
|
||||
self.guild_id = guild_id
|
||||
class BirthdayService:
|
||||
def __init__(self, user_id: int, guild_id: int) -> None:
|
||||
self.user_id: int = user_id
|
||||
self.guild_id: int = guild_id
|
||||
|
||||
def set(self, birthday):
|
||||
query = """
|
||||
def set(self, birthday: datetime.date) -> None:
|
||||
query: str = """
|
||||
INSERT INTO birthdays (user_id, guild_id, birthday)
|
||||
VALUES (%s, %s, %s)
|
||||
ON DUPLICATE KEY UPDATE birthday = VALUES(birthday);
|
||||
|
@ -19,8 +18,8 @@ class Birthday:
|
|||
|
||||
database.execute_query(query, (self.user_id, self.guild_id, birthday))
|
||||
|
||||
def delete(self):
|
||||
query = """
|
||||
def delete(self) -> None:
|
||||
query: str = """
|
||||
DELETE FROM birthdays
|
||||
WHERE user_id = %s AND guild_id = %s;
|
||||
"""
|
||||
|
@ -28,27 +27,26 @@ class Birthday:
|
|||
database.execute_query(query, (self.user_id, self.guild_id))
|
||||
|
||||
@staticmethod
|
||||
def get_birthdays_today():
|
||||
query = """
|
||||
def get_birthdays_today() -> list[tuple[int, int]]:
|
||||
query: str = """
|
||||
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")
|
||||
today: str = datetime.datetime.now(ZoneInfo("US/Eastern")).strftime("%m-%d")
|
||||
|
||||
return database.select_query(query, (today,))
|
||||
|
||||
@staticmethod
|
||||
def get_upcoming_birthdays(guild_id):
|
||||
query = """
|
||||
def get_upcoming_birthdays(guild_id: int) -> list[tuple[int, str]]:
|
||||
query: str = """
|
||||
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,))
|
||||
data: list[tuple[int, str]] = database.select_query(query, (guild_id,))
|
||||
|
||||
return [(row[0], row[1]) for row in data]
|
||||
return [(int(row[0]), str(row[1])) for row in data]
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
from typing import List, Optional, Tuple
|
||||
|
||||
from db import database
|
||||
|
||||
|
||||
|
@ -7,7 +5,7 @@ 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:
|
||||
def add_to_blacklist(self, reason: str | None = None) -> None:
|
||||
"""
|
||||
Adds a user to the blacklist with the given reason.
|
||||
|
||||
|
@ -37,5 +35,5 @@ class BlacklistUserService:
|
|||
FROM blacklist_user
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
result: List[Tuple[bool]] = database.select_query(query, (user_id,))
|
||||
result: list[tuple[bool]] = database.select_query(query, (user_id,))
|
||||
return any(active for (active,) in result)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from typing import Optional, Dict, Any, List
|
||||
from typing import Any
|
||||
|
||||
from db.database import execute_query, select_query_one, select_query_dict
|
||||
from db.database import execute_query, select_query_dict, select_query_one
|
||||
|
||||
|
||||
class CaseService:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
def create_case(
|
||||
|
@ -13,10 +13,10 @@ class CaseService:
|
|||
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,
|
||||
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 = """
|
||||
|
@ -24,10 +24,11 @@ class CaseService:
|
|||
FROM cases
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
case_number = select_query_one(query, (guild_id,))
|
||||
case_number: int | None = select_query_one(query, (guild_id,))
|
||||
|
||||
if case_number is None:
|
||||
raise ValueError("Failed to retrieve the next case number.")
|
||||
msg: str = "Failed to retrieve the next case number."
|
||||
raise ValueError(msg)
|
||||
|
||||
# Insert the new case
|
||||
query: str = """
|
||||
|
@ -54,8 +55,8 @@ class CaseService:
|
|||
|
||||
return int(case_number)
|
||||
|
||||
def close_case(self, guild_id, case_number):
|
||||
query = """
|
||||
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
|
||||
|
@ -66,9 +67,9 @@ class CaseService:
|
|||
self,
|
||||
guild_id: int,
|
||||
case_number: int,
|
||||
new_reason: Optional[str] = None,
|
||||
new_reason: str | None = None,
|
||||
) -> bool:
|
||||
query = """
|
||||
query: str = """
|
||||
UPDATE cases
|
||||
SET reason = COALESCE(%s, reason),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
|
@ -84,88 +85,84 @@ class CaseService:
|
|||
)
|
||||
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"""
|
||||
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_case_by_id(self, case_id: int) -> Optional[Dict[str, Any]]:
|
||||
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
|
||||
"""
|
||||
result: List[Dict[str, Any]] = select_query_dict(query, (case_id,))
|
||||
return result[0] if result else None
|
||||
return self._fetch_single_case(query, (case_id,))
|
||||
|
||||
def fetch_case_by_guild_and_number(
|
||||
self,
|
||||
guild_id: int,
|
||||
case_number: int,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
) -> dict[str, Any] | None:
|
||||
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
|
||||
return self._fetch_single_case(query, (guild_id, case_number))
|
||||
|
||||
def fetch_cases_by_guild(self, guild_id: int) -> List[Dict[str, Any]]:
|
||||
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
|
||||
return self._fetch_cases(query, (guild_id,))
|
||||
|
||||
def fetch_cases_by_target(
|
||||
self,
|
||||
guild_id: int,
|
||||
target_id: int,
|
||||
) -> List[Dict[str, Any]]:
|
||||
) -> 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
|
||||
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]]:
|
||||
) -> 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
|
||||
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]]:
|
||||
) -> 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
|
||||
return self._fetch_cases(query, (guild_id, action_type.upper()))
|
|
@ -1,28 +1,30 @@
|
|||
from typing import Any
|
||||
|
||||
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
|
||||
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):
|
||||
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 = """
|
||||
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,
|
||||
|
@ -38,35 +40,24 @@ class GuildConfig:
|
|||
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):
|
||||
def _extracted_from_fetch_or_create_config_14(self, query: str) -> None:
|
||||
result: tuple[Any, ...] = database.select_query(query, (self.guild_id,))[0]
|
||||
(
|
||||
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,
|
||||
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
|
||||
|
||||
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 = """
|
||||
def push(self) -> None:
|
||||
query: str = """
|
||||
UPDATE guild_config
|
||||
SET
|
||||
birthday_channel_id = %s,
|
||||
|
@ -102,18 +93,18 @@ class GuildConfig:
|
|||
)
|
||||
|
||||
@staticmethod
|
||||
def get_prefix(message):
|
||||
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 = """
|
||||
query: str = """
|
||||
SELECT prefix
|
||||
FROM guild_config
|
||||
WHERE guild_id = %s
|
||||
"""
|
||||
|
||||
prefix = database.select_query_one(
|
||||
prefix: str | None = database.select_query_one(
|
||||
query,
|
||||
(message.guild.id if message.guild else None,),
|
||||
)
|
||||
|
@ -121,8 +112,8 @@ class GuildConfig:
|
|||
return prefix or "."
|
||||
|
||||
@staticmethod
|
||||
def get_prefix_from_guild_id(guild_id):
|
||||
query = """
|
||||
def get_prefix_from_guild_id(guild_id: int) -> str:
|
||||
query: str = """
|
||||
SELECT prefix
|
||||
FROM guild_config
|
||||
WHERE guild_id = %s
|
||||
|
@ -131,11 +122,11 @@ class GuildConfig:
|
|||
return database.select_query_one(query, (guild_id,)) or "."
|
||||
|
||||
@staticmethod
|
||||
def set_prefix(guild_id, prefix):
|
||||
def set_prefix(guild_id: int, prefix: str) -> None:
|
||||
"""
|
||||
Sets the prefix for a given guild.
|
||||
"""
|
||||
query = """
|
||||
query: str = """
|
||||
UPDATE guild_config
|
||||
SET prefix = %s
|
||||
WHERE guild_id = %s;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue