diff --git a/.gitignore b/.gitignore index 75b8dc11..219c0a24 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,4 @@ next-env.d.ts # project -!/byte_bot/byte_bot/lib/ -!/byte_bot/server/lib/ -!/byte_bot/server/lib/ !/docs/web/api/lib/ diff --git a/byte_bot/__init__.py b/byte_bot/__init__.py deleted file mode 100644 index cb7b7ead..00000000 --- a/byte_bot/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Byte Bot.""" - -from __future__ import annotations - -from rich import get_console -from rich.traceback import install as rich_tracebacks - -from byte_bot import app, byte, cli, server, utils -from byte_bot.__metadata__ import __version__ - -__all__ = ( - "__version__", - "app", - "byte", - "cli", - "server", - "utils", -) - -rich_tracebacks( - console=get_console(), - suppress=( - "click", - "rich", - "saq", - "litestar", - "rich_click", - ), - show_locals=False, -) -"""Pre-configured traceback handler. - -Suppresses some of the frames by default to reduce the amount printed to -the screen. -""" diff --git a/byte_bot/__main__.py b/byte_bot/__main__.py deleted file mode 100644 index e8b46507..00000000 --- a/byte_bot/__main__.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Application Entrypoint.""" - -from __future__ import annotations - -__all__ = ("run_cli",) - - -def run_cli() -> None: - """Application Entrypoint.""" - import os - import sys - from pathlib import Path - - current_path = Path(__file__).parent.parent.resolve() - sys.path.append(str(current_path)) - os.environ.setdefault("LITESTAR_APP", "byte_bot.app:app") - try: - from litestar.__main__ import run_cli as run_litestar_cli - - except ImportError as exc: - print( # noqa: T201 - "Could not load required libraries. ", - "Please check your installation and make sure you activated any necessary virtual environment", - ) - print(exc) # noqa: T201 - sys.exit(1) - run_litestar_cli() - - -if __name__ == "__main__": - run_cli() diff --git a/byte_bot/__metadata__.py b/byte_bot/__metadata__.py deleted file mode 100644 index 3ee57404..00000000 --- a/byte_bot/__metadata__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Source of truth for project metadata.""" - -from __future__ import annotations - -import importlib.metadata - -__all__ = ("__project__", "__version__") - -__version__ = importlib.metadata.version("byte-bot") -"""Version of the app.""" -__project__ = importlib.metadata.metadata("byte-bot")["Name"] -"""Name of the app.""" diff --git a/byte_bot/app.py b/byte_bot/app.py deleted file mode 100644 index 600e5b8e..00000000 --- a/byte_bot/app.py +++ /dev/null @@ -1,74 +0,0 @@ -"""ASGI application factory.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from litestar import Litestar - -__all__ = ["create_app"] - - -def create_app() -> Litestar: - """Application factory to instantiate a Litestar application. - - Returns: - Litestar: The Litestar application. - """ - from advanced_alchemy.exceptions import RepositoryError - from litestar import Litestar - from pydantic import SecretStr - - from byte_bot.server import domain - from byte_bot.server.lib import ( - cors, - db, - exceptions, - log, - openapi, - settings, - static_files, - template, - ) - from byte_bot.server.lib.dependencies import create_collection_dependencies - - dependencies = create_collection_dependencies() - - return Litestar( - # Handlers - exception_handlers={ - exceptions.ApplicationError: exceptions.exception_to_http_response, - RepositoryError: exceptions.exception_to_http_response, - }, - route_handlers=[*domain.routes], - # Configs - cors_config=cors.config, - logging_config=log.config, - openapi_config=openapi.config, - static_files_config=static_files.config, - template_config=template.config, - dependencies=dependencies, - # Lifecycle - before_send=[log.controller.BeforeSendHandler()], - on_shutdown=[], - on_startup=[lambda: log.configure(log.default_processors)], # type: ignore[arg-type] - on_app_init=[], - # Other - debug=settings.project.DEBUG, - middleware=[log.controller.middleware_factory], - signature_namespace=domain.signature_namespace, - type_encoders={SecretStr: str}, - plugins=[db.plugin], - ) - - -def create_bot() -> None: - """Application factory to instantiate a Discord bot. - - .. todo:: Move into this file. - """ - - -app = create_app() -bot = create_bot() diff --git a/byte_bot/byte/__init__.py b/byte_bot/byte/__init__.py deleted file mode 100644 index 0868872e..00000000 --- a/byte_bot/byte/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Byte Bot Bot.""" - -from __future__ import annotations - -from byte_bot.byte import bot, lib, plugins, views -from byte_bot.byte.lib.log import setup_logging - -__all__ = ("bot", "lib", "plugins", "setup_logging", "views") - -setup_logging() diff --git a/byte_bot/byte/bot.py b/byte_bot/byte/bot.py deleted file mode 100644 index 788f2903..00000000 --- a/byte_bot/byte/bot.py +++ /dev/null @@ -1,174 +0,0 @@ -"""Byte Bot.""" - -from __future__ import annotations - -import contextlib - -import discord -import httpx -from anyio import run -from discord import Activity, Forbidden, Intents, Member, Message, NotFound -from discord.ext.commands import Bot, CommandError, Context, ExtensionAlreadyLoaded -from dotenv import load_dotenv -from httpx import ConnectError - -from byte_bot.byte.lib import settings -from byte_bot.byte.lib.log import get_logger -from byte_bot.server.lib.settings import ServerSettings - -__all__ = [ - "Byte", - "run_bot", -] - -logger = get_logger() -load_dotenv() -server_settings = ServerSettings() - - -class Byte(Bot): - """Byte Bot Base Class.""" - - def __init__(self, command_prefix: list[str], intents: Intents, activity: Activity) -> None: - """Initialize the bot. - - Args: - command_prefix (str): Command prefix for the bot. - intents (discord.Intents): Intents for the bot. - activity (discord.Activity): Activity for the bot. - """ - super().__init__(command_prefix=command_prefix, intents=intents, activity=activity) - - async def setup_hook(self) -> None: - """Any setup we need can be here.""" - # Load cogs before syncing the tree. - await self.load_cogs() - dev_guild = discord.Object(id=settings.discord.DEV_GUILD_ID) - self.tree.copy_global_to(guild=dev_guild) - await self.tree.sync(guild=dev_guild) - - async def load_cogs(self) -> None: - """Load cogs.""" - cogs = [ - cog - for plugins_dir in settings.discord.PLUGINS_DIRS - for cog in plugins_dir.rglob("*.py") - if cog.stem != "__init__" - ] - - cogs_import_path = [".".join(cog.parts[cog.parts.index("byte_bot") : -1]) + "." + cog.stem for cog in cogs] - - for cog in cogs_import_path: - with contextlib.suppress(ExtensionAlreadyLoaded): - await self.load_extension(cog) - - async def on_ready(self) -> None: - """Handle bot ready event.""" - logger.info("%s has connected to Discord!", self.user) - - async def on_message(self, message: Message) -> None: - """Handle message events. - - Args: - message: Message object. - """ - await self.process_commands(message) - - async def on_command_error(self, ctx: Context, error: CommandError) -> None: - """Handle command errors. - - Args: - ctx: Context object. - error: Error object. - """ - err = getattr(error, "original", error) - if isinstance(err, Forbidden | NotFound): - return - - embed = discord.Embed(title="Command Error", description=str(error), color=discord.Color.red()) - if ctx.author.avatar: - embed.set_thumbnail(url=ctx.author.avatar.url) - embed.add_field(name="Command", value=ctx.command) - embed.add_field(name="Message", value=ctx.message.content) - embed.add_field(name="Channel", value=getattr(ctx.channel, "mention", str(ctx.channel))) - embed.add_field(name="Author", value=ctx.author.mention) - embed.add_field(name="Guild", value=ctx.guild.name if ctx.guild else "DM") - embed.add_field(name="Location", value=f"[Jump]({ctx.message.jump_url})") - embed.set_footer(text=f"Time: {ctx.message.created_at.strftime('%Y-%m-%d %H:%M:%S')}") - await ctx.send(embed=embed, ephemeral=True) - - @staticmethod - async def on_member_join(member: Member) -> None: - """Handle member join event. - - Args: - member: Member object. - """ - if not member.bot: - await member.send( - f"Welcome to {member.guild.name}! Please make sure to read the rules if you haven't already. " - f"Feel free to ask any questions you have in the help channel." - ) - - async def on_guild_join(self, guild: discord.Guild) -> None: - """Handle guild join event. - - Args: - guild: Guild object. - """ - await self.tree.sync(guild=guild) - api_url = f"http://{server_settings.HOST}:{server_settings.PORT}/api/guilds/create?guild_id={guild.id}&guild_name={guild.name}" - - try: - async with httpx.AsyncClient() as client: - response = await client.post(api_url) - - if response.status_code == httpx.codes.CREATED: - logger.info("successfully added guild %s (id: %s)", guild.name, guild.id) - embed = discord.Embed( - title="Guild Joined", - description=f"Joined guild {guild.name} (ID: {guild.id})", - color=discord.Color.green(), - ) - else: - embed = discord.Embed( - title="Guild Join Failed", - description=f"Joined guild, but failed to add guild {guild.name} (ID: {guild.id}) to database", - color=discord.Color.red(), - ) - - if dev_guild := self.get_guild(settings.discord.DEV_GUILD_ID): - if dev_channel := dev_guild.get_channel(settings.discord.DEV_GUILD_INTERNAL_ID): - if hasattr(dev_channel, "send"): - await dev_channel.send(embed=embed) # type: ignore[attr-defined] - else: - logger.error("dev channel not found.") - else: - logger.error("dev guild not found.") - except ConnectError: - logger.exception("failed to connect to api to add guild %s (id: %s)", guild.name, guild.id) - - -def run_bot() -> None: - """Run the bot.""" - intents = discord.Intents.default() - intents.message_content = True - intents.members = True - presence = discord.Activity( - name="!help", - type=discord.ActivityType.custom, - state="Serving Developers", - details="!help", - url=settings.discord.PRESENCE_URL, - ) - bot = Byte(command_prefix=settings.discord.COMMAND_PREFIX, intents=intents, activity=presence) - - async def start_bot() -> None: - """Start the bot.""" - await bot.start(settings.discord.TOKEN) - - run(start_bot) - - -if __name__ == "__main__": - run_bot() diff --git a/byte_bot/byte/lib/__init__.py b/byte_bot/byte/lib/__init__.py deleted file mode 100644 index 1736b81c..00000000 --- a/byte_bot/byte/lib/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -"""Byte library module.""" - -from byte_bot.byte.lib import common, log, settings, types, utils - -__all__ = [ - "common", - "common", - "log", - "log", - "settings", - "types", - "utils", -] diff --git a/byte_bot/byte/lib/checks.py b/byte_bot/byte/lib/checks.py deleted file mode 100644 index 10b07717..00000000 --- a/byte_bot/byte/lib/checks.py +++ /dev/null @@ -1,72 +0,0 @@ -""":doc:`Checks ` for Byte.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from discord.ext.commands import CheckFailure, Context, check - -from byte_bot.byte.lib import settings - -if TYPE_CHECKING: - from discord.ext.commands._types import Check - -__all__ = ("is_byte_dev", "is_guild_admin") - - -def is_guild_admin() -> Check: - """Check if the user is a guild admin. - - Returns: - A check function. - """ - - async def predicate(ctx: Context) -> bool: - """Check if the user is a guild admin. - - Args: - ctx: Context object. - - Returns: - True if the user is a guild admin, False otherwise. - """ - if not ctx.guild: - msg = "Command can only be used in a guild." - raise CheckFailure(msg) - if not (member := ctx.guild.get_member(ctx.author.id)): - msg = "Member not found in the guild." - raise CheckFailure(msg) - return member.guild_permissions.administrator - - return check(predicate) - - -def is_byte_dev() -> Check: - """Determines if the user is a Byte developer or owner. - - Returns: - A check function. - """ - - async def predicate(ctx: Context) -> bool: - """Check if the user is a Byte developer or owner. - - Args: - ctx: Context object. - - Returns: - True if the user is a Byte developer or owner, False otherwise. - """ - if await ctx.bot.is_owner(ctx.author) or ctx.author.id == settings.discord.DEV_USER_ID: - return True - - if not ctx.guild: - return False - - member = ctx.guild.get_member(ctx.author.id) - if not member: - return False - - return any(role.name == "byte-dev" for role in member.roles) - - return check(predicate) diff --git a/byte_bot/byte/lib/common/__init__.py b/byte_bot/byte/lib/common/__init__.py deleted file mode 100644 index 42f25733..00000000 --- a/byte_bot/byte/lib/common/__init__.py +++ /dev/null @@ -1,57 +0,0 @@ -"""Common variables and functions for use throughout Byte. - -.. todo:: temporary, these are not multi-guild friendly. -""" - -from typing import Any - -from byte_bot.byte.lib.common import assets, colors, guilds, links, mention - -__all__ = ( - "assets", - "colors", - "config_options", - "guilds", - "links", - "mention", -) - -config_options: list[dict[str, Any]] = [ - { - "label": "Server Settings", - "description": "Configure overall server settings", - "sub_settings": [ - {"label": "Prefix", "field": "prefix", "data_type": "String"}, - {"label": "Help Channel ID", "field": "help_channel_id", "data_type": "Integer"}, - {"label": "Sync Label", "field": "sync_label", "data_type": "String"}, - {"label": "Issue Linking", "field": "issue_linking", "data_type": "True/False"}, - {"label": "Comment Linking", "field": "comment_linking", "data_type": "True/False"}, - {"label": "PEP Linking", "field": "pep_linking", "data_type": "True/False"}, - ], - }, - { - "label": "GitHub Settings", - "description": "Configure GitHub settings", - "sub_settings": [ - {"label": "Discussion Sync", "field": "discussion_sync", "data_type": "True/False"}, - {"label": "GitHub Organization", "field": "github_organization", "data_type": "String"}, - {"label": "GitHub Repository", "field": "github_repository", "data_type": "String"}, - ], - }, - { - "label": "StackOverflow Settings", - "description": "Configure StackOverflow settings", - "sub_settings": [ - {"label": "Tag Name", "field": "tag_name", "data_type": "Comma-Separated String"}, - ], - }, - { - "label": "Allowed Users", - "description": "Configure allowed users", - "sub_settings": [ - {"label": "User ID", "field": "user_id", "data_type": "Integer"}, - ], - }, - # Forum Settings: Configure help and showcase forum settings - # Byte Settings: Configure meta-level Byte features -] diff --git a/byte_bot/byte/lib/common/assets.py b/byte_bot/byte/lib/common/assets.py deleted file mode 100644 index ea5e52d8..00000000 --- a/byte_bot/byte/lib/common/assets.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Common variables and functions for use throughout Byte. - -.. todo:: temporary, these are not multi-guild friendly. -""" - -from typing import Final - -# --- Assets -litestar_logo_white: Final = "https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20PNG%20-%20Transparent/Badge%20-%20White.png" -litestar_logo_yellow: Final = "https://raw.githubusercontent.com/litestar-org/branding/main/assets/Branding%20-%20PNG%20-%20Transparent/Badge%20-%20Blue%20and%20Yellow.png" -ruff_logo: Final = "https://raw.githubusercontent.com/JacobCoffee/byte/main/assets/ruff.png" -python_logo: Final = "https://raw.githubusercontent.com/JacobCoffee/byte/main/assets/python.png" diff --git a/byte_bot/byte/lib/common/colors.py b/byte_bot/byte/lib/common/colors.py deleted file mode 100644 index 622f0bb6..00000000 --- a/byte_bot/byte/lib/common/colors.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Common variables and functions for use throughout Byte. - -.. todo:: temporary, these are not multi-guild friendly. -""" - -from typing import Final - -# --- Colors -litestar_blue: Final = 0x202235 -litestar_yellow: Final = 0xEDB641 - -python_blue: Final = 0x4B8BBE -python_yellow: Final = 0xFFD43B - -astral_yellow: Final = 0xD7FF64 -astral_purple: Final = 0x261230 - -byte_teal: Final = 0x42B1A8 diff --git a/byte_bot/byte/lib/common/guilds.py b/byte_bot/byte/lib/common/guilds.py deleted file mode 100644 index 6f05964b..00000000 --- a/byte_bot/byte/lib/common/guilds.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Common variables and functions for use throughout Byte. - -.. todo:: temporary, these are not multi-guild friendly. -""" - -from typing import Final - -# --- Channel IDs -litestar_help_channel: Final = 1064114019373432912 diff --git a/byte_bot/byte/lib/common/links.py b/byte_bot/byte/lib/common/links.py deleted file mode 100644 index 28eb1289..00000000 --- a/byte_bot/byte/lib/common/links.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Common variables and functions for use throughout Byte. - -.. todo:: temporary, these are not multi-guild friendly. -""" - -from typing import Final - -# --- Links -mcve: Final = "https://stackoverflow.com/help/minimal-reproducible-example" -pastebin: Final = "https://paste.pythondiscord.com" -markdown_guide: Final = "https://support.discord.com/hc/en-us/articles/210298617-Markdown-Text-101-Chat-Formatting-Bold-Italic-Underline-#h_01GY0DAKGXDEHE263BCAYEGFJA" diff --git a/byte_bot/byte/lib/common/mention.py b/byte_bot/byte/lib/common/mention.py deleted file mode 100644 index f81c6bad..00000000 --- a/byte_bot/byte/lib/common/mention.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Helper functions for mentioning users, roles, and channels.""" - -from __future__ import annotations - -__all__ = ( - "mention_channel", - "mention_custom_emoji", - "mention_custom_emoji_animated", - "mention_guild_navigation", - "mention_role", - "mention_slash_command", - "mention_timestamp", - "mention_user", - "mention_user_nickname", -) - - -def mention_user(user_id: int) -> str: - """Mention a user by ID. - - Args: - user_id: The unique identifier for the user. - - Returns: - A formatted string that mentions the user. - """ - return f"<@{user_id}>" - - -def mention_user_nickname(user_id: int) -> str: - """Mention a user by ID with a nickname. - - Args: - user_id: The unique identifier for the user. - - Returns: - A formatted string that mentions the user with a nickname. - """ - return f"<@!{user_id}>" - - -def mention_channel(channel_id: int) -> str: - """Mention a channel by ID. - - Args: - channel_id: The unique identifier for the channel. - - Returns: - A formatted string that mentions the channel. - """ - return f"<#{channel_id}>" - - -def mention_role(role_id: int) -> str: - """Mention a role by ID. - - Args: - role_id: The unique identifier for the role. - - Returns: - A formatted string that mentions the role. - """ - return f"<@&{role_id}>" - - -def mention_slash_command(name: str, command_id: int) -> str: - """Mention a slash command by name and ID. - - Args: - name: The name of the slash command. - command_id: The unique identifier for the slash command. - - Returns: - A formatted string that mentions the slash command. - """ - return f"" - - -def mention_custom_emoji(name: str, emoji_id: int) -> str: - """Mention a custom emoji by name and ID. - - Args: - name: The name of the emoji. - emoji_id: The unique identifier for the emoji. - - Returns: - A formatted string that mentions the custom emoji. - """ - return f"<:{name}:{emoji_id}>" - - -def mention_custom_emoji_animated(name: str, emoji_id: int) -> str: - """Mention an animated custom emoji by name and ID. - - Args: - name: The name of the animated emoji. - emoji_id: The unique identifier for the animated emoji. - - Returns: - A formatted string that mentions the animated custom emoji. - """ - return f"" - - -def mention_timestamp(timestamp: int, style: str = "") -> str: - """Mention a timestamp, optionally with a style. - - Args: - timestamp: The Unix timestamp to format. - style: An optional string representing the timestamp style. - (Default `` ``, valid styles: ``t``, ``T``, ``d``, ``D``, ``f``, ``F``, ``R``) - - Returns: - A formatted string that represents the timestamp. - """ - return f"" if style else f"" - - -def mention_guild_navigation(guild_nav_type: str, guild_element_id: int) -> str: - """Mention a guild navigation element by type and ID. - - Args: - guild_nav_type: The type of the guild navigation element. - guild_element_id: The unique identifier for the element. - - Returns: - A formatted string that mentions the guild navigation element. - """ - return f"<{guild_element_id}:{guild_nav_type}>" diff --git a/byte_bot/byte/lib/log.py b/byte_bot/byte/lib/log.py deleted file mode 100644 index 5f860329..00000000 --- a/byte_bot/byte/lib/log.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Centralized logging configuration.""" - -import logging -import logging.config -import logging.handlers -from logging import Logger -from pathlib import Path - -from rich.console import Console -from rich.logging import RichHandler # noqa: F401 -from rich.traceback import install - -from byte_bot.byte.lib import settings - -__all__ = [ - "get_logger", - "setup_logging", -] - -install(show_locals=True, theme="dracula") -console = Console() - - -def setup_logging() -> None: - """Set up logging configuration based on the environment.""" - env = settings.project.ENVIRONMENT - log_file_path = settings.log.FILE - log_directory = log_file_path.parent - - if not Path(log_directory).exists(): - Path(log_directory).mkdir(parents=True, exist_ok=True) - - handlers = { - "file": { - "class": "logging.handlers.RotatingFileHandler", - "formatter": "simple", - "filename": settings.BASE_DIR / "logs" / "byte.log", - "maxBytes": 10485760, - "backupCount": 3, - "level": "INFO", - }, - "syslog": { - "class": "logging.handlers.SysLogHandler", - "formatter": "simple", - "address": "/dev/log", - "level": "INFO", - }, - "console": { - "class": "rich.logging.RichHandler", - "formatter": "simple", - "level": "DEBUG", - } - if env == "dev" - else { - "class": "logging.StreamHandler", - "formatter": "simple", - "level": "INFO", - }, - } - - logging_config = { - "version": 1, - "disable_existing_loggers": False, - "formatters": { - "simple": { - "format": settings.log.FORMAT, - }, - }, - "handlers": handlers, - "loggers": { - "discord": { - "level": settings.log.DISCORD_LEVEL, - "handlers": ["console", "file"], - "propagate": False, - }, - "httpcore": { - "level": settings.log.HTTP_CORE_LEVEL, - "handlers": ["console", "file"], - "propagate": False, - }, - "httpx": { - "level": settings.log.HTTPX_LEVEL, - "handlers": ["console", "file"], - "propagate": False, - }, - "websockets": { - "level": settings.log.WEBSOCKETS_LEVEL, - "handlers": ["console", "file"], - "propagate": False, - }, - "asyncio": { - "level": settings.log.ASYNCIO_LEVEL, - "handlers": ["console", "file"], - "propagate": False, - }, - "root": { - "level": "DEBUG" if settings.project.DEBUG else "INFO", - "handlers": ["console", "file"] if env == "dev" else ["file", "syslog"], - "propagate": False, - }, - }, - } - - logging.config.dictConfig(logging_config) - - -def get_logger(name: str = "__main__") -> Logger: - """Get a logger with the specified name. - - Args: - name (str): The name of the logger. - - Returns: - logging.Logger: The logger with the specified name. - """ - return logging.getLogger(name) diff --git a/byte_bot/byte/lib/settings.py b/byte_bot/byte/lib/settings.py deleted file mode 100644 index 7d74766e..00000000 --- a/byte_bot/byte/lib/settings.py +++ /dev/null @@ -1,167 +0,0 @@ -"""Project Settings.""" - -from __future__ import annotations - -import os -from pathlib import Path # noqa: TC003 -from typing import Final - -from dotenv import load_dotenv -from litestar.utils.module_loader import module_to_os_path -from pydantic import ValidationError, field_validator -from pydantic_settings import BaseSettings, SettingsConfigDict - -from byte_bot.__metadata__ import __version__ as version - -__all__ = [ - "DiscordSettings", - "LogSettings", - "ProjectSettings", - "discord", - "log", - "project", -] - -load_dotenv() - -DEFAULT_MODULE_NAME: str = "byte_bot" -BASE_DIR: Final = module_to_os_path(DEFAULT_MODULE_NAME) -PLUGINS_DIR: Final = module_to_os_path("byte_bot.byte.plugins") - - -class DiscordSettings(BaseSettings): - """Discord Settings.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="DISCORD_", extra="ignore") - - TOKEN: str - """Discord API token.""" - COMMAND_PREFIX: list[str] = ["!"] - """Command prefix for bot commands.""" - DEV_GUILD_ID: int - """Discord Guild ID for development.""" - DEV_USER_ID: int - """Discord User ID for development.""" - DEV_GUILD_INTERNAL_ID: int = 1136100160510902272 - """Internal channel ID for the development guild.""" - PLUGINS_LOC: Path = PLUGINS_DIR - """Base Path to plugins directory.""" - PLUGINS_DIRS: list[Path] = [PLUGINS_DIR] - """Directories to search for plugins.""" - PRESENCE_URL: str = "" - - @field_validator("COMMAND_PREFIX") - @classmethod - def assemble_command_prefix(cls, value: list[str]) -> list[str]: - """Assembles the bot command prefix based on the environment. - - Args: - value: Default value of ``COMMAND_PREFIX``. Currently ``["!"]`` - - Returns: - The assembled prefix string. - """ - env_urls = { - "prod": "byte ", - "test": "bit ", - "dev": "nibble ", - } - environment = os.getenv("ENVIRONMENT", "dev") - # Add env specific command prefix in addition to the default "!" - env_prefix = os.getenv("COMMAND_PREFIX", env_urls[environment]) - if env_prefix not in value: - value.append(env_prefix) - return value - - @field_validator("PRESENCE_URL") - @classmethod - def assemble_presence_url(cls, value: str) -> str: # noqa: ARG003 - """Assembles the bot presence url based on the environment. - - Args: - value: Not used. - - Returns: - The assembled prefix string. - """ - env_urls = { - "prod": "https://byte-bot.app/", - "test": "https://dev.byte-bot.app/", - "dev": "https://dev.byte-bot.app/", - } - environment = os.getenv("ENVIRONMENT", "dev") - return os.getenv("PRESENCE_URL", env_urls[environment]) - - -class LogSettings(BaseSettings): - """Logging config for the Project.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="LOG_", extra="ignore") - - LEVEL: int = 20 - """Stdlib log levels. - - Only emit logs at this level, or higher. - """ - DISCORD_LEVEL: int = 30 - """Sets the log level for the discord.py library.""" - WEBSOCKETS_LEVEL: int = 30 - """Sets the log level for the websockets library.""" - ASYNCIO_LEVEL: int = 20 - """Sets the log level for the asyncio library.""" - HTTP_CORE_LEVEL: int = 20 - """Sets the log level for the httpcore library. (Used in cert. validation)""" - HTTPX_LEVEL: int = 30 - """Sets the log level for the httpx library.""" - FORMAT: str = "[[ %(asctime)s ]] - [[ %(name)s ]] - [[ %(levelname)s ]] - %(message)s" - """Log format string.""" - FILE: Path = BASE_DIR / "logs" / "byte.log" - """Log file path.""" - - -class ProjectSettings(BaseSettings): - """Project Settings.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", extra="ignore") - - DEBUG: bool = False - """Run app with ``debug=True``.""" - ENVIRONMENT: str = "prod" - """``dev``, ``prod``, ``test``, etc.""" - VERSION: str = version - """The current version of the application.""" - - -# noinspection PyShadowingNames -def load_settings() -> tuple[ - DiscordSettings, - LogSettings, - ProjectSettings, -]: - """Load Settings file. - - Returns: - Settings: application settings - """ - try: - """Override Application reload dir.""" - - discord: DiscordSettings = DiscordSettings.model_validate({}) - log: LogSettings = LogSettings.model_validate({}) - project: ProjectSettings = ProjectSettings.model_validate({}) - - except ValidationError as error: - print(f"Could not load settings. Error: {error!r}") # noqa: T201 - raise error from error - return ( - discord, - log, - project, - ) - - -( - discord, - log, - project, -) = load_settings() diff --git a/byte_bot/byte/lib/types/__init__.py b/byte_bot/byte/lib/types/__init__.py deleted file mode 100644 index eeec4939..00000000 --- a/byte_bot/byte/lib/types/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Types and similar facilities used throughout library code.""" - -from byte_bot.byte.lib.types import astral, python - -__all__ = ("astral", "python") diff --git a/byte_bot/byte/lib/types/astral.py b/byte_bot/byte/lib/types/astral.py deleted file mode 100644 index cf0ee238..00000000 --- a/byte_bot/byte/lib/types/astral.py +++ /dev/null @@ -1,32 +0,0 @@ -"""Types for Astral views and plugins.""" - -from __future__ import annotations - -from typing import TypedDict - -__all__ = ("BaseRuffRule", "FormattedRuffRule", "RuffRule") - - -class BaseRuffRule(TypedDict): - """Base Ruff rule data.""" - - name: str - summary: str - fix: str - explanation: str - - -class RuffRule(BaseRuffRule): - """Ruff rule data.""" - - code: str - linter: str - message_formats: list[str] - preview: bool - - -class FormattedRuffRule(BaseRuffRule): - """Formatted Ruff rule data.""" - - rule_link: str - rule_anchor_link: str diff --git a/byte_bot/byte/lib/types/python.py b/byte_bot/byte/lib/types/python.py deleted file mode 100644 index f9c31239..00000000 --- a/byte_bot/byte/lib/types/python.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Types for Python related views and plugins.""" - -from __future__ import annotations - -from enum import StrEnum -from typing import TYPE_CHECKING, TypedDict - -if TYPE_CHECKING: - from datetime import datetime - -__all__ = ("PEP", "PEPHistoryItem", "PEPStatus", "PEPType") - - -class PEPType(StrEnum): - """Type of PEP. - - Based off of `PEP Types in PEP1 `_. - """ - - I = "Informational" # noqa: E741 - P = "Process" - S = "Standards Track" - - -class PEPStatus(StrEnum): - """Status of a PEP. - - .. note:: ``Active`` and ``Accepted`` both traditionally use ``A``, - but are differentiated here for clarity. - - Based off of `PEP Status in PEP1 `_. - """ - - A = "Active" - AA = "Accepted" - D = "Deferred" - __ = "Draft" - F = "Final" - P = "Provisional" - R = "Rejected" - S = "Superseded" - W = "Withdrawn" - - -class PEPHistoryItem(TypedDict, total=False): - """PEP history item. - - Sometimes these include a list of ``datetime`` objects, - other times they are a list of datetime and str - because they contain a date and an rST link. - """ - - date: str - link: str - - -class PEP(TypedDict): - """PEP data. - - Based off of the `PEPS API `_. - """ - - number: int - title: str - authors: list[str] | str - discussions_to: str - status: PEPStatus - type: PEPType - topic: str - created: datetime - python_version: list[float] | float - post_history: list[str] - resolution: str | None - requires: str | None - replaces: str | None - superseded_by: str | None - url: str diff --git a/byte_bot/byte/lib/utils.py b/byte_bot/byte/lib/utils.py deleted file mode 100644 index 64f53833..00000000 --- a/byte_bot/byte/lib/utils.py +++ /dev/null @@ -1,228 +0,0 @@ -"""Byte utilities.""" - -from __future__ import annotations - -import datetime as dt -import json -import re -import subprocess -from datetime import UTC, datetime -from itertools import islice -from typing import TYPE_CHECKING, TypeVar - -import httpx -from anyio import run_process -from ruff.__main__ import find_ruff_bin # type: ignore[import-untyped] - -from byte_bot.byte.lib.common.links import pastebin -from byte_bot.byte.lib.types.astral import FormattedRuffRule, RuffRule -from byte_bot.byte.lib.types.python import PEP, PEPStatus, PEPType - -if TYPE_CHECKING: - from collections.abc import Iterable - - -__all__ = ( - "PEP", - "FormattedRuffRule", - "PEPStatus", - "PEPType", - "RuffRule", - "chunk_sequence", - "format_resolution_link", - "format_ruff_rule", - "get_next_friday", - "linker", - "paste", - "query_all_peps", - "query_all_ruff_rules", - "run_ruff_format", -) - -_T = TypeVar("_T") - - -def linker(title: str, link: str, show_embed: bool = False) -> str: - """Create a Markdown link, optionally with an embed. - - Args: - title: The title of the link. - link: The URL of the link. - show_embed: Whether to show the embed or not. - - Returns: - A Markdown link. - """ - return f"[{title}]({link})" if show_embed else f"[{title}](<{link}>)" - - -def format_ruff_rule(rule_data: RuffRule) -> FormattedRuffRule: - """Format ruff rule data for embed-friendly output and append rule link. - - Args: - rule_data: The ruff rule data. - - Returns: - FormattedRuffRule: The formatted rule data. - """ - explanation_formatted = re.sub(r"## (.+)", r"**\1**", rule_data["explanation"]) - rule_code = rule_data["code"] - rule_name = rule_data["name"] - rule_link = f"https://docs.astral.sh/ruff/rules/{rule_name}" - rule_anchor_link = f"https://docs.astral.sh/ruff/rules/#{rule_code}" - - return { - "name": rule_data.get("name", "No name available"), - "summary": rule_data.get("summary", "No summary available"), - "explanation": explanation_formatted, - "fix": rule_data.get("fix", "No fix available"), - "rule_link": rule_link, - "rule_anchor_link": rule_anchor_link, - } - - -async def query_all_ruff_rules() -> list[RuffRule]: - """Query all Ruff linting rules. - - Returns: - list[RuffRule]: All ruff rules - """ - _ruff = find_ruff_bin() - try: - result = await run_process([_ruff, "rule", "--all", "--output-format", "json"]) - except subprocess.CalledProcessError as e: - stderr = getattr(e, "stderr", b"").decode() - msg = f"Error while querying all rules: {stderr}" - raise ValueError(msg) from e - else: - return json.loads(result.stdout.decode()) - - -def run_ruff_format(code: str) -> str: - """Formats code using Ruff. - - Args: - code: The code to format. - - Returns: - str: The formatted code. - """ - result = subprocess.run( - ["ruff", "format", "-"], # noqa: S607 - input=code, - capture_output=True, - text=True, - check=False, - ) - return result.stdout if result.returncode == 0 else code - - -async def paste(code: str) -> str: - """Uploads the given code to paste.pythondiscord.com. - - Args: - code: The formatted code to upload. - - Returns: - str: The URL of the uploaded paste. - """ - async with httpx.AsyncClient() as client: - response = await client.post( - f"{pastebin}/api/v1/paste", - json={ - "expiry": "1day", - "files": [{"name": "byte-bot_formatted_code.py", "lexer": "python", "content": code}], - }, - ) - response_data = response.json() - paste_link = response_data.get("link") - return paste_link or "Failed to upload formatted code." - - -def chunk_sequence(sequence: Iterable[_T], size: int) -> Iterable[tuple[_T, ...]]: - """NaΓ―ve chunking of an iterable. - - Args: - sequence (Iterable[_T]): Iterable to chunk - size (int): Size of chunk - - Yields: - Iterable[tuple[_T, ...]]: An n-tuple that contains chunked data - """ - _sequence = iter(sequence) - while chunk := tuple(islice(_sequence, size)): - yield chunk - - -def format_resolution_link(resolution: str | None) -> str: - """Formats the resolution URL into a markdown link. - - Args: - resolution (str): The resolution URL. - - Returns: - str: The formatted markdown link. - """ - if not resolution: - return "N/A" - if "discuss.python.org" in resolution: - return f"[via Discussion Forum]({resolution})" - if "mail.python.org" in resolution: - return f"[via Mailist]({resolution})" - return resolution - - -async def query_all_peps() -> list[PEP]: - """Query all PEPs from the PEPs Python.org API. - - Returns: - list[PEP]: All PEPs - """ - url = "https://peps.python.org/api/peps.json" - async with httpx.AsyncClient() as client: - response = await client.get(url) - response.raise_for_status() - data = response.json() - - return [ # type: ignore[reportReturnType] - { - "number": pep_info["number"], - "title": pep_info["title"], - "authors": pep_info["authors"].split(", "), - "discussions_to": pep_info["discussions_to"], - "status": PEPStatus(pep_info["status"]), - "type": PEPType(pep_info["type"]), - "topic": pep_info.get("topic", ""), - "created": datetime.strptime(pep_info["created"], "%d-%b-%Y").replace(tzinfo=UTC).strftime("%Y-%m-%d"), - "python_version": pep_info.get("python_version"), - "post_history": pep_info.get("post_history", []), - "resolution": format_resolution_link(pep_info.get("resolution", "N/A")), - "requires": pep_info.get("requires"), - "replaces": pep_info.get("replaces"), - "superseded_by": pep_info.get("superseded_by"), - "url": pep_info["url"], - } - for pep_info in data.values() - ] - - -def get_next_friday(now: datetime, delay: int | None = None) -> tuple[datetime, datetime]: - """Calculate the next Friday from ``now``. - - If ``delay``, calculate the Friday for ``delay`` weeks from now. - - Args: - now: The current date and time. - delay: The number of weeks to delay the calculation. - - Returns: - datetime: The next Friday, optionally for the week after next. - """ - days_ahead = 4 - now.weekday() - if days_ahead < 0: - days_ahead += 7 - if delay: - days_ahead += 7 * delay - start_dt = (now + dt.timedelta(days=days_ahead)).replace(hour=11, minute=0, second=0, microsecond=0) - end_dt = start_dt + dt.timedelta(hours=1) - return start_dt, end_dt diff --git a/byte_bot/byte/plugins/__init__.py b/byte_bot/byte/plugins/__init__.py deleted file mode 100644 index 51421d21..00000000 --- a/byte_bot/byte/plugins/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Pluggable modules for Byte.""" - -from . import admin, astral, custom, events, forums, general, github, python, testing - -__all__ = [ - "admin", - "astral", - "custom", - "events", - "forums", - "general", - "github", - "python", - "testing", -] diff --git a/byte_bot/byte/plugins/admin.py b/byte_bot/byte/plugins/admin.py deleted file mode 100644 index 1321610a..00000000 --- a/byte_bot/byte/plugins/admin.py +++ /dev/null @@ -1,202 +0,0 @@ -"""Plugins for admins. - -.. todo:: add an unload cog command. -""" - -import discord -import httpx -from discord import Interaction -from discord.app_commands import command as app_command -from discord.ext import commands -from discord.ext.commands import Bot, Cog, Context, command, group, is_owner -from httpx import ConnectError - -__all__ = ("AdminCommands", "setup") - -from byte_bot.byte.lib import settings -from byte_bot.byte.lib.checks import is_byte_dev -from byte_bot.byte.lib.log import get_logger -from byte_bot.server.lib.settings import ServerSettings - -logger = get_logger() -server_settings = ServerSettings() - - -class AdminCommands(Cog): - """Admin command cog.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "Admin Commands" # type: ignore[misc] - - @group(name="admin") - @is_byte_dev() - async def admin(self, ctx: Context) -> None: - """Commands for bot admins.""" - if ctx.invoked_subcommand is None: - await ctx.send("Invalid admin command passed...") - await ctx.send_help(ctx.command) - - @command(name="list-cogs", help="Lists all loaded cogs.", aliases=["lc"], hidden=True) - @is_owner() - async def list_cogs(self, ctx: Context) -> None: - """Lists all loaded cogs. - - Args: - ctx: Context object. - """ - cogs = [cog.split(".")[-1] for cog in self.bot.extensions] - await ctx.send(f"Loaded cogs: {', '.join(cogs)}") - - @command(name="reload", help="Reloads a cog.", aliases=["rl"], hidden=True) - @is_owner() - async def reload(self, ctx: Context, cog: str = "all") -> None: - """Reloads a cog or all cogs if specified. - - Args: - ctx: Context object. - cog: Name of cog to reload. Default is "all". - """ - if cog.lower() == "all": - await self.reload_all_cogs(ctx) - else: - await self.reload_single_cog(ctx, cog) - - async def reload_all_cogs(self, ctx: Context) -> None: - """Reload all cogs. - - Args: - ctx: Context object. - """ - results = [] - for extension in list(self.bot.extensions): - cog_name = extension.split(".")[-1] - result = await self.reload_single_cog(ctx, cog_name, send_message=False) - results.append(result) - results.append("All cogs reloaded!") - await ctx.send("\n".join(results)) - - async def reload_single_cog(self, ctx: Context, cog: str, send_message: bool = True) -> str: - """Reload a single cog.""" - try: - await self.bot.reload_extension(f"plugins.{cog}") - message = f"Cog `{cog}` reloaded!" - except (commands.ExtensionNotLoaded, commands.ExtensionNotFound) as e: - message = f"Error with cog `{cog}`: {e!s}" - - if send_message: - await ctx.send(message) - - return message - - @app_command(name="sync") - @is_byte_dev() - async def tree_sync(self, interaction: Interaction) -> None: - """Slash command to perform a global sync.""" - results = await self.bot.tree.sync() - await interaction.response.send_message("\n".join(i.name for i in results), ephemeral=True) - - @command(name="bootstrap-guild", help="Bootstrap existing guild to database (dev only).", hidden=True) - @is_byte_dev() - async def bootstrap_guild(self, ctx: Context, guild_id: int | None = None) -> None: - """Bootstrap an existing guild to the database. - - Args: - ctx: Context object. - guild_id: Guild ID to bootstrap. If not provided, uses current guild. - """ - guild = await self._get_target_guild(ctx, guild_id) - if not guild: - return - - await ctx.send(f"πŸ”„ Bootstrapping guild {guild.name} (ID: {guild.id})...") - - await self._sync_guild_commands(guild) - await self._register_guild_in_database(ctx, guild) - - async def _get_target_guild(self, ctx: Context, guild_id: int | None) -> discord.Guild | None: - """Get the target guild for bootstrapping.""" - target_guild_id = guild_id or (ctx.guild.id if ctx.guild else None) - - if not target_guild_id: - await ctx.send("❌ No guild ID provided and command not used in a guild.") - return None - - guild = self.bot.get_guild(target_guild_id) - if not guild: - await ctx.send(f"❌ Bot is not in guild with ID {target_guild_id}") - return None - - return guild - - async def _sync_guild_commands(self, guild: discord.Guild) -> None: - """Sync commands to the guild.""" - try: - await self.bot.tree.sync(guild=guild) - logger.info("Commands synced to guild %s (id: %s)", guild.name, guild.id) - except Exception: - logger.exception("Failed to sync commands to guild %s", guild.name) - - async def _register_guild_in_database(self, ctx: Context, guild: discord.Guild) -> None: - """Register guild in database via API.""" - api_url = f"http://{server_settings.HOST}:{server_settings.PORT}/api/guilds/create?guild_id={guild.id}&guild_name={guild.name}" - - try: - async with httpx.AsyncClient() as client: - response = await client.post(api_url) - await self._handle_api_response(ctx, guild, response) - except ConnectError: - error_msg = f"Failed to connect to API to bootstrap guild {guild.name} (id: {guild.id})" - logger.exception(error_msg) - await ctx.send(f"❌ {error_msg}") - - async def _handle_api_response(self, ctx: Context, guild: discord.Guild, response: httpx.Response) -> None: - """Handle API response for guild registration.""" - if response.status_code == httpx.codes.CREATED: - await self._send_success_message(ctx, guild) - await self._notify_dev_channel(guild) - elif response.status_code == httpx.codes.CONFLICT: - await ctx.send(f"⚠️ Guild {guild.name} already exists in database") - else: - error_msg = f"Failed to add guild to database (status: {response.status_code})" - logger.error(error_msg) - await ctx.send(f"❌ {error_msg}") - - async def _send_success_message(self, ctx: Context, guild: discord.Guild) -> None: - """Send success message to user.""" - logger.info("Successfully bootstrapped guild %s (id: %s)", guild.name, guild.id) - embed = discord.Embed( - title="Guild Bootstrapped", - description=f"Successfully bootstrapped guild {guild.name} (ID: {guild.id})", - color=discord.Color.green(), - ) - embed.add_field(name="Commands Synced", value="βœ…", inline=True) - embed.add_field(name="Database Entry", value="βœ…", inline=True) - await ctx.send(embed=embed) - - async def _notify_dev_channel(self, guild: discord.Guild) -> None: - """Notify dev channel about guild bootstrap.""" - dev_guild = self.bot.get_guild(settings.discord.DEV_GUILD_ID) - if not dev_guild: - return - - dev_channel = dev_guild.get_channel(settings.discord.DEV_GUILD_INTERNAL_ID) - if not dev_channel or not hasattr(dev_channel, "send"): - return - - embed = discord.Embed( - title="Guild Bootstrapped", - description=f"Guild {guild.name} (ID: {guild.id}) was manually bootstrapped", - color=discord.Color.blue(), - ) - await dev_channel.send(embed=embed) # type: ignore[attr-defined] - - -async def setup(bot: Bot) -> None: - """Add cog to bot. - - Args: - bot: Bot object. - """ - await bot.add_cog(AdminCommands(bot)) diff --git a/byte_bot/byte/plugins/astral.py b/byte_bot/byte/plugins/astral.py deleted file mode 100644 index 6a8e622f..00000000 --- a/byte_bot/byte/plugins/astral.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Plugins for Astral Inc. related software, including Ruff, uv, etc.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from discord import Embed, Interaction -from discord.app_commands import Choice, autocomplete -from discord.app_commands import command as app_command -from discord.ext.commands import Bot, Cog - -from byte_bot.byte.lib.common.assets import ruff_logo -from byte_bot.byte.lib.common.colors import astral_purple, astral_yellow -from byte_bot.byte.lib.utils import chunk_sequence, format_ruff_rule, query_all_ruff_rules -from byte_bot.byte.views.astral import RuffView - -if TYPE_CHECKING: - from byte_bot.byte.lib.types.astral import RuffRule - -__all__ = ("Astral", "setup") - - -class Astral(Cog): - """Astral cog.""" - - def __init__(self, bot: Bot, rules: list[RuffRule]) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "Astral Commands" - # make rule lookup faster - self._rules = {rule["code"]: rule for rule in rules} - - async def _rule_autocomplete(self, _: Interaction, current_rule: str) -> list[Choice[str]]: - # TODO: this can and should be made faster, rn this is slow, slow like the maintainer - return [ - Choice(name=f"{code} - {rule['name']}", value=code) - for code, rule in self._rules.items() - if current_rule.lower() in code.lower() - ][:25] - - @app_command(name="ruff") - @autocomplete(rule=_rule_autocomplete) - async def ruff_rule(self, interaction: Interaction, rule: str) -> None: - """Slash command to look up and display a Ruff linting rule. - - Args: - interaction: Interaction object. - rule: The rule to lookup. - """ - await interaction.response.send_message("Querying Ruff rule...", ephemeral=True) - - if (rule_details := self._rules.get(rule)) is None: - embed = Embed(title=f"Rule '{rule}' not found.", color=astral_purple) - await interaction.followup.send(embed=embed) - return - - formatted_rule_details = format_ruff_rule(rule_details) - docs_field = ( - f"- [Rule Documentation]({formatted_rule_details['rule_link']})\n" - f"- [Similar Rules]({formatted_rule_details['rule_anchor_link']})" - ) - - # TODO: investigate if we can clean this up - minified_embed = Embed(title=f"Ruff Rule: {formatted_rule_details['name']}", color=astral_yellow) - minified_embed.add_field(name="Summary", value=formatted_rule_details["summary"], inline=False) - minified_embed.add_field(name="Documentation", value=docs_field, inline=False) - minified_embed.set_thumbnail(url=ruff_logo) - - embed = Embed(title=f"Ruff Rule: {formatted_rule_details['name']}", color=astral_yellow) - embed.add_field(name="Summary", value=formatted_rule_details["summary"], inline=False) - - # TODO: Better chunking - for idx, chunk in enumerate(chunk_sequence(formatted_rule_details["explanation"], 1000)): - embed.add_field(name="" if idx else "Explanation", value="".join(chunk), inline=False) - - embed.add_field(name="Fix", value=formatted_rule_details["fix"], inline=False) - embed.add_field(name="Documentation", value=docs_field, inline=False) - embed.set_thumbnail(url=ruff_logo) - - view = RuffView(author=interaction.user.id, bot=self.bot, original_embed=embed, minified_embed=minified_embed) # type: ignore[call-arg] - await interaction.followup.send(embed=minified_embed, view=view) - - @app_command(name="format") - async def format_code(self, interaction: Interaction, code_block: str) -> None: # noqa: ARG002 - """Formats the provided code using Ruff and uploads the result to a pastebin. - - Args: - interaction: The Discord interaction object. - code_block: The block of code to format. - """ - await interaction.response.send_message("This feature is not yet ready.", ephemeral=True) - # await interaction.response.send_message("Ruffing your code up...", ephemeral=True) - # formatted_code = run_ruff_format(code_block) - # paste_link = await paste(formatted_code) - # await interaction.followup.send(f"I Ruffed your code up a little... {paste_link}", ephemeral=False) - - -async def setup(bot: Bot) -> None: - """Set up the Astral cog.""" - await bot.add_cog(Astral(bot, await query_all_ruff_rules())) diff --git a/byte_bot/byte/plugins/config.py b/byte_bot/byte/plugins/config.py deleted file mode 100644 index 4b4a3df2..00000000 --- a/byte_bot/byte/plugins/config.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Plugins for guild admins to configure Byte and its features.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from discord.app_commands import Choice, autocomplete -from discord.app_commands import command as app_command -from discord.ext.commands import Bot, Cog - -from byte_bot.byte.lib.common import config_options -from byte_bot.byte.views.config import ConfigView - -if TYPE_CHECKING: - from discord import Interaction - -__all__ = ("Config", "setup") - - -class Config(Cog): - """Config cog.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "Config Commands" - self.config_options = config_options - - async def _config_autocomplete(self, interaction: Interaction, current: str) -> list[Choice[str]]: # noqa: ARG002 - """Autocomplete config for the config dropdown (up?) slash command.""" - return [ - Choice(name=f"{option['label']} - {option['description']}", value=option["label"]) - for option in config_options - if current.lower() in option["label"].lower() - ][:25] - - @app_command(name="config") - @autocomplete(setting=_config_autocomplete) - async def config_rule(self, interaction: Interaction, setting: str | None = None) -> None: - """Slash command to configure Byte. - - Args: - interaction: Interaction object. - setting: The setting to configure. - """ - if setting: - if selected_option := next( - (option for option in config_options if option["label"] == setting), - None, - ): - view = ConfigView(preselected=selected_option["label"]) - await interaction.response.send_message( - f"Configure {selected_option['label']}:", - view=view, - ephemeral=True, - ) - else: - await interaction.response.send_message( - f"Invalid setting: {setting}. Please select a valid setting.", - ephemeral=True, - ) - else: - view = ConfigView() - await interaction.response.send_message("Select a configuration option:", view=view, ephemeral=True) - - -async def setup(bot: Bot) -> None: - """Set up the config cog.""" - await bot.add_cog(Config(bot)) diff --git a/byte_bot/byte/plugins/custom/__init__.py b/byte_bot/byte/plugins/custom/__init__.py deleted file mode 100644 index d0b7852f..00000000 --- a/byte_bot/byte/plugins/custom/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -"""Custom Pluggable modules for Byte.""" - -from byte_bot.byte.plugins.custom import litestar - -__all__ = [ - "litestar", -] diff --git a/byte_bot/byte/plugins/custom/litestar.py b/byte_bot/byte/plugins/custom/litestar.py deleted file mode 100644 index 5768f832..00000000 --- a/byte_bot/byte/plugins/custom/litestar.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Custom plugins for the Litestar Discord.""" - -from __future__ import annotations - -import datetime - -from dateutil.zoneinfo import gettz -from discord import Embed, EntityType, Interaction, Message, Object, PrivacyLevel -from discord.app_commands import command as app_command -from discord.enums import TextStyle -from discord.ext.commands import Bot, Cog, Context, command, group, is_owner -from discord.ui import Modal, TextInput -from discord.utils import MISSING -from httpx import codes - -from byte_bot.byte.lib.checks import is_byte_dev -from byte_bot.byte.lib.common.colors import litestar_yellow -from byte_bot.byte.lib.common.mention import mention_role, mention_user -from byte_bot.byte.lib.utils import get_next_friday -from byte_bot.server.domain.github.helpers import github_client - -__all__ = ("LitestarCommands", "setup") - - -class GitHubIssue(Modal, title="Create GitHub Issue"): - """Modal for GitHub issue creation.""" - - title_ = TextInput(label="title", placeholder="Title") - description = TextInput( - label="Description", - style=TextStyle.paragraph, - placeholder="Please enter an description of the bug you are encountering.", - ) - mcve = TextInput( - label="MCVE", - style=TextStyle.paragraph, - placeholder="Please provide a minimal, complete, and verifiable example of the issue.", - ) - logs = TextInput( - label="Logs", style=TextStyle.paragraph, placeholder="Please copy and paste any relevant log output." - ) - version = TextInput( - label="Litestar Version", placeholder="What version of Litestar are you using when encountering this issue?" - ) - - def __init__( - self, - *, - title: str = MISSING, - timeout: float | None = None, - custom_id: str = MISSING, - message: Message | None = None, - ) -> None: - self.jump_url: str | None = None - # NOTE: check how else to set default - super().__init__(title=title, timeout=timeout, custom_id=custom_id) - if message: - self.description.default = message.content - self.jump_url = message.jump_url - - async def on_submit(self, interaction: Interaction) -> None: - issue_reporter = interaction.user - issue_body_lines = [ - "### Reported by", - f"[{issue_reporter.display_name}](https://discord.com/users/{issue_reporter.id})" - f" in Discord: [#{getattr(interaction.channel, 'name', 'DM')}]({self.jump_url})", - "", - "### Description", - f"{self.description.value.strip()}", - "", - "### MCVE", - f"{self.mcve.value.strip()}", - "", - "### Logs", - f"{self.logs.value.strip()}", - "", - "### Litestar Version", - f"{self.version.value.strip()}", - ] - issue_body = "\n".join(issue_body_lines) - try: - response_wrapper = await github_client.rest.issues.async_create( - owner="litestar-org", repo="litestar", data={"title": self.title_.value, "body": issue_body} - ) - if codes.is_success(response_wrapper.status_code): - await interaction.response.send_message( - f"GitHub Issue created: {response_wrapper.parsed_data.html_url}", ephemeral=False - ) - else: - await interaction.response.send_message("Issue creation failed.", ephemeral=True) - - except Exception as e: # noqa: BLE001 - await interaction.response.send_message(f"An error occurred: {e!s}", ephemeral=True) - - -class LitestarCommands(Cog): - """Litestar command cog.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "Litestar Commands" # type: ignore[misc] - - @group(name="litestar") - @is_byte_dev() - async def litestar(self, ctx: Context[Bot]) -> None: - """Commands for the Litestar guild.""" - if ctx.invoked_subcommand is None: - await ctx.send("Invalid Litestar command passed...") - await ctx.send_help(ctx.command) - - @command( - name="apply-role-embed", - help="Apply the role information embed to a channel.", - aliases=["are"], - hidden=True, - ) - @is_owner() - async def apply_role_embed(self, ctx: Context[Bot]) -> None: - """Apply the role information embed to a channel. - - Args: - ctx: Context object. - """ - embed = Embed(title="Litestar Roles", color=litestar_yellow) - - embed.add_field(name="Organization Roles", value="\u200b", inline=False) - embed.add_field( - name=f"{mention_role(919261960921546815)}", value="Maintainers of the Litestar organization", inline=False - ) - embed.add_field( - name=f"{mention_role(1084813023044173866)}", - value="Members invited to the Litestar organization due to their contributions", - inline=False, - ) - embed.add_field( - name=f"{mention_role(1059206375814737930)}", - value="Users that have created 3rd party libraries for Litestar projects", - inline=False, - ) - embed.add_field( - name=f"{mention_role(1150487028400652339)}", - value="Users that were once maintainers of a Litestar project or the Litestar organization", - inline=False, - ) - embed.add_field( - name=f"{mention_role(919261960921546815)}", - value=f"Programs providing services within the community. (like {mention_user(1132179951567786015)}!)", - inline=False, - ) - - embed.add_field(name="Community Roles", value="\u200b", inline=False) - embed.add_field( - name=f"{mention_role(1102727999285121074)}", - value="Users that contribute financially through OpenCollective, Polar.sh, or GitHub Sponsors", - inline=False, - ) - embed.add_field( - name=f"{mention_role(1150686603740729375)}", - value="Users that help moderate the Litestar community", - inline=False, - ) - embed.add_field( - name=f"{mention_role(1152668020825661570)}", - value="Users that consistently help the members of the Litestar community", - inline=False, - ) - - await ctx.send(embed=embed) - - @app_command( - name="schedule-office-hours", - description="Schedule Office Hours event for the upcoming or the week after next Friday.", - ) - async def schedule_office_hours(self, interaction: Interaction, delay: int | None = None) -> None: - """Schedule Office Hours event for the upcoming or ``delay`` weeks after next Friday. - - Args: - interaction: Interaction object. - delay: Optional. Number of weeks to delay the event. - """ - if interaction.guild is None: - await interaction.response.send_message("This command can only be used in a guild.", ephemeral=True) - return - - now_cst = datetime.datetime.now(gettz("America/Chicago")) - start_dt, end_dt = get_next_friday(now_cst, delay) - existing_events = interaction.guild.scheduled_events - - for event in existing_events: - if ( - event.name == "Office Hours" - and event.start_time.astimezone(gettz("America/Chicago")).date() == start_dt.date() - ): - await interaction.response.send_message( - "An Office Hours event is already scheduled for that day.", ephemeral=True - ) - return - - await interaction.guild.create_scheduled_event( - name="Office Hours", - start_time=start_dt, - end_time=end_dt, - description="Join us for our weekly office hours!", - entity_type=EntityType.stage_instance, - privacy_level=PrivacyLevel.guild_only, - reason=f"Scheduled by {interaction.user} via /schedule-office-hours", - channel=Object(id=1215926860144443502), - ) - - formatted_date = f"" - start_time_formatted = f"" - end_time_formatted = f"" - - await interaction.response.send_message( - f"Office Hours event scheduled: {formatted_date} from {start_time_formatted} - {end_time_formatted}." - ) - - -async def setup(bot: Bot) -> None: - """Add cog to bot. - - Args: - bot: Bot object. - """ - await bot.add_cog(LitestarCommands(bot)) - # TODO: Only sync the appropriate guilds needed. - # This is a temporary fix to get the context menu working. - # The invite link was generated with correct permissions, but it seems that it still won't work? - # cc: @alc-alc - # ExtensionFailed: Extension 'byte_bot.byte.plugins.custom.litestar' raised an error: Forbidden: 403 Forbidden (error code: 50001): Missing Access # noqa: E501 - # - # cog = LitestarCommands(bot) - # await bot.add_cog(cog) - # await bot.tree.sync(guild=Object(id=919193495116337154)) - # await bot.tree.sync(guild=Object(id=discord.DEV_GUILD_ID)) diff --git a/byte_bot/byte/plugins/events.py b/byte_bot/byte/plugins/events.py deleted file mode 100644 index b9d079f9..00000000 --- a/byte_bot/byte/plugins/events.py +++ /dev/null @@ -1,63 +0,0 @@ -"""Plugins for events.""" - -from threading import Thread -from typing import cast - -from discord import Embed -from discord.ext.commands import Bot, Cog - -from byte_bot.byte.lib.common.assets import litestar_logo_yellow -from byte_bot.byte.lib.common.links import mcve -from byte_bot.byte.lib.utils import linker -from byte_bot.byte.views.forums import HelpThreadView - -__all__ = ("Events", "setup") - - -class Events(Cog): - """Events cog.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - - @Cog.listener() - async def on_thread_create(self, thread: Thread) -> None: - """Handle thread create event. - - .. todo:: parameterize the command prefix per guild, and the - mention tag per guild. - - Args: - thread (discord.Thread): Thread that was created. - """ - if thread.parent and thread.parent.name == "help": # type: ignore[attr-defined] - embed = Embed(title=f"Notes for {thread.name}", color=0x42B1A8) - if thread.owner: # type: ignore[attr-defined] - embed.add_field(name="At your assistance", value=f"{thread.owner.mention}", inline=False) # type: ignore[attr-defined] - embed.add_field( - name="No Response?", value="If no response in a reasonable time, ping @Member.", inline=True - ) - commands_to_solve = " or ".join( - f"`{command_prefix}solve`" for command_prefix in cast("list[str]", self.bot.command_prefix) - ) - embed.add_field(name="Closing", value=f"To close, type {commands_to_solve}.", inline=True) - embed.add_field( - name="MCVE", - value=f"Please include an {linker('MCVE', mcve)} so that we can reproduce your issue locally.", - inline=False, - ) - embed.set_thumbnail(url=litestar_logo_yellow) - if thread.owner: # type: ignore[attr-defined] - view = HelpThreadView(author=thread.owner, guild_id=thread.guild.id, bot=self.bot) # type: ignore[attr-defined] - await view.setup() - await thread.send(embed=embed, view=view) # type: ignore[attr-defined] - elif thread.parent and thread.parent.name == "forum": # type: ignore[attr-defined] - if thread.owner: # type: ignore[attr-defined] - reply = f"Thanks for posting, {thread.owner.mention}!" # type: ignore[attr-defined] - await thread.send(reply) # type: ignore[attr-defined] - - -async def setup(bot: Bot) -> None: - """Set up the Events cog.""" - await bot.add_cog(Events(bot)) diff --git a/byte_bot/byte/plugins/forums.py b/byte_bot/byte/plugins/forums.py deleted file mode 100644 index 403d128d..00000000 --- a/byte_bot/byte/plugins/forums.py +++ /dev/null @@ -1,99 +0,0 @@ -"""Plugins related to forums.""" - -import discord -from discord import Embed, Interaction, Member, Thread -from discord.app_commands import command as app_command -from discord.ext.commands import Bot, Cog, Context, command, hybrid_command - -from byte_bot.byte.lib.common.assets import litestar_logo_yellow -from byte_bot.byte.lib.common.links import mcve -from byte_bot.byte.lib.utils import linker - -__all__ = ("ForumCommands", "setup") - - -class ForumCommands(Cog): - """Forum command cog.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "Forum Commands" - - @hybrid_command( - name="solve", - help="Run `!solve` or `!s` to mark a forum post as solved and close it.", - aliases=["s"], - brief="Run `!solve` or `!s` to mark a forum post as solved and close it.", - description="Mark a forum post as solved and close it.", - ) - async def solved(self, ctx: Context) -> None: - """Mark the forum post as solved and close it. - - .. todo: Parameterize the tag name and allow each guild to assign it - in placed of "Solved" (Default can stay as solved, though) - - .. todo:: Parameterize the channel name (default of help, still). - Also, allow for a list of channels to be specified. - users may want to be able to mark things solved/closed in - Help, Suggestions, Showcase, etc. (or any other forum channel) - """ - _solved_tag = "Solved" - _tags_per_post = 5 - if isinstance(ctx.channel, Thread) and ctx.channel.parent and ctx.channel.parent.name == "help": - if solved_tag := discord.utils.find( - lambda t: t.name == _solved_tag, getattr(ctx.channel.parent, "available_tags", []) - ): - if ( - len(getattr(ctx.channel, "applied_tags", [])) == _tags_per_post - and solved_tag not in getattr(ctx.channel, "applied_tags", []) - and (applied_tags := getattr(ctx.channel, "applied_tags", [])) - ): - await ctx.channel.remove_tags(applied_tags[-1]) - await ctx.channel.add_tags(solved_tag, reason="Marked as solved.") - await ctx.send("Marked as solved and closed the help forum!", ephemeral=True) - await ctx.channel.edit(archived=True) - else: - await ctx.send(f"'{_solved_tag}' tag not found.") - else: - await ctx.send("This command can only be used in the help forum.") - - @command() - async def tags(self, ctx: Context) -> None: - """Get all tags in the channel. - - Args: - ctx: Context object. - """ - tags = getattr(ctx.channel, "applied_tags", []) - await ctx.send(f"Tags in this channel: {', '.join([tag.name for tag in tags])}") - - @app_command(name="mcve") - async def tree_sync(self, interaction: Interaction, user: Member) -> None: - """Slash command to request an MCVE from a user. - - Args: - interaction: Interaction object. - user: The user to target with the MCVE request. - """ - await interaction.response.send_message("Processing request...", ephemeral=True) - - embed = Embed(title="MCVE Needed to Reproduce!", color=0x42B1A8) - embed.add_field(name="Hi", value=f"{user.mention}", inline=True) - embed.add_field( - name="MCVE", - value=f"Please include an {linker('MCVE', mcve)} so that we can reproduce your issue locally.", - inline=True, - ) - embed.set_thumbnail(url=litestar_logo_yellow) - - await interaction.followup.send(embed=embed) - - -async def setup(bot: Bot) -> None: - """Add cog to bot. - - Args: - bot: Bot object. - """ - await bot.add_cog(ForumCommands(bot)) diff --git a/byte_bot/byte/plugins/general.py b/byte_bot/byte/plugins/general.py deleted file mode 100644 index 91308c16..00000000 --- a/byte_bot/byte/plugins/general.py +++ /dev/null @@ -1,49 +0,0 @@ -"""General plugins to be used wherever.""" - -from __future__ import annotations - -from discord import Embed, Interaction -from discord.app_commands import command as app_command -from discord.ext.commands import Bot, Cog - -from byte_bot.byte.lib.common.assets import litestar_logo_yellow -from byte_bot.byte.lib.common.links import markdown_guide, pastebin -from byte_bot.byte.lib.utils import linker - -__all__ = ("GeneralCommands", "setup") - - -class GeneralCommands(Cog): - """General commands.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "General Commands" # type: ignore[misc] - - @app_command(name="paste") - async def show_paste(self, interaction: Interaction) -> None: - """Slash command to show an embed for pasting code. - - Args: - interaction: Interaction object. - """ - embed = Embed(title="Paste long format code", color=0x42B1A8) - embed.add_field( - name="Paste Service", - value=f"You can easily paste long code by using the {linker('Paste', pastebin)} service.", - inline=True, - ) - embed.add_field( - name="Syntax Highlighting", - value="You can also use backticks to format your code. Read about it in the " - f"{linker('Discord Markdown Guide', markdown_guide)}.", - ) - embed.set_thumbnail(url=litestar_logo_yellow) - - await interaction.response.send_message(embed=embed) - - -async def setup(bot: Bot) -> None: - """Set up the General cog.""" - await bot.add_cog(GeneralCommands(bot)) diff --git a/byte_bot/byte/plugins/github.py b/byte_bot/byte/plugins/github.py deleted file mode 100644 index d064fbdd..00000000 --- a/byte_bot/byte/plugins/github.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Plugins for GitHub interactions.""" - -from __future__ import annotations - -from discord import Interaction, Message, TextStyle, app_commands -from discord.ext.commands import Bot, Cog -from discord.ui import Modal, TextInput -from discord.utils import MISSING -from httpx import codes - -from byte_bot.server.domain.github.helpers import github_client - -__all__ = ("GitHubCommands", "setup") - - -class GitHubIssue(Modal, title="Create GitHub Issue"): - """Modal for GitHub issue creation.""" - - title_ = TextInput(label="title", placeholder="Title") - description = TextInput( - label="Description", - style=TextStyle.paragraph, - placeholder="Please enter an description of the bug you are encountering.", - ) - mcve = TextInput( - label="MCVE", - style=TextStyle.paragraph, - placeholder="Please provide a minimal, complete, and verifiable example of the issue.", - ) - logs = TextInput( - label="Logs", style=TextStyle.paragraph, placeholder="Please copy and paste any relevant log output." - ) - version = TextInput( - label="Project Version", placeholder="What version of the project are you using when encountering this issue?" - ) - - def __init__( - self, - *, - title: str = MISSING, - timeout: float | None = None, - custom_id: str = MISSING, - message: Message | None = None, - ) -> None: - # NOTE: check how else to set default - super().__init__(title=title, timeout=timeout, custom_id=custom_id) - if message: - self.description.default = message.content - - async def on_submit(self, interaction: Interaction) -> None: - issue_reporter = interaction.user - issue_body_lines = [ - "### Reported by", - f"[{issue_reporter.display_name}](https://discord.com/users/{issue_reporter.id}) in Discord: {getattr(interaction.channel, 'name', 'DM')}", # noqa: E501 - "", - "### Description", - f"{self.description.value.strip()}", - "", - "### MCVE", - f"{self.mcve.value.strip()}", - "", - "### Logs", - f"{self.logs.value.strip()}", - "", - "### Project Version", - f"{self.version.value.strip()}", - ] - issue_body = "\n".join(issue_body_lines) - try: - response_wrapper = await github_client.rest.issues.async_create( - owner="litestar-org", repo="litestar", data={"title": self.title_.value, "body": issue_body} - ) - if codes.is_success(response_wrapper.status_code): - await interaction.response.send_message( - f"GitHub Issue created: {response_wrapper.parsed_data.html_url}", ephemeral=False - ) - else: - await interaction.response.send_message("Issue creation failed.", ephemeral=True) - - except Exception as e: # noqa: BLE001 - await interaction.response.send_message(f"An error occurred: {e!s}", ephemeral=True) - - -class GitHubCommands(Cog): - """GitHub command cog.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "GitHub Commands" # type: ignore[misc] - self.context_menu = app_commands.ContextMenu( - # TODO: Name changed to not conflict with the other one, discord shows both - name="Create GitHub Issue", - callback=self.create_github_issue_modal, - ) - bot.tree.add_command(self.context_menu) - - async def create_github_issue_modal(self, interaction: Interaction, message: Message) -> None: - """Context menu command to create a GitHub issue from a Discord message. - - Args: - interaction: Interaction object. - message: Message object. - """ - await interaction.response.send_modal(GitHubIssue(message=message)) - - -async def setup(bot: Bot) -> None: - """Add cog to bot. - - Args: - bot: Bot object. - """ - await bot.add_cog(GitHubCommands(bot)) diff --git a/byte_bot/byte/plugins/python.py b/byte_bot/byte/plugins/python.py deleted file mode 100644 index 14431cf7..00000000 --- a/byte_bot/byte/plugins/python.py +++ /dev/null @@ -1,89 +0,0 @@ -"""Plugins for Python related things, including PyPI, PEPs, etc.""" - -from __future__ import annotations - -from discord import Embed, Interaction -from discord.app_commands import Choice, autocomplete -from discord.app_commands import command as app_command -from discord.ext.commands import Bot, Cog - -from byte_bot.byte.lib.common.assets import python_logo -from byte_bot.byte.lib.common.colors import python_blue, python_yellow -from byte_bot.byte.lib.utils import PEP, query_all_peps -from byte_bot.byte.views.abstract_views import ExtendedEmbed, Field -from byte_bot.byte.views.python import PEPView - -__all__ = ("Python", "setup") - - -class Python(Cog): - """Python cog.""" - - def __init__(self, bot: Bot, peps: list[PEP]) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "Python Commands" - self._peps = {pep["number"]: pep for pep in peps} - - async def _pep_autocomplete(self, interaction: Interaction, current_pep: str) -> list[Choice[str]]: # noqa: ARG002 - """Autocomplete for PEP numbers and titles. - - .. warning:: ``interaction`` is not used, but is required. - """ - return [ - Choice(name=f"PEP {number} - {pep['title']}", value=str(number)) - for number, pep in self._peps.items() - if current_pep.lower() in str(number) or current_pep.lower() in pep["title"].lower() - ][:25] - - @app_command(name="pep") - @autocomplete(pep=_pep_autocomplete) - async def peps(self, interaction: Interaction, pep: int) -> None: - """Slash command to look up and display a Python Enhancement Proposal (PEP). - - Args: - interaction: Interaction object. - pep: The PEP number to lookup. - """ - await interaction.response.send_message(f"Querying PEP {pep}...", ephemeral=True) - - if (pep_details := self._peps.get(pep)) is None: - embed = Embed(title=f"PEP {pep} not found... Maybe you should submit it!", color=python_yellow) - await interaction.followup.send(embed=embed, ephemeral=True) - return - - fields: list[Field] = [ - {"name": "Status", "value": pep_details["status"], "inline": True}, - {"name": "Python Version", "value": pep_details["python_version"], "inline": True}, - {"name": "Created", "value": str(pep_details["created"]), "inline": True}, - {"name": "Resolution", "value": pep_details.get("resolution", "N/A"), "inline": False}, - {"name": "Type", "value": pep_details["type"], "inline": True}, - {"name": "Topic", "value": pep_details.get("topic", "N/A"), "inline": True}, - {"name": "Requires", "value": pep_details.get("requires", "N/A"), "inline": True}, - {"name": "Replaces", "value": pep_details.get("replaces", "N/A"), "inline": True}, - {"name": "Superseded By", "value": pep_details.get("superseded_by", "N/A"), "inline": True}, - {"name": "Authors", "value": ", ".join(pep_details.get("authors", ["N/A"])), "inline": False}, - {"name": "Discussions To", "value": pep_details.get("discussions_to", "N/A"), "inline": False}, - {"name": "Post History", "value": pep_details.get("post_history", "N/A"), "inline": False}, - ] - - minified_embed = ExtendedEmbed.from_field_dicts( - title=f"PEP {pep_details['number']}: {pep_details['title']}", color=python_blue, fields=fields[:4] - ) - minified_embed.set_thumbnail(url=python_logo) - full_embed = minified_embed.deepcopy() - full_embed.add_field_dicts(fields[4:]) - - # Ensure the Documentation field is always last - minified_embed.add_field(name="Documentation", value=f"[PEP Documentation]({pep_details['url']})", inline=False) - full_embed.add_field(name="Documentation", value=f"[PEP Documentation]({pep_details['url']})", inline=False) - - view = PEPView( # type: ignore[call-arg] - author=interaction.user.id, bot=self.bot, original_embed=full_embed, minified_embed=minified_embed - ) - await interaction.followup.send(embed=minified_embed, view=view) - - -async def setup(bot: Bot) -> None: - """Set up the Python cog.""" - await bot.add_cog(Python(bot, await query_all_peps())) diff --git a/byte_bot/byte/plugins/testing.py b/byte_bot/byte/plugins/testing.py deleted file mode 100644 index 81734e1b..00000000 --- a/byte_bot/byte/plugins/testing.py +++ /dev/null @@ -1,35 +0,0 @@ -"""Plugins for testing purposes.""" - -from discord.ext.commands import Bot, Cog, Context, command, group - - -class TestCog(Cog, name="Ping"): - """Testing commands.""" - - def __init__(self, bot: Bot) -> None: - """Initialize cog.""" - self.bot = bot - self.__cog_name__ = "Testing Commands" - - @group(name="testing") - async def testing(self, ctx: Context) -> None: - """Testing command group.""" - if ctx.invoked_subcommand is None: - await ctx.send(f"Hey, uh... {ctx.subcommand_passed} is not a valid testing command {ctx.author.mention}...") - await ctx.send_help(ctx.command) - - @command( - name="ping", - help="Run `!ping` or `!p` to get a `pong!` response.", - aliases=["p"], - brief="Run `!ping` or `!p` to get a `pong!` response.", - description="Ping the bot.", - ) - async def ping(self, ctx: Context) -> None: - """Responds with 'pong'.""" - await ctx.send(f"pong to the {ctx.guild.name} guild!") - - -async def setup(bot: Bot) -> None: - """Load the Testing cog.""" - await bot.add_cog(TestCog(bot)) diff --git a/byte_bot/byte/py.typed b/byte_bot/byte/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/byte_bot/byte/views/__init__.py b/byte_bot/byte/views/__init__.py deleted file mode 100644 index 9f3e85ca..00000000 --- a/byte_bot/byte/views/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Views for the bot.""" - -from byte_bot.byte.views import abstract_views, astral, forums, python - -__all__ = [ - "abstract_views", - "astral", - "forums", - "python", -] diff --git a/byte_bot/byte/views/abstract_views.py b/byte_bot/byte/views/abstract_views.py deleted file mode 100644 index e6194b09..00000000 --- a/byte_bot/byte/views/abstract_views.py +++ /dev/null @@ -1,193 +0,0 @@ -"""Inheritable views that include extra functionality for base Views classes.""" - -from __future__ import annotations - -from copy import deepcopy -from typing import TYPE_CHECKING, Any, Literal, ParamSpec, TypedDict - -from discord import ButtonStyle, Colour, Embed, Interaction -from discord.ui import Button, View, button - -if TYPE_CHECKING: - from datetime import datetime - from typing import NotRequired, Self - - from discord.ext.commands import Bot - -__all__ = ("ButtonEmbedView", "ExtendedEmbed", "Field") - -P = ParamSpec("P") - - -class ButtonEmbedView(View): - """Base view including common buttons.""" - - def __init__( - self, - author: int, - bot: Bot, - original_embed: Embed, - minified_embed: Embed, - *args, - **kwargs, # type: ignore[misc] - ) -> None: - """Initialize the view. - - Args: - author: Author ID. - bot: Bot object. - original_embed: The original embed to display. - minified_embed: The minified embed to display. - *args: Variable length argument list. - **kwargs: Arbitrary keyword arguments. - """ - super().__init__(*args, **kwargs) - self.author_id = author - self.bot = bot - self.original_embed = original_embed - self.minified_embed = minified_embed - - async def delete_interaction_check(self, interaction: Interaction) -> bool: - """Check if the user is the author or a guild admin. - - .. note:: Only checks for the ``delete`` button, as we want to expose - the ``learn more`` button to anyone. - - Args: - interaction: Interaction object. - - Returns: - True if the user is the author or a guild admin, False otherwise. - """ - if interaction.user.id == self.author_id or ( - getattr(interaction.user, "guild_permissions", None) and interaction.user.guild_permissions.administrator # type: ignore[attr-defined] - ): - return True - await interaction.response.send_message( - "You do not have permission to interact with this message.", ephemeral=True - ) - return False - - async def delete_button_callback(self, interaction: Interaction) -> None: - """Delete the message this view is attached to. - - Args: - interaction: Interaction object. - """ - if await self.delete_interaction_check(interaction) and interaction.message is not None: - await interaction.message.delete() - - async def learn_more_button_callback(self, interaction: Interaction) -> None: - """Send the original embed to the user privately. - - Args: - interaction: Interaction object. - """ - await interaction.response.send_message(embed=self.original_embed, ephemeral=True) - - @button(label="Delete", style=ButtonStyle.red, custom_id="delete_button") - async def delete_button(self, interaction: Interaction, _: Button[Self]) -> None: - """Button to delete the message this view is attached to. - - Args: - interaction: Interaction object. - _: Button object. - """ - await self.delete_button_callback(interaction) - - @button(label="Learn More", style=ButtonStyle.green, custom_id="learn_more_button") - async def learn_more_button(self, interaction: Interaction, _: Button[Self]) -> None: - """Button to privately message the requesting user the full embed. - - Args: - interaction: Interaction object. - _: Button object. - """ - await self.learn_more_button_callback(interaction) - - -class Field(TypedDict): - """Field type for ``ExtendedEmbed``. - - .. note:: types are matching the ones in ``Embed.add_fields``. - """ - - name: Any - value: Any - inline: NotRequired[bool] - - -class ExtendedEmbed(Embed): - """Extended Embed class for discord.py.""" - - def add_field_dict(self, field: Field) -> Self: - """Add a field to the embed. - - Args: - field (Field): The field to add to the embed. - - Returns: - Self: The embed with the field added. - """ - self.add_field(**field) - return self - - def add_field_dicts(self, fields: list[Field]) -> Self: - """Add multiple fields to the embed. - - Args: - fields (list[Field]): A list of fields to add to the embed. - - Returns: - Self: The embed with the fields added. - """ - for field in fields: - self.add_field_dict(field) - return self - - @classmethod - def from_field_dicts( - cls, - colour: int | Colour | None = None, - color: int | Colour | None = None, - title: Any | None = None, - type: Literal["rich", "image", "video", "gifv", "article", "link"] = "rich", # noqa: A002 - url: Any | None = None, - description: Any | None = None, - timestamp: datetime | None = None, - fields: list[Field] | None = None, - ) -> Self: - """Create an embed from a list of fields. - - Args: - colour (int | Colour | None): The colour of the embed. - color (int | Colour | None): The colour of the embed. - title (Any | None): The title of the embed. - type (Literal["rich", "image", "video", "gifv", "article", "link"]): The type of the embed. - url (Any | None): The URL of the embed. - description (Any | None): The description of the embed. - timestamp (datetime | None): The timestamp of the embed. - fields (list[Field] | None): A list of fields to add to the embed. - - Returns: - Self: The embed with the fields added. - """ - embed = cls( - colour=colour, - color=color, - title=title, - type=type, - url=url, - description=description, - timestamp=timestamp, - ) - embed.add_field_dicts(fields or []) - return embed - - def deepcopy(self) -> Self: - """Create a deep copy of the embed. - - Returns: - Self: A deep copy of the embed. - """ - return deepcopy(self) diff --git a/byte_bot/byte/views/astral.py b/byte_bot/byte/views/astral.py deleted file mode 100644 index 6c26b31e..00000000 --- a/byte_bot/byte/views/astral.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Discord UI views used in Astral commands.""" - -from __future__ import annotations - -from byte_bot.byte.lib.log import get_logger -from byte_bot.byte.views.abstract_views import ButtonEmbedView - -__all__ = ("RuffView",) - -logger = get_logger() - - -class RuffView(ButtonEmbedView): - """View for the Ruff embed.""" diff --git a/byte_bot/byte/views/config.py b/byte_bot/byte/views/config.py deleted file mode 100644 index 0e7e1da8..00000000 --- a/byte_bot/byte/views/config.py +++ /dev/null @@ -1,265 +0,0 @@ -"""Discord UI views used in Byte config commands.""" - -from __future__ import annotations - -from typing import Any - -from discord import ButtonStyle, Interaction, SelectOption, TextStyle -from discord.ui import Button, Modal, Select, TextInput, View - -from byte_bot.byte.lib.common import config_options -from byte_bot.byte.lib.log import get_logger - -__all__ = ("ConfigView",) - -logger = get_logger() - - -class FinishButton(Button): - """Finish button.""" - - def __init__(self) -> None: - """Initialize button.""" - super().__init__(style=ButtonStyle.success, label="Finished") - - async def callback(self, interaction: Interaction) -> None: - """Callback for button. - - Args: - interaction: Interaction object. - """ - await interaction.response.send_message("Configuration complete!", ephemeral=True) - if self.view is not None: - self.view.stop() - - -class BackButton(Button): - """Back button.""" - - def __init__(self) -> None: - """Initialize button.""" - super().__init__(style=ButtonStyle.secondary, label="Back") - - async def callback(self, interaction: Interaction) -> None: - """Callback for button. - - Args: - interaction: Interaction object. - """ - view = ConfigView() - await interaction.response.edit_message(content="Select a configuration option:", view=view) - - -class CancelButton(Button): - """Cancel button.""" - - def __init__(self) -> None: - """Initialize button.""" - super().__init__(style=ButtonStyle.danger, label="Cancel") - - async def callback(self, interaction: Interaction) -> None: - """Callback for button. - - Args: - interaction: Interaction object. - """ - await interaction.response.send_message("Configuration cancelled.", ephemeral=True) - if self.view is not None: - self.view.stop() - - -class ConfigSelect(Select): - """Configuration select dropdown menu.""" - - def __init__(self, preselected: str | None = None) -> None: - """Initialize select. - - Args: - preselected: Preselected option, if given. - """ - options = [SelectOption(label=option["label"], description=option["description"]) for option in config_options] - super().__init__(placeholder="Choose a setting...", min_values=1, max_values=1, options=options) - - if preselected: - for option in options: - if option.label == preselected: - option.default = True - break - - async def callback(self, interaction: Interaction) -> None: - """Callback for select. - - Args: - interaction: Interaction object. - """ - selected_option = next(option for option in config_options if option["label"] == self.values[0]) - if "sub_settings" in selected_option: - view = ConfigKeyView(selected_option) - await interaction.response.edit_message(content=f"Select a key for {selected_option['label']}:", view=view) - else: - modal = ConfigModal(title=f"Configure {selected_option['label']}") - await interaction.response.send_modal(modal) - - -class ConfigKeySelect(Select): - """Configuration key select dropdown menu.""" - - def __init__(self, option: dict[str, Any]) -> None: - """Initialize select. - - Args: - option: The selected configuration option. - """ - self.option = option - options = [ - SelectOption(label=sub_setting["label"], description=sub_setting.get("description", "")) - for sub_setting in option["sub_settings"] - ] - super().__init__(placeholder="Choose a key...", min_values=1, max_values=1, options=options) - - async def callback(self, interaction: Interaction) -> None: - """Callback for select. - - Args: - interaction: Interaction object. - """ - selected_sub_setting = self.values[0] - selected_sub_setting = next( - sub_setting for sub_setting in self.option["sub_settings"] if sub_setting["label"] == selected_sub_setting - ) - modal = ConfigModal( - title=f"{self.option['label']} - {selected_sub_setting['label']}", - sub_setting=selected_sub_setting, - option=self.option, - ) - await interaction.response.send_modal(modal) - - -class ConfigView(View): - """Configuration view.""" - - def __init__(self, preselected: str | None = None) -> None: - """Initialize view. - - Args: - preselected: Preselected option, if given. - """ - super().__init__(timeout=None) - self.add_item(ConfigSelect(preselected)) - self.add_item(FinishButton()) - self.add_item(CancelButton()) - - -class ConfigKeyView(View): - """Configuration key view.""" - - def __init__(self, option: dict[str, Any]) -> None: - """Initialize view. - - Args: - option: The selected configuration option. - """ - super().__init__(timeout=None) - self.add_item(ConfigKeySelect(option)) - self.add_item(BackButton()) - self.add_item(CancelButton()) - - -class ConfigModal(Modal): - """Configuration modal.""" - - def __init__( - self, - title: str, - sub_setting: dict[str, str] | None = None, - sub_settings: list[dict[str, str]] | None = None, - option: dict[str, Any] | None = None, - ) -> None: - """Initialize modal. - - Args: - title: Title of modal. - sub_setting: The selected sub-setting, if applicable. - sub_settings: List of sub-settings, if configuring all keys. - option: The selected configuration option, if applicable. - """ - super().__init__(title=title + "\n\n") - self.option = option - - if sub_settings: - for _sub_setting in sub_settings: - self.add_item( - TextInput( - label=_sub_setting["label"], - style=TextStyle.short, - custom_id=_sub_setting["field"], - placeholder=f"Enter {_sub_setting['label']} ({_sub_setting['data_type']})", - required=True, - min_length=4 if _sub_setting["data_type"] == "True/False" else 1, - max_length=5 - if _sub_setting["data_type"] == "True/False" - else 100 - if _sub_setting["data_type"] in ["String", "Integer"] - else 300 - if _sub_setting["data_type"] == "Comma-separated list" - else None, - ) - ) - elif sub_setting: - self.add_item( - TextInput( - label=sub_setting["label"], - style=TextStyle.short, - placeholder=f"Enter {sub_setting['label']} ({sub_setting['data_type']})", - required=True, - min_length=4 if sub_setting["data_type"] == "True/False" else 1, - max_length=5 - if sub_setting["data_type"] == "True/False" - else 100 - if sub_setting["data_type"] in ["String", "Integer"] - else 300 - if sub_setting["data_type"] == "Comma-separated list" - else None, - ) - ) - else: - self.add_item( - TextInput( - label="Configuration Value", - style=TextStyle.short, - placeholder="Enter your configuration value...", - required=True, - ) - ) - - async def on_submit(self, interaction: Interaction) -> None: - """Handle modal submission. - - Args: - interaction: Interaction object. - """ - config_values = {item.custom_id: item.value for item in self.children if hasattr(item, "custom_id")} # type: ignore[attr-defined] - await interaction.response.send_message(f"Configuration values received: {config_values}", ephemeral=True) - - if self.option: - view = ConfigKeyView(self.option) - await interaction.followup.send( - f"Select another key for {self.option['label']} or click 'Back' to return to the main menu.", - view=view, - ephemeral=True, - ) - else: - view = ConfigView() - await interaction.followup.send( - "Select another setting or click 'Finished' when done.", view=view, ephemeral=True - ) - - async def on_error(self, interaction: Interaction, error: Exception) -> None: - """Handle modal submission error. - - Args: - interaction: Interaction object. - error: Error object. - """ - await interaction.response.send_message("Oops! Something went wrong.", ephemeral=True) - logger.exception("Error occurred while processing config modal submission", exc_info=error) diff --git a/byte_bot/byte/views/forums.py b/byte_bot/byte/views/forums.py deleted file mode 100644 index 4be91341..00000000 --- a/byte_bot/byte/views/forums.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Discord UI views used in forums.""" - -from discord import ButtonStyle, Interaction, Member -from discord.ext.commands import Bot -from discord.ui import Button, View, button - -from byte_bot.byte.lib.log import get_logger -from byte_bot.server.domain.guilds.dependencies import provides_guilds_service -from byte_bot.server.lib.db import config - -__all__ = ("HelpThreadView",) - -logger = get_logger() - - -class HelpThreadView(View): - """View for the help thread.""" - - def __init__(self, author: Member, guild_id: int, bot: Bot, *, timeout: float | None = 180.0) -> None: - """Initialize the view.""" - super().__init__(timeout=timeout) - self.author = author - self.bot = bot - self.guild_id = guild_id - - async def setup(self) -> None: - """Asynchronously setup guild details and add button. - - .. todo:: Think about this more - If we plan on decoupling this - should be a call to an endpoint like we do in ``byte.bot.Byte.on_guild_join``. - """ - # noinspection PyBroadException - try: - async with config.get_session() as session: - guilds_service = await anext(provides_guilds_service(db_session=session)) - guild_settings = await guilds_service.get(self.guild_id, id_attribute="guild_id") - - if guild_settings and guild_settings.github_config: - guild_repo = guild_settings.github_config.github_repository - self.add_item( - Button(label="Open GitHub Issue", style=ButtonStyle.blurple, url=f"{guild_repo}/new/choose") - ) - else: - logger.warning("no github configuration found for guild %s", self.guild_id) - except Exception: - logger.exception("failed to setup view for guild %s", self.guild_id) - - async def delete_interaction_check(self, interaction: Interaction) -> bool: - """Check if the user is the author or an admin. - - Args: - interaction (Interaction): Interaction object. - - Returns: - bool: True if the user is the author or an admin, False otherwise. - """ - return interaction.user == self.author or ( - hasattr(interaction.user, "guild_permissions") and interaction.user.guild_permissions.administrator # type: ignore[attr-defined] - ) - - @button(label="Solve", style=ButtonStyle.green, custom_id="solve_button") - async def solve_button_callback(self, interaction: Interaction, button: Button) -> None: # noqa: ARG002 - """Mark the thread as solved. - - Args: - interaction: Interaction object. - button: Button object. - """ - await interaction.response.defer() - - if interaction.message: - ctx = await self.bot.get_context(interaction.message) - solve_command = self.bot.get_command("solve") - if solve_command is not None: - ctx.command = solve_command - ctx.invoked_with = "solve" - ctx.args.append(ctx) - logger.info( - "invoking solve command for %s by %s on thread %s", - ctx.channel, - interaction.user, - interaction.channel, - ) - - # noinspection PyBroadException - try: - if solve_command is not None: - await solve_command.invoke(ctx) - await interaction.followup.send("Marked as solved and closed the help forum!", ephemeral=True) - else: - await interaction.followup.send("Solve command not found. Please try again.", ephemeral=True) - except Exception: - logger.exception("failed to invoke solve command") - await interaction.followup.send("Failed to mark as solved. Please try again.", ephemeral=True) - - @button(label="Remove", style=ButtonStyle.red, custom_id="remove_button") - async def remove_button_callback(self, interaction: Interaction, button: Button) -> None: # noqa: ARG002 - """Remove the view and embed. - - Args: - interaction: Interaction object. - button: Button object. - """ - if interaction.message: - content = interaction.message.content or "\u200b" - logger.info("removing view for %s by %s", interaction.channel, interaction.user) - await interaction.message.edit(content=content, embed=None, view=None) diff --git a/byte_bot/byte/views/python.py b/byte_bot/byte/views/python.py deleted file mode 100644 index df055b03..00000000 --- a/byte_bot/byte/views/python.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Discord UI views used in Python commands.""" - -from __future__ import annotations - -from byte_bot.byte.lib.log import get_logger -from byte_bot.byte.views.abstract_views import ButtonEmbedView - -__all__ = ("PEPView",) - -logger = get_logger() - - -class PEPView(ButtonEmbedView): - """View for the Python PEP embed.""" diff --git a/byte_bot/cli.py b/byte_bot/cli.py deleted file mode 100644 index 0ea0a19f..00000000 --- a/byte_bot/cli.py +++ /dev/null @@ -1,230 +0,0 @@ -"""Project CLI.""" - -from __future__ import annotations - -import multiprocessing -import subprocess -import sys -from typing import Any - -import click -from rich import get_console - -from byte_bot.server.lib import log, settings - -__all__ = [ - "run_all", - "run_bot", - "run_web", -] - -console = get_console() -"""Pre-configured CLI Console.""" - -logger = log.get_logger() - - -def frontend() -> None: - """Run the tailwind compiler.""" - if settings.project.ENVIRONMENT == "prod" or not settings.project.DEV_MODE: - logger.info("🎨 Skipping Tailwind Compiler in production environment.") - return - - log.config.configure() - logger.info("🎨 Starting Tailwind Compiler.") - try: - subprocess.run( - [ # noqa: S607 - "tailwindcss", - "-i", - "byte_bot/server/domain/web/resources/input.css", - "-o", - "byte_bot/server/domain/web/resources/style.css", - "--watch", - ], - check=True, - ) - finally: - for process in multiprocessing.active_children(): - process.terminate() - logger.info("🎨 Tailwind Compiler Shutdown complete") - sys.exit() - - -def bot() -> None: - """Run the bot.""" - log.config.configure() - logger.info("πŸ€– Starting Byte.") - try: - subprocess.run(["python", "byte_bot/byte/bot.py"], check=True) # noqa: S607 - finally: - for process in multiprocessing.active_children(): - process.terminate() - logger.info("πŸ€– Byte Bot Shutdown complete") - sys.exit() - - -@click.group(name="run-bot", invoke_without_command=True, help="Starts the bot.") -def run_bot() -> None: - """Run the bot.""" - bot_process = multiprocessing.Process(target=bot) - bot_process.start() - bot_process.join() - - -def web( - host: str, - port: int | None, - http_workers: int | None, - reload: bool | None, - verbose: bool | None, - debug: bool | None, -) -> None: - """Run the API server.""" - log.config.configure() - settings.server.HOST = host or settings.server.HOST - settings.server.PORT = port or settings.server.PORT - settings.server.RELOAD = reload or settings.server.RELOAD if settings.server.RELOAD is not None else None - settings.server.HTTP_WORKERS = http_workers or settings.server.HTTP_WORKERS - settings.project.DEBUG = debug or settings.project.DEBUG - settings.log.LEVEL = 10 if verbose or settings.project.DEBUG else settings.log.LEVEL - - try: - logger.info("πŸ–₯️ Starting Litestar Web Server.") - reload_dirs = settings.server.RELOAD_DIRS if settings.server.RELOAD else None - process_args = { - "reload": bool(settings.server.RELOAD), - "host": settings.server.HOST, - "port": settings.server.PORT, - "workers": 1 if bool(settings.server.RELOAD or settings.project.DEV_MODE) else settings.server.HTTP_WORKERS, - "factory": settings.server.APP_LOC_IS_FACTORY, - "loop": "uvloop", - "no-access-log": True, - "timeout-keep-alive": settings.server.KEEPALIVE, - } - if reload_dirs: - process_args["reload-dir"] = " ".join(reload_dirs) - subprocess.run( # noqa: S603 - ["uvicorn", settings.server.APP_LOC, *_convert_uvicorn_args(process_args)], # noqa: S607 - check=True, - ) - finally: - for process in multiprocessing.active_children(): - process.terminate() - logger.info("πŸ–₯️ Server Shutdown complete") - sys.exit() - - -@click.group(name="run-web", invoke_without_command=True, help="Starts the application server.") -@click.option( - "-H", - "--host", - help="Host interface to listen on. Use 0.0.0.0 for all available interfaces.", - type=click.STRING, - default=settings.server.HOST, - required=False, - show_default=True, -) -@click.option( - "-p", - "--port", - help="Port to bind.", - type=click.INT, - default=settings.server.PORT, - required=False, - show_default=True, -) -@click.option( - "-W", - "--http-workers", - help="The number of HTTP worker processes for handling requests.", - type=click.IntRange(min=1, max=multiprocessing.cpu_count() + 1), - default=multiprocessing.cpu_count() + 1, - required=False, - show_default=True, -) -@click.option("-r", "--reload", help="Enable reload", is_flag=True, default=False, type=bool) -@click.option("-v", "--verbose", help="Enable verbose logging.", is_flag=True, default=False, type=bool) -@click.option("-d", "--debug", help="Enable debugging.", is_flag=True, default=False, type=bool) -def run_web( - host: str, - port: int | None, - http_workers: int | None, - reload: bool | None, - verbose: bool | None, - debug: bool | None, -) -> None: - """Run the API server.""" - web_process = multiprocessing.Process(target=web, args=(host, port, http_workers, reload, verbose, debug)) - web_process.start() - web_process.join() - - -@click.option( - "-H", - "--host", - help="Host interface to listen on. Use 0.0.0.0 for all available interfaces.", - type=click.STRING, - default=settings.server.HOST, - required=False, - show_default=True, -) -@click.option( - "-p", - "--port", - help="Port to bind.", - type=click.INT, - default=settings.server.PORT, - required=False, - show_default=True, -) -@click.option( - "-W", - "--http-workers", - help="The number of HTTP worker processes for handling requests.", - type=click.IntRange(min=1, max=multiprocessing.cpu_count() + 1), - default=multiprocessing.cpu_count() + 1, - required=False, - show_default=True, -) -@click.option("-r", "--reload", help="Enable reload", is_flag=True, default=False, type=bool) -@click.option("-v", "--verbose", help="Enable verbose logging.", is_flag=True, default=False, type=bool) -@click.option("-d", "--debug", help="Enable debugging.", is_flag=True, default=False, type=bool) -@click.command(name="run-all", help="Starts the bot and the application server.") -def run_all( - host: str, - port: int | None, - http_workers: int | None, - reload: bool | None, - verbose: bool | None, - debug: bool | None, -) -> None: - """Runs both the bot and the web server.""" - bot_process = multiprocessing.Process(target=bot) - web_process = multiprocessing.Process(target=web, args=(host, port, http_workers, reload, verbose, debug)) - - processes = [bot_process, web_process] - - if settings.project.ENVIRONMENT != "prod" and settings.project.DEV_MODE: - frontend_process = multiprocessing.Process(target=frontend) - processes.append(frontend_process) - - for process in processes: - process.start() - - for process in processes: - process.join() - - -def _convert_uvicorn_args(args: dict[str, Any]) -> list[str]: - process_args: list[str] = [] - for arg, value in args.items(): - if isinstance(value, list): - process_args.extend(f"--{arg}={val}" for val in value) - if isinstance(value, bool): - if value: - process_args.append(f"--{arg}") - else: - process_args.append(f"--{arg}={value}") - - return process_args diff --git a/byte_bot/py.typed b/byte_bot/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/byte_bot/server/__init__.py b/byte_bot/server/__init__.py deleted file mode 100644 index 84fee56e..00000000 --- a/byte_bot/server/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Byte Bot Server.""" - -from __future__ import annotations - -from byte_bot.server import domain, lib - -__all__ = ( - "domain", - "lib", -) diff --git a/byte_bot/server/domain/__init__.py b/byte_bot/server/domain/__init__.py deleted file mode 100644 index f75ae06d..00000000 --- a/byte_bot/server/domain/__init__.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Application Modules.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from advanced_alchemy.service import OffsetPagination -from litestar.contrib.repository.filters import FilterTypes - -from byte_bot.server.domain import db, guilds, system, urls, web - -if TYPE_CHECKING: - from collections.abc import Mapping - from typing import Any - - from litestar.types import ControllerRouterHandler - -__all__ = [ - "db", - "routes", - "signature_namespace", - "system", - "urls", - "web", -] - -routes: list[ControllerRouterHandler] = [ - system.controllers.system.SystemController, - web.controllers.web.WebController, - guilds.controllers.GuildsController, -] -"""Routes for the application.""" - -signature_namespace: Mapping[str, Any] = { - "FilterTypes": FilterTypes, - "OffsetPagination": OffsetPagination, -} -"""Namespace for the application signature.""" diff --git a/byte_bot/server/domain/db/__init__.py b/byte_bot/server/domain/db/__init__.py deleted file mode 100644 index bbed3f5f..00000000 --- a/byte_bot/server/domain/db/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Domain for database models.""" - -from byte_bot.server.domain.db import models - -__all__ = ["models"] diff --git a/byte_bot/server/domain/db/models.py b/byte_bot/server/domain/db/models.py deleted file mode 100644 index 9854b92c..00000000 --- a/byte_bot/server/domain/db/models.py +++ /dev/null @@ -1,241 +0,0 @@ -"""Shared models.""" - -from __future__ import annotations - -from uuid import UUID # noqa: TC003 - -from advanced_alchemy.base import UUIDAuditBase -from sqlalchemy import BigInteger, ForeignKey, String, UniqueConstraint -from sqlalchemy.ext.associationproxy import AssociationProxy, association_proxy -from sqlalchemy.orm import Mapped, mapped_column, relationship - -__all__ = ("AllowedUsersConfig", "ForumConfig", "GitHubConfig", "Guild", "SOTagsConfig", "User") - - -class Guild(UUIDAuditBase): - """Guild configuration. - - A single guild will contain base defaults (e.g., ``prefix``, boolean flags for linking, etc.) - with configurable options that can be set by the guild owner or ``allowed_users``. - - Part of the feature set of Byte is that you have interactivity with your git repositories, - StackOverflow, Discord forums, and other external services. - - Here, a guild should be able to configure their own GitHub organization, StackOverflow tags, etc. - """ - - __tablename__ = "guild" # type: ignore[reportAssignmentType] - __table_args__ = {"comment": "Configuration for a Discord guild."} - - guild_id: Mapped[int] = mapped_column(BigInteger, unique=True, index=True) - guild_name: Mapped[str] = mapped_column(String(100)) - prefix: Mapped[str] = mapped_column(String(5), server_default="!", default="!") - help_channel_id: Mapped[int | None] = mapped_column(BigInteger) - showcase_channel_id: Mapped[int | None] = mapped_column(BigInteger) - sync_label: Mapped[str | None] - issue_linking: Mapped[bool] = mapped_column(default=False) - comment_linking: Mapped[bool] = mapped_column(default=False) - pep_linking: Mapped[bool] = mapped_column(default=False) - - # ================= - # ORM Relationships - # ================= - github_config: Mapped[GitHubConfig | None] = relationship( - lazy="noload", - back_populates="guild", - cascade="save-update, merge, delete", - ) - sotags_configs: Mapped[list[SOTagsConfig]] = relationship( - lazy="noload", - back_populates="guild", - cascade="all, delete-orphan", - ) - allowed_users: Mapped[list[AllowedUsersConfig]] = relationship( - lazy="noload", - back_populates="guild", - cascade="save-update, merge, delete", - ) - forum_config: Mapped[ForumConfig | None] = relationship( - lazy="noload", - back_populates="guild", - cascade="save-update, merge, delete", - ) - - -class GitHubConfig(UUIDAuditBase): - """GitHub configuration. - - A guild will be able to configure which organization or user they want as a default - base, which repository they want as a default, and whether they would like to sync - discussions with forum posts. - """ - - __tablename__ = "github_config" # type: ignore[reportAssignmentType] - __table_args__ = {"comment": "GitHub configuration for a guild."} - - guild_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("guild.guild_id", ondelete="cascade")) - discussion_sync: Mapped[bool] = mapped_column(default=False) - github_organization: Mapped[str | None] - github_repository: Mapped[str | None] - - # ================= - # ORM Relationships - # ================= - guild: Mapped[Guild] = relationship( - back_populates="github_config", - innerjoin=True, - lazy="noload", - cascade="save-update, merge, delete", - ) - - -class SOTagsConfig(UUIDAuditBase): - """SQLAlchemy association model for a guild's Stack Overflow tags config.""" - - __tablename__ = "so_tags_config" # type: ignore[reportAssignmentType] - __table_args__ = ( - UniqueConstraint("guild_id", "tag_name"), - {"comment": "Configuration for a Discord guild's Stack Overflow tags."}, - ) - - guild_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("guild.guild_id", ondelete="cascade")) - guild_name: AssociationProxy[str] = association_proxy("guild", "guild_name") - tag_name: Mapped[str] = mapped_column(String(50)) - - # ================= - # ORM Relationships - # ================= - guild: Mapped[Guild] = relationship( - back_populates="sotags_configs", - foreign_keys="SOTagsConfig.guild_id", - innerjoin=True, - lazy="noload", - ) - - -class AllowedUsersConfig(UUIDAuditBase): - """SQLAlchemy association model for a guild's allowed users' config. - - A guild normally has a set of users to perform administrative actions, but sometimes - we don't want to give full administrative access to a user. - - This model allows us to configure which users are allowed to perform administrative - actions on Byte specifically without giving them full administrative access to the Discord guild. - - .. todo:: More preferably, this should be more generalized to a user OR role ID. - """ - - __tablename__ = "allowed_users" # type: ignore[reportAssignmentType] - __table_args__ = ( - UniqueConstraint("guild_id", "user_id"), - {"comment": "Configuration for allowed users in a Discord guild."}, - ) - - guild_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("guild.guild_id", ondelete="cascade")) - user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id", ondelete="cascade")) - - guild_name: AssociationProxy[str] = association_proxy("guild", "guild_name") - user_name: AssociationProxy[str] = association_proxy("user", "name") - - # ================= - # ORM Relationships - # ================= - guild: Mapped[Guild] = relationship( - back_populates="allowed_users", - foreign_keys="AllowedUsersConfig.guild_id", - innerjoin=True, - lazy="noload", - ) - user: Mapped[User] = relationship( - back_populates="guilds_allowed", - foreign_keys="AllowedUsersConfig.user_id", - innerjoin=True, - lazy="noload", - ) - - -class User(UUIDAuditBase): - """SQLAlchemy model representing a user. - - .. todo:: This may not really be needed? - - Currently, a user in the Byte context is an individual user assigned to the - guild's allowed users config. - - In the future we may want to expand this to allow for more granular permissions. - """ - - __tablename__ = "user" # type: ignore[reportAssignmentType] - __table_args__ = {"comment": "A user."} - - name: Mapped[str] = mapped_column(String(100)) - avatar_url: Mapped[str | None] - discriminator: Mapped[str] = mapped_column(String(4)) - - # ================= - # ORM Relationships - # ================= - guilds_allowed: Mapped[list[AllowedUsersConfig]] = relationship( - back_populates="user", - lazy="noload", - cascade="save-update, merge, delete", - ) - - -class ForumConfig(UUIDAuditBase): - """Forum configuration. - - A guild will be able to set whether they want help and/or showcase forums. - * If they already have them set up, they can configure the channel IDs for them. - * If they don't have them set up, they can configure the category and channel names for them - Byte will then create the channels for them. - * If they don't want them, they can disable them. - - Help forum settings include: - * Respond with help embed, including a link to 'Open a GitHub Issue' - if the `GitHubConfig:github_organization` and `GitHubConfig:github_repository` are set. - Also includes `Solve` button to mark as solved and close the thread. - * Automatic thread closing after a certain period of inactivity. - * Uploading of threads into GitHub discussions. - * Pinging of defined roles when a thread has not received a response from someone with those roles - after a certain period of time. - - Showcase forum settings include: - * Respond with showcase embed, including a link to 'Add to awesome-$repo' - if the `GitHubConfig:github_organization` and `GitHubConfig:github_awesome` are set. - * Automatic thread closing after a certain period of inactivity. - * Uploading of threads into GitHub discussions. - """ - - __tablename__ = "forum_config" # type: ignore[reportAssignmentType] - __table_args__ = {"comment": "Forum configuration for a guild."} - - guild_id: Mapped[int] = mapped_column(BigInteger, ForeignKey("guild.guild_id", ondelete="cascade")) - - """Help forum settings.""" - help_forum: Mapped[bool] = mapped_column(default=False) - help_forum_category: Mapped[str | None] - help_channel_id: AssociationProxy[int | None] = association_proxy("guild", "help_channel_id") - help_thread_auto_close: Mapped[bool] = mapped_column(default=False) - help_thread_auto_close_days: Mapped[int | None] - help_thread_notify: Mapped[bool] = mapped_column(default=False) - help_thread_notify_roles: Mapped[str | None] - help_thread_notify_days: Mapped[int | None] - help_thread_sync: AssociationProxy[bool] = association_proxy("guild", "github_config.discussion_sync") - - """Showcase forum settings.""" - showcase_forum: Mapped[bool] = mapped_column(default=False) - showcase_forum_category: Mapped[str | None] - showcase_channel_id: AssociationProxy[int | None] = association_proxy("guild", "showcase_channel_Id") - showcase_thread_auto_close: Mapped[bool] = mapped_column(default=False) - showcase_thread_auto_close_days: Mapped[int | None] - - # ================= - # ORM Relationships - # ================= - guild: Mapped[Guild] = relationship( - back_populates="forum_config", - innerjoin=True, - lazy="noload", - cascade="save-update, merge, delete", - ) diff --git a/byte_bot/server/domain/github/__init__.py b/byte_bot/server/domain/github/__init__.py deleted file mode 100644 index 4d545299..00000000 --- a/byte_bot/server/domain/github/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""GitHub domain.""" - -from byte_bot.server.domain.github import helpers - -__all__ = ("helpers",) diff --git a/byte_bot/server/domain/github/helpers.py b/byte_bot/server/domain/github/helpers.py deleted file mode 100644 index 233748e7..00000000 --- a/byte_bot/server/domain/github/helpers.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Helper functions for use within the GitHub domain.""" - -from __future__ import annotations - -from githubkit import AppInstallationAuthStrategy, GitHub # type: ignore[reportMissingImports] - -from byte_bot.server.lib import settings - -__all__ = ("github_client",) - -github_client = GitHub( - AppInstallationAuthStrategy( - app_id=settings.github.APP_ID, - private_key=settings.github.APP_PRIVATE_KEY, - installation_id=44969171, # TODO: This should be dynamic depending upon the installation. - client_id=settings.github.APP_CLIENT_ID, - client_secret=settings.github.APP_CLIENT_SECRET, - ) -) diff --git a/byte_bot/server/domain/guilds/__init__.py b/byte_bot/server/domain/guilds/__init__.py deleted file mode 100644 index b798d3c7..00000000 --- a/byte_bot/server/domain/guilds/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Guilds domain.""" - -from byte_bot.server.domain.guilds import controllers - -__all__ = ("controllers",) diff --git a/byte_bot/server/domain/guilds/controllers.py b/byte_bot/server/domain/guilds/controllers.py deleted file mode 100644 index 437c9dde..00000000 --- a/byte_bot/server/domain/guilds/controllers.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Guild controller.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from litestar import Controller, get, patch, post -from litestar.di import Provide -from litestar.params import Dependency, Parameter - -from byte_bot.server.domain.guilds import urls -from byte_bot.server.domain.guilds.dependencies import ( - provides_allowed_users_config_service, - provides_forum_config_service, - provides_github_config_service, - provides_guilds_service, - provides_sotags_config_service, -) -from byte_bot.server.domain.guilds.schemas import ( - AllowedUsersConfigSchema, - ForumConfigSchema, - GitHubConfigSchema, - GuildSchema, - SOTagsConfigSchema, - UpdateableGuildSettingEnum, -) -from byte_bot.server.domain.guilds.services import ( - AllowedUsersConfigService, # noqa: TC001 - ForumConfigService, # noqa: TC001 - GitHubConfigService, # noqa: TC001 - GuildsService, # noqa: TC001 - SOTagsConfigService, # noqa: TC001 -) - -if TYPE_CHECKING: - from advanced_alchemy.filters import FilterTypes - from advanced_alchemy.service import OffsetPagination - -__all__ = ("GuildsController",) - - -class GuildsController(Controller): - """Controller for guild-based routes.""" - - tags = ["Guilds"] - dependencies = { - "guilds_service": Provide(provides_guilds_service), - "github_service": Provide(provides_github_config_service), - "sotags_service": Provide(provides_sotags_config_service), - "allowed_users_service": Provide(provides_allowed_users_config_service), - "forum_service": Provide(provides_forum_config_service), - } - - @get( - operation_id="Guilds", - name="guilds:list", - summary="List Guilds", - path=urls.GUILD_LIST, - ) - async def list_guilds( - self, - guilds_service: GuildsService, - filters: list[FilterTypes] = Dependency(skip_validation=True), - ) -> OffsetPagination[GuildSchema]: - """List guilds. - - Args: - guilds_service (GuildsService): Guilds service - filters (list[FilterTypes]): Filters - - Returns: - list[Guild]: List of guilds - """ - results, total = await guilds_service.list_and_count(*filters) - return guilds_service.to_schema(data=results, total=total, filters=filters, schema_type=GuildSchema) - - @post( - operation_id="CreateGuild", - name="guilds:create", - summary="Create a new guild.", - path=urls.GUILD_CREATE, - ) - async def create_guild( - self, - guilds_service: GuildsService, - guild_id: int = Parameter( - title="Guild ID", - description="The guild ID.", - ), - guild_name: str = Parameter( - title="Guild Name", - description="The guild name.", - ), - ) -> str: - """Create a guild. - - Args: - guilds_service (GuildsService): Guilds service - guild_id (int): Guild ID - guild_name (str): Guild name - - Returns: - Guild: Created guild object - """ - new_guild = {"guild_id": guild_id, "guild_name": guild_name} - await guilds_service.create(new_guild) - return f"Guild {guild_name} created." - - @patch( - operation_id="UpdateGuild", - name="guilds:update", - summary="Update a guild.", - path=urls.GUILD_UPDATE, - ) - async def update_guild( - self, - guilds_service: GuildsService, - guild_id: int = Parameter( - title="Guild ID", - description="The guild ID.", - ), - setting: UpdateableGuildSettingEnum = Parameter( # type: ignore[name-defined] - title="Setting", - description="The setting to update.", - ), - value: str | int = Parameter( - title="Value", - description="The new value for the setting.", - ), - ) -> GuildSchema | OffsetPagination[GuildSchema]: - """Update a guild by ID. - - Args: - guilds_service (GuildsService): Guilds service - guild_id (Guild.guild_id): Guild ID - setting (str): Setting to update - value (str | int): New value for the setting - - Returns: - Guild: Updated guild object - """ - result = await guilds_service.get(guild_id, id_attribute="guild_id") - # todo: this is a placeholder, update to grab whichever setting is being update, and update the corresponding - # tables value based on the setting parameter - await guilds_service.update({setting.value: value}, item_id=guild_id) - return guilds_service.to_schema(schema_type=GuildSchema, data=result) - - @get( - operation_id="GuildDetail", - name="guilds:detail", - summary="Get guild details.", - path=urls.GUILD_DETAIL, - ) - async def get_guild( - self, - guilds_service: GuildsService, - guild_id: int = Parameter( - title="Guild ID", - description="The guild ID.", - ), - ) -> GuildSchema: - """Get a guild by ID. - - Args: - guilds_service (GuildsService): Guilds service - guild_id (int): Guild ID - - Returns: - Guild: Guild object - """ - result = await guilds_service.get(guild_id, id_attribute="guild_id") - return guilds_service.to_schema(schema_type=GuildSchema, data=result) - - @get( - operation_id="GitHubDetail", - name="guilds:github-config", - summary="Get GitHub config for a guild.", - path=urls.GUILD_GITHUB_DETAIL, - ) - async def get_guild_github_config( - self, - github_service: GitHubConfigService, - guild_id: int = Parameter( - title="Guild ID", - description="The guild ID.", - ), - ) -> GitHubConfigSchema | OffsetPagination[GitHubConfigSchema]: - """Get a guild's GitHub config by ID. - - TODO(#88): a helper method that we can use outside of routes would be nice. - - Args: - github_service (GitHubConfigService): GitHub config service - guild_id (int): Guild ID - - Returns: - GitHubConfig: GitHub config object - """ - result = await github_service.get(guild_id, id_attribute="guild_id") - return github_service.to_schema(schema_type=GitHubConfigSchema, data=result) - - @get( - operation_id="SOTagsDetail", - name="guilds:sotags-config", - summary="Get StackOverflow tags config for a guild.", - path=urls.GUILD_SOTAGS_DETAIL, - ) - async def get_guild_sotags_config( - self, - sotags_service: SOTagsConfigService, - guild_id: int = Parameter( - title="Guild ID", - description="The guild ID.", - ), - ) -> SOTagsConfigSchema | OffsetPagination[SOTagsConfigSchema]: - """Get a guild's StackOverflow tags config by ID. - - Args: - sotags_service (SOTagsConfigService): StackOverflow tags config service - guild_id (int): Guild ID - - Returns: - SOTagsConfig: StackOverflow tags config object - """ - result = await sotags_service.get(guild_id, id_attribute="guild_id") - return sotags_service.to_schema(schema_type=SOTagsConfigSchema, data=result) - - @get( - operation_id="AllowedUsersDetail", - name="guilds:allowed-users-config", - summary="Get allowed users config for a guild.", - path=urls.GUILD_ALLOWED_USERS_DETAIL, - ) - async def get_guild_allowed_users_config( - self, - allowed_users_service: AllowedUsersConfigService, - guild_id: int = Parameter( - title="Guild ID", - description="The guild ID.", - ), - ) -> AllowedUsersConfigSchema | OffsetPagination[AllowedUsersConfigSchema]: - """Get a guild's allowed users config by ID. - - Args: - allowed_users_service (AllowedUsersConfigService): Allowed users config service - guild_id (int): Guild ID - - Returns: - AllowedUsersConfig: Allowed users config object - """ - result = await allowed_users_service.get(guild_id, id_attribute="guild_id") - return allowed_users_service.to_schema(schema_type=AllowedUsersConfigSchema, data=result) - - @get( - operation_id="ForumDetail", - name="guilds:forum-config", - summary="Get forum config for a guild.", - path=urls.GUILD_FORUM_DETAIL, - ) - async def get_guild_forum_config( - self, - forum_service: ForumConfigService, - guild_id: int = Parameter( - title="Guild ID", - description="The guild ID.", - ), - ) -> ForumConfigSchema | OffsetPagination[ForumConfigSchema]: - """Get a guild's forum config by ID. - - Args: - forum_service (ForumConfigService): Forum config service - guild_id (int): Guild ID - - Returns: - ForumConfig: Forum config object - """ - result = await forum_service.get(guild_id, id_attribute="guild_id") - return forum_service.to_schema(schema_type=ForumConfigSchema, data=result) diff --git a/byte_bot/server/domain/guilds/dependencies.py b/byte_bot/server/domain/guilds/dependencies.py deleted file mode 100644 index 39201bbf..00000000 --- a/byte_bot/server/domain/guilds/dependencies.py +++ /dev/null @@ -1,169 +0,0 @@ -"""Dependencies for guilds.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from sqlalchemy import select -from sqlalchemy.orm import joinedload, noload, selectinload - -from byte_bot.server.domain.db.models import AllowedUsersConfig, ForumConfig, GitHubConfig, Guild, SOTagsConfig -from byte_bot.server.domain.guilds.services import ( - AllowedUsersConfigService, - ForumConfigService, - GitHubConfigService, - GuildsService, - SOTagsConfigService, -) -from byte_bot.server.lib import log - -if TYPE_CHECKING: - from collections.abc import AsyncGenerator - - from sqlalchemy.ext.asyncio import AsyncSession - -__all__ = ( - "provides_allowed_users_config_service", - "provides_forum_config_service", - "provides_github_config_service", - "provides_guilds_service", - "provides_sotags_config_service", -) - -logger = log.get_logger() - - -async def provides_guilds_service(db_session: AsyncSession) -> AsyncGenerator[GuildsService, None]: - """Construct Guilds-based repository and service objects for the request. - - Args: - db_session (AsyncSession): SQLAlchemy AsyncSession - - Yields: - GuildsService: GuildConfig-based service - """ - async with GuildsService.new( - session=db_session, - statement=select(Guild) - .order_by(Guild.guild_name) - .options( - selectinload(Guild.github_config).options( - joinedload(GitHubConfig.guild, innerjoin=True).options(noload("*")), - ), - selectinload(Guild.sotags_configs).options( - joinedload(SOTagsConfig.guild, innerjoin=True).options(noload("*")), - ), - selectinload(Guild.allowed_users).options( - joinedload(AllowedUsersConfig.guild, innerjoin=True).options(noload("*")), - ), - selectinload(Guild.forum_config).options( - joinedload(ForumConfig.guild, innerjoin=True).options(noload("*")), - ), - ), - ) as service: - try: - yield service # type: ignore[misc] - finally: - ... - - -async def provides_github_config_service(db_session: AsyncSession) -> AsyncGenerator[GitHubConfigService, None]: # type: ignore[misc] - """Construct GitHubConfig-based repository and service objects for the request. - - Args: - db_session (AsyncSession): SQLAlchemy AsyncSession - - Yields: - GitHubConfigService: GitHubConfig-based service - """ - async with GuildsService.new( - session=db_session, - statement=select(Guild) - .order_by(Guild.guild_name) - .options( - selectinload(Guild.github_config).options( - joinedload(GitHubConfig.guild, innerjoin=True).options(noload("*")), - ), - selectinload(Guild.sotags_configs).options( - joinedload(SOTagsConfig.guild, innerjoin=True).options(noload("*")), - ), - selectinload(Guild.allowed_users).options( - joinedload(AllowedUsersConfig.guild, innerjoin=True).options(noload("*")), - ), - ), - ) as service: - try: - yield service # type: ignore[misc] - finally: - ... - - -async def provides_sotags_config_service(db_session: AsyncSession) -> AsyncGenerator[SOTagsConfigService, None]: - """Construct SOTagsConfig-based repository and service objects for the request. - - Args: - db_session (AsyncSession): SQLAlchemy AsyncSession - - Yields: - SOTagsConfigService: SOTagsConfig-based service - """ - async with SOTagsConfigService.new( - session=db_session, - statement=select(SOTagsConfig) - .order_by(SOTagsConfig.tag_name) - .options( - selectinload(SOTagsConfig.guild).options(noload("*")), - ), - ) as service: - try: - yield service # type: ignore[misc] - finally: - ... - - -async def provides_allowed_users_config_service( - db_session: AsyncSession, -) -> AsyncGenerator[AllowedUsersConfigService, None]: - """Construct AllowedUsersConfig-based repository and service objects for the request. - - Args: - db_session (AsyncSession): SQLAlchemy AsyncSession - - Yields: - AllowedUsersConfigService: AllowedUsersConfig-based service - """ - async with AllowedUsersConfigService.new( - session=db_session, - statement=select(AllowedUsersConfig) - .order_by(AllowedUsersConfig.user_id) - .options( - selectinload(AllowedUsersConfig.guild).options(noload("*")), - ), - ) as service: - try: - yield service # type: ignore[misc] - finally: - ... - - -async def provides_forum_config_service(db_session: AsyncSession) -> AsyncGenerator[ForumConfigService, None]: - """Construct ForumConfig-based repository and service objects for the request. - - Args: - db_session (AsyncSession): SQLAlchemy AsyncSession - - Yields: - ForumConfigService: ForumConfig-based service - """ - async with ForumConfigService.new( - session=db_session, - statement=select(ForumConfig) - .order_by(ForumConfig.help_forum) - .options( - selectinload(ForumConfig.guild).options(noload("*")), - ), - ) as service: - try: - yield service # type: ignore[misc] - finally: - ... diff --git a/byte_bot/server/domain/guilds/helpers.py b/byte_bot/server/domain/guilds/helpers.py deleted file mode 100644 index f050cca1..00000000 --- a/byte_bot/server/domain/guilds/helpers.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Helper functions to be used for interacting with Guild data.""" - -from __future__ import annotations - -from byte_bot.server.domain.guilds.dependencies import provides_guilds_service -from byte_bot.server.lib.db import config - -__all__ = ("get_byte_server_count",) - - -async def get_byte_server_count() -> int: - """Get the server count for Byte. - - Returns: - int: The server counts for Byte or 0 if there are none. - """ - async with config.get_session() as session: - guilds_service = await anext(provides_guilds_service(db_session=session)) - total = await guilds_service.count() - return total or 0 diff --git a/byte_bot/server/domain/guilds/schemas.py b/byte_bot/server/domain/guilds/schemas.py deleted file mode 100644 index af3ee03e..00000000 --- a/byte_bot/server/domain/guilds/schemas.py +++ /dev/null @@ -1,183 +0,0 @@ -"""API Schemas for guild domain.""" - -from __future__ import annotations - -from enum import Enum -from uuid import UUID # noqa: TC003 - -from pydantic import Field - -from byte_bot.server.lib.schema import CamelizedBaseModel - -__all__ = ( - "AllowedUsersConfigSchema", - "ForumConfigSchema", - "GitHubConfigSchema", - "GuildCreate", - "GuildSchema", - "GuildUpdate", - "SOTagsConfigSchema", - "UpdateableGuildSetting", - "UpdateableGuildSettingEnum", -) - - -class GitHubConfigSchema(CamelizedBaseModel): - """Schema for validating GitHub configuration.""" - - guild_id: UUID - discussion_sync: bool - github_organization: str | None - github_repository: str | None - - -class SOTagsConfigSchema(CamelizedBaseModel): - """Schema for validating StackOverflow tags configuration.""" - - guild_id: UUID - tag_name: str - - -class AllowedUsersConfigSchema(CamelizedBaseModel): - """Schema for validating allowed users for certain admin actions within a guild.""" - - guild_id: UUID - user_id: UUID - - -class ForumConfigSchema(CamelizedBaseModel): - """Schema for validating forum configuration.""" - - guild_id: UUID - help_forum: bool = Field(title="Help Forum", description="Is the help forum enabled.") - help_forum_category: str - help_thread_auto_close: bool - help_thread_auto_close_days: int - help_thread_notify: bool - help_thread_notify_roles: list[int] - help_thread_notify_days: int - help_thread_sync: bool - showcase_forum: bool - showcase_forum_category: str - showcase_thread_auto_close: bool - showcase_thread_auto_close_days: int - - -class GuildSchema(CamelizedBaseModel): - """Schema representing an existing guild.""" - - internal_id: UUID = Field(title="Internal ID", description="The internal database record ID.", alias="id") - guild_id: int = Field(title="Guild ID", description="The guild ID.") - guild_name: str = Field(title="Name", description="The guild name.") - prefix: str | None = Field(title="Prefix", description="The prefix for the guild.") - help_channel_id: int | None = Field(title="Help Channel ID", description="The channel ID for the help channel.") - sync_label: str | None = Field( - title="Sync Label", description="The forum label to use for GitHub discussion syncs." - ) - issue_linking: bool | None = Field(title="Issue Linking", description="Is issue linking enabled.") - comment_linking: bool | None = Field(title="Comment Linking", description="Is comment linking enabled.") - pep_linking: bool | None = Field(title="PEP Linking", description="Is PEP linking enabled.") - github_config: GitHubConfigSchema | None = Field( - title="GitHub Config", description="The GitHub configuration for the guild." - ) - sotags_configs: list[SOTagsConfigSchema] = Field( - title="StackOverflow Tags Configs", description="The StackOverflow tags configuration for the guild." - ) - allowed_users: list[AllowedUsersConfigSchema] = Field( - title="Allowed Users", description="The allowed users configuration for the guild." - ) - forum_config: ForumConfigSchema | None = Field( - title="Forum Config", description="The forum configuration for the guild." - ) - - -class GuildCreate(CamelizedBaseModel): - """Schema representing a guild create request. - - .. todo:: Add owner ID - """ - - guild_id: int = Field(title="Guild ID", description="The guild ID.", alias="id") - name: str = Field(title="Name", description="The guild name.") - - -class GuildUpdate(CamelizedBaseModel): - """Schema representing a guild update request.""" - - guild_id: int = Field(title="Guild ID", description="The guild ID.", alias="id") - prefix: str | None = Field(title="Prefix", description="The prefix for the guild.") - help_channel_id: int | None = Field(title="Help Channel ID", description="The channel ID for the help channel.") - sync_label: str | None = Field( - title="Sync Label", description="The forum label to use for GitHub discussion syncs." - ) - issue_linking: bool | None = Field(title="Issue Linking", description="Is issue linking enabled.") - comment_linking: bool | None = Field(title="Comment Linking", description="Is comment linking enabled.") - pep_linking: bool | None = Field(title="PEP Linking", description="Is PEP linking enabled.") - - -class UpdateableGuildSetting(CamelizedBaseModel): - """Allowed settings that admins can update for their guild.""" - - """Guild Model Settings""" - prefix: str = Field(title="Prefix", description="The prefix for the guild.") - help_channel_id: int = Field(title="Help Channel ID", description="The channel ID for the help forum.") - showcase_channel_id: int = Field(title="Showcase Channel ID", description="The channel ID for the showcase forum.") - sync_label: str = Field(title="Sync Label", description="The forum label to use for GitHub discussion syncs.") - issue_linking: bool = Field(title="Issue Linking", description="Is issue linking enabled.") - comment_linking: bool = Field(title="Comment Linking", description="Is comment linking enabled.") - pep_linking: bool = Field(title="PEP Linking", description="Is PEP linking enabled.") - - """GitHub Config Settings""" - discussion_sync: bool = Field(title="Discussion Sync", description="Is GitHub discussion sync enabled.") - github_organization: str = Field(title="GitHub Organization", description="The GitHub organization to sync.") - github_repository: str = Field(title="GitHub Repository", description="The GitHub repository to sync.") - - """StackOverflow Tags Config Settings""" - tag_name: list[str] = Field( - title="StackOverflow Tag(s)", - description="The StackOverflow tag(s) to sync.", - examples=["litestar", "byte", "python"], - ) - - """Allowed Users Config Settings""" - allowed_user_id: int = Field(title="User ID", description="The user or role ID to allow.") - - """Forum Config Settings""" - """Help Forum""" - help_forum: bool = Field(title="Help Forum", description="Is the help forum enabled.") - help_forum_category: str = Field(title="Help Forum Category", description="The help forum category.") - help_thread_auto_close: bool = Field( - title="Help Thread Auto Close", description="Is the help thread auto close enabled." - ) - help_thread_auto_close_days: int = Field( - title="Help Thread Auto Close Days", description="The days to auto close help threads after inactivity." - ) - help_thread_notify: bool = Field( - title="Help Thread Notify", description="Whether to notify roles for unresponded help threads." - ) - help_thread_notify_roles: list[int] = Field( - title="Help Thread Notify Roles", description="The roles to notify for unresponded help threads." - ) - help_thread_notify_days: int = Field( - title="Help Thread Notify Days", description="The days to notify `notify_roles` after not receiving a response." - ) - help_thread_sync: bool = Field( - title="Help Thread Sync", description="Is the help thread GitHub discussions sync enabled." - ) - - """Showcase forum""" - showcase_forum: bool = Field(title="Showcase Forum", description="Is the showcase forum enabled.") - showcase_forum_category: str = Field(title="Showcase Forum Category", description="The showcase forum category.") - showcase_thread_auto_close: bool = Field( - title="Showcase Thread Auto Close", description="Is the showcase thread auto close enabled." - ) - showcase_thread_auto_close_days: int = Field( - title="Showcase Thread Auto Close Days", description="The days to auto close showcase threads after inactivity." - ) - - -# idk -UpdateableGuildSettingEnum = Enum( # type: ignore[misc] - "UpdateableGuildSettingEnum", - {field_name.upper(): field_name for field_name in UpdateableGuildSetting.model_fields}, -) diff --git a/byte_bot/server/domain/guilds/services.py b/byte_bot/server/domain/guilds/services.py deleted file mode 100644 index 037610f3..00000000 --- a/byte_bot/server/domain/guilds/services.py +++ /dev/null @@ -1,149 +0,0 @@ -"""Guild services.""" - -from __future__ import annotations - -from advanced_alchemy.repository import SQLAlchemyAsyncRepository, SQLAlchemyAsyncSlugRepository -from advanced_alchemy.service import ModelDictT, SQLAlchemyAsyncRepositoryService - -from byte_bot.server.domain.db.models import AllowedUsersConfig, ForumConfig, GitHubConfig, Guild, SOTagsConfig -from byte_bot.server.lib import log - -__all__ = ( - "AllowedUsersConfigRepository", - "AllowedUsersConfigService", - "ForumConfigRepository", - "ForumConfigService", - "GitHubConfigRepository", - "GitHubConfigService", - "GuildsRepository", - "GuildsService", - "SOTagsConfigRepository", - "SOTagsConfigService", -) - -logger = log.get_logger() - - -class GuildsRepository(SQLAlchemyAsyncSlugRepository[Guild]): - """Guilds SQLAlchemy Repository.""" - - model_type = Guild - - -class GuildsService(SQLAlchemyAsyncRepositoryService[Guild]): - """Handles basic operations for a guild.""" - - repository_type = GuildsRepository - match_fields = ["guild_id"] - - async def to_model(self, data: ModelDictT[Guild], operation: str | None = None) -> Guild: - """Convert data to a model. - - Args: - data (ModelDictT[Guild]): Data to convert to a model - operation (str | None): Operation to perform on the data - - Returns: - Project: Converted model - """ - return await super().to_model(data, operation) - - -class GitHubConfigRepository(SQLAlchemyAsyncRepository[GitHubConfig]): - """GitHubConfig SQLAlchemy Repository.""" - - model_type = GitHubConfig - - -class GitHubConfigService(SQLAlchemyAsyncRepositoryService[GitHubConfig]): - """Handles basic operations for the guilds' GitHub config.""" - - repository_type = GitHubConfigRepository - match_fields = ["guild_id"] - - async def to_model(self, data: ModelDictT[GitHubConfig], operation: str | None = None) -> GitHubConfig: - """Convert data to a model. - - Args: - data (GitHubConfig | dict[str, Any]): Data to convert to a model - operation (str | None): Operation to perform on the data - - Returns: - Project: Converted model - """ - return await super().to_model(data, operation) - - -class SOTagsConfigRepository(SQLAlchemyAsyncRepository[SOTagsConfig]): - """SOTagsConfig SQLAlchemy Repository.""" - - model_type = SOTagsConfig - - -class SOTagsConfigService(SQLAlchemyAsyncRepositoryService[SOTagsConfig]): - """Handles basic operations for the guilds' StackOverflow tags config.""" - - repository_type = SOTagsConfigRepository - match_fields = ["guild_id"] - - async def to_model(self, data: ModelDictT[SOTagsConfig], operation: str | None = None) -> SOTagsConfig: - """Convert data to a model. - - Args: - data (SOTagsConfig | dict[str, Any]): Data to convert to a model - operation (str | None): Operation to perform on the data - - Returns: - Project: Converted model - """ - return await super().to_model(data, operation) - - -class AllowedUsersConfigRepository(SQLAlchemyAsyncRepository[AllowedUsersConfig]): - """AllowedUsersConfig SQLAlchemy Repository.""" - - model_type = AllowedUsersConfig - - -class AllowedUsersConfigService(SQLAlchemyAsyncRepositoryService[AllowedUsersConfig]): - """Handles basic operations for the guilds' Allowed Users config.""" - - repository_type = AllowedUsersConfigRepository - match_fields = ["guild_id"] - - async def to_model(self, data: ModelDictT[AllowedUsersConfig], operation: str | None = None) -> AllowedUsersConfig: - """Convert data to a model. - - Args: - data (AllowedUsersConfig | dict[str, Any]): Data to convert to a model - operation (str | None): Operation to perform on the data - - Returns: - Project: Converted model - """ - return await super().to_model(data, operation) - - -class ForumConfigRepository(SQLAlchemyAsyncRepository[ForumConfig]): - """ForumConfig SQLAlchemy Repository.""" - - model_type = ForumConfig - - -class ForumConfigService(SQLAlchemyAsyncRepositoryService[ForumConfig]): - """Handles basic operations for the guilds' Forum config.""" - - repository_type = AllowedUsersConfigRepository - match_fields = ["guild_id"] - - async def to_model(self, data: ModelDictT[ForumConfig], operation: str | None = None) -> ForumConfig: - """Convert data to a model. - - Args: - data (ForumConfig | dict[str, Any]): Data to convert to a model - operation (str | None): Operation to perform on the data - - Returns: - Project: Converted model - """ - return await super().to_model(data, operation) diff --git a/byte_bot/server/domain/guilds/urls.py b/byte_bot/server/domain/guilds/urls.py deleted file mode 100644 index d1ee2af7..00000000 --- a/byte_bot/server/domain/guilds/urls.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Guild URLs.""" - -from __future__ import annotations - -from typing import Final - -from byte_bot.server.domain.urls import OPENAPI_SCHEMA - -# --- API - -# -- General -GUILD_CREATE: Final = f"{OPENAPI_SCHEMA}/guilds/create" -"""Create guild URL.""" -GUILD_LIST: Final = f"{OPENAPI_SCHEMA}/guilds/list" -"""Guild list URL.""" - -# -- Specific -GUILD_UPDATE: Final = f"{OPENAPI_SCHEMA}/guilds/{{guild_id:int}}/update" -"""Update guild URL.""" -GUILD_DETAIL: Final = f"{OPENAPI_SCHEMA}/guilds/{{guild_id:int}}/info" -"""Guild detail URL.""" -GUILD_GITHUB_DETAIL: Final = f"{OPENAPI_SCHEMA}/guilds/{{guild_id:int}}/github/info" -"""Guild GitHub detail URL.""" -GUILD_SOTAGS_DETAIL: Final = f"{OPENAPI_SCHEMA}/guilds/{{guild_id:int}}/sotags/info" -"""Guild StackOverflow tags detail URL.""" -GUILD_ALLOWED_USERS_DETAIL: Final = f"{OPENAPI_SCHEMA}/guilds/{{guild_id:int}}/allowed_users/info" -"""Guild allowed users detail URL.""" -GUILD_FORUM_DETAIL: Final = f"{OPENAPI_SCHEMA}/guilds/{{guild_id:int}}/forum/info" -"""Guild forum detail URL.""" diff --git a/byte_bot/server/domain/system/__init__.py b/byte_bot/server/domain/system/__init__.py deleted file mode 100644 index def7c394..00000000 --- a/byte_bot/server/domain/system/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -"""System domain.""" - -from __future__ import annotations - -from byte_bot.server.domain.system import controllers, dtos, helpers - -__all__ = [ - "controllers", - "dtos", - "helpers", -] diff --git a/byte_bot/server/domain/system/controllers/__init__.py b/byte_bot/server/domain/system/controllers/__init__.py deleted file mode 100644 index f73d4e9a..00000000 --- a/byte_bot/server/domain/system/controllers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""System domain controllers.""" - -from __future__ import annotations - -from byte_bot.server.domain.system.controllers import system - -__all__ = [ - "system", -] diff --git a/byte_bot/server/domain/system/controllers/system.py b/byte_bot/server/domain/system/controllers/system.py deleted file mode 100644 index ec42248b..00000000 --- a/byte_bot/server/domain/system/controllers/system.py +++ /dev/null @@ -1,71 +0,0 @@ -"""System Controller.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from litestar import Controller, MediaType, get -from litestar.response import Response - -from byte_bot.server.domain import urls -from byte_bot.server.domain.system.dtos import SystemHealth -from byte_bot.server.domain.system.helpers import check_byte_status, check_database_status -from byte_bot.server.lib import log - -if TYPE_CHECKING: - from sqlalchemy.ext.asyncio import AsyncSession - -__all__ = ["SystemController"] - -logger = log.get_logger() - - -class SystemController(Controller): - """System Controller.""" - - opt = {"exclude_from_auth": True} - - @get( - operation_id="SystemHealth", - name="system:health", - path=urls.SYSTEM_HEALTH, - media_type=MediaType.JSON, - cache=False, - tags=["System"], - summary="Health Check", - description="Execute a health check against backend components including database and bot status.", - signature_namespace={"SystemHealth": SystemHealth}, - ) - async def check_system_health(self, db_session: AsyncSession) -> Response[SystemHealth]: - """Check the overall system health. - - Args: - db_session (AsyncSession): Database session. - - Returns: - Response[SystemHealth]: System health. - """ - database_status = await check_database_status(db_session) - byte_status = await check_byte_status() - statuses = [database_status, byte_status] - - if all(status == "offline" for status in statuses): - overall_status = "offline" - elif "offline" in statuses or "degraded" in statuses: - overall_status = "degraded" - else: - overall_status = "healthy" - - status_code = 200 if overall_status == "healthy" else 503 if overall_status == "degraded" else 500 - # noinspection PyTypeChecker - system_health_detail = SystemHealth( - database_status=database_status, - byte_status=byte_status, - overall_status=overall_status, - ) - - return Response( - content=system_health_detail, - status_code=status_code, - media_type=MediaType.JSON, - ) diff --git a/byte_bot/server/domain/system/dtos.py b/byte_bot/server/domain/system/dtos.py deleted file mode 100644 index 4281ffc9..00000000 --- a/byte_bot/server/domain/system/dtos.py +++ /dev/null @@ -1,24 +0,0 @@ -"""System domain DTOS.""" - -from dataclasses import dataclass -from typing import Annotated, Literal - -from litestar.dto import DataclassDTO - -from byte_bot.server.lib import dto, settings - -__all__ = ["SystemHealth", "SystemHealthDTO"] - - -@dataclass -class SystemHealth: - """System Health.""" - - database_status: Literal["online", "offline", "degraded"] - byte_status: Literal["online", "offline", "degraded"] - overall_status: Literal["healthy", "offline", "degraded"] - app: str = settings.project.NAME - version: str = settings.project.BUILD_NUMBER - - -SystemHealthDTO = DataclassDTO[Annotated[SystemHealth, dto.config()]] diff --git a/byte_bot/server/domain/system/helpers.py b/byte_bot/server/domain/system/helpers.py deleted file mode 100644 index 8a901db4..00000000 --- a/byte_bot/server/domain/system/helpers.py +++ /dev/null @@ -1,67 +0,0 @@ -"""System domain helper functions.""" - -from __future__ import annotations - -from time import time -from typing import TYPE_CHECKING - -from sqlalchemy import text - -__all__ = ("check_byte_status", "check_database_status") - - -if TYPE_CHECKING: - from sqlalchemy.ext.asyncio import AsyncSession - - from byte_bot.server.lib.types import Status - -DEGRADED_THRESHOLD = 2.0 -"""float: Threshold in seconds for degraded status.""" - - -async def check_database_status(db_session: AsyncSession) -> Status: - """Check database health. - - Args: - db_session (AsyncSession): Database session. - - Returns: - Status: Database status. - """ - try: - start_time = time() - await db_session.execute(text("select 1")) - end_time = time() - response_time = end_time - start_time - if response_time > DEGRADED_THRESHOLD: - return "degraded" - except ConnectionRefusedError: - return "offline" - return "online" - - -async def check_byte_status() -> Status: - """Check Byte status. - - .. todo:: This is a stub. Need to figure out how to call the current bot instance from here. - - .. code-block:: python - :caption: Example usage of ``check_byte_status`` - - async def healthcheck(self) -> Status: - latency = round(self.bot.latency * 1000, 2) - ratelimited = self.bot.is_ws_ratelimited() - ready = self.bot.is_ready() - if closed := self.bot.is_closed(): - return "offline" - latency_threshold = 1000 - return ( - "degraded" - if not ready or ratelimited or latency > latency_threshold - else "online" - ) - - Returns: - Status: Byte status. - """ - return "offline" diff --git a/byte_bot/server/domain/urls.py b/byte_bot/server/domain/urls.py deleted file mode 100644 index 810f0ba6..00000000 --- a/byte_bot/server/domain/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Domain URLs.""" - -from __future__ import annotations - -from typing import Final - -# --- System - -INDEX: Final = "/" -"""Index URL.""" -SITE_ROOT: Final = "/{path:str}" -"""Site root URL.""" -OPENAPI_SCHEMA: Final = "/api" -"""OpenAPI schema URL.""" -SYSTEM_HEALTH: Final = "/health" -"""System health URL.""" - -# --- Bot - -# --- Reports diff --git a/byte_bot/server/domain/web/__init__.py b/byte_bot/server/domain/web/__init__.py deleted file mode 100644 index bb5431ab..00000000 --- a/byte_bot/server/domain/web/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Web domain.""" - -from __future__ import annotations - -from byte_bot.server.domain.web import controllers - -__all__ = [ - "controllers", -] diff --git a/byte_bot/server/domain/web/controllers/__init__.py b/byte_bot/server/domain/web/controllers/__init__.py deleted file mode 100644 index 2ac958bc..00000000 --- a/byte_bot/server/domain/web/controllers/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Web domain controllers.""" - -from __future__ import annotations - -from byte_bot.server.domain.web.controllers import web - -__all__ = [ - "web", -] diff --git a/byte_bot/server/domain/web/controllers/web.py b/byte_bot/server/domain/web/controllers/web.py deleted file mode 100644 index 467c3889..00000000 --- a/byte_bot/server/domain/web/controllers/web.py +++ /dev/null @@ -1,123 +0,0 @@ -"""Web Controller.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -from litestar import Controller, get -from litestar.response import Template -from litestar.status_codes import HTTP_200_OK - -from byte_bot.server.domain import urls -from byte_bot.server.domain.guilds.helpers import get_byte_server_count -from byte_bot.server.domain.system.helpers import check_byte_status, check_database_status - -if TYPE_CHECKING: - from sqlalchemy.ext.asyncio import AsyncSession - -__all__ = ("WebController",) - - -class WebController(Controller): - """Web Controller.""" - - opt = {"exclude_from_auth": True} - - @get( - [urls.INDEX, urls.SITE_ROOT], - operation_id="WebIndex", - name="frontend:index", - status_code=HTTP_200_OK, - include_in_schema=False, - opt={"exclude_from_auth": True}, - ) - async def index(self, db_session: AsyncSession) -> Template: - """Serve site root.""" - server_count = await get_byte_server_count() - byte_status = await check_byte_status() - database_status = await check_database_status(db_session) - statuses = [database_status, byte_status] - - if all(status == "offline" for status in statuses): - overall_status = "offline" - elif "offline" in statuses or "degraded" in statuses: - overall_status = "degraded" - else: - overall_status = "healthy" - - return Template( - template_name="index.html", context={"server_count": server_count, "overall_status": overall_status} - ) - - # add dashboard - @get( - path="/dashboard", - operation_id="WebDashboard", - name="frontend:dashboard", - status_code=HTTP_200_OK, - include_in_schema=False, - opt={"exclude_from_auth": True}, - ) - async def dashboard(self) -> Template: - """Serve dashboard.""" - return Template(template_name="dashboard.html") - - @get( - path="/about", - operation_id="WebAbout", - name="frontend:about", - status_code=HTTP_200_OK, - include_in_schema=False, - opt={"exclude_from_auth": True}, - ) - async def about(self) -> Template: - """Serve about page.""" - return Template(template_name="about.html") - - @get( - path="/contact", - operation_id="WebContact", - name="frontend:contact", - status_code=HTTP_200_OK, - include_in_schema=False, - opt={"exclude_from_auth": True}, - ) - async def contact(self) -> Template: - """Serve contact page.""" - return Template(template_name="contact.html") - - @get( - path="/privacy", - operation_id="WebPrivacy", - name="frontend:privacy", - status_code=HTTP_200_OK, - include_in_schema=False, - opt={"exclude_from_auth": True}, - ) - async def privacy(self) -> Template: - """Serve privacy page.""" - return Template(template_name="privacy.html") - - @get( - path="/terms", - operation_id="WebTerms", - name="frontend:terms", - status_code=HTTP_200_OK, - include_in_schema=False, - opt={"exclude_from_auth": True}, - ) - async def terms(self) -> Template: - """Serve terms page.""" - return Template(template_name="terms.html") - - @get( - path="/cookies", - operation_id="WebCookies", - name="frontend:cookies", - status_code=HTTP_200_OK, - include_in_schema=False, - opt={"exclude_from_auth": True}, - ) - async def cookies(self) -> Template: - """Serve cookies page.""" - return Template(template_name="cookies.html") diff --git a/byte_bot/server/domain/web/resources/badge.png b/byte_bot/server/domain/web/resources/badge.png deleted file mode 100644 index 19bcbb02..00000000 Binary files a/byte_bot/server/domain/web/resources/badge.png and /dev/null differ diff --git a/byte_bot/server/domain/web/resources/input.css b/byte_bot/server/domain/web/resources/input.css deleted file mode 100644 index 5b4a5cf8..00000000 --- a/byte_bot/server/domain/web/resources/input.css +++ /dev/null @@ -1,3 +0,0 @@ -@import "tailwind/_base.css"; -@import "tailwind/_components.css"; -@import "tailwind/_utilities.css"; diff --git a/byte_bot/server/domain/web/resources/logo.svg b/byte_bot/server/domain/web/resources/logo.svg deleted file mode 100644 index 5d4e444b..00000000 --- a/byte_bot/server/domain/web/resources/logo.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/byte_bot/server/domain/web/resources/powered-by-litestar.svg b/byte_bot/server/domain/web/resources/powered-by-litestar.svg deleted file mode 100644 index 50c240f9..00000000 --- a/byte_bot/server/domain/web/resources/powered-by-litestar.svg +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/byte_bot/server/domain/web/resources/splash-bg.png b/byte_bot/server/domain/web/resources/splash-bg.png deleted file mode 100644 index 2e6ccd48..00000000 Binary files a/byte_bot/server/domain/web/resources/splash-bg.png and /dev/null differ diff --git a/byte_bot/server/domain/web/resources/style.css b/byte_bot/server/domain/web/resources/style.css deleted file mode 100644 index d17fb1db..00000000 --- a/byte_bot/server/domain/web/resources/style.css +++ /dev/null @@ -1,3658 +0,0 @@ -*, ::before, ::after { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -::backdrop { - --tw-border-spacing-x: 0; - --tw-border-spacing-y: 0; - --tw-translate-x: 0; - --tw-translate-y: 0; - --tw-rotate: 0; - --tw-skew-x: 0; - --tw-skew-y: 0; - --tw-scale-x: 1; - --tw-scale-y: 1; - --tw-pan-x: ; - --tw-pan-y: ; - --tw-pinch-zoom: ; - --tw-scroll-snap-strictness: proximity; - --tw-gradient-from-position: ; - --tw-gradient-via-position: ; - --tw-gradient-to-position: ; - --tw-ordinal: ; - --tw-slashed-zero: ; - --tw-numeric-figure: ; - --tw-numeric-spacing: ; - --tw-numeric-fraction: ; - --tw-ring-inset: ; - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: rgb(59 130 246 / 0.5); - --tw-ring-offset-shadow: 0 0 #0000; - --tw-ring-shadow: 0 0 #0000; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - --tw-blur: ; - --tw-brightness: ; - --tw-contrast: ; - --tw-grayscale: ; - --tw-hue-rotate: ; - --tw-invert: ; - --tw-saturate: ; - --tw-sepia: ; - --tw-drop-shadow: ; - --tw-backdrop-blur: ; - --tw-backdrop-brightness: ; - --tw-backdrop-contrast: ; - --tw-backdrop-grayscale: ; - --tw-backdrop-hue-rotate: ; - --tw-backdrop-invert: ; - --tw-backdrop-opacity: ; - --tw-backdrop-saturate: ; - --tw-backdrop-sepia: ; - --tw-contain-size: ; - --tw-contain-layout: ; - --tw-contain-paint: ; - --tw-contain-style: ; -} - -/* -! tailwindcss v3.4.13 | MIT License | https://tailwindcss.com -*/ - -/* -1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) -2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) -*/ - -*, -::before, -::after { - box-sizing: border-box; - /* 1 */ - border-width: 0; - /* 2 */ - border-style: solid; - /* 2 */ - border-color: #e5e7eb; - /* 2 */ -} - -::before, -::after { - --tw-content: ''; -} - -/* -1. Use a consistent sensible line-height in all browsers. -2. Prevent adjustments of font size after orientation changes in iOS. -3. Use a more readable tab size. -4. Use the user's configured `sans` font-family by default. -5. Use the user's configured `sans` font-feature-settings by default. -6. Use the user's configured `sans` font-variation-settings by default. -7. Disable tap highlights on iOS -*/ - -html, -:host { - line-height: 1.5; - /* 1 */ - -webkit-text-size-adjust: 100%; - /* 2 */ - -moz-tab-size: 4; - /* 3 */ - -o-tab-size: 4; - tab-size: 4; - /* 3 */ - font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; - /* 4 */ - font-feature-settings: normal; - /* 5 */ - font-variation-settings: normal; - /* 6 */ - -webkit-tap-highlight-color: transparent; - /* 7 */ -} - -/* -1. Remove the margin in all browsers. -2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. -*/ - -body { - margin: 0; - /* 1 */ - line-height: inherit; - /* 2 */ -} - -/* -1. Add the correct height in Firefox. -2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) -3. Ensure horizontal rules are visible by default. -*/ - -hr { - height: 0; - /* 1 */ - color: inherit; - /* 2 */ - border-top-width: 1px; - /* 3 */ -} - -/* -Add the correct text decoration in Chrome, Edge, and Safari. -*/ - -abbr:where([title]) { - -webkit-text-decoration: underline dotted; - text-decoration: underline dotted; -} - -/* -Remove the default font size and weight for headings. -*/ - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: inherit; - font-weight: inherit; -} - -/* -Reset links to optimize for opt-in styling instead of opt-out. -*/ - -a { - color: inherit; - text-decoration: inherit; -} - -/* -Add the correct font weight in Edge and Safari. -*/ - -b, -strong { - font-weight: bolder; -} - -/* -1. Use the user's configured `mono` font-family by default. -2. Use the user's configured `mono` font-feature-settings by default. -3. Use the user's configured `mono` font-variation-settings by default. -4. Correct the odd `em` font sizing in all browsers. -*/ - -code, -kbd, -samp, -pre { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - /* 1 */ - font-feature-settings: normal; - /* 2 */ - font-variation-settings: normal; - /* 3 */ - font-size: 1em; - /* 4 */ -} - -/* -Add the correct font size in all browsers. -*/ - -small { - font-size: 80%; -} - -/* -Prevent `sub` and `sup` elements from affecting the line height in all browsers. -*/ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* -1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) -2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) -3. Remove gaps between table borders by default. -*/ - -table { - text-indent: 0; - /* 1 */ - border-color: inherit; - /* 2 */ - border-collapse: collapse; - /* 3 */ -} - -/* -1. Change the font styles in all browsers. -2. Remove the margin in Firefox and Safari. -3. Remove default padding in all browsers. -*/ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; - /* 1 */ - font-feature-settings: inherit; - /* 1 */ - font-variation-settings: inherit; - /* 1 */ - font-size: 100%; - /* 1 */ - font-weight: inherit; - /* 1 */ - line-height: inherit; - /* 1 */ - letter-spacing: inherit; - /* 1 */ - color: inherit; - /* 1 */ - margin: 0; - /* 2 */ - padding: 0; - /* 3 */ -} - -/* -Remove the inheritance of text transform in Edge and Firefox. -*/ - -button, -select { - text-transform: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Remove default button styles. -*/ - -button, -input:where([type='button']), -input:where([type='reset']), -input:where([type='submit']) { - -webkit-appearance: button; - /* 1 */ - background-color: transparent; - /* 2 */ - background-image: none; - /* 2 */ -} - -/* -Use the modern Firefox focus style for all focusable elements. -*/ - -:-moz-focusring { - outline: auto; -} - -/* -Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) -*/ - -:-moz-ui-invalid { - box-shadow: none; -} - -/* -Add the correct vertical alignment in Chrome and Firefox. -*/ - -progress { - vertical-align: baseline; -} - -/* -Correct the cursor style of increment and decrement buttons in Safari. -*/ - -::-webkit-inner-spin-button, -::-webkit-outer-spin-button { - height: auto; -} - -/* -1. Correct the odd appearance in Chrome and Safari. -2. Correct the outline style in Safari. -*/ - -[type='search'] { - -webkit-appearance: textfield; - /* 1 */ - outline-offset: -2px; - /* 2 */ -} - -/* -Remove the inner padding in Chrome and Safari on macOS. -*/ - -::-webkit-search-decoration { - -webkit-appearance: none; -} - -/* -1. Correct the inability to style clickable types in iOS and Safari. -2. Change font properties to `inherit` in Safari. -*/ - -::-webkit-file-upload-button { - -webkit-appearance: button; - /* 1 */ - font: inherit; - /* 2 */ -} - -/* -Add the correct display in Chrome and Safari. -*/ - -summary { - display: list-item; -} - -/* -Removes the default spacing and border for appropriate elements. -*/ - -blockquote, -dl, -dd, -h1, -h2, -h3, -h4, -h5, -h6, -hr, -figure, -p, -pre { - margin: 0; -} - -fieldset { - margin: 0; - padding: 0; -} - -legend { - padding: 0; -} - -ol, -ul, -menu { - list-style: none; - margin: 0; - padding: 0; -} - -/* -Reset default styling for dialogs. -*/ - -dialog { - padding: 0; -} - -/* -Prevent resizing textareas horizontally by default. -*/ - -textarea { - resize: vertical; -} - -/* -1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) -2. Set the default placeholder color to the user's configured gray 400 color. -*/ - -input::-moz-placeholder, textarea::-moz-placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -input::placeholder, -textarea::placeholder { - opacity: 1; - /* 1 */ - color: #9ca3af; - /* 2 */ -} - -/* -Set the default cursor for buttons. -*/ - -button, -[role="button"] { - cursor: pointer; -} - -/* -Make sure disabled buttons don't get the pointer cursor. -*/ - -:disabled { - cursor: default; -} - -/* -1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) -2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) - This can trigger a poorly considered lint error in some tools but is included by design. -*/ - -img, -svg, -video, -canvas, -audio, -iframe, -embed, -object { - display: block; - /* 1 */ - vertical-align: middle; - /* 2 */ -} - -/* -Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) -*/ - -img, -video { - max-width: 100%; - height: auto; -} - -/* Make elements with the HTML hidden attribute stay hidden by default */ - -[hidden] { - display: none; -} - -[type='text'],input:where(:not([type])),[type='email'],[type='url'],[type='password'],[type='number'],[type='date'],[type='datetime-local'],[type='month'],[type='search'],[type='tel'],[type='time'],[type='week'],[multiple],textarea,select { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - background-color: #fff; - border-color: #6b7280; - border-width: 1px; - border-radius: 0px; - padding-top: 0.5rem; - padding-right: 0.75rem; - padding-bottom: 0.5rem; - padding-left: 0.75rem; - font-size: 1rem; - line-height: 1.5rem; - --tw-shadow: 0 0 #0000; -} - -[type='text']:focus, input:where(:not([type])):focus, [type='email']:focus, [type='url']:focus, [type='password']:focus, [type='number']:focus, [type='date']:focus, [type='datetime-local']:focus, [type='month']:focus, [type='search']:focus, [type='tel']:focus, [type='time']:focus, [type='week']:focus, [multiple]:focus, textarea:focus, select:focus { - outline: 2px solid transparent; - outline-offset: 2px; - --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); - --tw-ring-offset-width: 0px; - --tw-ring-offset-color: #fff; - --tw-ring-color: #2563eb; - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); - border-color: #2563eb; -} - -input::-moz-placeholder, textarea::-moz-placeholder { - color: #6b7280; - opacity: 1; -} - -input::placeholder,textarea::placeholder { - color: #6b7280; - opacity: 1; -} - -::-webkit-datetime-edit-fields-wrapper { - padding: 0; -} - -::-webkit-date-and-time-value { - min-height: 1.5em; - text-align: inherit; -} - -::-webkit-datetime-edit { - display: inline-flex; -} - -::-webkit-datetime-edit,::-webkit-datetime-edit-year-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-meridiem-field { - padding-top: 0; - padding-bottom: 0; -} - -select { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e"); - background-position: right 0.5rem center; - background-repeat: no-repeat; - background-size: 1.5em 1.5em; - padding-right: 2.5rem; - -webkit-print-color-adjust: exact; - print-color-adjust: exact; -} - -[multiple],[size]:where(select:not([size="1"])) { - background-image: initial; - background-position: initial; - background-repeat: unset; - background-size: initial; - padding-right: 0.75rem; - -webkit-print-color-adjust: unset; - print-color-adjust: unset; -} - -[type='checkbox'],[type='radio'] { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; - padding: 0; - -webkit-print-color-adjust: exact; - print-color-adjust: exact; - display: inline-block; - vertical-align: middle; - background-origin: border-box; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - flex-shrink: 0; - height: 1rem; - width: 1rem; - color: #2563eb; - background-color: #fff; - border-color: #6b7280; - border-width: 1px; - --tw-shadow: 0 0 #0000; -} - -[type='checkbox'] { - border-radius: 0px; -} - -[type='radio'] { - border-radius: 100%; -} - -[type='checkbox']:focus,[type='radio']:focus { - outline: 2px solid transparent; - outline-offset: 2px; - --tw-ring-inset: var(--tw-empty,/*!*/ /*!*/); - --tw-ring-offset-width: 2px; - --tw-ring-offset-color: #fff; - --tw-ring-color: #2563eb; - --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); - --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); - box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); -} - -[type='checkbox']:checked,[type='radio']:checked { - border-color: transparent; - background-color: currentColor; - background-size: 100% 100%; - background-position: center; - background-repeat: no-repeat; -} - -[type='checkbox']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M12.207 4.793a1 1 0 010 1.414l-5 5a1 1 0 01-1.414 0l-2-2a1 1 0 011.414-1.414L6.5 9.086l4.293-4.293a1 1 0 011.414 0z'/%3e%3c/svg%3e"); -} - -@media (forced-colors: active) { - [type='checkbox']:checked { - -webkit-appearance: auto; - -moz-appearance: auto; - appearance: auto; - } -} - -[type='radio']:checked { - background-image: url("data:image/svg+xml,%3csvg viewBox='0 0 16 16' fill='white' xmlns='http://www.w3.org/2000/svg'%3e%3ccircle cx='8' cy='8' r='3'/%3e%3c/svg%3e"); -} - -@media (forced-colors: active) { - [type='radio']:checked { - -webkit-appearance: auto; - -moz-appearance: auto; - appearance: auto; - } -} - -[type='checkbox']:checked:hover,[type='checkbox']:checked:focus,[type='radio']:checked:hover,[type='radio']:checked:focus { - border-color: transparent; - background-color: currentColor; -} - -[type='checkbox']:indeterminate { - background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3e%3c/svg%3e"); - border-color: transparent; - background-color: currentColor; - background-size: 100% 100%; - background-position: center; - background-repeat: no-repeat; -} - -@media (forced-colors: active) { - [type='checkbox']:indeterminate { - -webkit-appearance: auto; - -moz-appearance: auto; - appearance: auto; - } -} - -[type='checkbox']:indeterminate:hover,[type='checkbox']:indeterminate:focus { - border-color: transparent; - background-color: currentColor; -} - -[type='file'] { - background: unset; - border-color: inherit; - border-width: 0; - border-radius: 0; - padding: 0; - font-size: unset; - line-height: inherit; -} - -[type='file']:focus { - outline: 1px solid ButtonText; - outline: 1px auto -webkit-focus-ring-color; -} - -:root, -[data-theme] { - background-color: hsl(var(--b1) / var(--tw-bg-opacity, 1)); - color: hsl(var(--bc) / var(--tw-text-opacity, 1)); -} - -html { - -webkit-tap-highlight-color: transparent; -} - -:root { - --p: 175 46% 48%; - --pf: 175 46% 41%; - --sf: 167 46% 58%; - --af: 160 54% 72%; - --nf: 0 0% 0%; - --b2: 60 5% 85%; - --b3: 60 5% 78%; - --bc: 60 0% 18%; - --pc: 173 25% 12%; - --sc: 166 17% 14%; - --ac: 159 11% 16%; - --nc: 145 0% 78%; - --inc: 153 23% 14%; - --suc: 149 34% 88%; - --wac: 34 29% 13%; - --erc: 12 38% 14%; - --rounded-box: 1rem; - --rounded-btn: 0.5rem; - --rounded-badge: 1.9rem; - --animation-btn: 0.25s; - --animation-input: .2s; - --btn-text-case: uppercase; - --btn-focus-scale: 0.95; - --border-btn: 1px; - --tab-border: 1px; - --tab-radius: 0.5rem; - --s: 167 46% 65%; - --a: 160 54% 79%; - --n: 0 0% 5%; - --b1: 60 5% 92%; - --in: 156 72% 67%; - --su: 161 94% 30%; - --wa: 36 59% 59%; - --er: 10 97% 66%; -} - -.container { - width: 100%; -} - -@media (min-width: 640px) { - .container { - max-width: 640px; - } -} - -@media (min-width: 768px) { - .container { - max-width: 768px; - } -} - -@media (min-width: 1024px) { - .container { - max-width: 1024px; - } -} - -@media (min-width: 1280px) { - .container { - max-width: 1280px; - } -} - -@media (min-width: 1536px) { - .container { - max-width: 1536px; - } -} - -.alert { - display: grid; - width: 100%; - grid-auto-flow: row; - align-content: flex-start; - align-items: center; - justify-items: center; - gap: 1rem; - text-align: center; - border-width: 1px; - --tw-border-opacity: 1; - border-color: hsl(var(--b2) / var(--tw-border-opacity)); - padding: 1rem; - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - border-radius: var(--rounded-box, 1rem); - --alert-bg: hsl(var(--b2)); - --alert-bg-mix: hsl(var(--b1)); - background-color: var(--alert-bg); -} - -@media (min-width: 640px) { - .alert { - grid-auto-flow: column; - grid-template-columns: auto minmax(auto,1fr); - justify-items: start; - text-align: left; - } -} - -.artboard { - width: 100%; -} - -.avatar { - position: relative; - display: inline-flex; -} - -.avatar > div { - display: block; - aspect-ratio: 1 / 1; - overflow: hidden; -} - -.avatar img { - height: 100%; - width: 100%; - -o-object-fit: cover; - object-fit: cover; -} - -.avatar.placeholder > div { - display: flex; - align-items: center; - justify-content: center; -} - -.badge { - display: inline-flex; - align-items: center; - justify-content: center; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - height: 1.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - width: -moz-fit-content; - width: fit-content; - padding-left: 0.563rem; - padding-right: 0.563rem; - border-width: 1px; - --tw-border-opacity: 1; - border-color: hsl(var(--b2) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - border-radius: var(--rounded-badge, 1.9rem); -} - -@media (hover:hover) { - .link-hover:hover { - text-decoration-line: underline; - } - - .label a:hover { - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - } - - .menu li > *:not(ul):not(.menu-title):not(details):active, -.menu li > *:not(ul):not(.menu-title):not(details).active, -.menu li > details > summary:active { - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); - } - - .table tr.hover:hover, - .table tr.hover:nth-child(even):hover { - --tw-bg-opacity: 1; - background-color: hsl(var(--b2) / var(--tw-bg-opacity)); - } -} - -.btn { - display: inline-flex; - flex-shrink: 0; - cursor: pointer; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - flex-wrap: wrap; - align-items: center; - justify-content: center; - border-color: transparent; - border-color: hsl(var(--b2) / var(--tw-border-opacity)); - text-align: center; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - border-radius: var(--rounded-btn, 0.5rem); - height: 3rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; - line-height: 1em; - min-height: 3rem; - gap: 0.5rem; - font-weight: 600; - text-decoration-line: none; - border-width: var(--border-btn, 1px); - animation: button-pop var(--animation-btn, 0.25s) ease-out; - text-transform: var(--btn-text-case, uppercase); - --tw-border-opacity: 1; - --tw-bg-opacity: 1; - background-color: hsl(var(--b2) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - outline-color: hsl(var(--bc) / 1); -} - -.btn-disabled, - .btn[disabled], - .btn:disabled { - pointer-events: none; -} - -.btn-circle { - height: 3rem; - width: 3rem; - border-radius: 9999px; - padding: 0px; -} - -.btn-group > input[type="radio"].btn { - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.btn-group > input[type="radio"].btn:before { - content: attr(data-title); -} - -.btn:is(input[type="checkbox"]), -.btn:is(input[type="radio"]) { - width: auto; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; -} - -.btn:is(input[type="checkbox"]):after, -.btn:is(input[type="radio"]):after { - --tw-content: attr(aria-label); - content: var(--tw-content); -} - -.card { - position: relative; - display: flex; - flex-direction: column; - border-radius: var(--rounded-box, 1rem); -} - -.card:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.card figure { - display: flex; - align-items: center; - justify-content: center; -} - -.card.image-full { - display: grid; -} - -.card.image-full:before { - position: relative; - content: ""; - z-index: 10; - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - opacity: 0.75; - border-radius: var(--rounded-box, 1rem); -} - -.card.image-full:before, - .card.image-full > * { - grid-column-start: 1; - grid-row-start: 1; -} - -.card.image-full > figure img { - height: 100%; - -o-object-fit: cover; - object-fit: cover; -} - -.card.image-full > .card-body { - position: relative; - z-index: 20; - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); -} - -.chat { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - -moz-column-gap: 0.75rem; - column-gap: 0.75rem; - padding-top: 0.25rem; - padding-bottom: 0.25rem; -} - -.chat-image { - grid-row: span 2 / span 2; - align-self: flex-end; -} - -.chat-bubble { - position: relative; - display: block; - width: -moz-fit-content; - width: fit-content; - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - max-width: 90%; - border-radius: var(--rounded-box, 1rem); - min-height: 2.75rem; - min-width: 2.75rem; - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); -} - -.chat-bubble:before { - position: absolute; - bottom: 0px; - height: 0.75rem; - width: 0.75rem; - background-color: inherit; - content: ""; - -webkit-mask-size: contain; - mask-size: contain; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; -} - -.chat-start { - place-items: start; - grid-template-columns: auto 1fr; -} - -.chat-start .chat-header { - grid-column-start: 2; -} - -.chat-start .chat-footer { - grid-column-start: 2; -} - -.chat-start .chat-image { - grid-column-start: 1; -} - -.chat-start .chat-bubble { - grid-column-start: 2; - border-bottom-left-radius: 0px; -} - -.chat-start .chat-bubble:before { - -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); - mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); - left: -0.75rem; -} - -[dir="rtl"] .chat-start .chat-bubble:before { - -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); - mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); -} - -.chat-end .chat-image { - grid-column-start: 2; -} - -.chat-end .chat-bubble { - grid-column-start: 1; - border-bottom-right-radius: 0px; -} - -.chat-end .chat-bubble:before { - -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); - mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); - left: 100%; -} - -[dir="rtl"] .chat-end .chat-bubble:before { - -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); - mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); -} - -.dropdown { - position: relative; - display: inline-block; -} - -.dropdown > *:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.dropdown .dropdown-content { - position: absolute; -} - -.dropdown:is(:not(details)) .dropdown-content { - visibility: hidden; - opacity: 0; - transform-origin: top; - --tw-scale-x: .95; - --tw-scale-y: .95; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; -} - -.dropdown-end .dropdown-content { - right: 0px; -} - -.dropdown-left .dropdown-content { - top: 0px; - right: 100%; - bottom: auto; - transform-origin: right; -} - -.dropdown-right .dropdown-content { - left: 100%; - top: 0px; - bottom: auto; - transform-origin: left; -} - -.dropdown-bottom .dropdown-content { - bottom: auto; - top: 100%; - transform-origin: top; -} - -.dropdown-top .dropdown-content { - bottom: 100%; - top: auto; - transform-origin: bottom; -} - -.dropdown-end.dropdown-right .dropdown-content { - bottom: 0px; - top: auto; -} - -.dropdown-end.dropdown-left .dropdown-content { - bottom: 0px; - top: auto; -} - -.dropdown.dropdown-open .dropdown-content, -.dropdown:not(.dropdown-hover):focus .dropdown-content, -.dropdown:focus-within .dropdown-content { - visibility: visible; - opacity: 1; -} - -@media (hover: hover) { - .dropdown.dropdown-hover:hover .dropdown-content { - visibility: visible; - opacity: 1; - } - - .btn:hover { - --tw-border-opacity: 1; - border-color: hsl(var(--b3) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--b3) / var(--tw-bg-opacity)); - } - - .btn-primary:hover { - --tw-border-opacity: 1; - border-color: hsl(var(--pf) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf) / var(--tw-bg-opacity)); - } - - .btn-info:hover { - --tw-border-opacity: 1; - border-color: hsl(var(--in) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--in) / var(--tw-bg-opacity)); - } - - .btn.glass:hover { - --glass-opacity: 25%; - --glass-border-opacity: 15%; - } - - .btn-ghost:hover { - --tw-border-opacity: 0; - background-color: hsl(var(--bc) / var(--tw-bg-opacity)); - --tw-bg-opacity: 0.2; - } - - .btn-outline.btn-primary:hover { - --tw-border-opacity: 1; - border-color: hsl(var(--pf) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); - } - - .btn-outline.btn-info:hover { - --tw-border-opacity: 1; - border-color: hsl(var(--in) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--in) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--inc) / var(--tw-text-opacity)); - } - - .btn-disabled:hover, - .btn[disabled]:hover, - .btn:disabled:hover { - --tw-border-opacity: 0; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-bg-opacity: 0.2; - color: hsl(var(--bc) / var(--tw-text-opacity)); - --tw-text-opacity: 0.2; - } - - .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover { - --tw-border-opacity: 1; - border-color: hsl(var(--pf) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf) / var(--tw-bg-opacity)); - } - - .dropdown.dropdown-hover:hover .dropdown-content { - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - } - - :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(.active):hover, :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(.active):hover { - cursor: pointer; - background-color: hsl(var(--bc) / 0.1); - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - outline: 2px solid transparent; - outline-offset: 2px; - } -} - -.dropdown:is(details) summary::-webkit-details-marker { - display: none; -} - -.footer { - display: grid; - width: 100%; - grid-auto-flow: row; - place-items: start; - row-gap: 2.5rem; - -moz-column-gap: 1rem; - column-gap: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.footer > * { - display: grid; - place-items: start; - gap: 0.5rem; -} - -@media (min-width: 48rem) { - .footer { - grid-auto-flow: column; - } - - .footer-center { - grid-auto-flow: row dense; - } -} - -.label { - display: flex; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - align-items: center; - justify-content: space-between; - padding-left: 0.25rem; - padding-right: 0.25rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.hero { - display: grid; - width: 100%; - place-items: center; - background-size: cover; - background-position: center; -} - -.hero > * { - grid-column-start: 1; - grid-row-start: 1; -} - -.hero-content { - z-index: 0; - display: flex; - align-items: center; - justify-content: center; - max-width: 80rem; - gap: 1rem; - padding: 1rem; -} - -.indicator { - position: relative; - display: inline-flex; - width: -moz-max-content; - width: max-content; -} - -.indicator :where(.indicator-item) { - z-index: 1; - position: absolute; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - white-space: nowrap; -} - -.input { - flex-shrink: 1; - height: 3rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 1rem; - line-height: 2; - line-height: 1.5rem; - border-width: 1px; - border-color: hsl(var(--bc) / var(--tw-border-opacity)); - --tw-border-opacity: 0; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); - border-radius: var(--rounded-btn, 0.5rem); -} - -.input-group > .input { - isolation: isolate; -} - -.input-group > *, - .input-group > .input, - .input-group > .textarea, - .input-group > .select { - border-radius: 0px; -} - -.link { - cursor: pointer; - text-decoration-line: underline; -} - -.link-hover { - text-decoration-line: none; -} - -.menu { - display: flex; - flex-direction: column; - flex-wrap: wrap; - font-size: 0.875rem; - line-height: 1.25rem; - padding: 0.5rem; -} - -.menu :where(li ul) { - position: relative; - white-space: nowrap; - margin-left: 1rem; - padding-left: 0.5rem; -} - -.menu :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)), - .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { - display: grid; - grid-auto-flow: column; - align-content: flex-start; - align-items: center; - gap: 0.5rem; - grid-auto-columns: minmax(auto, max-content) auto max-content; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; -} - -.menu li.disabled { - cursor: not-allowed; - -webkit-user-select: none; - -moz-user-select: none; - user-select: none; - color: hsl(var(--bc) / 0.3); -} - -.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)) { - display: none; -} - -:where(.menu li) { - position: relative; - display: flex; - flex-shrink: 0; - flex-direction: column; - flex-wrap: wrap; - align-items: stretch; -} - -:where(.menu li) .badge { - justify-self: end; -} - -.mockup-code { - position: relative; - overflow: hidden; - overflow-x: auto; - min-width: 18rem; - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - padding-top: 1.25rem; - padding-bottom: 1.25rem; - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); - border-radius: var(--rounded-box, 1rem); -} - -.mockup-code pre[data-prefix]:before { - content: attr(data-prefix); - display: inline-block; - text-align: right; - width: 2rem; - opacity: 0.5; -} - -.navbar { - display: flex; - align-items: center; - padding: var(--navbar-padding, 0.5rem); - min-height: 4rem; - width: 100%; -} - -:where(.navbar > *) { - display: inline-flex; - align-items: center; -} - -.navbar-start { - width: 50%; - justify-content: flex-start; -} - -.navbar-center { - flex-shrink: 0; -} - -.navbar-end { - width: 50%; - justify-content: flex-end; -} - -.stats { - display: inline-grid; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - border-radius: var(--rounded-box, 1rem); -} - -:where(.stats) { - grid-auto-flow: column; - overflow-x: auto; -} - -.stat { - display: inline-grid; - width: 100%; - grid-template-columns: repeat(1, 1fr); - -moz-column-gap: 1rem; - column-gap: 1rem; - border-color: hsl(var(--bc) / var(--tw-border-opacity)); - --tw-border-opacity: 0.1; - padding-left: 1.5rem; - padding-right: 1.5rem; - padding-top: 1rem; - padding-bottom: 1rem; -} - -.stat-figure { - grid-column-start: 2; - grid-row: span 3 / span 3; - grid-row-start: 1; - place-self: center; - justify-self: end; -} - -.stat-title { - grid-column-start: 1; - white-space: nowrap; - color: hsl(var(--bc) / 0.6); -} - -.stat-value { - grid-column-start: 1; - white-space: nowrap; - font-size: 2.25rem; - line-height: 2.5rem; - font-weight: 800; -} - -.stat-desc { - grid-column-start: 1; - white-space: nowrap; - font-size: 0.75rem; - line-height: 1rem; - color: hsl(var(--bc) / 0.6); -} - -.table { - position: relative; - width: 100%; - text-align: left; - font-size: 0.875rem; - line-height: 1.25rem; - border-radius: var(--rounded-box, 1rem); -} - -.table :where(.table-pin-rows thead tr) { - position: sticky; - top: 0px; - z-index: 1; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); -} - -.table :where(.table-pin-rows tfoot tr) { - position: sticky; - bottom: 0px; - z-index: 1; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); -} - -.table :where(.table-pin-cols tr th) { - position: sticky; - left: 0px; - right: 0px; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); -} - -.textarea { - flex-shrink: 1; - min-height: 3rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; - line-height: 2; - border-width: 1px; - border-color: hsl(var(--bc) / var(--tw-border-opacity)); - --tw-border-opacity: 0; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); - border-radius: var(--rounded-btn, 0.5rem); -} - -.toast { - position: fixed; - display: flex; - min-width: -moz-fit-content; - min-width: fit-content; - flex-direction: column; - white-space: nowrap; - gap: 0.5rem; - padding: 1rem; -} - -.alert-info { - border-color: hsl(var(--in) / 0.2); - --tw-text-opacity: 1; - color: hsl(var(--inc) / var(--tw-text-opacity)); - --alert-bg: hsl(var(--in)); - --alert-bg-mix: hsl(var(--b1)); -} - -.alert-success { - border-color: hsl(var(--su) / 0.2); - --tw-text-opacity: 1; - color: hsl(var(--suc) / var(--tw-text-opacity)); - --alert-bg: hsl(var(--su)); - --alert-bg-mix: hsl(var(--b1)); -} - -.alert-warning { - border-color: hsl(var(--wa) / 0.2); - --tw-text-opacity: 1; - color: hsl(var(--wac) / var(--tw-text-opacity)); - --alert-bg: hsl(var(--wa)); - --alert-bg-mix: hsl(var(--b1)); -} - -.avatar-group :where(.avatar) { - overflow: hidden; - border-radius: 9999px; - border-width: 4px; - --tw-border-opacity: 1; - border-color: hsl(var(--b1) / var(--tw-border-opacity)); -} - -.badge-primary { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} - -.badge-outline.badge-primary { - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} - -.btm-nav > *:where(.active) { - border-top-width: 2px; - --tw-bg-opacity: 1; - background-color: hsl(var(--b1) / var(--tw-bg-opacity)); -} - -.btm-nav > * .label { - font-size: 1rem; - line-height: 1.5rem; -} - -.btn:active:hover, - .btn:active:focus { - animation: button-pop 0s ease-out; - transform: scale(var(--btn-focus-scale, 0.97)); -} - -.btn:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; -} - -.btn-primary { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); - outline-color: hsl(var(--p) / 1); -} - -.btn-primary.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--pf) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf) / var(--tw-bg-opacity)); -} - -.btn-info { - --tw-border-opacity: 1; - border-color: hsl(var(--in) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--in) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--inc) / var(--tw-text-opacity)); - outline-color: hsl(var(--in) / 1); -} - -.btn-info.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--in) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--in) / var(--tw-bg-opacity)); -} - -.btn.glass { - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - outline-color: currentColor; -} - -.btn.glass.btn-active { - --glass-opacity: 25%; - --glass-border-opacity: 15%; -} - -.btn-ghost { - border-width: 1px; - border-color: transparent; - background-color: transparent; - color: currentColor; - --tw-shadow: 0 0 #0000; - --tw-shadow-colored: 0 0 #0000; - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); - outline-color: currentColor; -} - -.btn-ghost.btn-active { - --tw-border-opacity: 0; - background-color: hsl(var(--bc) / var(--tw-bg-opacity)); - --tw-bg-opacity: 0.2; -} - -.btn-outline.btn-primary { - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} - -.btn-outline.btn-primary.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--pf) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--pf) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} - -.btn-outline.btn-info { - --tw-text-opacity: 1; - color: hsl(var(--in) / var(--tw-text-opacity)); -} - -.btn-outline.btn-info.btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--in) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--in) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--inc) / var(--tw-text-opacity)); -} - -.btn.btn-disabled, - .btn[disabled], - .btn:disabled { - --tw-border-opacity: 0; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-bg-opacity: 0.2; - color: hsl(var(--bc) / var(--tw-text-opacity)); - --tw-text-opacity: 0.2; -} - -.btn-group > input[type="radio"]:checked.btn, - .btn-group > .btn-active { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} - -.btn-group > input[type="radio"]:checked.btn:focus-visible, .btn-group > .btn-active:focus-visible { - outline-style: solid; - outline-width: 2px; - outline-color: hsl(var(--p) / 1); -} - -.btn:is(input[type="checkbox"]:checked), -.btn:is(input[type="radio"]:checked) { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} - -.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible { - outline-color: hsl(var(--p) / 1); -} - -@keyframes button-pop { - 0% { - transform: scale(var(--btn-focus-scale, 0.98)); - } - - 40% { - transform: scale(1.02); - } - - 100% { - transform: scale(1); - } -} - -.card :where(figure:first-child) { - overflow: hidden; - border-start-start-radius: inherit; - border-start-end-radius: inherit; - border-end-start-radius: unset; - border-end-end-radius: unset; -} - -.card :where(figure:last-child) { - overflow: hidden; - border-start-start-radius: unset; - border-start-end-radius: unset; - border-end-start-radius: inherit; - border-end-end-radius: inherit; -} - -.card:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} - -.card.bordered { - border-width: 1px; - --tw-border-opacity: 1; - border-color: hsl(var(--b2) / var(--tw-border-opacity)); -} - -.card.compact .card-body { - padding: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -.card.image-full :where(figure) { - overflow: hidden; - border-radius: inherit; -} - -@keyframes checkmark { - 0% { - background-position-y: 5px; - } - - 50% { - background-position-y: -2px; - } - - 100% { - background-position-y: 0; - } -} - -.dropdown.dropdown-open .dropdown-content, -.dropdown:focus .dropdown-content, -.dropdown:focus-within .dropdown-content { - --tw-scale-x: 1; - --tw-scale-y: 1; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.footer-title { - margin-bottom: 0.5rem; - font-weight: 700; - text-transform: uppercase; - opacity: 0.5; -} - -.label-text { - font-size: 0.875rem; - line-height: 1.25rem; - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} - -.input[list]::-webkit-calendar-picker-indicator { - line-height: 1em; -} - -.input-bordered { - --tw-border-opacity: 0.2; -} - -.input:focus { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: hsl(var(--bc) / 0.2); -} - -.input-primary { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); -} - -.input-primary:focus { - outline-color: hsl(var(--p) / 1); -} - -.input-disabled, - .input:disabled, - .input[disabled] { - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: hsl(var(--b2) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--b2) / var(--tw-bg-opacity)); - --tw-text-opacity: 0.2; -} - -.input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder { - color: hsl(var(--bc) / var(--tw-placeholder-opacity)); - --tw-placeholder-opacity: 0.2; -} - -.input-disabled::placeholder, - .input:disabled::placeholder, - .input[disabled]::placeholder { - color: hsl(var(--bc) / var(--tw-placeholder-opacity)); - --tw-placeholder-opacity: 0.2; -} - -.link:focus { - outline: 2px solid transparent; - outline-offset: 2px; -} - -.link:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; -} - -:where(.menu li:empty) { - background-color: hsl(var(--bc) / 0.1); - margin: 0.5rem 1rem; - height: 1px; -} - -.menu :where(li ul):before { - position: absolute; - left: 0px; - top: 0.75rem; - bottom: 0.75rem; - width: 1px; - background-color: hsl(var(--bc) / 0.1); - content: ""; -} - -.menu :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)), -.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - text-align: left; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-timing-function: cubic-bezier(0, 0, 0.2, 1); - transition-duration: 200ms; - border-radius: var(--rounded-btn, 0.5rem); - text-wrap: balance; -} - -:where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(summary):not(.active).focus, - :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):not(summary):not(.active):focus, - :where(.menu li:not(.menu-title):not(.disabled) > *:not(ul):not(details):not(.menu-title)):is(summary):not(.active):focus-visible, - :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active).focus, - :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):not(summary):not(.active):focus, - :where(.menu li:not(.menu-title):not(.disabled) > details > summary:not(.menu-title)):is(summary):not(.active):focus-visible { - cursor: pointer; - background-color: hsl(var(--bc) / 0.1); - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - outline: 2px solid transparent; - outline-offset: 2px; -} - -.menu li > *:not(ul):not(.menu-title):not(details):active, -.menu li > *:not(ul):not(.menu-title):not(details).active, -.menu li > details > summary:active { - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--nc) / var(--tw-text-opacity)); -} - -.menu :where(li > details > summary)::-webkit-details-marker { - display: none; -} - -.menu :where(li > details > summary):after, -.menu :where(li > .menu-dropdown-toggle):after { - justify-self: end; - display: block; - margin-top: -0.5rem; - height: 0.5rem; - width: 0.5rem; - transform: rotate(45deg); - transition-property: transform, margin-top; - transition-duration: 0.3s; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - content: ""; - transform-origin: 75% 75%; - box-shadow: 2px 2px; - pointer-events: none; -} - -.menu :where(li > details[open] > summary):after, -.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after { - transform: rotate(225deg); - margin-top: 0; -} - -.mockup-code:before { - content: ""; - margin-bottom: 1rem; - display: block; - height: 0.75rem; - width: 0.75rem; - border-radius: 9999px; - opacity: 0.3; - box-shadow: 1.4em 0, 2.8em 0, 4.2em 0; -} - -.mockup-code pre { - padding-right: 1.25rem; -} - -.mockup-code pre:before { - content: ""; - margin-right: 2ch; -} - -.mockup-phone { - display: inline-block; - border: 4px solid #444; - border-radius: 50px; - background-color: #000; - padding: 10px; - margin: 0 auto; - overflow: hidden; -} - -.mockup-phone .camera { - position: relative; - top: 0px; - left: 0px; - background: #000; - height: 25px; - width: 150px; - margin: 0 auto; - border-bottom-left-radius: 17px; - border-bottom-right-radius: 17px; - z-index: 11; -} - -.mockup-phone .camera:before { - content: ""; - position: absolute; - top: 35%; - left: 50%; - width: 50px; - height: 4px; - border-radius: 5px; - background-color: #0c0b0e; - transform: translate(-50%, -50%); -} - -.mockup-phone .camera:after { - content: ""; - position: absolute; - top: 20%; - left: 70%; - width: 8px; - height: 8px; - border-radius: 5px; - background-color: #0f0b25; -} - -.mockup-phone .display { - overflow: hidden; - border-radius: 40px; - margin-top: -25px; -} - -.mockup-browser .mockup-browser-toolbar .input { - position: relative; - margin-left: auto; - margin-right: auto; - display: block; - height: 1.75rem; - width: 24rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - --tw-bg-opacity: 1; - background-color: hsl(var(--b2) / var(--tw-bg-opacity)); - padding-left: 2rem; -} - -.mockup-browser .mockup-browser-toolbar .input:before { - content: ""; - position: absolute; - top: 50%; - left: 0.5rem; - aspect-ratio: 1 / 1; - height: 0.75rem; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-radius: 9999px; - border-width: 2px; - border-color: currentColor; - opacity: 0.6; -} - -.mockup-browser .mockup-browser-toolbar .input:after { - content: ""; - position: absolute; - top: 50%; - left: 1.25rem; - height: 0.5rem; - --tw-translate-y: 25%; - --tw-rotate: -45deg; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); - border-radius: 9999px; - border-width: 1px; - border-color: currentColor; - opacity: 0.6; -} - -@keyframes modal-pop { - 0% { - opacity: 0; - } -} - -@keyframes progress-loading { - 50% { - background-position-x: -115%; - } -} - -@keyframes radiomark { - 0% { - box-shadow: 0 0 0 12px hsl(var(--b1)) inset, 0 0 0 12px hsl(var(--b1)) inset; - } - - 50% { - box-shadow: 0 0 0 3px hsl(var(--b1)) inset, 0 0 0 3px hsl(var(--b1)) inset; - } - - 100% { - box-shadow: 0 0 0 4px hsl(var(--b1)) inset, 0 0 0 4px hsl(var(--b1)) inset; - } -} - -@keyframes rating-pop { - 0% { - transform: translateY(-0.125em); - } - - 40% { - transform: translateY(-0.125em); - } - - 100% { - transform: translateY(0); - } -} - -:where(.stats) > :not([hidden]) ~ :not([hidden]) { - --tw-divide-x-reverse: 0; - border-right-width: calc(1px * var(--tw-divide-x-reverse)); - border-left-width: calc(1px * calc(1 - var(--tw-divide-x-reverse))); - --tw-divide-y-reverse: 0; - border-top-width: calc(0px * calc(1 - var(--tw-divide-y-reverse))); - border-bottom-width: calc(0px * var(--tw-divide-y-reverse)); -} - -.table :where(th, td) { - padding-left: 1rem; - padding-right: 1rem; - padding-top: 0.75rem; - padding-bottom: 0.75rem; - vertical-align: middle; -} - -.table tr.active, - .table tr.active:nth-child(even), - .table-zebra tbody tr:nth-child(even) { - --tw-bg-opacity: 1; - background-color: hsl(var(--b2) / var(--tw-bg-opacity)); -} - -.table-zebra tr.active, - .table-zebra tr.active:nth-child(even), - .table-zebra-zebra tbody tr:nth-child(even) { - --tw-bg-opacity: 1; - background-color: hsl(var(--b3) / var(--tw-bg-opacity)); -} - -.table :where(thead, tbody) :where(tr:not(:last-child)), - .table :where(thead, tbody) :where(tr:first-child:last-child) { - border-bottom-width: 1px; - --tw-border-opacity: 1; - border-bottom-color: hsl(var(--b2) / var(--tw-border-opacity)); -} - -.table :where(thead, tfoot) { - white-space: nowrap; - font-size: 0.75rem; - line-height: 1rem; - font-weight: 700; - color: hsl(var(--bc) / 0.6); -} - -.textarea-bordered { - --tw-border-opacity: 0.2; -} - -.textarea:focus { - outline-style: solid; - outline-width: 2px; - outline-offset: 2px; - outline-color: hsl(var(--bc) / 0.2); -} - -.textarea-primary { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); -} - -.textarea-primary:focus { - outline-color: hsl(var(--p) / 1); -} - -.textarea-disabled, - .textarea:disabled, - .textarea[disabled] { - cursor: not-allowed; - --tw-border-opacity: 1; - border-color: hsl(var(--b2) / var(--tw-border-opacity)); - --tw-bg-opacity: 1; - background-color: hsl(var(--b2) / var(--tw-bg-opacity)); - --tw-text-opacity: 0.2; -} - -.textarea-disabled::-moz-placeholder, .textarea:disabled::-moz-placeholder, .textarea[disabled]::-moz-placeholder { - color: hsl(var(--bc) / var(--tw-placeholder-opacity)); - --tw-placeholder-opacity: 0.2; -} - -.textarea-disabled::placeholder, - .textarea:disabled::placeholder, - .textarea[disabled]::placeholder { - color: hsl(var(--bc) / var(--tw-placeholder-opacity)); - --tw-placeholder-opacity: 0.2; -} - -.toast > * { - animation: toast-pop 0.25s ease-out; -} - -@keyframes toast-pop { - 0% { - transform: scale(0.9); - opacity: 0; - } - - 100% { - transform: scale(1); - opacity: 1; - } -} - -.rounded-box { - border-radius: var(--rounded-box, 1rem); -} - -.artboard-demo { - display: flex; - flex: none; - flex-direction: column; - align-items: center; - justify-content: center; -} - -.artboard.phone { - width: 320px; -} - -.artboard.phone-1 { - width: 320px; - height: 568px; -} - -.artboard.phone-1.horizontal, - .artboard.phone-1.artboard-horizontal { - width: 568px; - height: 320px; -} - -.artboard.phone-2 { - width: 375px; - height: 667px; -} - -.artboard.phone-2.horizontal, - .artboard.phone-2.artboard-horizontal { - width: 667px; - height: 375px; -} - -.artboard.phone-3 { - width: 414px; - height: 736px; -} - -.artboard.phone-3.horizontal, - .artboard.phone-3.artboard-horizontal { - width: 736px; - height: 414px; -} - -.artboard.phone-4 { - width: 375px; - height: 812px; -} - -.artboard.phone-4.horizontal, - .artboard.phone-4.artboard-horizontal { - width: 812px; - height: 375px; -} - -.artboard.phone-5 { - width: 414px; - height: 896px; -} - -.artboard.phone-5.horizontal, - .artboard.phone-5.artboard-horizontal { - width: 896px; - height: 414px; -} - -.artboard.phone-6 { - width: 320px; - height: 1024px; -} - -.artboard.phone-6.horizontal, - .artboard.phone-6.artboard-horizontal { - width: 1024px; - height: 320px; -} - -.badge-xs { - height: 0.75rem; - font-size: 0.75rem; - line-height: .75rem; - padding-left: 0.313rem; - padding-right: 0.313rem; -} - -.btm-nav-xs > *:where(.active) { - border-top-width: 1px; -} - -.btm-nav-sm > *:where(.active) { - border-top-width: 2px; -} - -.btm-nav-md > *:where(.active) { - border-top-width: 2px; -} - -.btm-nav-lg > *:where(.active) { - border-top-width: 4px; -} - -.btn-sm { - height: 2rem; - padding-left: 0.75rem; - padding-right: 0.75rem; - min-height: 2rem; - font-size: 0.875rem; -} - -.btn-wide { - width: 16rem; -} - -.btn-square:where(.btn-sm) { - height: 2rem; - width: 2rem; - padding: 0px; -} - -.btn-circle:where(.btn-xs) { - height: 1.5rem; - width: 1.5rem; - border-radius: 9999px; - padding: 0px; -} - -.btn-circle:where(.btn-sm) { - height: 2rem; - width: 2rem; - border-radius: 9999px; - padding: 0px; -} - -.btn-circle:where(.btn-md) { - height: 3rem; - width: 3rem; - border-radius: 9999px; - padding: 0px; -} - -.btn-circle:where(.btn-lg) { - height: 4rem; - width: 4rem; - border-radius: 9999px; - padding: 0px; -} - -.indicator :where(.indicator-item) { - right: 0px; - left: auto; - top: 0px; - bottom: auto; - --tw-translate-x: 50%; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-start) { - right: auto; - left: 0px; - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-center) { - right: 50%; - left: 50%; - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-end) { - right: 0px; - left: auto; - --tw-translate-x: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-bottom) { - top: auto; - bottom: 0px; - --tw-translate-y: 50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-middle) { - top: 50%; - bottom: 50%; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.indicator :where(.indicator-item.indicator-top) { - top: 0px; - bottom: auto; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -:where(.toast) { - right: 0px; - left: auto; - top: auto; - bottom: 0px; - --tw-translate-x: 0px; - --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.toast:where(.toast-start) { - right: auto; - left: 0px; - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.toast:where(.toast-center) { - right: 50%; - left: 50%; - --tw-translate-x: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.toast:where(.toast-end) { - right: 0px; - left: auto; - --tw-translate-x: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.toast:where(.toast-bottom) { - top: auto; - bottom: 0px; - --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.toast:where(.toast-middle) { - top: 50%; - bottom: auto; - --tw-translate-y: -50%; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.toast:where(.toast-top) { - top: 0px; - bottom: auto; - --tw-translate-y: 0px; - transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); -} - -.artboard-demo { - --tw-bg-opacity: 1; - background-color: hsl(var(--b3) / var(--tw-bg-opacity)); - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); - border-radius: var(--rounded-box, 1rem); - box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); -} - -.avatar.online:before { - content: ""; - position: absolute; - z-index: 10; - display: block; - border-radius: 9999px; - --tw-bg-opacity: 1; - background-color: hsl(var(--su) / var(--tw-bg-opacity)); - outline-style: solid; - outline-width: 2px; - outline-color: hsl(var(--b1) / 1); - width: 15%; - height: 15%; - top: 7%; - right: 7%; -} - -.avatar.offline:before { - content: ""; - position: absolute; - z-index: 10; - display: block; - border-radius: 9999px; - --tw-bg-opacity: 1; - background-color: hsl(var(--b3) / var(--tw-bg-opacity)); - outline-style: solid; - outline-width: 2px; - outline-color: hsl(var(--b1) / 1); - width: 15%; - height: 15%; - top: 7%; - right: 7%; -} - -.btn-group .btn:not(:first-child):not(:last-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.btn-group .btn:first-child:not(:last-child) { - margin-top: -0px; - margin-left: -1px; - border-top-left-radius: var(--rounded-btn, 0.5rem); - border-top-right-radius: 0; - border-bottom-left-radius: var(--rounded-btn, 0.5rem); - border-bottom-right-radius: 0; -} - -.btn-group .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: var(--rounded-btn, 0.5rem); - border-bottom-left-radius: 0; - border-bottom-right-radius: var(--rounded-btn, 0.5rem); -} - -.btn-group-horizontal .btn:not(:first-child):not(:last-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.btn-group-horizontal .btn:first-child:not(:last-child) { - margin-top: -0px; - margin-left: -1px; - border-top-left-radius: var(--rounded-btn, 0.5rem); - border-top-right-radius: 0; - border-bottom-left-radius: var(--rounded-btn, 0.5rem); - border-bottom-right-radius: 0; -} - -.btn-group-horizontal .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: var(--rounded-btn, 0.5rem); - border-bottom-left-radius: 0; - border-bottom-right-radius: var(--rounded-btn, 0.5rem); -} - -.btn-group-vertical .btn:first-child:not(:last-child) { - margin-top: -1px; - margin-left: -0px; - border-top-left-radius: var(--rounded-btn, 0.5rem); - border-top-right-radius: var(--rounded-btn, 0.5rem); - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -.btn-group-vertical .btn:last-child:not(:first-child) { - border-top-left-radius: 0; - border-top-right-radius: 0; - border-bottom-left-radius: var(--rounded-btn, 0.5rem); - border-bottom-right-radius: var(--rounded-btn, 0.5rem); -} - -.menu-sm :where(li:not(.menu-title) > *:not(ul):not(details):not(.menu-title)), - .menu-sm :where(li:not(.menu-title) > details > summary:not(.menu-title)) { - padding-left: 0.75rem; - padding-right: 0.75rem; - padding-top: 0.25rem; - padding-bottom: 0.25rem; - font-size: 0.875rem; - line-height: 1.25rem; - border-radius: var(--rounded-btn, 0.5rem); -} - -.menu-sm .menu-title { - padding-left: 0.75rem; - padding-right: 0.75rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border-width: 0; -} - -.visible { - visibility: visible; -} - -.absolute { - position: absolute; -} - -.relative { - position: relative; -} - -.left-1 { - left: 0.25rem; -} - -.top-1 { - top: 0.25rem; -} - -.isolate { - isolation: isolate; -} - -.z-\[1\] { - z-index: 1; -} - -.col-span-full { - grid-column: 1 / -1; -} - -.mx-auto { - margin-left: auto; - margin-right: auto; -} - -.my-4 { - margin-top: 1rem; - margin-bottom: 1rem; -} - -.my-6 { - margin-top: 1.5rem; - margin-bottom: 1.5rem; -} - -.-mt-32 { - margin-top: -8rem; -} - -.mb-2 { - margin-bottom: 0.5rem; -} - -.mb-4 { - margin-bottom: 1rem; -} - -.ml-2 { - margin-left: 0.5rem; -} - -.mt-1 { - margin-top: 0.25rem; -} - -.mt-10 { - margin-top: 2.5rem; -} - -.mt-2 { - margin-top: 0.5rem; -} - -.mt-3 { - margin-top: 0.75rem; -} - -.mt-6 { - margin-top: 1.5rem; -} - -.mt-auto { - margin-top: auto; -} - -.block { - display: block; -} - -.inline-block { - display: inline-block; -} - -.inline { - display: inline; -} - -.flex { - display: flex; -} - -.inline-flex { - display: inline-flex; -} - -.table { - display: table; -} - -.grid { - display: grid; -} - -.contents { - display: contents; -} - -.h-12 { - height: 3rem; -} - -.h-24 { - height: 6rem; -} - -.h-5 { - height: 1.25rem; -} - -.h-6 { - height: 1.5rem; -} - -.h-8 { - height: 2rem; -} - -.h-\[calc\(50vh-10em\)\] { - height: calc(50vh - 10em); -} - -.h-full { - height: 100%; -} - -.min-h-full { - min-height: 100%; -} - -.min-h-screen { - min-height: 100vh; -} - -.w-10 { - width: 2.5rem; -} - -.w-16 { - width: 4rem; -} - -.w-36 { - width: 9rem; -} - -.w-5 { - width: 1.25rem; -} - -.w-52 { - width: 13rem; -} - -.w-6 { - width: 1.5rem; -} - -.w-8 { - width: 2rem; -} - -.w-auto { - width: auto; -} - -.w-full { - width: 100%; -} - -.max-w-2xl { - max-width: 42rem; -} - -.max-w-7xl { - max-width: 80rem; -} - -.max-w-md { - max-width: 28rem; -} - -.max-w-sm { - max-width: 24rem; -} - -.max-w-xl { - max-width: 36rem; -} - -.max-w-xs { - max-width: 20rem; -} - -.shrink-0 { - flex-shrink: 0; -} - -.list-disc { - list-style-type: disc; -} - -.grid-flow-col { - grid-auto-flow: column; -} - -.grid-cols-1 { - grid-template-columns: repeat(1, minmax(0, 1fr)); -} - -.flex-col { - flex-direction: column; -} - -.items-center { - align-items: center; -} - -.justify-end { - justify-content: flex-end; -} - -.justify-center { - justify-content: center; -} - -.justify-between { - justify-content: space-between; -} - -.gap-4 { - gap: 1rem; -} - -.gap-x-6 { - -moz-column-gap: 1.5rem; - column-gap: 1.5rem; -} - -.gap-x-8 { - -moz-column-gap: 2rem; - column-gap: 2rem; -} - -.gap-y-16 { - row-gap: 4rem; -} - -.gap-y-8 { - row-gap: 2rem; -} - -.space-y-8 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(2rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(2rem * var(--tw-space-y-reverse)); -} - -.overflow-hidden { - overflow: hidden; -} - -.overflow-x-auto { - overflow-x: auto; -} - -.overflow-y-auto { - overflow-y: auto; -} - -.rounded-full { - border-radius: 9999px; -} - -.rounded-lg { - border-radius: 0.5rem; -} - -.rounded-md { - border-radius: 0.375rem; -} - -.border-b { - border-bottom-width: 1px; -} - -.border-t { - border-top-width: 1px; -} - -.border-base-300 { - --tw-border-opacity: 1; - border-color: hsl(var(--b3) / var(--tw-border-opacity)); -} - -.border-byte-teal { - --tw-border-opacity: 1; - border-color: rgb(66 177 168 / var(--tw-border-opacity)); -} - -.border-primary { - --tw-border-opacity: 1; - border-color: hsl(var(--p) / var(--tw-border-opacity)); -} - -.border-white\/10 { - border-color: rgb(255 255 255 / 0.1); -} - -.bg-base-100\/60 { - background-color: hsl(var(--b1) / 0.6); -} - -.bg-byte-white { - --tw-bg-opacity: 1; - background-color: rgb(235 235 233 / var(--tw-bg-opacity)); -} - -.bg-info { - --tw-bg-opacity: 1; - background-color: hsl(var(--in) / var(--tw-bg-opacity)); -} - -.bg-primary { - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); -} - -.bg-white { - --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity)); -} - -.bg-contain { - background-size: contain; -} - -.bg-no-repeat { - background-repeat: no-repeat; -} - -.fill-current { - fill: currentColor; -} - -.stroke-current { - stroke: currentColor; -} - -.p-10 { - padding: 2.5rem; -} - -.p-2 { - padding: 0.5rem; -} - -.p-4 { - padding: 1rem; -} - -.px-10 { - padding-left: 2.5rem; - padding-right: 2.5rem; -} - -.px-3 { - padding-left: 0.75rem; - padding-right: 0.75rem; -} - -.px-4 { - padding-left: 1rem; - padding-right: 1rem; -} - -.px-5 { - padding-left: 1.25rem; - padding-right: 1.25rem; -} - -.px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; -} - -.py-10 { - padding-top: 2.5rem; - padding-bottom: 2.5rem; -} - -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.py-24 { - padding-top: 6rem; - padding-bottom: 6rem; -} - -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; -} - -.py-5 { - padding-top: 1.25rem; - padding-bottom: 1.25rem; -} - -.py-6 { - padding-top: 1.5rem; - padding-bottom: 1.5rem; -} - -.pb-12 { - padding-bottom: 3rem; -} - -.pb-32 { - padding-bottom: 8rem; -} - -.pb-5 { - padding-bottom: 1.25rem; -} - -.pl-5 { - padding-left: 1.25rem; -} - -.pl-9 { - padding-left: 2.25rem; -} - -.pt-12 { - padding-top: 3rem; -} - -.text-center { - text-align: center; -} - -.align-middle { - vertical-align: middle; -} - -.font-mono { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} - -.text-2xl { - font-size: 1.5rem; - line-height: 2rem; -} - -.text-3xl { - font-size: 1.875rem; - line-height: 2.25rem; -} - -.text-5xl { - font-size: 3rem; - line-height: 1; -} - -.text-base { - font-size: 1rem; - line-height: 1.5rem; -} - -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} - -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - -.text-xl { - font-size: 1.25rem; - line-height: 1.75rem; -} - -.text-xs { - font-size: 0.75rem; - line-height: 1rem; -} - -.font-bold { - font-weight: 700; -} - -.font-semibold { - font-weight: 600; -} - -.normal-case { - text-transform: none; -} - -.leading-6 { - line-height: 1.5rem; -} - -.leading-7 { - line-height: 1.75rem; -} - -.leading-8 { - line-height: 2rem; -} - -.leading-none { - line-height: 1; -} - -.leading-normal { - line-height: 1.5; -} - -.tracking-tight { - letter-spacing: -0.025em; -} - -.text-base-content { - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} - -.text-black { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); -} - -.text-blue-800 { - --tw-text-opacity: 1; - color: rgb(30 64 175 / var(--tw-text-opacity)); -} - -.text-byte-blue { - --tw-text-opacity: 1; - color: rgb(123 206 188 / var(--tw-text-opacity)); -} - -.text-gray-400 { - --tw-text-opacity: 1; - color: rgb(156 163 175 / var(--tw-text-opacity)); -} - -.text-gray-600 { - --tw-text-opacity: 1; - color: rgb(75 85 99 / var(--tw-text-opacity)); -} - -.text-info-content { - --tw-text-opacity: 1; - color: hsl(var(--inc) / var(--tw-text-opacity)); -} - -.text-primary { - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} - -.text-primary-content { - --tw-text-opacity: 1; - color: hsl(var(--pc) / var(--tw-text-opacity)); -} - -.text-secondary { - --tw-text-opacity: 1; - color: hsl(var(--s) / var(--tw-text-opacity)); -} - -.text-success { - --tw-text-opacity: 1; - color: hsl(var(--su) / var(--tw-text-opacity)); -} - -.text-warning { - --tw-text-opacity: 1; - color: hsl(var(--wa) / var(--tw-text-opacity)); -} - -.text-white { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.no-underline { - text-decoration-line: none; -} - -.decoration-blue-800 { - text-decoration-color: #1e40af; -} - -.decoration-byte-teal { - text-decoration-color: #42b1a8; -} - -.antialiased { - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.opacity-50 { - opacity: 0.5; -} - -.shadow { - --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); - --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-2xl { - --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25); - --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.shadow-sm { - --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - -.transition-colors { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -.duration-300 { - transition-duration: 300ms; -} - -.hover\:bg-\[\#5865F2\]\/70:hover { - background-color: rgb(88 101 242 / 0.7); -} - -.hover\:bg-error:hover { - --tw-bg-opacity: 1; - background-color: hsl(var(--er) / var(--tw-bg-opacity)); -} - -.hover\:bg-success:hover { - --tw-bg-opacity: 1; - background-color: hsl(var(--su) / var(--tw-bg-opacity)); -} - -.hover\:bg-warning:hover { - --tw-bg-opacity: 1; - background-color: hsl(var(--wa) / var(--tw-bg-opacity)); -} - -.hover\:text-\[\#0A66C2\]:hover { - --tw-text-opacity: 1; - color: rgb(10 102 194 / var(--tw-text-opacity)); -} - -.hover\:text-\[\#5865F2\]:hover { - --tw-text-opacity: 1; - color: rgb(88 101 242 / var(--tw-text-opacity)); -} - -.hover\:text-\[\#FF4500\]:hover { - --tw-text-opacity: 1; - color: rgb(255 69 0 / var(--tw-text-opacity)); -} - -.hover\:text-black:hover { - --tw-text-opacity: 1; - color: rgb(0 0 0 / var(--tw-text-opacity)); -} - -.hover\:text-blue-400:hover { - --tw-text-opacity: 1; - color: rgb(96 165 250 / var(--tw-text-opacity)); -} - -.hover\:text-neutral-900:hover { - --tw-text-opacity: 1; - color: rgb(23 23 23 / var(--tw-text-opacity)); -} - -.hover\:text-primary:hover { - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} - -.hover\:text-red-600:hover { - --tw-text-opacity: 1; - color: rgb(220 38 38 / var(--tw-text-opacity)); -} - -.hover\:underline:hover { - text-decoration-line: underline; -} - -.focus\:ring-byte-teal:focus { - --tw-ring-opacity: 1; - --tw-ring-color: rgb(66 177 168 / var(--tw-ring-opacity)); -} - -.focus-visible\:outline:focus-visible { - outline-style: solid; -} - -.focus-visible\:outline-2:focus-visible { - outline-width: 2px; -} - -.focus-visible\:outline-offset-2:focus-visible { - outline-offset: 2px; -} - -.focus-visible\:outline-primary:focus-visible { - outline-color: hsl(var(--p) / 1); -} - -.dark\:bg-byte-dark:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(12 12 12 / var(--tw-bg-opacity)); -} - -.dark\:bg-byte-dark\/95:is(.dark *) { - background-color: rgb(12 12 12 / 0.95); -} - -.dark\:bg-neutral:is(.dark *) { - --tw-bg-opacity: 1; - background-color: hsl(var(--n) / var(--tw-bg-opacity)); -} - -.dark\:bg-neutral\/60:is(.dark *) { - background-color: hsl(var(--n) / 0.6); -} - -.dark\:text-base-100:is(.dark *) { - --tw-text-opacity: 1; - color: hsl(var(--b1) / var(--tw-text-opacity)); -} - -.dark\:text-base-100\/80:is(.dark *) { - color: hsl(var(--b1) / 0.8); -} - -.dark\:text-base-content:is(.dark *) { - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} - -.dark\:text-blue-800:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(30 64 175 / var(--tw-text-opacity)); -} - -.dark\:text-byte-blue:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(123 206 188 / var(--tw-text-opacity)); -} - -.dark\:text-byte-teal:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(66 177 168 / var(--tw-text-opacity)); -} - -.dark\:text-primary:is(.dark *) { - --tw-text-opacity: 1; - color: hsl(var(--p) / var(--tw-text-opacity)); -} - -.dark\:text-white:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(255 255 255 / var(--tw-text-opacity)); -} - -.dark\:hover\:bg-primary:hover:is(.dark *) { - --tw-bg-opacity: 1; - background-color: hsl(var(--p) / var(--tw-bg-opacity)); -} - -.dark\:hover\:text-base-content:hover:is(.dark *) { - --tw-text-opacity: 1; - color: hsl(var(--bc) / var(--tw-text-opacity)); -} - -.hover\:dark\:text-blue-800:is(.dark *):hover { - --tw-text-opacity: 1; - color: rgb(30 64 175 / var(--tw-text-opacity)); -} - -.hover\:dark\:text-byte-blue:is(.dark *):hover { - --tw-text-opacity: 1; - color: rgb(123 206 188 / var(--tw-text-opacity)); -} - -@media (min-width: 640px) { - .sm\:col-span-4 { - grid-column: span 4 / span 4; - } - - .sm\:grid-cols-6 { - grid-template-columns: repeat(6, minmax(0, 1fr)); - } - - .sm\:gap-y-20 { - row-gap: 5rem; - } - - .sm\:px-6 { - padding-left: 1.5rem; - padding-right: 1.5rem; - } - - .sm\:py-24 { - padding-top: 6rem; - padding-bottom: 6rem; - } - - .sm\:text-4xl { - font-size: 2.25rem; - line-height: 2.5rem; - } -} - -@media (min-width: 768px) { - .md\:place-self-center { - place-self: center; - } - - .md\:justify-self-end { - justify-self: end; - } -} - -@media (min-width: 1024px) { - .lg\:mx-0 { - margin-left: 0px; - margin-right: 0px; - } - - .lg\:max-w-lg { - max-width: 32rem; - } - - .lg\:max-w-none { - max-width: none; - } - - .lg\:grid-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .lg\:flex-row { - flex-direction: row; - } - - .lg\:px-8 { - padding-left: 2rem; - padding-right: 2rem; - } - - .lg\:pr-8 { - padding-right: 2rem; - } - - .lg\:pt-4 { - padding-top: 1rem; - } -} diff --git a/byte_bot/server/domain/web/resources/tailwind/_base.css b/byte_bot/server/domain/web/resources/tailwind/_base.css deleted file mode 100644 index 2f02db53..00000000 --- a/byte_bot/server/domain/web/resources/tailwind/_base.css +++ /dev/null @@ -1 +0,0 @@ -@tailwind base; diff --git a/byte_bot/server/domain/web/resources/tailwind/_components.css b/byte_bot/server/domain/web/resources/tailwind/_components.css deleted file mode 100644 index 020aabaf..00000000 --- a/byte_bot/server/domain/web/resources/tailwind/_components.css +++ /dev/null @@ -1 +0,0 @@ -@tailwind components; diff --git a/byte_bot/server/domain/web/resources/tailwind/_utilities.css b/byte_bot/server/domain/web/resources/tailwind/_utilities.css deleted file mode 100644 index 65dd5f63..00000000 --- a/byte_bot/server/domain/web/resources/tailwind/_utilities.css +++ /dev/null @@ -1 +0,0 @@ -@tailwind utilities; diff --git a/byte_bot/server/domain/web/templates/about.html b/byte_bot/server/domain/web/templates/about.html deleted file mode 100644 index 524259a9..00000000 --- a/byte_bot/server/domain/web/templates/about.html +++ /dev/null @@ -1,35 +0,0 @@ - -{% extends 'base/base.html' %} -{% block extrahead %}{% endblock extrahead %} -{% block extrastyle %}{% endblock extrastyle %} -{% block title %}Home{% endblock %} -{% block body %} - -
-
- Byte Bot Logo -
-

About Byte

-

- Byte is a project started to help Discord servers ran by developers, open source projects, and other like-minded - communities. It is a bot that is designed to help you manage your server, and make it easier for you to do so. -

- -
-
-
-{% endblock body %} {% block extrajs %}{% endblock extrajs %} diff --git a/byte_bot/server/domain/web/templates/base/base.html b/byte_bot/server/domain/web/templates/base/base.html deleted file mode 100644 index f5d2e7ba..00000000 --- a/byte_bot/server/domain/web/templates/base/base.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - {% block title %}{% endblock %} {{ title }} - - - - {% block extrahead %}{% endblock %} {% block extrastyle %}{% endblock %} - - - -
- {% block nav %} {% include 'base/nav.html' %} {% endblock nav %} {% block headercontent %}{% endblock - headercontent %} - -
-
- {% block basecontent %} -
{% block body %} {% endblock body %}
-
-
- Service started successfully! -
-
- - Byte had an accident: - - Check the logs - - -
-
- - Byte was invited to - Python Discord - ! - -
-
- {% endblock basecontent %} -
-
- - {% block content %}{% endblock content %} - -
{% block footer %} {% include 'base/footer.html' %} {% endblock footer %}
-
- - - - - {% block extrajs %} {% endblock extrajs %} - diff --git a/byte_bot/server/domain/web/templates/base/footer.html b/byte_bot/server/domain/web/templates/base/footer.html deleted file mode 100644 index eb64bb5c..00000000 --- a/byte_bot/server/domain/web/templates/base/footer.html +++ /dev/null @@ -1,159 +0,0 @@ -{% set active_page = active_page|default('home') -%} - - - - diff --git a/byte_bot/server/domain/web/templates/base/nav.html b/byte_bot/server/domain/web/templates/base/nav.html deleted file mode 100644 index 863d929b..00000000 --- a/byte_bot/server/domain/web/templates/base/nav.html +++ /dev/null @@ -1,159 +0,0 @@ -
- -
diff --git a/byte_bot/server/domain/web/templates/contact.html b/byte_bot/server/domain/web/templates/contact.html deleted file mode 100644 index 37dd46b6..00000000 --- a/byte_bot/server/domain/web/templates/contact.html +++ /dev/null @@ -1,62 +0,0 @@ - -{% extends 'base/base.html' %} -{% block extrahead %}{% endblock extrahead %} -{% block extrastyle %} {% endblock extrastyle %} -{% block title %}Home{% endblock %} -{% block body %} - -
-
-
-

Contact

-

What would you like to say?

- -
-
- -
- -
-
-
-
-
- -
- -
-

-
-
-
- -
- - -
-
- -{% endblock body %} {% block extrajs %}{% endblock extrajs %} diff --git a/byte_bot/server/domain/web/templates/cookies.html b/byte_bot/server/domain/web/templates/cookies.html deleted file mode 100644 index f2cc0220..00000000 --- a/byte_bot/server/domain/web/templates/cookies.html +++ /dev/null @@ -1,185 +0,0 @@ - -{% extends 'base/base.html' %} -{% block extrahead %}{% endblock extrahead %} -{% block extrastyle %}{% endblock extrastyle %} -{% block title %}Cookies{% endblock %} -{% block body %} - -
-

Cookies Policy

-

Last updated: July 30, 2023

-

- This Cookies Policy explains what Cookies are and how We use them. You should read this policy, so You can - understand what type of cookies We use, or the information We collect using Cookies and how that information is - used. -

-

Interpretation and Definitions

-

Interpretation

-

- The words of which the initial letter is capitalized have meanings defined under the following conditions. The - following definitions shall have the same meaning regardless of whether they appear in singular or in plural. -

-

Definitions

-

For the purposes of this Cookies Policy:

-
    -
  • - Service - (referred to as either "Byte", " Bot, "We", "Us" or "Our" in this - Cookies Policy) refers to Byte Bot. -
  • -
  • - Cookies - means small files that are placed on Your computer, mobile device or any other device by a website, containing - details of your browsing history on that website among its many uses. -
  • -
  • - Website - refers to Byte Bot, accessible from - - https://byte-bot.app - -
  • -
  • - You - means the individual accessing or using the Website, or a company, or any legal entity on behalf of which such - individual is accessing or using the Website, as applicable. -
  • -
-

The use of the Cookies

-

Type of Cookies We Use

-

- Cookies can be "Persistent" or "Session" Cookies. Persistent Cookies remain on your personal - computer or mobile device when You go offline, while Session Cookies are deleted as soon as You close your web - browser. -

-

We use both session and persistent Cookies for the purposes set out below:

-
    -
  • -

    Necessary / Essential Cookies

    -

    - Type: Session Cookies -

    -

    - Administered by: Us -

    -

    - Purpose: These Cookies are essential to provide You with services available through the Website and to enable - You to use some of its features. They help to authenticate users and prevent fraudulent use of user accounts. - Without these Cookies, the services that You have asked for cannot be provided, and We only use these Cookies to - provide You with those services. -

    -
  • -
  • -

    Functionality Cookies

    -

    - Type: Persistent Cookies -

    -

    - Administered by: Us -

    -

    - Purpose: These Cookies allow us to remember choices You make when You use the Website, such as remembering your - login details or language preference. The purpose of these Cookies is to provide You with a more personal - experience and to avoid You having to re-enter your preferences every time You use the Website. -

    -
  • -
-

Your Choices Regarding Cookies

-

- If You prefer to avoid the use of Cookies on the Website, first You must disable the use of Cookies in your browser - and then delete the Cookies saved in your browser associated with this website. You may use this option for - preventing the use of Cookies at any time. -

-

- If You do not accept Our Cookies, You may experience some inconvenience in your use of the Website and some features - may not function properly. -

-

- If You'd like to delete Cookies or instruct your web browser to delete or refuse Cookies, please visit the help - pages of your web browser. -

- -

For any other web browser, please visit your web browser's official web pages.

-

The Cookie Monster shows up

-

- In the event that the - - Cookie Monster - - shows up, please contact us at - - monster@byte-bot.app - - and we will take care of him. -

-

Contact Us

-

If you have any questions about this Cookies Policy, You can contact us:

- -
-{% endblock body %} {% block extrajs %}{% endblock extrajs %} diff --git a/byte_bot/server/domain/web/templates/dashboard.html b/byte_bot/server/domain/web/templates/dashboard.html deleted file mode 100644 index 42414214..00000000 --- a/byte_bot/server/domain/web/templates/dashboard.html +++ /dev/null @@ -1,490 +0,0 @@ - -{% extends 'base/base.html' %} -{% block extrahead %}{% endblock extrahead %} -{% block nav %} - {% set navbar_color = 'bg-primary dark:text-base-content' %} - {% include 'base/nav.html' %} -{% endblock nav %} -{% block title %}Dashboard{% endblock %} -{% block headercontent %} - -
-
-
-

Dashboard

-
-
-
-{% endblock headercontent %} {% block content %} -
-
-
-
-
- - - - - - - -
-
Server Count
-
1
-
100% more than before Byte was born
-
- -
-
- - - - -
-
Messages Sent
-
42,069
-
11% more than last month
-
- -
-
-
-
- Byte Logo -
-
-
-
Uptime
-
99.99%
-
2 issues in the last 24 hours
-
-
-
-

Activity

-
-
-
-
- Byte Logo -
-
-
- Service started 26 hours ago! -
- -
-
-
-
- Byte Logo -
-
-
- Connected to 1 servers -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- Connected to 1 servers -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- Connected to 1 servers -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- Connected to 1 servers -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- Connected to 1 servers -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- Connected to 1 servers -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
- -
-
-
- Byte Logo -
-
-
- User invited Byte to - Furry Dev Convention - ! -
- -
-
-
-
-
-{% endblock content %} {% block extrajs %}{% endblock extrajs %} diff --git a/byte_bot/server/domain/web/templates/index.html b/byte_bot/server/domain/web/templates/index.html deleted file mode 100644 index 59d329d8..00000000 --- a/byte_bot/server/domain/web/templates/index.html +++ /dev/null @@ -1,142 +0,0 @@ -{% extends 'base/base.html' %} {% block extrahead %}{% endblock extrahead %} {% block extrastyle %} - -{% endblock extrastyle %} {% block title %}Home{% endblock %} {% block body %} - -
- - - - New Byte Bot version available! -
- - Changelog -
-
- -
-
-
-
-
-

Discord x Litestar

-

- Developer Experience First -

-

- Byte is built on the Litestar framework, ready to serve developer-oriented Discord servers with great tools - to make their lives easier. -

-
-
-
- - - - - - Ready for invite -
-
- Invite Byte to your server and start using it right away. - - - - - -
-
-
-
litestar run byte-bot
-
starting service...
-
Byte is ready for action!
-
-
-
-
-
-
i c u
-
-
-
-
- Byte Logo -

- Byte: - - {{ overall_status | default('Unknown') | upper }} - -

-

- Currently in -
- {{ server_count }} server{{ 's' if server_count != 1 else ''}} -

-
- - - - - -

Invite Byte

-
- Powered by Litestar -
-
-
-
-
-
-
-{% endblock body %} {% block extrajs %}{% endblock extrajs %} diff --git a/byte_bot/server/domain/web/templates/privacy.html b/byte_bot/server/domain/web/templates/privacy.html deleted file mode 100644 index a777dfda..00000000 --- a/byte_bot/server/domain/web/templates/privacy.html +++ /dev/null @@ -1,1560 +0,0 @@ -{# NOTE: This is pretty silly, just playing around, but this was generated with https://termly.io #} - -{% extends 'base/base.html' %} -{% block extrahead %}{% endblock extrahead %} -{% block extrastyle %}{% endblock extrastyle %} -{% block title %}Home{% endblock %} -{% block body %} - -
-
-

Cookies Policy

-

Last updated: July 30, 2023

-
-
-
-
- This privacy notice for Byte (" - Bot - ," " - Byte - ," " - we - ," " - us - ," or " - our - " ), describes how and why we might collect, store, use, and/or share ( " - process - " ) your information when you use our services ( " - Services - " ), such as when you: -
- -
-
-
    -
  • - Engage with us in other related ways, including any sales, marketing, or events -
  • -
-
- Questions or concerns? - Reading this privacy notice will help you understand your privacy rights and choices. If you do not agree with - our policies and practices, please do not use our Services. If you still have any questions or concerns, please - contact us at - - privacy@byte-bot.app - - . -
-
-
-
SUMMARY OF KEY POINTS
-
-
- - - This summary provides key points from our privacy notice, but you can find out more details about any of - these topics by clicking the link following each key point or by using our - - - - table of contents - - below to find the section you are looking for. -
-
-
- What personal information do we process? - When you visit, use, or navigate our Services, we may process personal information depending on how you interact - with Byte and the Services, the choices you make, and the products and features you use. Learn more about - - personal information you disclose to us - - . -
-
-
- Do we process any sensitive personal information? - We do not process sensitive personal information. -
-
-
- Do we receive any information from third parties? - We do not receive any information from third parties. -
-
-
- How do we process your information? - We process your information to provide, improve, and administer our Services, communicate with you, for security - and fraud prevention, and to comply with law. We may also process your information for other purposes with your - consent. We process your information only when we have a valid legal reason to do so. Learn more about - - how we process your information - - . -
-
-
- In what situations and with which parties do we share personal information? - We may share information in specific situations and with specific third parties. Learn more about - - when and with whom we share your personal information - - . -
-
-
- How do we keep your information safe? - We have organizational and technical processes and procedures in place to protect your personal information. - However, no electronic transmission over the internet or information storage technology can be guaranteed to be - 100% secure, so we cannot promise or guarantee that hackers, cybercriminals, or other unauthorized third parties - will not be able to defeat our security and improperly collect, access, steal, or modify your information. Learn - more about - - how we keep your information safe - - . -
-
-
- What are your rights? - Depending on where you are located geographically, the applicable privacy law may mean you have certain rights - regarding your personal information. Learn more about - - your privacy rights - - . -
-
-
- How do you exercise your rights? - The easiest way to exercise your rights is by visiting - - https://byte-bot.app/request-data - - , or by contacting us. We will consider and act upon any request in accordance with applicable data protection - laws. -
-
-
- Want to learn more about what Byte does with any information we collect? - - Review the privacy notice in full - - . -
-
-
-
- TABLE OF CONTENTS -
-
- -
-
-
- 1. WHAT INFORMATION DO WE COLLECT? -
-
-
- Personal information you disclose to us -
-
-
-
- In Short: - We collect personal information that you provide to us. -
-
-
-
- We collect personal information that you voluntarily provide to us when you express an interest in obtaining - information about us or our products and Services, when you participate in activities on the Services, or - otherwise when you contact us. -
-
-
-
- Personal Information Provided by You. - The personal information that we collect depends on the context of your interactions with us and the Services, - the choices you make, and the products and features you use. The personal information we collect may include the - following: -
- -
-
- Sensitive Information. - We do not process sensitive information. -
-
-
-
- Payment Data. - We may collect data necessary to process your payment if you make purchases, such as your payment instrument - number, and the security code associated with your payment instrument. All payment data is stored by AMEX, - Mastercard and Visa. You may find their privacy notice link(s) here: - - https://www.americanexpress.com/us/company/privacy-center/online-privacy-disclosures/ - - , - - https://www.mastercard.us/en-us/vision/corp-responsibility/commitment-to-privacy/privacy.html - - and - - https://usa.visa.com/legal/privacy-policy.html - - . -
-
-
-
- Social Media Login Data. - We may provide you with the option to register with us using your existing social media account details, like - your Facebook, Twitter, or other social media account. If you choose to register in this way, we will collect - the information described in the section called " - - HOW DO WE HANDLE YOUR SOCIAL LOGINS? - - " below. -
-
-
-
- All personal information that you provide to us must be true, complete, and accurate, and you must notify us of - any changes to such personal information. -
-
-
-
- 2. HOW DO WE PROCESS YOUR INFORMATION? -
-
-
-
- In Short: - - We process your information to provide, improve, and administer our Services, communicate with you, for - security and fraud prevention, and to comply with law. We may also process your information for other - purposes with your consent. - -
-
-
-
- - We process your personal information for a variety of reasons, depending on how you interact with our - Services, including: - -
- -
-
-
- 3. WHAT LEGAL BASES DO WE RELY ON TO PROCESS YOUR INFORMATION? -
-
-
- - In Short: - We only process your personal information when we believe it is necessary and we have a valid legal reason - (i.e. , legal basis) to do so under applicable law, like with your consent, to comply with laws, to provide - you with services to enter into or fulfill our contractual obligations, to protect your rights, or to fulfill - our legitimate business interests. - -
-
-
- - If you are located in the EU or UK, this section applies to you. - -
-
-
- The General Data Protection Regulation (GDPR) and UK GDPR require us to explain the valid legal bases we rely on - in order to process your personal information. As such, we may rely on the following legal bases to process your - personal information: -
- -
-
- -
- -
-
-
- - If you are located in Canada, this section applies to you. - -
-
-
- We may process your information if you have given us specific permission (i.e. , express consent) to use your - personal information for a specific purpose, or in situations where your permission can be inferred (i.e. , - implied consent). You can - withdraw your consent - at any time. -
-
-
- In some exceptional cases, we may be legally permitted under applicable law to process your information - without your consent, including, for example: -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
- -
-
-
- 4. WHEN AND WITH WHOM DO WE SHARE YOUR PERSONAL INFORMATION? -
-
-
- In Short: - - We may share information in specific situations described in this section and/or with the following third - parties. - -
-
-
-
We may need to share your personal information in the following situations:
- -
-
-
-
-
-
-
-
- 5. DO WE USE COOKIES AND OTHER TRACKING TECHNOLOGIES? -
-
-
- In Short: - We may use cookies and other tracking technologies to collect and store your information. -
-
-
- We may use cookies and similar tracking technologies (like web beacons and pixels) to access or - store information. Specific information about how we use such technologies and how you can refuse - certain cookies is set out in our Cookie Notice : - - https://byte-bot/cookies - - . -
-
-
- 6. HOW DO WE HANDLE YOUR SOCIAL LOGINS? -
-
-
- In Short: - - If you choose to register or log in to our Services using a social media account, we may have - access to certain information about you. - -
-
-
- Our Services offer you the ability to register and log in using your third-party social media - account details (like your Facebook or Twitter logins). Where you choose to do this, we will receive - certain profile information about you from your social media provider. The profile information we - receive may vary depending on the social media provider concerned, but will often include your name, - email address, friends list, and profile picture, as well as other information you choose to make - public on such a social media platform. -
-
-
- We will use the information we receive only for the purposes that are described in this privacy - notice or that are otherwise made clear to you on the relevant Services. Please note that we do not - control, and are not responsible for, other uses of your personal information by your third-party - social media provider. We recommend that you review their privacy notice to understand how they - collect, use, and share your personal information, and how you can set your privacy preferences on - their sites and apps. -
-
-
- 7. HOW LONG DO WE KEEP YOUR INFORMATION? -
-
-
- In Short: - - We keep your information for as long as necessary to fulfill the purposes outlined in this privacy - notice unless otherwise required by law. - -
-
-
- We will only keep your personal information for as long as it is necessary for the purposes set out - in this privacy notice, unless a longer retention period is required or permitted by law (such as - tax, accounting, or other legal requirements). No purpose in this notice will require us keeping - your personal information for longer than 90 days . -
-
-
- When we have no ongoing legitimate business need to process your personal information, we will - either delete or anonymize such information, or, if this is not possible (for example, because your - personal information has been stored in backup archives), then we will securely store your personal - information and isolate it from any further processing until deletion is possible. -
-
-
- 8. HOW DO WE KEEP YOUR INFORMATION SAFE? -
-
-
- In Short: - - We aim to protect your personal information through a system of organizational and technical - security measures. - -
-
-
- We have implemented appropriate and reasonable technical and organizational security measures - designed to protect the security of any personal information we process. However, despite our - safeguards and efforts to secure your information, no electronic transmission over the Internet or - information storage technology can be guaranteed to be 100% secure, so we cannot promise or - guarantee that hackers, cybercriminals, or other unauthorized third parties will not be able to - defeat our security and improperly collect, access, steal, or modify your information. Although we - will do our best to protect your personal information, transmission of personal information to and - from our Services is at your own risk. You should only access the Services within a secure - environment. -
-
-
- 9. WHAT ARE YOUR PRIVACY RIGHTS? -
-
-
- In Short: - - In some regions, such as the European Economic Area (EEA), United Kingdom (UK), and Canada , you - have rights that allow you greater access to and control over your personal information. You may - review, change, or terminate your account at any time. - -
-
-
- In some regions (like the EEA, UK, and Canada), you have certain rights under applicable data - protection laws. These may include the right (i) to request access and obtain a copy of your - personal information, (ii) to request rectification or erasure; (iii) to restrict the processing of - your personal information; and (iv) if applicable, to data portability. In certain circumstances, - you may also have the right to object to the processing of your personal information. You can make - such a request by contacting us by using the contact details provided in the section " - HOW CAN YOU CONTACT US ABOUT THIS NOTICE? - " below. -
-
-
- We will consider and act upon any request in accordance with applicable data protection laws. -
-
-
- If you are located in the EEA or UK and you believe we are unlawfully processing your personal - information, you also have the right to complain to your - - Member State data protection authority - - or - - UK data protection authority - - . -
-
-
- If you are located in Switzerland, you may contact the - - Federal Data Protection and Information Commissioner - - . -
-
-
- Withdrawing your consent: - If we are relying on your consent to process your personal information, which may be express and/or - implied consent depending on the applicable law, you have the right to withdraw your consent at any - time. You can withdraw your consent at any time by contacting us by using the contact details - provided in the section " - - HOW CAN YOU CONTACT US ABOUT THIS NOTICE? - - " below . -
-
-
- However, please note that this will not affect the lawfulness of the processing before its - withdrawal nor, when applicable law allows, will it affect the processing of your personal - information conducted in reliance on lawful processing grounds other than consent. -
-
-
- Cookies and similar technologies: - Most Web browsers are set to accept cookies by default. If you prefer, you can usually choose to set - your browser to remove cookies and to reject cookies. If you choose to remove cookies or reject - cookies, this could affect certain features or services of our Services. You may also - - opt out of interest-based advertising by advertisers - - on our Services. For further information, please see our Cookie Notice: - - https://byte-bot/cookies - - . -
-
-
- If you have questions or comments about your privacy rights, you may email us at - - privacy@byte-bot.app - - . -
-
-
- 10. CONTROLS FOR DO-NOT-TRACK FEATURES -
-
-
- Most web browsers and some mobile operating systems and mobile applications include a Do-Not-Track ( - "DNT" ) feature or setting you can activate to signal your privacy preference not to have data about - your online browsing activities monitored and collected. At this stage no uniform technology - standard for recognizing and implementing DNT signals has been finalized . As such, we do not - currently respond to DNT browser signals or any other mechanism that automatically communicates your - choice not to be tracked online. If a standard for online tracking is adopted that we must follow in - the future, we will inform you about that practice in a revised version of this privacy notice. -
-
-
- 11. DO CALIFORNIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? -
-
-
- In Short: - - Yes, if you are a resident of California, you are granted specific rights regarding access to your - personal information. - -
-
-
- California Civil Code Section 1798.83, also known as the "Shine The Light" law, permits our users - who are California residents to request and obtain from us, once a year and free of charge, - information about categories of personal information (if any) we disclosed to third parties for - direct marketing purposes and the names and addresses of all third parties with which we shared - personal information in the immediately preceding calendar year. If you are a California resident - and would like to make such a request, please submit your request in writing to us using the contact - information provided below. -
-
-
- If you are under 18 years of age, reside in California, and have a registered account with Services, - you have the right to request removal of unwanted data that you publicly post on the Services. To - request removal of such data, please contact us using the contact information provided below and - include the email address associated with your account and a statement that you reside in - California. We will make sure the data is not publicly displayed on the Services, but please be - aware that the data may not be completely or comprehensively removed from all our systems (e.g. , - backups, etc.). -
-
-
- CCPA Privacy Notice -
-
-
-
The California Code of Regulations defines a "resident" as:
-
-
-
- (1) every individual who is in the State of California for other than a temporary or transitory - purpose and -
-
- (2) every individual who is domiciled in the State of California who is outside the State of - California for a temporary or transitory purpose -
-
-
All other individuals are defined as "non-residents."
-
-
- If this definition of "resident" applies to you, we must adhere to certain rights and obligations - regarding your personal information. -
-
-
- What categories of personal information do we collect? -
-
-
- We have collected the following categories of personal information in the past twelve (12) months: -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
CategoryExamplesCollected
-
A. Identifiers
-
-
- Contact details, such as real name, alias, postal address, telephone or mobile contact - number, unique personal identifier, online identifier, Internet Protocol address, email - address, and account name -
-
-
-
NO
-
-
-
- B. Personal information categories listed in the California Customer Records statute -
-
-
- Name, contact information, education, employment, employment history, and financial - information -
-
-
-
NO
-
-
-
- C. Protected classification characteristics under California or federal law -
-
-
Gender and date of birth
-
-
-
NO
-
-
-
D. Commercial information
-
-
- Transaction information, purchase history, financial details, and payment information -
-
-
-
NO
-
-
-
E. Biometric information
-
-
Fingerprints and voiceprints
-
-
-
NO
-
-
-
F. Internet or other similar network activity
-
-
- Browsing history, search history, online behavior , interest data, and interactions with - our and other websites, applications, systems, and advertisements -
-
-
-
NO
-
-
-
G. Geolocation data
-
-
Device location
-
-
-
NO
-
-
-
- H. Audio, electronic, visual, thermal, olfactory, or similar information -
-
-
- Images and audio, video or call recordings created in connection with our business - activities -
-
-
-
NO
-
-
-
I. Professional or employment-related information
-
-
- Business contact details in order to provide you our Services at a business level or job - title, work history, and professional qualifications if you apply for a job with us -
-
-
-
NO
-
-
-
J. Education Information
-
-
Student records and directory information
-
-
-
NO
-
-
-
K. Inferences drawn from other personal information
-
-
- Inferences drawn from any of the collected personal information listed above to create a - profile or summary about, for example, an individual’s preferences and characteristics -
-
-
-
NO
-
-
L. Sensitive Personal InformationΒ  -
- NO -
-
-
-
-
-
-
-
- We may also collect other personal information outside of these categories through instances - where you interact with us in person, online, or by phone or mail in the context of: -
- -
- -
- -
- How do we use and share your personal information? -
-
-
- More information about our data collection and sharing practices can be found in this privacy - notice and our Cookie Notice: - - https://byte-bot/cookies - - . -
-
-
- You may contact us by email at - - privacy@byte-bot.app - - , or by referring to the contact details at the bottom of this document. -
-
-
- If you are using an authorized agent to exercise your right to opt out we may deny a request if - the authorized agent does not submit proof that they have been validly authorized to act on your - behalf. -
-
-
- Will your information be shared with anyone else? -
-
-
- We may disclose your personal information with our service providers pursuant to a written - contract between us and each service provider. Each service provider is a for-profit entity that - processes the information on our behalf, following the same strict privacy protection - obligations mandated by the CCPA. -
-
-
- We may use your personal information for our own business purposes, such as for undertaking - internal research for technological development and demonstration. This is not considered to be - "selling" of your personal information. -
-
-
- Byte has not disclosed, sold, or shared any personal information to third parties for a business - or commercial purpose in the preceding twelve (12) months. Byte will not sell or share personal - information in the future belonging to website visitors, users, and other consumers. -
-
-
- Your rights with respect to your personal data -
-
-
- Right to request deletion of the data β€” Request to delete -
-
-
- You can ask for the deletion of your personal information. If you ask us to delete your personal - information, we will respect your request and delete your personal information, subject to - certain exceptions provided by law, such as (but not limited to) the exercise by another - consumer of his or her right to free speech, our compliance requirements resulting from a legal - obligation, or any processing that may be required to protect against illegal activities. -
-
-
- Right to be informed β€” Request to know -
-
-
Depending on the circumstances, you have a right to know:
- -
- -
- -
- -
- -
- -
- -
- -
- In accordance with applicable law, we are not obligated to provide or delete consumer - information that is de-identified in response to a consumer request or to re-identify individual - data to verify a consumer request. -
-
-
- Right to Non-Discrimination for the Exercise of a Consumer’s Privacy Rights -
-
-
- We will not discriminate against you if you exercise your privacy rights. -
-
-
- Right to Limit Use and Disclosure of Sensitive Personal Information -
-
-
-
We do not process consumer's sensitive personal information.
-
-
-
- Verification process -
-
-
- Upon receiving your request, we will need to verify your identity to determine you are the same - person about whom we have the information in our system. These verification efforts require us - to ask you to provide information so that we can match it with information you have previously - provided us. For instance, depending on the type of request you submit, we may ask you to - provide certain information so that we can match the information you provide with the - information we already have on file, or we may contact you through a communication method (e.g. - , phone or email) that you have previously provided to us. We may also use other verification - methods as the circumstances dictate. -
-
-
- We will only use personal information provided in your request to verify your identity or - authority to make the request. To the extent possible, we will avoid requesting additional - information from you for the purposes of verification. However, if we cannot verify your - identity from the information already maintained by us, we may request that you provide - additional information for the purposes of verifying your identity and for security or - fraud-prevention purposes. We will delete such additionally provided information as soon as we - finish verifying you. -
-
-
- Other privacy rights -
-
- -
- -
- -
- -
- To exercise these rights, you can contact us by email at - - privacy@byte-bot.app - - , or by referring to the contact details at the bottom of this document. If you have a complaint - about how we handle your data, we would like to hear from you. -
-
-
- 12. DO VIRGINIA RESIDENTS HAVE SPECIFIC PRIVACY RIGHTS? -
-
-
- - In Short: - Yes, if you are a resident of Virginia, you may be granted specific rights regarding access to - and use of your personal information. - -
-
-
- Virginia CDPA Privacy Notice -
-
-
Under the Virginia Consumer Data Protection Act (CDPA):
-
-
- "Consumer" means a natural person who is a resident of the Commonwealth acting only in an - individual or household context. It does not include a natural person acting in a commercial or - employment context. -
-
-
- "Personal data" means any information that is linked or reasonably linkable to an identified or - identifiable natural person. "Personal data" does not include de-identified data or publicly - available information. -
-
-
- "Sale of personal data" means the exchange of personal data for monetary consideration. -
-
-
- If this definition "consumer" applies to you, we must adhere to certain rights and obligations - regarding your personal data. -
-
-
- The information we collect, use, and disclose about you will vary depending on how you interact - with Byte and our Services. To find out more, please visit the following links: -
- - - -
-
- Your rights with respect to your personal data -
-
- -
- -
- -
- -
- -
- -
- Byte has not sold any personal data to third parties for business or commercial purposes. Byte - will not sell personal data in the future belonging to website visitors, users, and other - consumers. -
-
-
- Exercise your rights provided under the Virginia CDPA -
-
-
- More information about our data collection and sharing practices can be found in this privacy - notice and our Cookie Notice: - - https://byte-bot/cookies - - . -
-
-
- You may contact us by email at - - privacy@byte-bot.app - - , by visiting - - https://byte-bot.app/request-data - - , or by referring to the contact details at the bottom of this document. -
-
-
- If you are using an authorized agent to exercise your rights, we may deny a request if the - authorized agent does not submit proof that they have been validly authorized to act on your - behalf. -
-
-
- Verification process -
-
-
- We may request that you provide additional information reasonably necessary to verify you and - your consumer's request. If you submit the request through an authorized agent, we may need to - collect additional information to verify your identity before processing your request. -
-
-
- Upon receiving your request, we will respond without undue delay, but in all cases, within - forty-five (45) days of receipt. The response period may be extended once by forty-five (45) - additional days when reasonably necessary. We will inform you of any such extension within the - initial 45-day response period, together with the reason for the extension. -
-
-
Right to appeal
-
-
- If we decline to take action regarding your request, we will inform you of our decision and - reasoning behind it. If you wish to appeal our decision, please email us at - - privacy@byte-bot.app - - . Within sixty (60) days of receipt of an appeal, we will inform you in writing of any action - taken or not taken in response to the appeal, including a written explanation of the reasons for - the decisions. If your appeal is denied, you may contact the - - Attorney General to submit a complaint - - . -
-
-
- 13. DO WE MAKE UPDATES TO THIS NOTICE? -
-
-
- - In Short: - Yes, we will update this notice as necessary to stay compliant with relevant laws. - -
-
-
- We may update this privacy notice from time to time. The updated version will be indicated by an - updated "Revised" date and the updated version will be effective as soon as it is accessible. If - we make material changes to this privacy notice, we may notify you either by prominently posting - a notice of such changes or by directly sending you a notification. We encourage you to review - this privacy notice frequently to be informed of how we are protecting your information. -
-
-
- 14. HOW CAN YOU CONTACT US ABOUT THIS NOTICE? -
-
-
- If you have questions or comments about this notice, you may email us at - - privacy@byte-bot.app - - . -
-
-
-
- 15. HOW CAN YOU REVIEW, UPDATE, OR DELETE THE DATA WE COLLECT FROM YOU? -
-
-
- Based on the applicable laws of your country, you may have the right to request access to the - personal information we collect from you, change that information, or delete it. To request to - review, update, or delete your personal information, please visit: - - https://byte-bot.app/request-data - - . -
-
-
-
-
-
-
-
-
-
-
- {% endblock body %} -
diff --git a/byte_bot/server/domain/web/templates/status.html b/byte_bot/server/domain/web/templates/status.html deleted file mode 100644 index dba19f7f..00000000 --- a/byte_bot/server/domain/web/templates/status.html +++ /dev/null @@ -1,7 +0,0 @@ - -{% extends 'base/base.html' %} -{% block extrahead %}{% endblock extrahead %} -{% block extrastyle %}{% endblock extrastyle %} -{% block title %}Home{% endblock %} -{% block body %} {% endblock body %} -{% block extrajs %}{% endblockextrajs %} diff --git a/byte_bot/server/domain/web/templates/terms.html b/byte_bot/server/domain/web/templates/terms.html deleted file mode 100644 index f51b199c..00000000 --- a/byte_bot/server/domain/web/templates/terms.html +++ /dev/null @@ -1,1178 +0,0 @@ -{# NOTE: This is pretty silly, just playing around, but this was generated with https://termly.io #} - -{% extends 'base/base.html' %} -{% block extrahead %}{% endblock extrahead %} -{% block extrastyle %}{% endblock extrastyle %} -{% block title %}Home{% endblock %} -{% block body %} - -
-
    -
    -
    TERMS OF SERVICE
    -
    -
    Last updated July 30, 2023
    -
    -
    AGREEMENT TO OUR LEGAL TERMS
    -
    -
    -
    -
    - We are the creator of Byte (" - Bot - ,", " - Byte - ," " - we - ," " - us - ," " - our - "). -
    -
    -
    -
    -
    - We operate the website - - https://byte-bot.app - - (the " - Site - "), as well as any other related products and services that refer or link to these legal terms (the " - Legal Terms - ") (collectively, the " - Services - "). -
    -
    -
    Byte Bot - The Discord Bot for Developer Discord Servers.
    -
    -
    - You can contact us by email at - - hello@byte-bot.app - - . -
    -
    -
    - These Legal Terms constitute a legally binding agreement made between you, whether personally or on behalf of an - entity (" - you - "), and the Service maintainer, concerning your access to and use of the Services. You agree that by accessing - the Services, you have read, understood, and agreed to be bound by all of these Legal Terms. IF YOU DO NOT AGREE - WITH ALL OF THESE LEGAL TERMS, THEN YOU ARE EXPRESSLY PROHIBITED FROM USING THE SERVICES AND YOU MUST - DISCONTINUE USE IMMEDIATELY. -
    -
    -
    - Supplemental terms and conditions or documents that may be posted on the Services from time to time are hereby - expressly incorporated herein by reference. We reserve the right, in our sole discretion, to make changes or - modifications to these Legal Terms from time to time. We will alert you about any changes by updating the "Last - updated" date of these Legal Terms, and you waive any right to receive specific notice of each such change. It - is your responsibility to periodically review these Legal Terms to stay informed of updates. You will be subject - to, and will be deemed to have been made aware of and to have accepted, the changes in any revised Legal Terms - by your continued use of the Services after the date such revised Legal Terms are posted. -
    -
    -
    -
  • -
    - The Services are intended for users who are at least 13 years of age. All users who are minors in the - jurisdiction in which they reside (generally under the age of 18) must have the permission of, and be directly - supervised by, their parent or guardian to use the Services. If you are a minor, you must have your parent or - guardian read and agree to these Legal Terms prior to you using the Services. -
    -
    -
    We recommend that you print a copy of these Legal Terms for your records.
    -
    -
    TABLE OF CONTENTS
    -
    - -
    -
    -
    -
    1. OUR SERVICES
    -
    -
    - The information provided when using the Services is not intended for distribution to or use by any person or - entity in any jurisdiction or country where such distribution or use would be contrary to law or regulation or - which would subject us to any registration requirement within such jurisdiction or country. Accordingly, those - persons who choose to access the Services from other locations do so on their own initiative and are solely - responsible for compliance with local laws, if and to the extent local laws are applicable. -
    -
    -
    - The Services are not tailored to comply with industry-specific regulations (Health Insurance Portability and - Accountability Act (HIPAA), Federal Information Security Management Act (FISMA), etc.), so if your - interactions would be subjected to such laws, you may not use the Services. You may not use the Services in a - way that would violate the Gramm-Leach-Bliley Act (GLBA). -
    -
    -
    -
    2. INTELLECTUAL PROPERTY RIGHTS
    -
    -
    -
    Our intellectual property
    -
    -
    - We are the owner or the licensee of all intellectual property rights in our Services, including all source - code, databases, functionality, software, website designs, audio, video, text, photographs, and graphics in - the Services (collectively, the "Content"), as well as the trademarks, service marks, and logos contained - therein (the "Marks"). -
    -
    -
    - Our Content and Marks are protected by copyright and trademark laws (and various other intellectual property - rights and unfair competition laws) and treaties in the United States and around the world. -
    -
    -
    - The Content and Marks are provided in or through the Services "AS IS" for your personal, non-commercial use - only. -
    -
    -
    Your use of our Services
    -
    -
    - Subject to your compliance with these Legal Terms, including the " - PROHIBITED ACTIVITIES - " section below, we grant you a non-exclusive, non-transferable, revocable license to: -
    -
      -
    • access the Services; and
    • -
    • download or print a copy of any portion of the Content to which you have properly gained access.
    • -
    -
    solely for your personal, non-commercial use.
    -
    -
    - Except as set out in this section or elsewhere in our Legal Terms, no part of the Services and no Content or - Marks may be copied, reproduced, aggregated, republished, uploaded, posted, publicly displayed, encoded, - translated, transmitted, distributed, sold, licensed, or otherwise exploited for any commercial purpose - whatsoever, without our express prior written permission. -
    -
    -
    - If you wish to make any use of the Services, Content, or Marks other than as set out in this section or - elsewhere in our Legal Terms, please address your request to: - - hello@byte-bot.app - - . If we ever grant you the permission to post, reproduce, or publicly display any part of our Services or - Content, you must identify us as the owners or licensors of the Services, Content, or Marks and ensure that - any copyright or proprietary notice appears or is visible on posting, reproducing, or displaying our Content. -
    -
    -
    -
    -
    We reserve all rights not expressly granted to you in and to the Services, Content, and Marks.
    -
    -
    - Any breach of these Intellectual Property Rights will constitute a material breach of our Legal Terms and your - right to use our Services will terminate immediately. -
    -
    -
    Your submissions
    -
    -
    - Please review this section and the " - - PROHIBITED ACTIVITIES - - " section carefully prior to using our Services to understand the (a) rights you give us and (b) obligations - you have when you post or upload any content through the Services. -
    -
    -
    - Submissions: - By directly sending us any question, comment, suggestion, idea, feedback, or other information about the - Services ("Submissions"), you agree to assign to us all intellectual property rights in such Submission. You - agree that we shall own this Submission and be entitled to its unrestricted use and dissemination for any - lawful purpose, commercial or otherwise, without acknowledgment or compensation to you. -
    -
    -
    - You are responsible for what you post or upload: - By sending us Submissions through any part of the Services you: -
    -
      -
    • - confirm that you have read and agree with our " - PROHIBITED ACTIVITIES - " and will not post, send, publish, upload, or transmit through the Services any Submission that is illegal, - harassing, hateful, harmful, defamatory, obscene, bullying, abusive, discriminatory, threatening to any - person or group, sexually explicit, false, inaccurate, deceitful, or misleading; -
    • -
    • to the extent permissible by applicable law, waive any and all moral rights to any such Submission;
    • -
    • - warrant that any such Submission are original to you or that you have the necessary rights and licenses to - submit such Submissions and that you have full authority to grant us the above-mentioned rights in relation - to your Submissions; and -
    • -
    • warrant and represent that your Submissions do not constitute confidential information.
    • -
    -
    - You are solely responsible for your Submissions and you expressly agree to reimburse us for any and all losses - that we may suffer because of your breach of (a) this section, (b) any third party’s intellectual property - rights, or (c) applicable law. -
    -
    -
    -
    -
    - - - 3. - - USER REPRESENTATIONS - -
    -
    -
    -
    -
    - By using the Services, you represent and warrant that: (1) you have the legal capacity, and you agree to - comply with these Legal Terms; (2) you are not under the age of 13; (3) you are not a minor in the - jurisdiction in which you reside, or if a minor, you have received parental permission to use the Services; - (4) you will not access the Services through automated or non-human means, whether through a bot, script or - otherwise; (5) you will not use the Services for any illegal or unauthorized purpose; and (6) your use of the - Services will not violate any applicable law or regulation. -
    -
    -
    -
    -
    -
    -
    - If you provide any information that is untrue, inaccurate, not current, or incomplete, we have the right - to suspend or terminate your account and refuse any and all current or future use of the Services (or any - portion thereof). -
    -
    -
    -
    -
    -
    -
    -
    - - - 4. - - PURCHASES AND PAYMENT - -
    -
    -
    -
    We accept the following forms of payment:
    -
    - - -
    -
    - You agree to provide current, complete, and accurate purchase and account information for all purchases made via - the Services. You further agree to promptly update account and payment information, including email address, - payment method, and payment card expiration date, so that we can complete your transactions and contact you as - needed. Sales tax will be added to the price of purchases as deemed required by us. We may change prices at any - time. All payments shall be in US dollars. -
    -
    -
    -
    - You agree to pay all charges at the prices then in effect for your purchases and any applicable shipping fees, - and you authorize us to charge your chosen payment provider for any such amounts upon placing your order. If - your order is subject to recurring charges, then you consent to our charging your payment method on a - recurring basis without requiring your prior approval for each recurring charge, until such time as you cancel - the applicable order. We reserve the right to correct any errors or mistakes in pricing, even if we have - already requested or received payment. -
    -
    -
    -
    -
    - We reserve the right to refuse any order placed through the Services. We may, in our sole discretion, limit or - cancel quantities purchased per person, per household, or per order. These restrictions may include orders - placed by or under the same customer account, the same payment method, and/or orders that use the same billing - or shipping address. We reserve the right to limit or prohibit orders that, in our sole judgment, appear to be - placed by dealers, resellers, or distributors. -
    -
    -
    5. FREE TRIAL
    -
    -
    - We offer a 7-day free trial to new users who register with the Services. The account will be charged according - to the user's chosen subscription at the end of the free trial. -
    -
    -
    6. CANCELLATION
    -
    -
    - You can cancel your subscription at any time by logging into your account. Your cancellation will take effect - at the end of the current paid term. -
    -
    -
    - If you are unsatisfied with our Services, please email us at - - hello@byte-bot.app - - . -
    -
    -
    7. SOFTWARE
    -
    -
    - We may include software for use in connection with our Services. If such software is accompanied by an end - user license agreement ("EULA"), the terms of the EULA will govern your use of the software. If such software - is not accompanied by a EULA, then we grant to you a non-exclusive, revocable, personal, and non-transferable - license to use such software solely in connection with our services and in accordance with these Legal Terms. - Any software and any related documentation is provided "AS IS" without warranty of any kind, either express or - implied, including, without limitation, the implied warranties of merchantability, fitness for a particular - purpose, or non-infringement. You accept any and all risk arising out of use or performance of any software. - You may not reproduce or redistribute any software except in accordance with the EULA or these Legal Terms. -
    -
    -
    -
    - - - 8. - - PROHIBITED ACTIVITIES - -
    -
    -
    -
    -
    - You may not access or use the Services for any purpose other than that for which we make the Services - available. The Services may not be used in connection with any commercial endeavors except those that are - specifically endorsed or approved by us. -
    -
    -
    -
    -
    -
    -
    -
    As a user of the Services, you agree not to:
    -
    -
      -
    • - Systematically retrieve data or other content from the Services to create or compile, directly or - indirectly, a collection, compilation, database, or directory without written permission from us. -
    • -
    • - Trick, defraud, or mislead us and other users, especially in any attempt to learn sensitive account - information such as user passwords. -
    • -
    • - Circumvent, disable, or otherwise interfere with security-related features of the Services, including - features that prevent or restrict the use or copying of any Content or enforce limitations on the use - of the Services and/or the Content contained therein. -
    • -
    • Disparage, tarnish, or otherwise harm, in our opinion, us and/or the Services.
    • -
    • - Use any information obtained from the Services in order to harass, abuse, or harm another person. -
    • -
    • Make improper use of our support services or submit false reports of abuse or misconduct.
    • -
    • Use the Services in a manner inconsistent with any applicable laws or regulations.
    • -
    • Engage in unauthorized framing of or linking to the Services.
    • -
    • - Upload or transmit (or attempt to upload or to transmit) viruses, Trojan horses, or other material, - including excessive use of capital letters and spamming (continuous posting of repetitive text), that - interferes with any party’s uninterrupted use and enjoyment of the Services or modifies, impairs, - disrupts, alters, or interferes with the use, features, functions, operation, or maintenance of the - Services. -
    • -
    • - Engage in any automated use of the system, such as using scripts to send comments or messages, or - using any data mining, robots, or similar data gathering and extraction tools. -
    • -
    • Delete the copyright or other proprietary rights notice from any Content.
    • -
    • Attempt to impersonate another user or person or use the username of another user.
    • -
    • - Upload or transmit (or attempt to upload or to transmit) any material that acts as a passive or active - information collection or transmission mechanism, including without limitation, clear graphics - interchange formats ("gifs"), 1Γ—1 pixels, web bugs, cookies, or other similar devices (sometimes - referred to as "spyware" or "passive collection mechanisms" or "pcms"). -
    • -
    • - Interfere with, disrupt, or create an undue burden on the Services or the networks or services - connected to the Services. -
    • -
    • - Harass, annoy, intimidate, or threaten any of our employees or agents engaged in providing any portion - of the Services to you. -
    • -
    • - Attempt to bypass any measures of the Services designed to prevent or restrict access to the Services, - or any portion of the Services. -
    • -
    • - Copy or adapt the Services' software, including but not limited to Flash, PHP, HTML, JavaScript, or - other code. -
    • -
    • - Except as permitted by applicable law, decipher, decompile, disassemble, or reverse engineer any of - the software comprising or in any way making up a part of the Services. -
    • -
    • - Except as may be the result of standard search engine or Internet browser usage, use, launch, develop, - or distribute any automated system, including without limitation, any spider, robot, cheat utility, - scraper, or offline reader that accesses the Services, or use or launch any unauthorized script or - other software. -
    • -
    • Use a buying agent or purchasing agent to make purchases on the Services.
    • -
    • - Make any unauthorized use of the Services, including collecting usernames and/or email addresses of - users by electronic or other means for the purpose of sending unsolicited email, or creating user - accounts by automated means or under false pretenses. -
    • -
    • - Use the Services as part of any effort to compete with us or otherwise use the Services and/or the - Content for any revenue-generating endeavor or commercial enterprise. -
    • -
    • Use the Services to advertise or offer to sell goods and services.
    • -
    • Sell or otherwise transfer your profile.
    • -
    • - Use the Services in such a way that violates the terms, Discord Code of Conduct, Discord Community - Guidelines -
    • -
    -
    -
    -
    -
    -
    -
    - - - 9. - - USER GENERATED CONTRIBUTIONS - -
    -
    -
    -
    -
    - The Services does not offer users to submit or post content. We may provide you with the opportunity to - create, submit, post, display, transmit, perform, publish, distribute, or broadcast content and - materials to us or on the Services, including but not limited to text, writings, video, audio, - photographs, graphics, comments, suggestions, or personal information or other material (collectively, - "Contributions"). Contributions may be viewable by other users of the Services and through third-party - websites. When you create or make available any Contributions, you thereby represent and warrant that: -
    -
    -
    -
    -
      -
    • - The creation, distribution, transmission, public display, or performance, and the accessing, downloading, or - copying of your Contributions do not and will not infringe the proprietary rights, including but not limited - to the copyright, patent, trademark, trade secret, or moral rights of any third party. -
    • -
    • - You are the creator and owner of or have the necessary licenses, rights, consents, releases, and permissions - to use and to authorize us, the Services, and other users of the Services to use your Contributions in any - manner contemplated by the Services and these Legal Terms. -
    • -
    • - You have the written consent, release, and/or permission of each and every identifiable individual person in - your Contributions to use the name or likeness of each and every such identifiable individual person to - enable inclusion and use of your Contributions in any manner contemplated by the Services and these Legal - Terms. -
    • -
    • Your Contributions are not false, inaccurate, or misleading.
    • -
    • - Your Contributions are not unsolicited or unauthorized advertising, promotional materials, pyramid schemes, - chain letters, spam, mass mailings, or other forms of solicitation. -
    • -
    • - Your Contributions are not obscene, lewd, lascivious, filthy, violent, harassing, libelous, slanderous, or - otherwise objectionable (as determined by us). -
    • -
    • Your Contributions do not ridicule, mock, disparage, intimidate, or abuse anyone.
    • -
    • - Your Contributions are not used to harass or threaten (in the legal sense of those terms) any other person - and to promote violence against a specific person or class of people. -
    • -
    • Your Contributions do not violate any applicable law, regulation, or rule.
    • -
    • Your Contributions do not violate the privacy or publicity rights of any third party.
    • -
    • - Your Contributions do not violate any applicable law concerning child pornography, or otherwise intended to - protect the health or well-being of minors. -
    • -
    • - Your Contributions do not include any offensive comments that are connected to race, national origin, - gender, sexual preference, or physical handicap. -
    • -
    • - Your Contributions do not otherwise violate, or link to material that violates, any provision of these Legal - Terms, or any applicable law or regulation. -
    • -
    -
    -
    - Any use of the Services in violation of the foregoing violates these Legal Terms and may result in, among - other things, termination or suspension of your rights to use the Services. -
    -
    -
    -
    -
    - - - 10. - - CONTRIBUTION LICENSE - -
    -
    -
    -
    - You and Services agree that we may access, store, process, and use any information and personal data that you - provide and your choices (including settings). -
    -
    -
    - By submitting suggestions or other feedback regarding the Services, you agree that we can use and share such - feedback for any purpose without compensation to you. -
    -
    -
    - We do not assert any ownership over your Contributions. You retain full ownership of all of your Contributions - and any intellectual property rights or other proprietary rights associated with your Contributions. We are - not liable for any statements or representations in your Contributions provided by you in any area on the - Services. You are solely responsible for your Contributions to the Services and you expressly agree to - exonerate us from any and all responsibility and to refrain from any legal action against us regarding your - Contributions. -
    -
    -
    -
    -
    - - 11. - THIRD-PARTY WEBSITES AND CONTENT - -
    -
    -
    - The Services may contain (or you may be sent via the Site) links to other websites ("Third-Party Websites") as - well as articles, photographs, text, graphics, pictures, designs, music, sound, video, information, - applications, software, and other content or items belonging to or originating from third parties ("Third-Party - Content"). Such Third-Party Websites and Third-Party Content are not investigated, monitored, or checked for - accuracy, appropriateness, or completeness by us, and we are not responsible for any Third-Party Websites - accessed through the Services or any Third-Party Content posted on, available through, or installed from the - Services, including the content, accuracy, offensiveness, opinions, reliability, privacy practices, or other - policies of or contained in the Third-Party Websites or the Third-Party Content. Inclusion of, linking to, or - permitting the use or installation of any Third-Party Websites or any Third-Party Content does not imply - approval or endorsement thereof by us. If you decide to leave the Services and access the Third-Party Websites - or to use or install any Third-Party Content, you do so at your own risk, and you should be aware these Legal - Terms no longer govern. You should review the applicable terms and policies, including privacy and data - gathering practices, of any website to which you navigate from the Services or relating to any applications you - use or install from the Services. Any purchases you make through Third-Party Websites will be through other - websites and from other companies, and we take no responsibility whatsoever in relation to such purchases which - are exclusively between you and the applicable third party. You agree and acknowledge that we do not endorse the - products or services offered on Third-Party Websites and you shall hold us blameless from any harm caused by - your purchase of such products or services. Additionally, you shall hold us blameless from any losses sustained - by you or harm caused to you relating to or resulting in any way from any Third-Party Content or any contact - with Third-Party Websites. -
    -
    -
    - - - 12. - - SERVICES MANAGEMENT - -
    -
    -
    - We reserve the right, but not the obligation, to: (1) monitor the Services for violations of these Legal Terms; - (2) take appropriate legal action against anyone who, in our sole discretion, violates the law or these Legal - Terms, including without limitation, reporting such user to law enforcement authorities; (3) in our sole - discretion and without limitation, refuse, restrict access to, limit the availability of, or disable (to the - extent technologically feasible) any of your Contributions or any portion thereof; (4) in our sole discretion - and without limitation, notice, or liability, to remove from the Services or otherwise disable all files and - content that are excessive in size or are in any way burdensome to our systems; and (5) otherwise manage the - Services in a manner designed to protect our rights and property and to facilitate the proper functioning of the - Services. -
    -
    -
    - - 13. - PRIVACY POLICY - -
    -
    -
    - We care about data privacy and security. By using the Services, you agree to be bound by our Privacy Policy - posted on the Services, which is incorporated into these Legal Terms. Please be advised the Services are hosted - in the United States. If you access the Services from any other region of the world with laws or other - requirements governing personal data collection, use, or disclosure that differ from applicable laws in the - United States, then through your continued use of the Services, you are transferring your data to the United - States, and you expressly consent to have your data transferred to and processed in the United States. Further, - we do not knowingly accept, request, or solicit information from children or knowingly market to children. - Therefore, in accordance with the U.S. Children’s Online Privacy Protection Act, if we receive actual knowledge - that anyone under the age of 13 has provided personal information to us without the requisite and verifiable - parental consent, we will delete that information from the Services as quickly as is reasonably practical. -
    -
    -
    - - - 14. - - TERM AND TERMINATION - -
    -
    -
    - These Legal Terms shall remain in full force and effect while you use the Services. WITHOUT LIMITING ANY OTHER - PROVISION OF THESE LEGAL TERMS, WE RESERVE THE RIGHT TO, IN OUR SOLE DISCRETION AND WITHOUT NOTICE OR LIABILITY, - DENY ACCESS TO AND USE OF THE SERVICES (INCLUDING BLOCKING CERTAIN IP ADDRESSES), TO ANY PERSON FOR ANY REASON - OR FOR NO REASON, INCLUDING WITHOUT LIMITATION FOR BREACH OF ANY REPRESENTATION, WARRANTY, OR COVENANT CONTAINED - IN THESE LEGAL TERMS OR OF ANY APPLICABLE LAW OR REGULATION. WE MAY TERMINATE YOUR USE OR PARTICIPATION IN THE - SERVICES OR DELETE ANY CONTENT OR INFORMATION THAT YOU POSTED AT ANY TIME, WITHOUT WARNING, IN OUR SOLE - DISCRETION. -
    -
    -
    - If we terminate or suspend your account for any reason, you are prohibited from registering and creating a new - account under your name, a fake or borrowed name, or the name of any third party, even if you may be acting on - behalf of the third party. In addition to terminating or suspending your account, we reserve the right to take - appropriate legal action, including without limitation pursuing civil, criminal, and injunctive redress. -
    -
    -
    - - - 15. - - MODIFICATIONS AND INTERRUPTIONS - -
    -
    -
    - We reserve the right to change, modify, or remove the contents of the Services at any time or for any reason at - our sole discretion without notice. However, we have no obligation to update any information on our Services. We - will not be liable to you or any third party for any modification, price change, suspension, or discontinuance - of the Services. -
    -
    -
    - We cannot guarantee the Services will be available at all times. We may experience hardware, software, or other - problems or need to perform maintenance related to the Services, resulting in interruptions, delays, or errors. - We reserve the right to change, revise, update, suspend, discontinue, or otherwise modify the Services at any - time or for any reason without notice to you. You agree that we have no liability whatsoever for any loss, - damage, or inconvenience caused by your inability to access or use the Services during any downtime or - discontinuance of the Services. Nothing in these Legal Terms will be construed to obligate us to maintain and - support the Services or to supply any corrections, updates, or releases in connection therewith. -
    -
    -
    - - - 16. - - GOVERNING LAW - -
    -
    -
    - These Legal Terms and your use of the Services are governed by and construed in accordance with the laws of the - State of Alabama applicable to agreements made and to be entirely performed within the State of Alabama, without - regard to its conflict of law principles. -
    -
    -
    - - - 17. - - DISPUTE RESOLUTION - -
    -
    -
    Informal Negotiations
    -
    -
    - To expedite resolution and control the cost of any dispute, controversy, or claim related to these Legal Terms - (each a "Dispute" and collectively, the "Disputes") brought by either you or us (individually, a "Party" and - collectively, the "Parties"), the Parties agree to first attempt to negotiate any Dispute (except those Disputes - expressly provided below) informally for at least thirty (30) days before initiating arbitration. Such informal - negotiations commence upon written notice from one Party to the other Party. -
    -
    -
    Binding Arbitration
    -
    -
    - If the Parties are unable to resolve a Dispute through informal negotiations, the Dispute (except those Disputes - expressly excluded below) will be finally and exclusively resolved by binding arbitration. YOU UNDERSTAND THAT - WITHOUT THIS PROVISION, YOU WOULD HAVE THE RIGHT TO SUE IN COURT AND HAVE A JURY TRIAL. The arbitration shall be - commenced and conducted under the Commercial Arbitration Rules of the American Arbitration Association ("AAA") - and, where appropriate, the AAA’s Supplementary Procedures for Consumer Related Disputes ("AAA Consumer Rules"), - both of which are available at the - - American Arbitration Association (AAA) website - - . Your arbitration fees and your share of arbitrator compensation shall be governed by the AAA Consumer Rules - and, where appropriate, limited by the AAA Consumer Rules. The arbitration may be conducted in person, through - the submission of documents, by phone, or online. The arbitrator will make a decision in writing, but need not - provide a statement of reasons unless requested by either Party. The arbitrator must follow applicable law, and - any award may be challenged if the arbitrator fails to do so. Except where otherwise required by the applicable - AAA rules or applicable law, the arbitration will take place in the U.S. Armed Forces - Americas. Except as - otherwise provided herein, the Parties may litigate in court to compel arbitration, stay proceedings pending - arbitration, or to confirm, modify, vacate, or enter judgment on the award entered by the arbitrator. -
    -
    -
    - If for any reason, a Dispute proceeds in court rather than arbitration, the Dispute shall be commenced or - prosecuted in the state and federal courts located in the U.S. Armed Forces - Europe , and the Parties hereby - consent to, and waive all defenses of lack of personal jurisdiction, and forum non conveniens with respect to - venue and jurisdiction in such state and federal courts. Application of the United Nations Convention on - Contracts for the International Sale of Goods and the Uniform Computer Information Transaction Act (UCITA) are - excluded from these Legal Terms. -
    -
    -
    - In no event shall any Dispute brought by either Party related in any way to the Services be commenced more than - zero (0) years after the cause of action arose. If this provision is found to be illegal or unenforceable, then - neither Party will elect to arbitrate any Dispute falling within that portion of this provision found to be - illegal or unenforceable and such Dispute shall be decided by a court of competent jurisdiction within the - courts listed for jurisdiction above, and the Parties agree to submit to the personal jurisdiction of that - court. -
    -
    -
    Restrictions
    -
    -
    - The Parties agree that any arbitration shall be limited to the Dispute between the Parties individually. To the - full extent permitted by law, (a) no arbitration shall be joined with any other proceeding; (b) there is no - right or authority for any Dispute to be arbitrated on a class-action basis or to utilize class action - procedures; and (c) there is no right or authority for any Dispute to be brought in a purported representative - capacity on behalf of the public or any other persons. -
    -
    -
    - Exceptions to Informal Negotiations and Arbitration -
    -
    -
    - The Parties agree that the following Disputes are not subject to the above provisions concerning informal - negotiations binding arbitration: (a) any Disputes seeking to enforce or protect, or concerning the validity of, - any of the intellectual property rights of a Party; (b) any Dispute related to, or arising from, allegations of - theft, piracy, invasion of privacy, or unauthorized use; and (c) any claim for injunctive relief. If this - provision is found to be illegal or unenforceable, then neither Party will elect to arbitrate any Dispute - falling within that portion of this provision found to be illegal or unenforceable and such Dispute shall be - decided by a court of competent jurisdiction within the courts listed for jurisdiction above, and the Parties - agree to submit to the personal jurisdiction of that court. -
    -
    -
    - - - 18. - - CORRECTIONS - -
    -
    -
    - There may be information on the Services that contains typographical errors, inaccuracies, or omissions, - including descriptions, pricing, availability, and various other information. We reserve the right to correct - any errors, inaccuracies, or omissions and to change or update the information on the Services at any time, - without prior notice. -
    -
    -
    - - 19. - DISCLAIMER - -
    -
    -
    - THE SERVICES ARE PROVIDED ON AN AS-IS AND AS-AVAILABLE BASIS. YOU AGREE THAT YOUR USE OF THE SERVICES WILL BE AT - YOUR SOLE RISK. TO THE FULLEST EXTENT PERMITTED BY LAW, WE DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, IN - CONNECTION WITH THE SERVICES AND YOUR USE THEREOF, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF - MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. WE MAKE NO WARRANTIES OR - REPRESENTATIONS ABOUT THE ACCURACY OR COMPLETENESS OF THE SERVICES' CONTENT OR THE CONTENT OF ANY WEBSITES OR - MOBILE APPLICATIONS LINKED TO THE SERVICES AND WE WILL ASSUME NO LIABILITY OR RESPONSIBILITY FOR ANY (1) ERRORS, - MISTAKES, OR INACCURACIES OF CONTENT AND MATERIALS, (2) PERSONAL INJURY OR PROPERTY DAMAGE, OF ANY NATURE - WHATSOEVER, RESULTING FROM YOUR ACCESS TO AND USE OF THE SERVICES, (3) ANY UNAUTHORIZED ACCESS TO OR USE OF OUR - SECURE SERVERS AND/OR ANY AND ALL PERSONAL INFORMATION AND/OR FINANCIAL INFORMATION STORED THEREIN, (4) ANY - INTERRUPTION OR CESSATION OF TRANSMISSION TO OR FROM THE SERVICES, (5) ANY BUGS, VIRUSES, TROJAN HORSES, OR THE - LIKE WHICH MAY BE TRANSMITTED TO OR THROUGH THE SERVICES BY ANY THIRD PARTY, AND/OR (6) ANY ERRORS OR OMISSIONS - IN ANY CONTENT AND MATERIALS OR FOR ANY LOSS OR DAMAGE OF ANY KIND INCURRED AS A RESULT OF THE USE OF ANY - CONTENT POSTED, TRANSMITTED, OR OTHERWISE MADE AVAILABLE VIA THE SERVICES. WE DO NOT WARRANT, ENDORSE, - GUARANTEE, OR ASSUME RESPONSIBILITY FOR ANY PRODUCT OR SERVICE ADVERTISED OR OFFERED BY A THIRD PARTY THROUGH - THE SERVICES, ANY HYPERLINKED WEBSITE, OR ANY WEBSITE OR MOBILE APPLICATION FEATURED IN ANY BANNER OR OTHER - ADVERTISING, AND WE WILL NOT BE A PARTY TO OR IN ANY WAY BE RESPONSIBLE FOR MONITORING ANY TRANSACTION BETWEEN - YOU AND ANY THIRD-PARTY PROVIDERS OF PRODUCTS OR SERVICES. AS WITH THE PURCHASE OF A PRODUCT OR SERVICE THROUGH - ANY MEDIUM OR IN ANY ENVIRONMENT, YOU SHOULD USE YOUR BEST JUDGMENT AND EXERCISE CAUTION WHERE APPROPRIATE. -
    -
    -
    - - - 20. - - LIMITATIONS OF LIABILITY - -
    -
    -
    - IN NO EVENT WILL WE OR OUR DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE TO YOU OR ANY THIRD PARTY FOR ANY DIRECT, - INDIRECT, CONSEQUENTIAL, EXEMPLARY, INCIDENTAL, SPECIAL, OR PUNITIVE DAMAGES, INCLUDING LOST PROFIT, LOST - REVENUE, LOSS OF DATA, OR OTHER DAMAGES ARISING FROM YOUR USE OF THE SERVICES, EVEN IF WE HAVE BEEN ADVISED OF - THE POSSIBILITY OF SUCH DAMAGES. NOTWITHSTANDING ANYTHING TO THE CONTRARY CONTAINED HEREIN, OUR LIABILITY TO YOU - FOR ANY CAUSE WHATSOEVER AND REGARDLESS OF THE FORM OF THE ACTION, WILL AT ALL TIMES BE LIMITED TO THE AMOUNT - PAID, IF ANY, BY YOU TO US DURING THE zero (0) mONTH PERIOD PRIOR TO ANY CAUSE OF ACTION ARISING. CERTAIN US - STATE LAWS AND INTERNATIONAL LAWS DO NOT ALLOW LIMITATIONS ON IMPLIED WARRANTIES OR THE EXCLUSION OR LIMITATION - OF CERTAIN DAMAGES. IF THESE LAWS APPLY TO YOU, SOME OR ALL OF THE ABOVE DISCLAIMERS OR LIMITATIONS MAY NOT - APPLY TO YOU, AND YOU MAY HAVE ADDITIONAL RIGHTS. -
    -
    -
    - - - 21. - - INDEMNIFICATION - -
    -
    -
    - You agree to defend, indemnify, and hold us harmless, including our subsidiaries, affiliates, and all of our - respective officers, agents, partners, and employees, from and against any loss, damage, liability, claim, or - demand, including reasonable attorneys’ fees and expenses, made by any third party due to or arising out of: (1) - use of the Services; (2) breach of these Legal Terms; (3) any breach of your representations and warranties set - forth in these Legal Terms; (4) your violation of the rights of a third party, including but not limited to - intellectual property rights; or (5) any overt harmful act toward any other user of the Services with whom you - connected via the Services. Notwithstanding the foregoing, we reserve the right, at your expense, to assume the - exclusive defense and control of any matter for which you are required to indemnify us, and you agree to - cooperate, at your expense, with our defense of such claims. We will use reasonable efforts to notify you of any - such claim, action, or proceeding which is subject to this indemnification upon becoming aware of it. -
    -
    -
    - - - 22. - - USER DATA - -
    -
    -
    - We will maintain certain data that you transmit to the Services for the purpose of managing the performance of - the Services, as well as data relating to your use of the Services. Although we perform regular routine backups - of data, you are solely responsible for all data that you transmit or that relates to any activity you have - undertaken using the Services. You agree that we shall have no liability to you for any loss or corruption of - any such data, and you hereby waive any right of action against us arising from any such loss or corruption of - such data. -
    -
    -
    - - - 23. - - ELECTRONIC COMMUNICATIONS, TRANSACTIONS, AND SIGNATURES - -
    -
    -
    - Visiting the Services, sending us emails, and completing online forms constitute electronic communications. You - consent to receive electronic communications, and you agree that all agreements, notices, disclosures, and other - communications we provide to you electronically, via email and on the Services, satisfy any legal requirement - that such communication be in writing. YOU HEREBY AGREE TO THE USE OF ELECTRONIC SIGNATURES, CONTRACTS, ORDERS, - AND OTHER RECORDS, AND TO ELECTRONIC DELIVERY OF NOTICES, POLICIES, AND RECORDS OF TRANSACTIONS INITIATED OR - COMPLETED BY US OR VIA THE SERVICES. You hereby waive any rights or requirements under any statutes, - regulations, rules, ordinances, or other laws in any jurisdiction which require an original signature or - delivery or retention of non-electronic records, or to payments or the granting of credits by any means other - than electronic means. -
    -
    -
    - - - 24. - - CALIFORNIA USERS AND RESIDENTS - -
    -
    -
    - If any complaint with us is not satisfactorily resolved, you can contact the Complaint Assistance Unit of the - Division of Consumer Services of the California Department of Consumer Affairs in writing at 1625 North Market - Blvd., Suite N 112, Sacramento, California 95834 or by telephone at (800) 952-5210 or (916) 445-1254. -
    -
    -
    - - - 25. - - MISCELLANEOUS - -
    -
    -
    - These Legal Terms and any policies or operating rules posted by us on the Services or in respect to the Services - constitute the entire agreement and understanding between you and us. Our failure to exercise or enforce any - right or provision of these Legal Terms shall not operate as a waiver of such right or provision. These Legal - Terms operate to the fullest extent permissible by law. We may assign any or all of our rights and obligations - to others at any time. We shall not be responsible or liable for any loss, damage, delay, or failure to act - caused by any cause beyond our reasonable control. If any provision or part of a provision of these Legal Terms - is determined to be unlawful, void, or unenforceable, that provision or part of the provision is deemed - severable from these Legal Terms and does not affect the validity and enforceability of any remaining - provisions. There is no joint venture, partnership, employment or agency relationship created between you and us - as a result of these Legal Terms or use of the Services. You agree that these Legal Terms will not be construed - against us by virtue of having drafted them. You hereby waive any and all defenses you may have based on the - electronic form of these Legal Terms and the lack of signing by the parties hereto to execute these Legal Terms. -
    -
    -
    - - - 26. - - CONTACT US - -
    -
    -
    - In order to resolve a complaint regarding the Services or to receive further information regarding use of the - Services, please contact us at: -
    -
    - -
  • -
-
-
-{% endblock body %} diff --git a/byte_bot/server/lib/__init__.py b/byte_bot/server/lib/__init__.py deleted file mode 100644 index 6a5f7a00..00000000 --- a/byte_bot/server/lib/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -"""Server Lib.""" - -from __future__ import annotations - -from byte_bot.server.lib import ( - constants, - cors, - db, - dependencies, - dto, - exceptions, - log, - openapi, - schema, - serialization, - settings, - static_files, - template, - types, -) - -__all__ = [ - "constants", - "cors", - "db", - "dependencies", - "dto", - "exceptions", - "log", - "openapi", - "schema", - "serialization", - "settings", - "static_files", - "template", - "types", -] diff --git a/byte_bot/server/lib/constants.py b/byte_bot/server/lib/constants.py deleted file mode 100644 index 39599a15..00000000 --- a/byte_bot/server/lib/constants.py +++ /dev/null @@ -1,22 +0,0 @@ -"""Byte server constants.""" - -from __future__ import annotations - -__all__ = [ - "CACHE_EXPIRATION", - "DB_SESSION_DEPENDENCY_KEY", - "DEFAULT_PAGINATION_SIZE", - "DTO_INFO_KEY", - "USER_DEPENDENCY_KEY", -] - -DB_SESSION_DEPENDENCY_KEY = "db_session" -"""The name of the key used for dependency injection of the database session.""" -USER_DEPENDENCY_KEY = "current_user" -"""The name of the key used for dependency injection of the database session.""" -DTO_INFO_KEY = "info" -"""The name of the key used for storing DTO information.""" -DEFAULT_PAGINATION_SIZE = 20 -"""Default page size to use.""" -CACHE_EXPIRATION: int = 60 -"""Default cache key expiration in seconds.""" diff --git a/byte_bot/server/lib/cors.py b/byte_bot/server/lib/cors.py deleted file mode 100644 index 1fc6ef12..00000000 --- a/byte_bot/server/lib/cors.py +++ /dev/null @@ -1,8 +0,0 @@ -"""CORS config.""" - -from litestar.config.cors import CORSConfig - -from byte_bot.server.lib import settings - -config = CORSConfig(allow_origins=settings.project.BACKEND_CORS_ORIGINS) -"""Default CORS config.""" diff --git a/byte_bot/server/lib/db/__init__.py b/byte_bot/server/lib/db/__init__.py deleted file mode 100644 index cf634b6f..00000000 --- a/byte_bot/server/lib/db/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Core DB Package.""" - -from __future__ import annotations - -from byte_bot.server.lib.db import orm -from byte_bot.server.lib.db.base import ( - async_session_factory, - config, - engine, - plugin, - session, -) - -__all__ = [ - "async_session_factory", - "config", - "engine", - "orm", - "plugin", - "session", -] diff --git a/byte_bot/server/lib/db/alembic.ini b/byte_bot/server/lib/db/alembic.ini deleted file mode 100644 index efc4dae3..00000000 --- a/byte_bot/server/lib/db/alembic.ini +++ /dev/null @@ -1,10 +0,0 @@ -# Advanced Alchemy Alembic Asyncio Config - -[alembic] -prepend_sys_path = byte_bot:. -script_location = byte_bot/server/lib/db/migrations -file_template = %%(year)d-%%(month).2d-%%(day).2d_%%(slug)s_%%(rev)s -timezone = UTC -truncate_slug_length = 40 -version_path_separator = os # Use os.pathsep. Default configuration used for new projects. -output_encoding = utf-8 diff --git a/byte_bot/server/lib/db/base.py b/byte_bot/server/lib/db/base.py deleted file mode 100644 index f63a7b59..00000000 --- a/byte_bot/server/lib/db/base.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Database session and engine.""" - -from __future__ import annotations - -from contextlib import asynccontextmanager -from typing import TYPE_CHECKING, Any - -from advanced_alchemy.config import AlembicAsyncConfig -from advanced_alchemy.extensions.litestar.plugins.init.config import SQLAlchemyAsyncConfig -from advanced_alchemy.extensions.litestar.plugins.init.config.asyncio import autocommit_before_send_handler -from advanced_alchemy.extensions.litestar.plugins.init.plugin import SQLAlchemyInitPlugin -from sqlalchemy import event -from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine -from sqlalchemy.pool import NullPool - -from byte_bot.server.lib import constants, serialization, settings - -__all__ = ["async_session_factory", "config", "engine", "plugin", "session"] - -if TYPE_CHECKING: - from collections.abc import AsyncIterator - - from sqlalchemy.ext.asyncio import AsyncSession - -engine = create_async_engine( - settings.db.URL, - future=True, - json_serializer=serialization.to_json, - json_deserializer=serialization.from_json, - echo=settings.db.ECHO, - echo_pool=True if settings.db.ECHO_POOL == "debug" else settings.db.ECHO_POOL, - # max_overflow=settings.db.POOL_MAX_OVERFLOW, - # pool_size=settings.db.POOL_SIZE, - # pool_timeout=settings.db.POOL_TIMEOUT, - pool_recycle=settings.db.POOL_RECYCLE, - pool_pre_ping=settings.db.POOL_PRE_PING, - # pool_use_lifo=True, - poolclass=NullPool if settings.db.POOL_DISABLE else None, - connect_args=settings.db.CONNECT_ARGS, -) -async_session_factory: async_sessionmaker[AsyncSession] = async_sessionmaker(engine, expire_on_commit=False) -"""Database session factory. - -See `async_sessionmaker `_. -""" - - -@event.listens_for(engine.sync_engine, "connect") -def _sqla_on_connect(dbapi_connection: Any, _: Any) -> Any: # pragma: no cover - """On connect event handler. - - Using msgspec for serialization of the json column values means that the - output is binary, not ``str`` like ``json.dumps`` would output. - SQLAlchemy expects that the json serializer returns ``str`` and calls ``.encode()`` on the value to - turn it to bytes before writing to the JSONB column. I'd need to either wrap ``serialization.to_json`` to - return a ``str`` so that SQLAlchemy could then convert it to binary, or do the following, which - changes the behavior of the dialect to expect a binary value from the serializer. - """ - - def encoder(bin_value: bytes) -> bytes: - return b"\x01" + serialization.to_json(bin_value) - - def decoder(bin_value: bytes) -> Any: - # the byte is the \x01 prefix for jsonb used by PostgreSQL. - # asyncpg returns it when format='binary' - return serialization.from_json(bin_value[1:]) - - dbapi_connection.await_( - dbapi_connection.driver_connection.set_type_codec( - "jsonb", - encoder=encoder, - decoder=decoder, - schema="pg_catalog", - format="binary", - ), - ) - dbapi_connection.await_( - dbapi_connection.driver_connection.set_type_codec( - "json", - encoder=encoder, - decoder=decoder, - schema="pg_catalog", - format="binary", - ), - ) - - -config = SQLAlchemyAsyncConfig( - session_dependency_key=constants.DB_SESSION_DEPENDENCY_KEY, - engine_instance=engine, - session_maker=async_session_factory, - before_send_handler=autocommit_before_send_handler, - alembic_config=AlembicAsyncConfig( - version_table_name=settings.db.MIGRATION_DDL_VERSION_TABLE, - script_config=settings.db.MIGRATION_CONFIG, - script_location=settings.db.MIGRATION_PATH, - ), -) - -plugin = SQLAlchemyInitPlugin(config=config) - - -@asynccontextmanager -async def session() -> AsyncIterator[AsyncSession]: - """Use this to get a database session where you can't in Litestar. - - .. deprecated:: Use :meth:`config.get_session ` - instead. Introduced in `Advanced Alchemy v0.5.2 `_. - - Returns: - AsyncIterator[AsyncSession] - """ - async with async_session_factory() as session: - yield session diff --git a/byte_bot/server/lib/db/migrations/__init__.py b/byte_bot/server/lib/db/migrations/__init__.py deleted file mode 100644 index 7da68b29..00000000 --- a/byte_bot/server/lib/db/migrations/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""DB Migrations.""" diff --git a/byte_bot/server/lib/db/migrations/env.py b/byte_bot/server/lib/db/migrations/env.py deleted file mode 100644 index 5e77ab9c..00000000 --- a/byte_bot/server/lib/db/migrations/env.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Alembic environment for migrations.""" - -from __future__ import annotations - -import asyncio -from typing import TYPE_CHECKING, cast - -from advanced_alchemy.base import orm_registry -from alembic import context -from alembic.autogenerate import rewriter -from alembic.operations import ops -from sqlalchemy import Column, pool -from sqlalchemy.ext.asyncio import AsyncEngine, async_engine_from_config - -if TYPE_CHECKING: - from advanced_alchemy.alembic.commands import AlembicCommandConfig - from alembic.runtime.environment import EnvironmentContext - from sqlalchemy.engine import Connection - -__all__ = ["do_run_migrations", "run_migrations_offline", "run_migrations_online"] - -# this is the Alembic Config object, which provides -# access to the values within the .ini file in use. -config: AlembicCommandConfig = context.config # type: ignore # noqa: PGH003 - -# add your model's MetaData object here -# for 'autogenerate' support -target_metadata = orm_registry.metadata - -# other values from the config, defined by the needs of env.py, -# can be acquired: -# ... etc. - -writer = rewriter.Rewriter() - - -@writer.rewrites(ops.CreateTableOp) -def order_columns( - context: EnvironmentContext, # noqa: ARG001 - revision: tuple[str, ...], # noqa: ARG001 - op: ops.CreateTableOp, -) -> ops.CreateTableOp: - """Orders ID first and the audit columns at the end.""" - special_names = {"id": -100, "sa_orm_sentinel": 3001, "created_at": 3002, "updated_at": 3003} - cols_by_key = [ - ( - special_names.get(col.key, index) if isinstance(col, Column) else 2000, - col.copy(), # type: ignore[attr-defined] - ) - for index, col in enumerate(op.columns) - ] - columns = [col for _, col in sorted(cols_by_key, key=lambda entry: entry[0])] - return ops.CreateTableOp( - op.table_name, - columns, - schema=op.schema, - # TODO: Remove when https://github.com/sqlalchemy/alembic/issues/1193 is fixed - _namespace_metadata=op._namespace_metadata, - **op.kw, - ) - - -def run_migrations_offline() -> None: - """Run migrations in 'offline' mode. - - This configures the context with just a URL - and not an Engine, though an Engine is acceptable - here as well. By skipping the Engine creation - we don't even need a DBAPI to be available. - - Calls to context.execute() here emit the given string to the - script output. - """ - context.configure( - url=config.db_url, - target_metadata=target_metadata, - literal_binds=True, - dialect_opts={"paramstyle": "named"}, - compare_type=config.compare_type, - version_table=config.version_table_name, - version_table_pk=config.version_table_pk, - user_module_prefix=config.user_module_prefix, - render_as_batch=config.render_as_batch, - process_revision_directives=writer, - ) - - with context.begin_transaction(): - context.run_migrations() - - -def do_run_migrations(connection: Connection) -> None: - """Run migrations.""" - context.configure( - connection=connection, - target_metadata=target_metadata, - compare_type=config.compare_type, - version_table=config.version_table_name, - version_table_pk=config.version_table_pk, - user_module_prefix=config.user_module_prefix, - render_as_batch=config.render_as_batch, - process_revision_directives=writer, - ) - - with context.begin_transaction(): - context.run_migrations() - - -async def run_migrations_online() -> None: - """Run migrations in 'online' mode. - - In this scenario we need to create an Engine and associate a - connection with the context. - """ - configuration = config.get_section(config.config_ini_section) or {} - configuration["sqlalchemy.url"] = config.db_url - - connectable = cast( - "AsyncEngine", - config.engine - or async_engine_from_config( - configuration, - prefix="sqlalchemy.", - poolclass=pool.NullPool, - future=True, - ), - ) - if connectable is None: - msg = ( - "Could not get engine from config. Please ensure your `alembic.ini` according to the official Alembic " - "documentation." - ) - raise RuntimeError( - msg, - ) - - async with connectable.connect() as connection: - await connection.run_sync(do_run_migrations) - - await connectable.dispose() - - -if context.is_offline_mode(): - run_migrations_offline() -else: - asyncio.run(run_migrations_online()) diff --git a/byte_bot/server/lib/db/migrations/script.py.mako b/byte_bot/server/lib/db/migrations/script.py.mako deleted file mode 100644 index 0268a212..00000000 --- a/byte_bot/server/lib/db/migrations/script.py.mako +++ /dev/null @@ -1,61 +0,0 @@ -# type: ignore -"""${message} - -Revision ID: ${up_revision} -Revises: ${down_revision | comma,n} -Create Date: ${create_date} - -""" -from __future__ import annotations - -import warnings -from typing import TYPE_CHECKING - -import sqlalchemy as sa -from alembic import op -from advanced_alchemy.types import GUID, ORA_JSONB, DateTimeUTC -from sqlalchemy import Text # noqa: F401 -${imports if imports else ""} -if TYPE_CHECKING: - from collections.abc import Sequence - -__all__ = ["downgrade", "upgrade", "schema_upgrades", "schema_downgrades", "data_upgrades", "data_downgrades"] - -sa.GUID = GUID -sa.DateTimeUTC = DateTimeUTC -sa.ORA_JSONB = ORA_JSONB - -# revision identifiers, used by Alembic. -revision = ${repr(up_revision)} -down_revision = ${repr(down_revision)} -branch_labels = ${repr(branch_labels)} -depends_on = ${repr(depends_on)} - - -def upgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - schema_upgrades() - data_upgrades() - -def downgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - data_downgrades() - schema_downgrades() - -def schema_upgrades() -> None: - """schema upgrade migrations go here.""" - ${upgrades if upgrades else "pass"} - -def schema_downgrades() -> None: - """schema downgrade migrations go here.""" - ${downgrades if downgrades else "pass"} - -def data_upgrades() -> None: - """Add any optional data upgrade migrations here!""" - -def data_downgrades() -> None: - """Add any optional data downgrade migrations here!""" diff --git a/byte_bot/server/lib/db/migrations/versions/002_simplify_models.py b/byte_bot/server/lib/db/migrations/versions/002_simplify_models.py deleted file mode 100644 index b7b0b1f9..00000000 --- a/byte_bot/server/lib/db/migrations/versions/002_simplify_models.py +++ /dev/null @@ -1,244 +0,0 @@ -# type: ignore -""" - -Revision ID: feebdacfdd91 -Revises: 43165a559e89 -Create Date: 2023-12-18 03:20:32.171148+00:00 - -""" - -from __future__ import annotations - -import warnings - -import sqlalchemy as sa -from advanced_alchemy.types import GUID, ORA_JSONB, DateTimeUTC -from alembic import op -from sqlalchemy import Text # noqa: F401 -from sqlalchemy.dialects import postgresql - -__all__ = ["data_downgrades", "data_upgrades", "downgrade", "schema_downgrades", "schema_upgrades", "upgrade"] - -sa.GUID = GUID -sa.DateTimeUTC = DateTimeUTC -sa.ORA_JSONB = ORA_JSONB - -# revision identifiers, used by Alembic. -revision = "feebdacfdd91" -down_revision = "43165a559e89" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - schema_upgrades() - data_upgrades() - - -def downgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - data_downgrades() - schema_downgrades() - - -def schema_upgrades() -> None: - """schema upgrade migrations go here.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "guild", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.Column("guild_name", sa.String(length=100), nullable=False), - sa.Column("prefix", sa.String(length=5), server_default="!", nullable=False), - sa.Column("help_channel_id", sa.BigInteger(), nullable=True), - sa.Column("sync_label", sa.String(), nullable=True), - sa.Column("issue_linking", sa.Boolean(), nullable=False), - sa.Column("comment_linking", sa.Boolean(), nullable=False), - sa.Column("pep_linking", sa.Boolean(), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id", name=op.f("pk_guild")), - ) - with op.batch_alter_table("guild", schema=None) as batch_op: - batch_op.create_index(batch_op.f("ix_guild_guild_id"), ["guild_id"], unique=True) - - op.create_table( - "allowed_users", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.Column("user_id", sa.GUID(length=16), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["guild_id"], ["guild.guild_id"], name=op.f("fk_allowed_users_guild_id_guild"), ondelete="cascade" - ), - sa.ForeignKeyConstraint( - ["user_id"], ["user.id"], name=op.f("fk_allowed_users_user_id_user"), ondelete="cascade" - ), - sa.PrimaryKeyConstraint("id", name=op.f("pk_allowed_users")), - sa.UniqueConstraint("guild_id", "user_id", name=op.f("uq_allowed_users_guild_id")), - ) - op.create_table( - "so_tags", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.GUID(length=16), nullable=False), - sa.Column("tag_name", sa.String(length=50), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.ForeignKeyConstraint(["guild_id"], ["guild.id"], name=op.f("fk_so_tags_guild_id_guild"), ondelete="cascade"), - sa.PrimaryKeyConstraint("id", name=op.f("pk_so_tags")), - sa.UniqueConstraint("guild_id", "tag_name", name=op.f("uq_so_tags_guild_id")), - ) - op.drop_table("guild_sotags_config") - op.drop_table("guild_github_config") - op.drop_table("guild_allowed_users_config") - op.drop_table("sotag_config") - op.drop_table("guild_config") - with op.batch_alter_table("github_config", schema=None) as batch_op: - batch_op.add_column(sa.Column("guild_id", sa.GUID(length=16), nullable=False)) - batch_op.create_foreign_key( - batch_op.f("fk_github_config_guild_id_guild"), "guild", ["guild_id"], ["id"], ondelete="cascade" - ) - batch_op.create_table_comment("GitHub configuration for a guild.", existing_comment=None) - - with op.batch_alter_table("user", schema=None) as batch_op: - batch_op.create_table_comment("A user.", existing_comment=None) - - # ### end Alembic commands ### - - -def schema_downgrades() -> None: - """schema downgrade migrations go here.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("user", schema=None) as batch_op: - batch_op.drop_table_comment(existing_comment="A user.") - - with op.batch_alter_table("github_config", schema=None) as batch_op: - batch_op.drop_table_comment(existing_comment="GitHub configuration for a guild.") - batch_op.drop_constraint(batch_op.f("fk_github_config_guild_id_guild"), type_="foreignkey") - batch_op.drop_column("guild_id") - - op.create_table( - "sotag_config", - sa.Column("id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("tag_name", sa.VARCHAR(length=50), autoincrement=False, nullable=False), - sa.Column("sa_orm_sentinel", sa.INTEGER(), autoincrement=False, nullable=True), - sa.PrimaryKeyConstraint("id", name="pk_sotag_config"), - postgresql_ignore_search_path=False, - ) - op.create_table( - "guild_allowed_users_config", - sa.Column("id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("guild_id", sa.BIGINT(), autoincrement=False, nullable=False), - sa.Column("user_id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("sa_orm_sentinel", sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint( - ["guild_id"], - ["guild_config.guild_id"], - name="fk_guild_allowed_users_config_guild_id_guild_config", - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["user_id"], ["user.id"], name="fk_guild_allowed_users_config_user_id_user", ondelete="CASCADE" - ), - sa.PrimaryKeyConstraint("id", name="pk_guild_allowed_users_config"), - sa.UniqueConstraint("guild_id", "user_id", name="uq_guild_allowed_users_config_guild_id"), - ) - op.create_table( - "guild_config", - sa.Column("id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("guild_id", sa.BIGINT(), autoincrement=False, nullable=False), - sa.Column("guild_name", sa.VARCHAR(length=100), autoincrement=False, nullable=False), - sa.Column( - "prefix", - sa.VARCHAR(length=5), - server_default=sa.text("'!'::character varying"), - autoincrement=False, - nullable=False, - ), - sa.Column("help_channel_id", sa.BIGINT(), autoincrement=False, nullable=False), - sa.Column("sync_label", sa.VARCHAR(), autoincrement=False, nullable=True), - sa.Column("issue_linking", sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column("comment_linking", sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column("pep_linking", sa.BOOLEAN(), autoincrement=False, nullable=False), - sa.Column("sa_orm_sentinel", sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.PrimaryKeyConstraint("id", name="pk_guild_config"), - postgresql_ignore_search_path=False, - ) - with op.batch_alter_table("guild_config", schema=None) as batch_op: - batch_op.create_index("ix_guild_config_help_channel_id", ["help_channel_id"], unique=False) - batch_op.create_index("ix_guild_config_guild_id", ["guild_id"], unique=False) - - op.create_table( - "guild_github_config", - sa.Column("id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("guild_id", sa.BIGINT(), autoincrement=False, nullable=False), - sa.Column("github_config_id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("sa_orm_sentinel", sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint( - ["github_config_id"], - ["github_config.id"], - name="fk_guild_github_config_github_config_id_github_config", - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["guild_id"], - ["guild_config.guild_id"], - name="fk_guild_github_config_guild_id_guild_config", - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id", name="pk_guild_github_config"), - sa.UniqueConstraint("guild_id", "github_config_id", name="uq_guild_github_config_guild_id"), - ) - op.create_table( - "guild_sotags_config", - sa.Column("id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("guild_id", sa.BIGINT(), autoincrement=False, nullable=False), - sa.Column("sotag_config_id", sa.UUID(), autoincrement=False, nullable=False), - sa.Column("sa_orm_sentinel", sa.INTEGER(), autoincrement=False, nullable=True), - sa.Column("created_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.Column("updated_at", postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=False), - sa.ForeignKeyConstraint( - ["guild_id"], - ["guild_config.guild_id"], - name="fk_guild_sotags_config_guild_id_guild_config", - ondelete="CASCADE", - ), - sa.ForeignKeyConstraint( - ["sotag_config_id"], - ["sotag_config.id"], - name="fk_guild_sotags_config_sotag_config_id_sotag_config", - ondelete="CASCADE", - ), - sa.PrimaryKeyConstraint("id", name="pk_guild_sotags_config"), - sa.UniqueConstraint("guild_id", "sotag_config_id", name="uq_guild_sotags_config_guild_id"), - ) - op.drop_table("so_tags") - op.drop_table("allowed_users") - with op.batch_alter_table("guild", schema=None) as batch_op: - batch_op.drop_index(batch_op.f("ix_guild_guild_id")) - - op.drop_table("guild") - # ### end Alembic commands ### - - -def data_upgrades() -> None: - """Add any optional data upgrade migrations here!""" - - -def data_downgrades() -> None: - """Add any optional data downgrade migrations here!""" diff --git a/byte_bot/server/lib/db/migrations/versions/003_forum_models.py b/byte_bot/server/lib/db/migrations/versions/003_forum_models.py deleted file mode 100644 index dd49bd2d..00000000 --- a/byte_bot/server/lib/db/migrations/versions/003_forum_models.py +++ /dev/null @@ -1,107 +0,0 @@ -# type: ignore -""" -Revision ID: 73a26ceab2c4 -Revises: feebdacfdd91 -Create Date: 2024-03-10 22:30:54.150566+00:00 -""" - -from __future__ import annotations - -import warnings - -import sqlalchemy as sa -from advanced_alchemy.types import GUID, ORA_JSONB, DateTimeUTC -from alembic import op -from sqlalchemy import Text # noqa: F401 - -__all__ = ["data_downgrades", "data_upgrades", "downgrade", "schema_downgrades", "schema_upgrades", "upgrade"] - -sa.GUID = GUID -sa.DateTimeUTC = DateTimeUTC -sa.ORA_JSONB = ORA_JSONB - -# revision identifiers, used by Alembic. -revision = "73a26ceab2c4" -down_revision = "feebdacfdd91" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - schema_upgrades() - data_upgrades() - - -def downgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - data_downgrades() - schema_downgrades() - - -def schema_upgrades() -> None: - """schema upgrade migrations go here.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "forum_config", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.GUID(length=16), nullable=False), - sa.Column("help_forum", sa.Boolean(), nullable=False), - sa.Column("help_forum_category", sa.String(), nullable=True), - sa.Column("help_thread_auto_close", sa.Boolean(), nullable=False), - sa.Column("help_thread_auto_close_days", sa.Integer(), nullable=True), - sa.Column("help_thread_notify", sa.Boolean(), nullable=False), - sa.Column("help_thread_notify_roles", sa.String(), nullable=True), - sa.Column("help_thread_notify_days", sa.Integer(), nullable=True), - sa.Column("showcase_forum", sa.Boolean(), nullable=False), - sa.Column("showcase_forum_category", sa.String(), nullable=True), - sa.Column("showcase_thread_auto_close", sa.Boolean(), nullable=False), - sa.Column("showcase_thread_auto_close_days", sa.Integer(), nullable=True), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["guild_id"], ["guild.id"], name=op.f("fk_forum_config_guild_id_guild"), ondelete="cascade" - ), - sa.PrimaryKeyConstraint("id", name=op.f("pk_forum_config")), - ) - with op.batch_alter_table("allowed_users", schema=None) as batch_op: - batch_op.create_table_comment("Configuration for allowed users in a Discord guild.", existing_comment=None) - - with op.batch_alter_table("guild", schema=None) as batch_op: - batch_op.add_column(sa.Column("showcase_channel_id", sa.BigInteger(), nullable=True)) - batch_op.create_table_comment("Configuration for a Discord guild.", existing_comment=None) - - with op.batch_alter_table("so_tags", schema=None) as batch_op: - batch_op.create_table_comment("Configuration for a Discord guild's Stack Overflow tags.", existing_comment=None) - - # ### end Alembic commands ### - - -def schema_downgrades() -> None: - """schema downgrade migrations go here.""" - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("so_tags", schema=None) as batch_op: - batch_op.drop_table_comment(existing_comment="Configuration for a Discord guild's Stack Overflow tags.") - - with op.batch_alter_table("guild", schema=None) as batch_op: - batch_op.drop_table_comment(existing_comment="Configuration for a Discord guild.") - batch_op.drop_column("showcase_channel_id") - - with op.batch_alter_table("allowed_users", schema=None) as batch_op: - batch_op.drop_table_comment(existing_comment="Configuration for allowed users in a Discord guild.") - - op.drop_table("forum_config") - # ### end Alembic commands ### - - -def data_upgrades() -> None: - """Add any optional data upgrade migrations here!""" - - -def data_downgrades() -> None: - """Add any optional data downgrade migrations here!""" diff --git a/byte_bot/server/lib/db/migrations/versions/004_snowflake_fixes.py b/byte_bot/server/lib/db/migrations/versions/004_snowflake_fixes.py deleted file mode 100644 index d81713af..00000000 --- a/byte_bot/server/lib/db/migrations/versions/004_snowflake_fixes.py +++ /dev/null @@ -1,178 +0,0 @@ -# type: ignore -""" - -Revision ID: f32ee278015d -Revises: 73a26ceab2c4 -Create Date: 2024-03-11 05:17:53.372688+00:00 - -""" - -from __future__ import annotations - -import warnings - -import sqlalchemy as sa -from advanced_alchemy.types import GUID, ORA_JSONB, DateTimeUTC -from alembic import op -from sqlalchemy import Text # noqa: F401 - -__all__ = ["data_downgrades", "data_upgrades", "downgrade", "schema_downgrades", "schema_upgrades", "upgrade"] - -sa.GUID = GUID -sa.DateTimeUTC = DateTimeUTC -sa.ORA_JSONB = ORA_JSONB - -# revision identifiers, used by Alembic. -revision = "f32ee278015d" -down_revision = "73a26ceab2c4" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - schema_upgrades() - data_upgrades() - - -def downgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - data_downgrades() - schema_downgrades() - - -def schema_upgrades() -> None: - """schema upgrade migrations go here.""" - from sqlalchemy import inspect - - inspector = inspect(op.get_bind()) - if "forum_config" in inspector.get_table_names(): - op.drop_table("forum_config") - if "github_config" in inspector.get_table_names(): - op.drop_table("github_config") - if "so_tags" in inspector.get_table_names(): - op.drop_table("so_tags") - - # Recreate sub-tables with original schema - op.create_table( - "forum_config", - sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(["guild_id"], ["guild.guild_id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - - op.create_table( - "github_config", - sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(["guild_id"], ["guild.guild_id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - - op.create_table( - "so_tags_config", - sa.Column("id", sa.BigInteger(), autoincrement=True, nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.ForeignKeyConstraint(["guild_id"], ["guild.guild_id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - - # ### commands auto generated by Alembic - please adjust! ### - with op.batch_alter_table("forum_config", schema=None) as batch_op: - batch_op.add_column(sa.Column("help_forum", sa.Boolean(), nullable=False)) - batch_op.add_column(sa.Column("help_forum_category", sa.String(), nullable=True)) - batch_op.add_column(sa.Column("help_thread_auto_close", sa.Boolean(), nullable=False)) - batch_op.add_column(sa.Column("help_thread_auto_close_days", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("help_thread_notify", sa.Boolean(), nullable=False)) - batch_op.add_column(sa.Column("help_thread_notify_roles", sa.String(), nullable=True)) - batch_op.add_column(sa.Column("help_thread_notify_days", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("showcase_forum", sa.Boolean(), nullable=False)) - batch_op.add_column(sa.Column("showcase_forum_category", sa.String(), nullable=True)) - batch_op.add_column(sa.Column("showcase_thread_auto_close", sa.Boolean(), nullable=False)) - batch_op.add_column(sa.Column("showcase_thread_auto_close_days", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False)) - batch_op.add_column(sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False)) - batch_op.drop_constraint("fk_forum_config_guild_id_guild", type_="foreignkey") - batch_op.create_foreign_key( - batch_op.f("fk_forum_config_guild_id_guild"), "guild", ["guild_id"], ["guild_id"], ondelete="cascade" - ) - batch_op.create_table_comment("Forum configuration for a guild.", existing_comment=None) - - with op.batch_alter_table("github_config", schema=None) as batch_op: - batch_op.add_column(sa.Column("discussion_sync", sa.Boolean(), nullable=False)) - batch_op.add_column(sa.Column("github_organization", sa.String(), nullable=True)) - batch_op.add_column(sa.Column("github_repository", sa.String(), nullable=True)) - batch_op.add_column(sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False)) - batch_op.add_column(sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False)) - batch_op.drop_constraint("fk_github_config_guild_id_guild", type_="foreignkey") - batch_op.create_foreign_key( - batch_op.f("fk_github_config_guild_id_guild"), "guild", ["guild_id"], ["guild_id"], ondelete="cascade" - ) - batch_op.create_table_comment("GitHub configuration for a guild.", existing_comment=None) - - with op.batch_alter_table("so_tags_config", schema=None) as batch_op: - batch_op.add_column(sa.Column("tag_name", sa.String(length=50), nullable=False)) - batch_op.add_column(sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True)) - batch_op.add_column(sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False)) - batch_op.add_column(sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False)) - batch_op.create_unique_constraint(batch_op.f("uq_so_tags_config_guild_id"), ["guild_id", "tag_name"]) - batch_op.drop_constraint("fk_so_tags_config_guild_id_guild", type_="foreignkey") - batch_op.create_foreign_key( - batch_op.f("fk_so_tags_config_guild_id_guild"), "guild", ["guild_id"], ["guild_id"], ondelete="cascade" - ) - batch_op.create_table_comment("Configuration for a Discord guild's Stack Overflow tags.", existing_comment=None) - - # ### end Alembic commands ### - - -def schema_downgrades() -> None: - """if we have to downgrade we're fucked.""" - from sqlalchemy import inspect - - inspector = inspect(op.get_bind()) - if "forum_config" in inspector.get_table_names(): - op.drop_table("forum_config") - if "github_config" in inspector.get_table_names(): - op.drop_table("github_config") - if "so_tags_config" in inspector.get_table_names(): - op.drop_table("so_tags_config") - - # Recreate sub-tables with original schema - op.create_table( - "forum_config", - sa.Column("id", sa.UUID(), nullable=False), - sa.Column("guild_id", sa.UUID(), nullable=False), - sa.ForeignKeyConstraint(["guild_id"], ["guild.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - - op.create_table( - "github_config", - sa.Column("id", sa.UUID(), nullable=False), - sa.Column("guild_id", sa.UUID(), nullable=False), - sa.ForeignKeyConstraint(["guild_id"], ["guild.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - - op.create_table( - "so_tags", - sa.Column("id", sa.UUID(), nullable=False), - sa.Column("guild_id", sa.UUID(), nullable=False), - sa.ForeignKeyConstraint(["guild_id"], ["guild.id"], ondelete="CASCADE"), - sa.PrimaryKeyConstraint("id"), - ) - - -def data_upgrades() -> None: - """Add any optional data upgrade migrations here!""" - - -def data_downgrades() -> None: - """Add any optional data downgrade migrations here!""" diff --git a/byte_bot/server/lib/db/migrations/versions/__init__.py b/byte_bot/server/lib/db/migrations/versions/__init__.py deleted file mode 100644 index 1edc01bd..00000000 --- a/byte_bot/server/lib/db/migrations/versions/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""DB migration versions.""" diff --git a/byte_bot/server/lib/db/migrations/versions/initial.py b/byte_bot/server/lib/db/migrations/versions/initial.py deleted file mode 100644 index 94d42503..00000000 --- a/byte_bot/server/lib/db/migrations/versions/initial.py +++ /dev/null @@ -1,189 +0,0 @@ -# type: ignore -"""Revision ID: 43165a559e89 -Revises: -Create Date: 2023-11-26 20:11:48.777676+00:00 - -""" - -from __future__ import annotations - -import warnings - -import sqlalchemy as sa -from advanced_alchemy.types import GUID, ORA_JSONB, DateTimeUTC -from alembic import op -from sqlalchemy import Text # noqa: F401 - -__all__ = ["data_downgrades", "data_upgrades", "downgrade", "schema_downgrades", "schema_upgrades", "upgrade"] - -sa.GUID = GUID -sa.DateTimeUTC = DateTimeUTC -sa.ORA_JSONB = ORA_JSONB - -# revision identifiers, used by Alembic. -revision = "43165a559e89" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - schema_upgrades() - data_upgrades() - - -def downgrade() -> None: - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=UserWarning) - with op.get_context().autocommit_block(): - data_downgrades() - schema_downgrades() - - -def schema_upgrades() -> None: - """Schema upgrade migrations go here.""" - # ### commands auto generated by Alembic - please adjust! ### - op.create_table( - "github_config", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("discussion_sync", sa.Boolean(), nullable=False), - sa.Column("github_organization", sa.String(), nullable=True), - sa.Column("github_repository", sa.String(), nullable=True), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id", name=op.f("pk_github_config")), - ) - op.create_table( - "guild_config", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.Column("guild_name", sa.String(length=100), nullable=False), - sa.Column("prefix", sa.String(length=5), server_default="!", nullable=False), - sa.Column("help_channel_id", sa.BigInteger(), nullable=False), - sa.Column("sync_label", sa.String(), nullable=True), - sa.Column("issue_linking", sa.Boolean(), nullable=False), - sa.Column("comment_linking", sa.Boolean(), nullable=False), - sa.Column("pep_linking", sa.Boolean(), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id", name=op.f("pk_guild_config")), - ) - with op.batch_alter_table("guild_config", schema=None) as batch_op: - batch_op.create_index(batch_op.f("ix_guild_config_guild_id"), ["guild_id"], unique=True) - batch_op.create_index(batch_op.f("ix_guild_config_help_channel_id"), ["help_channel_id"], unique=False) - - op.create_table( - "sotag_config", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("tag_name", sa.String(length=50), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.PrimaryKeyConstraint("id", name=op.f("pk_sotag_config")), - ) - op.create_table( - "user", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("name", sa.String(length=100), nullable=False), - sa.Column("avatar_url", sa.String(), nullable=True), - sa.Column("discriminator", sa.String(length=4), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.PrimaryKeyConstraint("id", name=op.f("pk_user")), - ) - op.create_table( - "guild_allowed_users_config", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.Column("user_id", sa.GUID(length=16), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["guild_id"], - ["guild_config.guild_id"], - name=op.f("fk_guild_allowed_users_config_guild_id_guild_config"), - ondelete="cascade", - ), - sa.ForeignKeyConstraint( - ["user_id"], ["user.id"], name=op.f("fk_guild_allowed_users_config_user_id_user"), ondelete="cascade" - ), - sa.PrimaryKeyConstraint("id", name=op.f("pk_guild_allowed_users_config")), - sa.UniqueConstraint("guild_id", "user_id", name=op.f("uq_guild_allowed_users_config_guild_id")), - ) - op.create_table( - "guild_github_config", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.Column("github_config_id", sa.GUID(length=16), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["github_config_id"], - ["github_config.id"], - name=op.f("fk_guild_github_config_github_config_id_github_config"), - ondelete="cascade", - ), - sa.ForeignKeyConstraint( - ["guild_id"], - ["guild_config.guild_id"], - name=op.f("fk_guild_github_config_guild_id_guild_config"), - ondelete="cascade", - ), - sa.PrimaryKeyConstraint("id", name=op.f("pk_guild_github_config")), - sa.UniqueConstraint("guild_id", "github_config_id", name=op.f("uq_guild_github_config_guild_id")), - ) - op.create_table( - "guild_sotags_config", - sa.Column("id", sa.GUID(length=16), nullable=False), - sa.Column("guild_id", sa.BigInteger(), nullable=False), - sa.Column("sotag_config_id", sa.GUID(length=16), nullable=False), - sa.Column("sa_orm_sentinel", sa.Integer(), nullable=True), - sa.Column("created_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.Column("updated_at", sa.DateTimeUTC(timezone=True), nullable=False), - sa.ForeignKeyConstraint( - ["guild_id"], - ["guild_config.guild_id"], - name=op.f("fk_guild_sotags_config_guild_id_guild_config"), - ondelete="cascade", - ), - sa.ForeignKeyConstraint( - ["sotag_config_id"], - ["sotag_config.id"], - name=op.f("fk_guild_sotags_config_sotag_config_id_sotag_config"), - ondelete="cascade", - ), - sa.PrimaryKeyConstraint("id", name=op.f("pk_guild_sotags_config")), - sa.UniqueConstraint("guild_id", "sotag_config_id", name=op.f("uq_guild_sotags_config_guild_id")), - ) - # ### end Alembic commands ### - - -def schema_downgrades() -> None: - """Schema downgrade migrations go here.""" - # ### commands auto generated by Alembic - please adjust! ### - op.drop_table("guild_sotags_config") - op.drop_table("guild_github_config") - op.drop_table("guild_allowed_users_config") - op.drop_table("user") - op.drop_table("sotag_config") - with op.batch_alter_table("guild_config", schema=None) as batch_op: - batch_op.drop_index(batch_op.f("ix_guild_config_help_channel_id")) - batch_op.drop_index(batch_op.f("ix_guild_config_guild_id")) - - op.drop_table("guild_config") - op.drop_table("github_config") - # ### end Alembic commands ### - - -def data_upgrades() -> None: - """Add any optional data upgrade migrations here!""" - - -def data_downgrades() -> None: - """Add any optional data downgrade migrations here!""" diff --git a/byte_bot/server/lib/db/orm.py b/byte_bot/server/lib/db/orm.py deleted file mode 100644 index fbe54416..00000000 --- a/byte_bot/server/lib/db/orm.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Application ORM configuration.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from advanced_alchemy.base import UUIDAuditBase as TimestampedDatabaseModel -from advanced_alchemy.base import UUIDBase as DatabaseModel -from advanced_alchemy.base import orm_registry -from advanced_alchemy.mixins.audit import AuditColumns - -if TYPE_CHECKING: - from advanced_alchemy.repository.typing import ModelT -from sqlalchemy import String -from sqlalchemy.orm import ( - Mapped, - declarative_mixin, - mapped_column, -) - -__all__ = ["AuditColumns", "DatabaseModel", "SlugKey", "TimestampedDatabaseModel", "model_from_dict", "orm_registry"] - - -@declarative_mixin -class SlugKey: - """Slug unique Field Model Mixin.""" - - __abstract__ = True - slug: Mapped[str] = mapped_column(String(length=100), index=True, nullable=False, unique=True, sort_order=-9) - - -def model_from_dict(model: ModelT, **kwargs: Any) -> ModelT: - """Return ORM Object from Dictionary.""" - data = {} - for column in model.__table__.columns: - column_val = kwargs.get(column.name, None) - if column_val is not None: - data[column.name] = column_val - return model(**data) # type: ignore[no-any-return, operator] diff --git a/byte_bot/server/lib/dependencies.py b/byte_bot/server/lib/dependencies.py deleted file mode 100644 index 49a2b15a..00000000 --- a/byte_bot/server/lib/dependencies.py +++ /dev/null @@ -1,254 +0,0 @@ -"""Application dependency providers.""" - -from __future__ import annotations - -from datetime import datetime -from typing import Literal -from uuid import UUID - -from advanced_alchemy.filters import ( - BeforeAfter, - CollectionFilter, - FilterTypes, - LimitOffset, - OrderBy, - SearchFilter, -) -from litestar.di import Provide -from litestar.params import Dependency, Parameter - -from byte_bot.server.lib import constants - -__all__ = [ - "BeforeAfter", - "CollectionFilter", - "FilterTypes", - "LimitOffset", - "OrderBy", - "SearchFilter", - "create_collection_dependencies", - "provide_created_filter", - "provide_filter_dependencies", - "provide_id_filter", - "provide_limit_offset_pagination", - "provide_order_by", - "provide_search_filter", - "provide_updated_filter", -] - -DTorNone = datetime | None -"""Aggregate type alias of the types supported for datetime filtering.""" -StringOrNone = str | None -"""Aggregate type alias of the types supported for string filtering.""" -UuidOrNone = UUID | None -"""Aggregate type alias of the types supported for UUID filtering.""" -BooleanOrNone = bool | None -"""Aggregate type alias of the types supported for boolean filtering.""" -SortOrderOrNone = Literal["asc", "desc"] | None -"""Aggregate type alias of the types supported for collection filtering.""" - -FILTERS_DEPENDENCY_KEY = "filters" -"""Dependency key for filters.""" -CREATED_FILTER_DEPENDENCY_KEY = "created_filter" -"""Dependency key for created filter.""" -ID_FILTER_DEPENDENCY_KEY = "id_filter" -"""Dependency key for id filter.""" -LIMIT_OFFSET_DEPENDENCY_KEY = "limit_offset" -"""Dependency key for limit/offset pagination.""" -UPDATED_FILTER_DEPENDENCY_KEY = "updated_filter" -"""Dependency key for updated filter.""" -ORDER_BY_DEPENDENCY_KEY = "order_by" -"""Dependency key for order by.""" -SEARCH_FILTER_DEPENDENCY_KEY = "search_filter" -"""Dependency key for search filter.""" -ACTIVE_FILTER_DEPENDENCY_KEY = "active_filter" -"""Dependency key for active filter.""" - - -def provide_active_filter( - is_active: bool = Parameter(title="Is active filter", query="Active", default=True, required=False), -) -> bool: - """Return type consumed by ``Repository.filter_on_field()``. - - Arguments: - is_active(bool): Filter for active records. - - Returns: - bool - """ - return is_active - - -def provide_id_filter( - ids: list[UUID] | None = Parameter(query="ids", default=None, required=False), -) -> CollectionFilter[UUID]: - """Return type consumed by ``Repository.filter_in_collection()``. - - Arguments: - ids(list[UUID] | None): List of IDs to filter on. - - Returns: - CollectionFilter[UUID] - """ - return CollectionFilter(field_name="id", values=ids or []) - - -def provide_created_filter( - before: DTorNone = Parameter(query="createdBefore", default=None, required=False), - after: DTorNone = Parameter(query="createdAfter", default=None, required=False), -) -> BeforeAfter: - """Return type consumed by ``Repository.filter_on_datetime_field()``. - - Arguments: - before(datetime | None): Filter for records created before this date/time. - after(datetime | None): Filter for records created after this date/time. - - Returns: - BeforeAfter - """ - return BeforeAfter("created_at", before, after) - - -def provide_search_filter( - field: StringOrNone = Parameter(title="Field to search", query="searchField", default=None, required=False), - search: StringOrNone = Parameter(title="Field to search", query="searchString", default=None, required=False), - ignore_case: BooleanOrNone = Parameter( - bool, - title="Search should be case sensitive", - query="searchIgnoreCase", - default=None, - required=False, - ), -) -> SearchFilter: - """Add offset/limit pagination. - - Return type consumed by ``Repository.apply_search_filter()``. - - Arguments: - field (str | None): Field to search. - search (str | None): String to search for. - ignore_case (bool | None): Whether to ignore case when searching. - """ - return SearchFilter(field_name=field, value=search, ignore_case=ignore_case or False) # type: ignore[arg-type] - - -def provide_order_by( - field_name: StringOrNone = Parameter(title="Order by field", query="orderBy", default=None, required=False), - sort_order: SortOrderOrNone = Parameter(title="Field to search", query="sortOrder", default="desc", required=False), -) -> OrderBy: - """Add offset/limit pagination. - - Return type consumed by ``Repository.apply_order_by()``. - - Arguments: - field_name(int): LIMIT to apply to select. - sort_order(int): OFFSET to apply to select. - - Returns: - OrderBy - """ - return OrderBy(field_name=field_name, sort_order=sort_order) # type: ignore[arg-type] - - -def provide_updated_filter( - before: DTorNone = Parameter(query="updatedBefore", default=None, required=False), - after: DTorNone = Parameter(query="updatedAfter", default=None, required=False), -) -> BeforeAfter: - """Add updated filter. - - Return type consumed by `Repository.filter_on_datetime_field()`. - - Arguments: - before(datetime | None): Filter for records updated before this date/time. - after(datetime | None): Filter for records updated after this date/time. - - Returns: - BeforeAfter - """ - return BeforeAfter("updated_at", before, after) - - -def provide_limit_offset_pagination( - current_page: int = Parameter(ge=1, query="currentPage", default=1, required=False), - page_size: int = Parameter( - query="pageSize", - ge=1, - default=constants.DEFAULT_PAGINATION_SIZE, - required=False, - ), -) -> LimitOffset: - """Add offset/limit pagination. - - Return type consumed by ``Repository.apply_limit_offset_pagination()``. - - Arguments: - current_page(int): Current page of results. - page_size(int): Number of results per page. - - Returns: - LimitOffset - """ - return LimitOffset(page_size, page_size * (current_page - 1)) - - -def provide_filter_dependencies( - created_filter: BeforeAfter = Dependency(skip_validation=True), - updated_filter: BeforeAfter = Dependency(skip_validation=True), - id_filter: CollectionFilter = Dependency(skip_validation=True), - limit_offset: LimitOffset = Dependency(skip_validation=True), - search_filter: SearchFilter = Dependency(skip_validation=True), - order_by: OrderBy = Dependency(skip_validation=True), -) -> list[FilterTypes]: - """Provide common collection route filtering dependencies. - - Add all filters to any route by including this function as a dependency, e.g.: - - .. code-block:: python - - @get - def get_collection_handler(filters: Filters) -> ...: ... - - The dependency is provided in the application layer, so only need to inject the dependency where - necessary. - - Arguments: - created_filter(BeforeAfter): Filter for records created before/after a certain date/time. - updated_filter(BeforeAfter): Filter for records updated before/after a certain date/time. - id_filter(CollectionFilter): Filter for records with a certain ID. - limit_offset(LimitOffset): Pagination filter. - search_filter(SearchFilter): Search filter. - order_by(OrderBy): Order by filter. - - Returns: - list[repository.FilterTypes]: List of filters to apply to query. - """ - filters: list[FilterTypes] = [] - if id_filter.values: - filters.append(id_filter) - filters.extend([created_filter, limit_offset, updated_filter]) - - if search_filter.field_name is not None and search_filter.value is not None: - filters.append(search_filter) - if order_by.field_name is not None: - filters.append(order_by) - return filters - - -def create_collection_dependencies() -> dict[str, Provide]: - """Create ORM dependencies. - - Creates a dictionary of ``provides`` for pagination endpoints. - - Returns: - dict[str, Provide]: Dictionary of ``provides``. - """ - return { - ACTIVE_FILTER_DEPENDENCY_KEY: Provide(provide_active_filter, sync_to_thread=False), - LIMIT_OFFSET_DEPENDENCY_KEY: Provide(provide_limit_offset_pagination, sync_to_thread=False), - UPDATED_FILTER_DEPENDENCY_KEY: Provide(provide_updated_filter, sync_to_thread=False), - CREATED_FILTER_DEPENDENCY_KEY: Provide(provide_created_filter, sync_to_thread=False), - ID_FILTER_DEPENDENCY_KEY: Provide(provide_id_filter, sync_to_thread=False), - SEARCH_FILTER_DEPENDENCY_KEY: Provide(provide_search_filter, sync_to_thread=False), - ORDER_BY_DEPENDENCY_KEY: Provide(provide_order_by, sync_to_thread=False), - FILTERS_DEPENDENCY_KEY: Provide(provide_filter_dependencies, sync_to_thread=False), - } diff --git a/byte_bot/server/lib/dto.py b/byte_bot/server/lib/dto.py deleted file mode 100644 index 56166321..00000000 --- a/byte_bot/server/lib/dto.py +++ /dev/null @@ -1,74 +0,0 @@ -"""DTO Library layer module.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Literal, overload - -from advanced_alchemy.extensions.litestar.dto import SQLAlchemyDTO, SQLAlchemyDTOConfig -from litestar.dto import DataclassDTO, dto_field -from litestar.dto.config import DTOConfig - -if TYPE_CHECKING: - from collections.abc import Set as AbstractSet - - from litestar.dto import RenameStrategy - -__all__ = ["DTOConfig", "DataclassDTO", "SQLAlchemyDTO", "config", "dto_field"] - - -@overload -def config( - backend: Literal["sqlalchemy"] = "sqlalchemy", - exclude: AbstractSet[str] | None = None, - rename_fields: dict[str, str] | None = None, - rename_strategy: RenameStrategy | None = None, - max_nested_depth: int | None = None, - partial: bool | None = None, -) -> SQLAlchemyDTOConfig: ... - - -@overload -def config( - backend: Literal["dataclass"] = "dataclass", - exclude: AbstractSet[str] | None = None, - rename_fields: dict[str, str] | None = None, - rename_strategy: RenameStrategy | None = None, - max_nested_depth: int | None = None, - partial: bool | None = None, -) -> DTOConfig: ... - - -# noinspection PyUnusedLocal -def config( - backend: Literal["dataclass", "sqlalchemy"] = "dataclass", # noqa: ARG001 - exclude: AbstractSet[str] | None = None, - rename_fields: dict[str, str] | None = None, - rename_strategy: RenameStrategy | None = None, - max_nested_depth: int | None = None, - partial: bool | None = None, -) -> DTOConfig | SQLAlchemyDTOConfig: - """Construct a DTO config. - - Args: - backend (Literal["dataclass", "sqlalchemy"], optional): Backend to use. Defaults to "dataclass". - exclude (AbstractSet[str] | None, optional): Fields to exclude. Defaults to None. - rename_fields (dict[str, str] | None, optional): Fields to rename. Defaults to None. - rename_strategy (RenameStrategy | None, optional): Rename strategy to use. Defaults to None. - max_nested_depth (int | None, optional): Max nested depth. Defaults to None. - partial (bool | None, optional): Whether to make the DTO partial. Defaults to None. - - Returns: - DTOConfig: Configured DTO class - """ - default_kwargs = {"rename_strategy": "camel", "max_nested_depth": 2} - if exclude: - default_kwargs["exclude"] = exclude - if rename_fields: - default_kwargs["rename_fields"] = rename_fields - if rename_strategy: - default_kwargs["rename_strategy"] = rename_strategy - if max_nested_depth: - default_kwargs["max_nested_depth"] = max_nested_depth - if partial: - default_kwargs["partial"] = partial - return DTOConfig(**default_kwargs) # type: ignore[arg-type] diff --git a/byte_bot/server/lib/exceptions.py b/byte_bot/server/lib/exceptions.py deleted file mode 100644 index 2f81d9a9..00000000 --- a/byte_bot/server/lib/exceptions.py +++ /dev/null @@ -1,120 +0,0 @@ -"""exception types. - -Also, defines functions that translate service and repository exceptions -into HTTP exceptions. -""" - -from __future__ import annotations - -import sys -from typing import TYPE_CHECKING - -from litestar.exceptions import ( - HTTPException, - InternalServerException, - NotFoundException, - PermissionDeniedException, -) -from litestar.middleware.exceptions._debug_response import create_debug_response -from litestar.middleware.exceptions.middleware import create_exception_response -from litestar.repository.exceptions import ConflictError, NotFoundError, RepositoryError -from litestar.status_codes import HTTP_409_CONFLICT, HTTP_500_INTERNAL_SERVER_ERROR -from structlog.contextvars import bind_contextvars - -if TYPE_CHECKING: - from typing import Any - - from litestar.connection import Request - from litestar.middleware.exceptions.middleware import ExceptionResponseContent - from litestar.response import Response - from litestar.types import Scope - -__all__ = ( - "ApplicationError", - "AuthorizationError", - "HealthCheckConfigurationError", - "MissingDependencyError", - "after_exception_hook_handler", -) - - -class ApplicationError(Exception): - """Base exception type for the lib's custom exception types.""" - - -class ApplicationClientError(ApplicationError): - """Base exception type for client errors.""" - - -class AuthorizationError(ApplicationClientError): - """A user tried to do something they shouldn't have.""" - - -class MissingDependencyError(ApplicationError, ValueError): - """A required dependency is not installed.""" - - def __init__(self, module: str, config: str | None = None) -> None: - """Missing Dependency Error. - - Args: - module: name of the package that should be installed - config: name of the extra to install the package. - """ - config = config or module - super().__init__( - f"You enabled {config} configuration but package {module!r} is not installed. " - f'You may need to run: "pip install byte-bot[{config}]"', - ) - - -class HealthCheckConfigurationError(ApplicationError): - """An error occurred while registering a health check.""" - - -class _HTTPConflictException(HTTPException): - """Request conflict with the current state of the target resource.""" - - status_code = HTTP_409_CONFLICT - - -async def after_exception_hook_handler(exc: Exception, _scope: Scope) -> None: - """Binds ``exc_info`` key with exception instance as value to structlog context vars. - - .. note:: This must be a coroutine so that it is not wrapped in a thread where we'll lose context. - - Args: - exc: the exception that was raised. - _scope: scope of the request - """ - if isinstance(exc, ApplicationError): - return - if isinstance(exc, HTTPException) and exc.status_code < HTTP_500_INTERNAL_SERVER_ERROR: - return - bind_contextvars(exc_info=sys.exc_info()) - - -def exception_to_http_response( - request: Request[Any, Any, Any], - exc: ApplicationError | RepositoryError, -) -> Response[ExceptionResponseContent]: - """Transform repository exceptions to HTTP exceptions. - - Args: - request: The request that experienced the exception. - exc: Exception raised during handling of the request. - - Returns: - Exception response appropriate to the type of original exception. - """ - if isinstance(exc, NotFoundError): - http_exc = NotFoundException(detail=str(exc)) - elif isinstance(exc, ConflictError | RepositoryError): - http_exc = _HTTPConflictException(detail=str(exc)) - elif isinstance(exc, AuthorizationError): - http_exc = PermissionDeniedException(detail=str(exc)) - else: - http_exc = InternalServerException(detail=str(exc)) - - if request.app.debug: - return create_debug_response(request, exc) - return create_exception_response(request, http_exc) diff --git a/byte_bot/server/lib/log/__init__.py b/byte_bot/server/lib/log/__init__.py deleted file mode 100644 index f1d62810..00000000 --- a/byte_bot/server/lib/log/__init__.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Logging Configuration.""" - -from __future__ import annotations - -import logging -import sys -from typing import TYPE_CHECKING - -import structlog -from litestar.logging.config import LoggingConfig - -from byte_bot.server.lib import settings -from byte_bot.server.lib.log import controller -from byte_bot.server.lib.log.utils import EventFilter, msgspec_json_renderer - -if TYPE_CHECKING: - from collections.abc import Sequence - from typing import Any - - from structlog import BoundLogger - from structlog.types import Processor - -__all__ = ( - "config", - "configure", - "controller", - "default_processors", - "get_logger", - "stdlib_processors", -) - - -default_processors = [ - structlog.contextvars.merge_contextvars, - controller.drop_health_logs, - structlog.processors.add_log_level, - structlog.processors.TimeStamper(fmt="iso", utc=True), -] -"""Default processors to apply to all loggers. See :mod:`structlog.processors` for more information.""" - -stdlib_processors = [ - structlog.processors.TimeStamper(fmt="iso", utc=True), - structlog.stdlib.add_log_level, - structlog.stdlib.ExtraAdder(), - EventFilter(["color_message"]), - structlog.stdlib.ProcessorFormatter.remove_processors_meta, -] -"""Processors to apply to the stdlib logger. See :mod:`structlog.stdlib` for more information.""" - -if sys.stderr.isatty() or "pytest" in sys.modules: - LoggerFactory: Any = structlog.WriteLoggerFactory - console_processor = structlog.dev.ConsoleRenderer( - colors=True, - exception_formatter=structlog.dev.plain_traceback, - ) - default_processors.extend([console_processor]) - stdlib_processors.append(console_processor) -else: - LoggerFactory = structlog.BytesLoggerFactory - default_processors.extend([msgspec_json_renderer]) - - -def configure(processors: Sequence[Processor]) -> None: - """Call to configure `structlog` on app startup. - - The calls to :func:`get_logger() ` in :mod:`controller.py ` - to the logger that is eventually called after this configurator function has been called. Therefore, nothing - should try to log via structlog before this is called. - - Args: - processors: A list of processors to apply to all loggers - - Returns: - None - """ - structlog.configure( - cache_logger_on_first_use=True, - logger_factory=LoggerFactory(), - processors=processors, - wrapper_class=structlog.make_filtering_bound_logger(settings.log.LEVEL), - ) - - -config = LoggingConfig( - root={"level": logging.getLevelName(settings.log.LEVEL), "handlers": ["queue_listener"]}, - formatters={ - "standard": {"()": structlog.stdlib.ProcessorFormatter, "processors": stdlib_processors}, - }, - loggers={ - "uvicorn.access": { - "propagate": False, - "level": settings.log.UVICORN_ACCESS_LEVEL, - "handlers": ["queue_listener"], - }, - "uvicorn.error": { - "propagate": False, - "level": settings.log.UVICORN_ERROR_LEVEL, - "handlers": ["queue_listener"], - }, - }, -) -"""Pre-configured log config for application deps. - -While we use structlog for internal app logging, we still want to ensure -that logs emitted by any of our dependencies are handled in a non- -blocking manner. -""" - - -def get_logger(*args: Any, **kwargs: Any) -> BoundLogger: - """Return a configured logger for the given name. - - Args: - *args: Positional arguments to pass to :func:`get_logger() ` - **kwargs: Keyword arguments to pass to :func:`get_logger() ` - - Returns: - Logger: A configured logger instance - """ - config.configure() - configure(default_processors) # type: ignore[arg-type] - return structlog.getLogger(*args, **kwargs) # type: ignore[no-any-return] diff --git a/byte_bot/server/lib/log/controller.py b/byte_bot/server/lib/log/controller.py deleted file mode 100644 index aaddfcf4..00000000 --- a/byte_bot/server/lib/log/controller.py +++ /dev/null @@ -1,234 +0,0 @@ -"""Logging config for the application. - -Ensures that the app and uvicorn loggers all log through the queue listener. - -Adds a filter for health check route logs. -""" - -from __future__ import annotations - -import logging -import re -from inspect import isawaitable -from typing import TYPE_CHECKING - -import structlog -from litestar.constants import SCOPE_STATE_RESPONSE_COMPRESSED -from litestar.data_extractors import ConnectionDataExtractor, ResponseDataExtractor -from litestar.enums import ScopeType -from litestar.status_codes import ( - HTTP_200_OK, - HTTP_300_MULTIPLE_CHOICES, - HTTP_500_INTERNAL_SERVER_ERROR, -) -from litestar.utils.scope import get_litestar_scope_state - -from byte_bot.server.lib import settings - -__all__ = ["BeforeSendHandler", "drop_health_logs", "middleware_factory"] - -if TYPE_CHECKING: - from typing import Any, Literal - - from litestar.connection import Request - from litestar.types.asgi_types import ASGIApp, Message, Receive, Scope, Send - from structlog.types import EventDict, WrappedLogger - -LOGGER = structlog.get_logger() - -HTTP_RESPONSE_START: Literal["http.response.start"] = "http.response.start" -HTTP_RESPONSE_BODY: Literal["http.response.body"] = "http.response.body" -REQUEST_BODY_FIELD: Literal["body"] = "body" - - -def drop_health_logs(_: WrappedLogger, __: str, event_dict: EventDict) -> EventDict: - """Prevent logging of successful health checks. - - Args: - _: Wrapped logger object. - __: Name of the wrapped method, e.g., "info", "warning", etc. - event_dict: Current context with current event, e.g, `{"a": 42, "event": "foo"}`. - - Returns: - `event_dict` for further processing if it does not represent a successful health check. - """ - is_http_log = event_dict["event"] == settings.log.HTTP_EVENT - is_health_log = event_dict.get("request", {}).get("path") == settings.api.HEALTH_PATH - is_success_status = HTTP_200_OK <= event_dict.get("response", {}).get("status_code", 0) < HTTP_300_MULTIPLE_CHOICES - if is_http_log and is_health_log and is_success_status: - raise structlog.DropEvent - return event_dict - - -def middleware_factory(app: ASGIApp) -> ASGIApp: - """Middleware to ensure that every request has a clean structlog context. - - Args: - app: The previous ASGI app in the call chain. - - Returns: - A new ASGI app that cleans the structlog contextvars. - """ - - async def middleware(scope: Scope, receive: Receive, send: Send) -> None: - """Clean up structlog contextvars. - - Args: - scope: ASGI connection scope. - receive: ASGI receive handler. - send: ASGI send handler. - """ - structlog.contextvars.clear_contextvars() - await app(scope, receive, send) - - return middleware - - -class BeforeSendHandler: - """Extraction of request and response data from connection scope.""" - - __slots__ = ( - "do_log_request", - "do_log_response", - "exclude_paths", - "include_compressed_body", - "logger", - "request_extractor", - "response_extractor", - ) - - def __init__(self) -> None: - """Configure the handler.""" - self.exclude_paths = re.compile(settings.log.EXCLUDE_PATHS) - self.do_log_request = bool(settings.log.REQUEST_FIELDS) - self.do_log_response = bool(settings.log.RESPONSE_FIELDS) - self.include_compressed_body = settings.log.INCLUDE_COMPRESSED_BODY - self.request_extractor = ConnectionDataExtractor( - extract_body="body" in settings.log.REQUEST_FIELDS, - extract_client="client" in settings.log.REQUEST_FIELDS, - extract_content_type="content_type" in settings.log.REQUEST_FIELDS, - extract_cookies="cookies" in settings.log.REQUEST_FIELDS, - extract_headers="headers" in settings.log.REQUEST_FIELDS, - extract_method="method" in settings.log.REQUEST_FIELDS, - extract_path="path" in settings.log.REQUEST_FIELDS, - extract_path_params="path_params" in settings.log.REQUEST_FIELDS, - extract_query="query" in settings.log.REQUEST_FIELDS, - extract_scheme="scheme" in settings.log.REQUEST_FIELDS, - obfuscate_cookies=settings.log.OBFUSCATE_COOKIES, - obfuscate_headers=settings.log.OBFUSCATE_HEADERS, - parse_body=False, - parse_query=False, - ) - self.response_extractor = ResponseDataExtractor( - extract_body="body" in settings.log.RESPONSE_FIELDS, - extract_headers="headers" in settings.log.RESPONSE_FIELDS, - extract_status_code="status_code" in settings.log.RESPONSE_FIELDS, - obfuscate_cookies=settings.log.OBFUSCATE_COOKIES, - obfuscate_headers=settings.log.OBFUSCATE_HEADERS, - ) - - async def __call__(self, message: Message, scope: Scope) -> None: - """Receives ASGI response messages and scope, and logs per configuration. - - Args: - message: ASGI response event. - scope: ASGI connection scope. - """ - if scope["type"] == ScopeType.HTTP and self.exclude_paths.findall(scope["path"]): - return - - if message["type"] == HTTP_RESPONSE_START: - scope["state"]["log_level"] = ( - logging.ERROR if message["status"] >= HTTP_500_INTERNAL_SERVER_ERROR else logging.INFO - ) - scope["state"][HTTP_RESPONSE_START] = message - # ignore intermediate content of streaming responses for now. - elif message["type"] == HTTP_RESPONSE_BODY and message["more_body"] is False: - scope["state"][HTTP_RESPONSE_BODY] = message - try: - if self.do_log_request: - await self.log_request(scope) - if self.do_log_response: - await self.log_response(scope) - await LOGGER.alog(scope["state"]["log_level"], settings.log.HTTP_EVENT) - # RuntimeError: Expected ASGI message 'http.response.body', but got 'http.response.start'. - except Exception as exc: # noqa: BLE001 # pylint: disable=broad-except - # just in-case something in the context causes the error - structlog.contextvars.clear_contextvars() - await LOGGER.aerror("Error in logging before-send handler!", exc_info=exc) - - async def log_request(self, scope: Scope) -> None: - """Handle extracting the request data and logging the message. - - Args: - scope: The ASGI connection scope. - - Returns: - None - """ - extracted_data = await self.extract_request_data(request=scope["app"].request_class(scope)) - structlog.contextvars.bind_contextvars(request=extracted_data) - - async def log_response(self, scope: Scope) -> None: - """Handle extracting the response data and logging the message. - - Args: - scope: The ASGI connection scope. - - Returns: - None - """ - extracted_data = self.extract_response_data(scope=scope) - structlog.contextvars.bind_contextvars(response=extracted_data) - - async def extract_request_data(self, request: Request) -> dict[str, Any]: - """Create a dictionary of values for the log. - - Args: - request: A [Request][litestar.connection.request.Request] instance. - - Returns: - An OrderedDict. - """ - data: dict[str, Any] = {} - extracted_data = self.request_extractor(connection=request) - missing = object() - for key in settings.log.REQUEST_FIELDS: - value = extracted_data.get(key, missing) - if value is missing: # pragma: no cover - continue - if isawaitable(value): - # Prevent Litestar from raising a RuntimeError - # when trying to read an empty request body. - try: - value = await value - except RuntimeError: - if key != REQUEST_BODY_FIELD: - raise # pragma: no cover - value = None - data[key] = value - return data - - def extract_response_data(self, scope: Scope) -> dict[str, Any]: - """Extract data from the response. - - Args: - scope: The ASGI connection scope. - - Returns: - An OrderedDict. - """ - data: dict[str, Any] = {} - extracted_data = self.response_extractor( - messages=(scope["state"][HTTP_RESPONSE_START], scope["state"][HTTP_RESPONSE_BODY]), - ) - missing = object() - response_body_compressed = get_litestar_scope_state(scope, SCOPE_STATE_RESPONSE_COMPRESSED) - for key in settings.log.RESPONSE_FIELDS: - value = extracted_data.get(key, missing) - if key == "body" and response_body_compressed and not self.include_compressed_body: - continue - if value is missing: # pragma: no cover - continue - data[key] = value - return data diff --git a/byte_bot/server/lib/log/utils.py b/byte_bot/server/lib/log/utils.py deleted file mode 100644 index 0946b9e7..00000000 --- a/byte_bot/server/lib/log/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Logging utilities. - -:func:`msgspec_json_renderer()` - A JSON Renderer for structlog using msgspec. - -Msgspec doesn't have an API consistent with the stdlib's :mod:`json` module, -which is required for :class:`Structlog's JSONRenderer `. - -:class:`EventFilter` - A structlog processor that removes keys from the log event if they exist. -""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -__all__ = ["EventFilter", "msgspec_json_renderer"] - -from byte_bot.server.lib import serialization - -if TYPE_CHECKING: - from collections.abc import Iterable - - from structlog.typing import EventDict, WrappedLogger - - -def msgspec_json_renderer(_: WrappedLogger, __: str, event_dict: EventDict) -> bytes: - """Structlog processor that uses :doc:`msgspec ` for JSON encoding. - - Args: - _: - __: - event_dict: The data to be logged. - - Returns: - The log event encoded to JSON by msgspec. - """ - return serialization.to_json(event_dict) - - -class EventFilter: - """Remove keys from the log event. - - Add an instance to the processor chain. - - Example: - # noqa - - .. code-block:: python - - structlog.configure( - ..., - processors=[ - ..., - EventFilter(["color_message"]), - ..., - ], - ) - - """ - - def __init__(self, filter_keys: Iterable[str]) -> None: - """Initialize the processor. - - Args: - filter_keys: Iterable of string keys to be excluded from the log event. - - Returns: - None - """ - self.filter_keys = filter_keys - - def __call__(self, _: WrappedLogger, __: str, event_dict: EventDict) -> EventDict: - """Receive the log event, and filter keys. - - Args: - _: - __: - event_dict: The data to be logged. - - Returns: - The log event with any key in ``self.filter_keys`` removed. - """ - for key in self.filter_keys: - event_dict.pop(key, None) - return event_dict diff --git a/byte_bot/server/lib/openapi.py b/byte_bot/server/lib/openapi.py deleted file mode 100644 index b111a696..00000000 --- a/byte_bot/server/lib/openapi.py +++ /dev/null @@ -1,25 +0,0 @@ -"""OpenAPI Config.""" - -from __future__ import annotations - -from litestar.openapi.config import OpenAPIConfig -from litestar.openapi.spec import Contact - -from byte_bot.server.lib import settings - -__all__ = ("config",) - -config = OpenAPIConfig( - title=settings.openapi.TITLE or settings.project.NAME, - description=settings.openapi.DESCRIPTION, - servers=settings.openapi.SERVERS, # type: ignore[arg-type] - external_docs=settings.openapi.EXTERNAL_DOCS, # type: ignore[arg-type] - version=settings.openapi.VERSION, - contact=Contact(name=settings.openapi.CONTACT_NAME, email=settings.openapi.CONTACT_EMAIL), - use_handler_docstrings=True, - root_schema_site="swagger", - path=settings.openapi.PATH, -) -"""OpenAPI config for the project. -See :class:`OpenAPISettings <.settings.OpenAPISettings>` for configuration. -""" diff --git a/byte_bot/server/lib/schema.py b/byte_bot/server/lib/schema.py deleted file mode 100644 index 7926e4d3..00000000 --- a/byte_bot/server/lib/schema.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Schema.""" - -from __future__ import annotations - -from pydantic import BaseModel as _BaseModel -from pydantic import ConfigDict - -from byte_bot.utils import camel_case - -__all__ = ["BaseModel", "CamelizedBaseModel"] - - -class BaseModel(_BaseModel): - """Base Settings.""" - - model_config = ConfigDict( - validate_assignment=True, - from_attributes=True, - use_enum_values=True, - arbitrary_types_allowed=True, - ) - - -class CamelizedBaseModel(BaseModel): - """Camelized Base pydantic schema.""" - - model_config = ConfigDict(populate_by_name=True, alias_generator=camel_case) diff --git a/byte_bot/server/lib/serialization.py b/byte_bot/server/lib/serialization.py deleted file mode 100644 index 44540f6b..00000000 --- a/byte_bot/server/lib/serialization.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Serialization Helpers.""" - -from __future__ import annotations - -import datetime -from json import JSONEncoder -from typing import Any -from uuid import UUID - -import msgspec -from pydantic import BaseModel - -__all__ = [ - "UUIDEncoder", - "convert_camel_to_snake_case", - "convert_datetime_to_gmt", - "convert_string_to_camel_case", - "from_json", - "from_msgpack", - "to_json", - "to_msgpack", -] - - -def _default(value: Any) -> str: - """Default encoder for msgspec. - - Args: - value: The value to encode - - Returns: - str: The encoded value - """ - if isinstance(value, BaseModel): - return str(value.dict(by_alias=True)) - try: - val = str(value) - except Exception as exc: - raise TypeError from exc - else: - return val - - -_msgspec_json_encoder = msgspec.json.Encoder(enc_hook=_default) -_msgspec_json_decoder = msgspec.json.Decoder() -_msgspec_msgpack_encoder = msgspec.msgpack.Encoder(enc_hook=_default) -_msgspec_msgpack_decoder = msgspec.msgpack.Decoder() - - -def to_json(value: Any) -> bytes: - """Encode json with the optimized :doc:`msgspec ` package. - - Args: - value: The value to encode - - Returns: - bytes: The encoded value - """ - return _msgspec_json_encoder.encode(value) - - -def from_json(value: bytes | str) -> Any: - """Decode to an object with the optimized :doc:`msgspec ` package. - - Args: - value: The value to decode - - Returns: - Any: The decoded value - """ - return _msgspec_json_decoder.decode(value) - - -def to_msgpack(value: Any) -> bytes: - """Encode json with the optimized :doc:`msgspec ` package. - - Args: - value: The value to encode - - Returns: - bytes: The encoded value - """ - return _msgspec_msgpack_encoder.encode(value) - - -def from_msgpack(value: bytes) -> Any: - """Decode to an object with the optimized :doc:`msgspec ` package. - - Args: - value: The value to decode - - Returns: - Any: The decoded value - """ - return _msgspec_msgpack_decoder.decode(value) - - -def convert_datetime_to_gmt(dt: datetime.datetime) -> str: - """Handle datetime serialization for nested timestamps. - - Args: - dt: The datetime to convert - - Returns: - str: The datetime converted to GMT - """ - if not dt.tzinfo: - dt = dt.replace(tzinfo=datetime.UTC) - return dt.isoformat().replace("+00:00", "Z") - - -def convert_string_to_camel_case(string: str) -> str: - """Convert a string to camel case. - - Args: - string: The string to convert - - Returns: - str: The string converted to camel case - """ - return "".join(word if index == 0 else word.capitalize() for index, word in enumerate(string.split("_"))) - - -def convert_camel_to_snake_case(string: str) -> str: - """Convert a string to snake case. - - Args: - string: The string to convert - - Returns: - str: The string converted to snake case - """ - return "".join(f"_{char.lower()}" if index > 0 and char.isupper() else char for index, char in enumerate(string)) - - -class UUIDEncoder(JSONEncoder): - """JSON Encoder for UUIDs.""" - - def default(self, o: Any) -> Any: - """Handle UUIDs. - - Args: - o: The object to encode - - Returns: - str: The encoded object - """ - return str(o) if isinstance(o, UUID) else super().default(o) diff --git a/byte_bot/server/lib/settings.py b/byte_bot/server/lib/settings.py deleted file mode 100644 index 08401156..00000000 --- a/byte_bot/server/lib/settings.py +++ /dev/null @@ -1,475 +0,0 @@ -"""Project Settings.""" - -from __future__ import annotations # noqa: I001 - -import base64 -import binascii -import os -from pathlib import Path -from typing import Any, Final, Literal -from collections.abc import Sequence # noqa: TC003 - -from dotenv import load_dotenv -from litestar.contrib.jinja import JinjaTemplateEngine -from litestar.data_extractors import RequestExtractorField, ResponseExtractorField # noqa: TC002 -from litestar.openapi.spec import Server -from litestar.utils.module_loader import module_to_os_path -from pydantic import ValidationError, field_validator -from pydantic.types import SecretBytes -from pydantic_settings import BaseSettings, SettingsConfigDict - -from byte_bot.__metadata__ import __version__ as version - -__all__ = ( - "APISettings", - "DatabaseSettings", - "GitHubSettings", - "LogSettings", - "OpenAPISettings", - "ProjectSettings", - "ServerSettings", - "TemplateSettings", - "load_settings", -) - - -load_dotenv() - -DEFAULT_MODULE_NAME = "byte_bot" -BASE_DIR: Final = module_to_os_path(DEFAULT_MODULE_NAME) -STATIC_DIR = Path(BASE_DIR / "server" / "domain" / "web" / "resources") -TEMPLATES_DIR = Path(BASE_DIR / "server" / "domain" / "web" / "templates") - - -class ServerSettings(BaseSettings): - """Server configurations.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="SERVER_", extra="ignore") - - APP_LOC: str = "byte_bot.app:create_app" - """Path to app executable, or factory.""" - APP_LOC_IS_FACTORY: bool = True - """Indicate if APP_LOC points to an executable or factory.""" - HOST: str = "localhost" - """Server network host.""" - KEEPALIVE: int = 65 - """Seconds to hold connections open.""" - PORT: int = 8000 - """Server port.""" - RELOAD: bool | None = False - """Turn on hot reloading.""" - RELOAD_DIRS: list[str] = [f"{BASE_DIR}"] - """Directories to watch for reloading. - - .. warning:: This only accepts a single directory for now, something is broken - - """ - HTTP_WORKERS: int | None = None - """Number of HTTP Worker processes to be spawned by Uvicorn.""" - - -class ProjectSettings(BaseSettings): - """Project Settings.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", extra="ignore") - - BUILD_NUMBER: str = version - """Identifier for CI build.""" - CHECK_DB_READY: bool = True - """Check for database readiness on startup.""" - CHECK_REDIS_READY: bool = True - """Check for redis readiness on startup.""" - DEBUG: bool = False - """Run ``Litestar`` with ``debug=True``.""" - ENVIRONMENT: str = "prod" - """``dev``, ``prod``, ``qa``, etc.""" - TEST_ENVIRONMENT_NAME: str = "test" - """Value of ENVIRONMENT used to determine if running tests. - - This should be the value of ``ENVIRONMENT`` in ``tests.env``. - """ - LOCAL_ENVIRONMENT_NAME: str = "local" - """Value of ENVIRONMENT used to determine if running in local development - mode. - - This should be the value of ``ENVIRONMENT`` in your local ``.env`` file. - """ - NAME: str = "Byte Bot" - """Application name.""" - SECRET_KEY: SecretBytes - """Secret key used for signing cookies and other things.""" - JWT_ENCRYPTION_ALGORITHM: str = "HS256" - """Algorithm used to encrypt JWTs.""" - BACKEND_CORS_ORIGINS: list[str] = ["*"] - """List of origins allowed to access the API.""" - STATIC_URL: str = "/static/" - """Default URL where static assets are located.""" - CSRF_COOKIE_NAME: str = "csrftoken" - """Name of the CSRF cookie.""" - CSRF_COOKIE_SECURE: bool = False - """Set the CSRF cookie to be secure.""" - STATIC_DIR: Path = STATIC_DIR - """Path to static assets.""" - DEV_MODE: bool = False - """Indicate if running in development mode.""" - - @property - def slug(self) -> str: - """Return a slugified name. - - Returns: - ``self.NAME``, all lowercase and hyphens instead of spaces. - """ - return "-".join(s.lower() for s in self.NAME.split()) - - @field_validator("BACKEND_CORS_ORIGINS") - @classmethod - def assemble_cors_origins( - cls, - value: str | list[str] | None, - ) -> list[str] | str: - """Parse a list of origins. - - Args: - value: A comma-separated string of origins, or a list of origins. - - Returns: - A list of origins. - - Raises: - ValueError: If ``value`` is not a list or string. - """ - if value is None: - return [] - if isinstance(value, list): - return value - if isinstance(value, str) and not value.startswith("["): - return [host.strip() for host in value.split(",")] - if isinstance(value, str) and value.startswith("[") and value.endswith("]"): - return list(value) - raise ValueError(value) - - @field_validator("SECRET_KEY", mode="before") - @classmethod - def generate_secret_key(cls, value: str | None) -> SecretBytes: - """Generate a secret key. - - Args: - value: A secret key, or ``None``. - - Returns: - A secret key. - """ - if value is None: - return SecretBytes(binascii.hexlify(os.urandom(32))) - return SecretBytes(value.encode()) - - -class APISettings(BaseSettings): - """API specific configuration.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="API_", extra="ignore") - - HEALTH_PATH: str = "/health" - """Route that the health check is served under.""" - OPENCOLLECTIVE_KEY: str | None = None - """OpenCollective API key.""" - OPENCOLLECTIVE_URL: str = "https://api.opencollective.com/graphql/v2" - """OpenCollective API URL. - - .. note:: This is the GraphQL endpoint, the REST endpoint is no longer maintained. - See also: `OpenCollective API Docs `_ - """ - POLAR_KEY: str | None = None - """Polar API key.""" - POLAR_URL: str = "https://api.polar.sh" - """Polar API URL. - - .. seealso:: `Polar API Docs `_ and - the `Public API #834 Issue `_. - """ - - -class LogSettings(BaseSettings): - """Logging config for the Project.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="LOG_", extra="ignore") - - """https://stackoverflow.com/a/1845097/6560549""" - EXCLUDE_PATHS: str = r"\A(?!x)x" - """Regex to exclude paths from logging.""" - HTTP_EVENT: str = "HTTP" - """Log event name for logs from ``litestar`` handlers.""" - INCLUDE_COMPRESSED_BODY: bool = False - """Include ``body`` of compressed responses in log output.""" - LEVEL: int = 20 - """Stdlib log levels. - - Only emit logs at this level, or higher. - """ - OBFUSCATE_COOKIES: set[str] = {"session"} - """Request cookie keys to obfuscate.""" - OBFUSCATE_HEADERS: set[str] = {"Authorization", "X-API-KEY"} - """Request header keys to obfuscate.""" - JOB_FIELDS: list[str] = [ - "function", - "kwargs", - "key", - "scheduled", - "attempts", - "completed", - "queued", - "started", - "result", - "error", - ] - """Attributes of the SAQ :class:`Job ` to be logged.""" - REQUEST_FIELDS: Sequence[RequestExtractorField] = [ # type: ignore[assignment] - "path", - "method", - "headers", - "cookies", - "query", - "path_params", - "body", - ] - """Attributes of the :class:`Request ` to be - logged.""" - RESPONSE_FIELDS: Sequence[ResponseExtractorField] = [ # type: ignore[assignment] - "status_code", - "cookies", - "headers", - # "body", # ! We don't want to log the response body. - ] - """Attributes of the :class:`Response ` to be - logged.""" - UVICORN_ACCESS_LEVEL: int = 30 - """Level to log uvicorn access logs.""" - UVICORN_ERROR_LEVEL: int = 20 - """Level to log uvicorn error logs.""" - - -# noinspection PyUnresolvedReferences -class OpenAPISettings(BaseSettings): - """Configures OpenAPI for the Project.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="OPENAPI_", extra="ignore") - - CONTACT_NAME: str = "Admin" - """Name of contact on document.""" - CONTACT_EMAIL: str = "hello@byte-bot.app" - """Email for contact on document.""" - TITLE: str | None = "Byte Bot" - """Document title.""" - VERSION: str = version - """Document version.""" - PATH: str = "/api" - """Path to access the root API documentation.""" - DESCRIPTION: str | None = """The Byte Bot API supports the Byte Discord bot. - You can find out more about this project in the - [docs](https://docs.byte-bot.app/latest).""" - SERVERS: list[dict[str, str]] = [] - """Servers to use for the OpenAPI documentation.""" - EXTERNAL_DOCS: dict[str, str] | None = { - "description": "Byte Bot API Docs", - "url": "https://docs.byte-bot.app/latest", - } - """External documentation for the API.""" - - @field_validator("SERVERS", mode="after") - def assemble_openapi_servers(cls, value: list[Server]) -> list[Server]: # noqa: ARG003 - """Assembles the OpenAPI servers based on the environment. - - Args: - value: The value of the SERVERS setting. - - Returns: - The assembled OpenAPI servers. - """ - servers = { - "prod": Server(url="https://byte-bot.app/", description="Production"), - "test": Server(url="https://dev.byte-bot.app/", description="Test"), - "dev": Server(url="http://0.0.0.0:8000", description="Development"), - } - environment = os.getenv("ENVIRONMENT", "dev") - - if environment == "prod": - return [servers["prod"]] - if environment == "test": - return [servers["test"], servers["prod"]] - return [servers["dev"], servers["test"], servers["prod"]] - - -class TemplateSettings(BaseSettings): - """Configures Templating for the project.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="TEMPLATE_", extra="ignore") - - ENGINE: type[JinjaTemplateEngine] = JinjaTemplateEngine - """Template engine to use. (Jinja2 or Mako)""" - - -class DatabaseSettings(BaseSettings): - """Configures the database for the application.""" - - model_config = SettingsConfigDict( - env_file=".env", - env_file_encoding="utf-8", - env_prefix="DB_", - case_sensitive=False, - extra="ignore", - ) - - ECHO: bool = False - """Enable SQLAlchemy engine logs.""" - ECHO_POOL: bool | Literal["debug"] = False - """Enable SQLAlchemy connection pool logs.""" - POOL_DISABLE: bool = True - """Disable SQLAlchemy pooling, same as setting pool to. - - See :class:`NullPool `. - """ - POOL_MAX_OVERFLOW: int = 10 - """See :class:`QueuePool `. - - .. warning:: This is arguably pretty high, - and shouldn't be raised past 10. - """ - POOL_SIZE: int = 5 - """See :class:`QueuePool `.""" - POOL_TIMEOUT: int = 30 - """See :class:`QueuePool `.""" - POOL_RECYCLE: int = 300 - """See :class:`QueuePool `.""" - POOL_PRE_PING: bool = False - """See :class:`QueuePool `.""" - CONNECT_ARGS: dict[str, Any] = {} - """Connection arguments to pass to the database driver.""" - URL: str = "postgresql+asyncpg://byte:bot@localhost:5432/byte" - """Database connection URL.""" - ENGINE: str | None = None - """Database engine.""" - USER: str = "byte" - """Database user.""" - PASSWORD: str = "bot" # noqa: S105 - """Database password.""" - HOST: str = "localhost" - """Database host.""" - PORT: int = 5432 - """Database port.""" - NAME: str = "byte" - """Database name.""" - MIGRATION_CONFIG: str = f"{BASE_DIR}/server/lib/db/alembic.ini" - """Path to Alembic config file.""" - MIGRATION_PATH: str = f"{BASE_DIR}/server/lib/db/migrations" - """Path to Alembic migration files.""" - MIGRATION_DDL_VERSION_TABLE: str = "ddl_version" - """Name of the table used to track DDL version.""" - - -class GitHubSettings(BaseSettings): - """Configures GitHub app for the project.""" - - model_config = SettingsConfigDict(case_sensitive=True, env_file=".env", env_prefix="GITHUB_", extra="ignore") - - NAME: str = "byte-bot-app" - """GitHub App name.""" - APP_ID: int = 480575 - """GitHub App ID.""" - APP_PRIVATE_KEY: str = "" - """GitHub App private key.""" - APP_CLIENT_ID: str = "Iv1.c3a5214c6642dedd" - """GitHub App client ID.""" - APP_CLIENT_SECRET: str = "" - """GitHub App client secret.""" - REDIRECT_URL: str = "http://127.0.0.1:3000/github/session" - """GitHub App redirect URL.""" - PERSONAL_ACCESS_TOKEN: str | None = None - """GitHub personal access token.""" - - @field_validator("APP_PRIVATE_KEY", mode="before") - def validate_and_load_private_key(cls, value: str) -> str: - """Validates and loads the GitHub App private key. - - Args: - value: The value of the APP_PRIVATE_KEY setting. - - Returns: - The validated and loaded GitHub App private key. - """ - try: - decoded_key = base64.b64decode(value).decode("utf-8") - except binascii.Error as e: - environment = os.getenv("ENVIRONMENT", "dev") - if environment != "dev": - msg = "The GitHub private key must be a valid base64 encoded string" - raise ValueError(msg) from e - - key_path = Path(BASE_DIR).parent / value - if key_path.is_file(): - return key_path.read_text() - msg = f"Private key file not found at {key_path}" - raise ValueError(msg) from e - # if not decoded_key.startswith("-----BEGIN RSA PRIVATE KEY-----") or not decoded_key.endswith( - # "-----END RSA PRIVATE KEY-----"): - # msg = "The GitHub private key must be a valid RSA key" - # raise ValueError(msg) - - return decoded_key - - -# noinspection PyShadowingNames -def load_settings() -> tuple[ - ProjectSettings, - APISettings, - OpenAPISettings, - TemplateSettings, - ServerSettings, - LogSettings, - DatabaseSettings, - GitHubSettings, -]: - """Load Settings file. - - Returns: - Settings: application settings - """ - try: - """Override Application reload dir.""" - - server: ServerSettings = ServerSettings.model_validate( - {"HOST": "0.0.0.0", "RELOAD_DIRS": [str(BASE_DIR)]}, # noqa: S104 - ) - project: ProjectSettings = ProjectSettings.model_validate({}) - api: APISettings = APISettings.model_validate({}) - openapi: OpenAPISettings = OpenAPISettings.model_validate({}) - template: TemplateSettings = TemplateSettings.model_validate({}) - log: LogSettings = LogSettings.model_validate({}) - database: DatabaseSettings = DatabaseSettings.model_validate({}) - github: GitHubSettings = GitHubSettings.model_validate({}) - - except ValidationError as error: - print(f"Could not load settings. Error: {error!r}") # noqa: T201 - raise error from error - return ( - project, - api, - openapi, - template, - server, - log, - database, - github, - ) - - -( - project, - api, - openapi, - template, - server, - log, - db, - github, -) = load_settings() diff --git a/byte_bot/server/lib/static_files.py b/byte_bot/server/lib/static_files.py deleted file mode 100644 index 10318411..00000000 --- a/byte_bot/server/lib/static_files.py +++ /dev/null @@ -1,27 +0,0 @@ -"""Static files configuration.""" - -from __future__ import annotations - -from pathlib import Path - -from litestar.static_files.config import StaticFilesConfig - -from byte_bot.server.lib import settings - -STATIC_DIRS = [settings.project.STATIC_DIR] -if settings.project.DEBUG: - STATIC_DIRS.append(Path(settings.BASE_DIR / "domain" / "web" / "resources")) - -config = [ - StaticFilesConfig( - directories=STATIC_DIRS, # type: ignore[arg-type] - path=settings.project.STATIC_URL, - name="web", - html_mode=True, - opt={"exclude_from_auth": True}, - ), -] -""" -Static files configuration. See :class:`OpenAPISettings <.settings.OpenAPISettings>` and -general :mod:`Settings.py <.settings>` for configuration. -""" diff --git a/byte_bot/server/lib/template.py b/byte_bot/server/lib/template.py deleted file mode 100644 index 1876d56c..00000000 --- a/byte_bot/server/lib/template.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Template config. - -See TemplateSettings for configuration. -""" - -from __future__ import annotations - -from litestar.template.config import TemplateConfig - -from byte_bot.server.lib import settings - -config = TemplateConfig( - directory=settings.TEMPLATES_DIR, - engine=settings.template.ENGINE, -) -""" -Template config for project. -See :class:`TemplateSettings <.settings.TemplateSettings>` for configuration. -""" diff --git a/byte_bot/server/lib/types.py b/byte_bot/server/lib/types.py deleted file mode 100644 index bc4a4836..00000000 --- a/byte_bot/server/lib/types.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Library module for type definitions to be used in the application.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any, Literal, TypeAlias, TypeVar - -from advanced_alchemy.extensions.litestar import SQLAlchemyDTO -from advanced_alchemy.filters import FilterTypes -from litestar.dto import DataclassDTO, DTOData -from litestar.types import DataclassProtocol -from sqlalchemy.orm import DeclarativeBase - -if TYPE_CHECKING: - from advanced_alchemy.service import SQLAlchemyAsyncRepositoryService - from pydantic import BaseModel - -# -- Database Types -SQLAlchemyModelT = TypeVar("SQLAlchemyModelT", bound=DeclarativeBase) -"""Type variable for SQLAlchemy models.""" -SQLAlchemyAsyncRepoServiceT = TypeVar("SQLAlchemyAsyncRepoServiceT", bound="SQLAlchemyAsyncRepositoryService") -"""Type variable for SQLAlchemy async repository services.""" -DataclassModelT = TypeVar("DataclassModelT", bound=DataclassProtocol) -"""Type variable for dataclass models.""" -ModelT: TypeAlias = SQLAlchemyModelT | DataclassModelT -"""Type alias for models.""" -FilterTypeT = TypeVar("FilterTypeT", bound=FilterTypes) -"""Type variable for filter types.""" - -# -- DTO Types -ModelDTOT = TypeVar("ModelDTOT", bound="BaseModel") -"""Type variable for models.""" -DTOT = TypeVar("DTOT", bound=DataclassProtocol | DeclarativeBase) -"""Type variable for DTOs.""" -DTOFactoryT = TypeVar("DTOFactoryT", bound=DataclassDTO | SQLAlchemyDTO) -"""Type variable for DTO factories.""" -ModelDictDTOT: TypeAlias = dict[str, Any] | ModelT | DTOData -"""Type alias for model or dict DTOs.""" -ModelDictListDTOT: TypeAlias = list[SQLAlchemyModelT | DataclassModelT | dict[str, Any]] | DTOData -"""Type alias for list of models, dicts, or DTOData.""" - -# -- App Types -Status: TypeAlias = Literal["online", "offline", "degraded"] -"""Type alias for health check status.""" diff --git a/byte_bot/server/py.typed b/byte_bot/server/py.typed deleted file mode 100644 index e69de29b..00000000 diff --git a/byte_bot/utils.py b/byte_bot/utils.py deleted file mode 100644 index 83647a8e..00000000 --- a/byte_bot/utils.py +++ /dev/null @@ -1,153 +0,0 @@ -"""General utility functions.""" - -from __future__ import annotations - -import base64 -import dataclasses -import re -import sys -import unicodedata -from importlib import import_module -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from pathlib import Path - from types import ModuleType - -__all__ = [ - "camel_case", - "case_insensitive_string_compare", - "dataclass_as_dict_shallow", - "import_string", - "slugify", -] - - -def slugify(value: str, allow_unicode: bool = False, separator: str | None = None) -> str: - """Convert a string to a slug. - - Args: - value: The string to slugify. - allow_unicode: Whether to allow unicode characters. - separator: The separator to use. - - Returns: - The slugified string. - """ - if allow_unicode: - value = unicodedata.normalize("NFKC", value) - else: - value = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode("ascii") - value = re.sub(r"[^\w\s-]", "", value.lower()) - if separator is not None: - return re.sub(r"[-\s]+", "-", value).strip("-_").replace("-", separator) - return re.sub(r"[-\s]+", "-", value).strip("-_") - - -def camel_case(string: str) -> str: - """Convert a string to camel case. - - Args: - string: The string to convert. - - Returns: - The camel cased string. - """ - return "".join(word if index == 0 else word.capitalize() for index, word in enumerate(string.split("_"))) - - -def case_insensitive_string_compare(a: str, b: str, /) -> bool: - """Compare two strings case insensitively. - - Args: - a: The first string. - b: The second string. - - Returns: - Whether the strings are equal. - """ - return a.strip().lower() == b.strip().lower() - - -def dataclass_as_dict_shallow(dataclass: Any, *, exclude_none: bool = False) -> dict[str, Any]: - """Convert a dataclass to a dict. - - Args: - dataclass: The dataclass to convert. - exclude_none: Whether to exclude None values. - - Returns: - The dataclass as a dict. - """ - ret: dict[str, Any] = {} - for field in dataclasses.fields(dataclass): - value = getattr(dataclass, field.name) - if exclude_none and value is None: - continue - ret[field.name] = value - return ret - - -def import_string(dotted_path: str) -> Any: - """Import a class/function from a dotted path. - - Args: - dotted_path: The dotted path to the class/function. - - Returns: - The imported class/function. - """ - - def _is_loaded(module: ModuleType | None) -> bool: - """Check if a module is loaded. - - Args: - module: The module to check. - - Returns: - Whether the module is loaded. - """ - spec = getattr(module, "__spec__", None) - initializing = getattr(spec, "_initializing", False) - return bool(module and spec and not initializing) - - def _cached_import(module_path: str, class_name: str) -> Any: - """Import a class/function from a dotted path. - - Args: - module_path: The dotted path to the module. - class_name: The name of the class/function. - - Returns: - The imported class/function. - """ - module = sys.modules.get(module_path) - if not _is_loaded(module): - module = import_module(module_path) - return getattr(module, class_name) - - try: - module_path, class_name = dotted_path.rsplit(".", 1) - except ValueError as e: - msg = "%s doesn't look like a module path" - raise ImportError(msg, dotted_path) from e - - try: - return _cached_import(module_path, class_name) - except AttributeError as e: - msg = "Module '%s' does not define a '%s' attribute/class" - raise ImportError(msg, module_path, class_name) from e - - -def encode_to_base64(file: Path) -> str: - """Encode a file to base64. - - Args: - file: The path to the PEM file. - - Returns: - The encoded contents of the PEM file. - """ - pem_contents = file.read_bytes() - encoded_contents = base64.b64encode(pem_contents) - return encoded_contents.decode("utf-8") diff --git a/docs/conf.py b/docs/conf.py index da749e09..552c7b1f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,23 +2,22 @@ from __future__ import annotations -import importlib.metadata import os import warnings from sqlalchemy.exc import SAWarning -from byte_bot.__metadata__ import __project__ - # -- Environmental Data ------------------------------------------------------ warnings.filterwarnings("ignore", category=SAWarning) warnings.filterwarnings("ignore", category=DeprecationWarning) # RemovedInSphinx80Warning # -- Project information ----------------------------------------------------- -project = __project__ +# NOTE: Hardcoded for now since byte-bot was split into microservices (byte-api, byte-bot, byte-common) +# TODO: Update docs structure to reflect new microservices architecture +project = "Byte Bot" copyright = "2023, Jacob Coffee" author = "Jacob Coffee" -release = os.getenv("_BYTE_BOT_DOCS_BUILD_VERSION", importlib.metadata.version("byte-bot").rsplit(".")[0]) +release = os.getenv("_BYTE_BOT_DOCS_BUILD_VERSION", "0.2.0") # -- General configuration --------------------------------------------------- extensions = [ diff --git a/docs/web/api/domain/db/models.rst b/docs/web/api/domain/db/models.rst index 38c4b1bc..1b8c150d 100644 --- a/docs/web/api/domain/db/models.rst +++ b/docs/web/api/domain/db/models.rst @@ -4,26 +4,29 @@ models Model definition for the domain. +.. note:: + Models have been moved to the byte-common shared package in the microservices architecture. + API Reference ------------- -.. automodule:: byte_bot.server.domain.db.models +.. automodule:: byte_common.models :members: Model Reference --------------- -.. sqla-model:: byte_bot.server.domain.db.models.Guild +.. sqla-model:: byte_common.models.Guild -.. sqla-model:: byte_bot.server.domain.db.models.GitHubConfig +.. sqla-model:: byte_common.models.GitHubConfig -.. sqla-model:: byte_bot.server.domain.db.models.SOTagsConfig +.. sqla-model:: byte_common.models.SOTagsConfig -.. sqla-model:: byte_bot.server.domain.db.models.AllowedUsersConfig +.. sqla-model:: byte_common.models.AllowedUsersConfig -.. sqla-model:: byte_bot.server.domain.db.models.User +.. sqla-model:: byte_common.models.User Mermaid Diagram --------------- -.. autoclasstree:: byte_bot.server.domain.db.models +.. autoclasstree:: byte_common.models diff --git a/pyproject.toml b/pyproject.toml index f1187144..601d8407 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,72 +1,6 @@ -[project] -name = "byte-bot" -version = "0.2.0" -description = "Byte is a bot destined to serve in developer-oriented servers." -authors = [ - { name = "Jacob Coffee", email = "jacob@z7x.org" }, -] -dependencies = [ - "python-dotenv>=1.0.0", - "discord-py>=2.3.2", - "pydantic>=2.5.2", - "litestar[jwt,opentelemetry,prometheus,standard,structlog]>=2.4.3", - "pydantic-settings>=2.1.0", - "anyio>=4.1.0", - "advanced-alchemy>=0.6.1", - "certifi>=2023.11.17", - "asyncpg>=0.29.0", - "githubkit[auth-app] @ git+https://github.com/yanyongyu/githubkit.git", - "PyJWT>=2.8.0", - "alembic>=1.13.0", - "ruff>=0.1.7", - "python-dateutil>=2.9.0.post0", -] -requires-python = ">=3.12,<4.0" -#readme = "README.md" -license = { text = "MIT" } -classifiers = [ - 'Development Status :: 2 - Pre-Alpha', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.12', - 'Intended Audience :: Developers', - 'Intended Audience :: Information Technology', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: MIT License', - 'Operating System :: Unix', - 'Operating System :: POSIX :: Linux', - 'Environment :: Console', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Internet', -] - -[project.urls] -Homepage = 'https://docs.byte-bot.app/' -Source = 'https://github.com/JacobCoffee/bytebot' -Documentation = 'https://docs.byte-bot.app' -Changelog = 'https://docs.byte-bot.app/latest/changelog/' -Funding = 'https://github.com/sponsors/JacobCoffee' -Issue = 'https://github.com/JacobCoffee/bytebot/issues/' -Discord = 'https://discord.gg/ZVG8hN6RrJ' -Twitter = 'https://twitter.com/_scriptr' -Reddit = 'https://reddit.com/monorepo' -Youtube = 'https://www.youtube.com/@monorepo' - -[project.scripts] -app = "byte_bot.__main__:run_cli" - -[project.entry-points."litestar.commands"] -run-bot = "byte_bot.cli:run_bot" -run-web = "byte_bot.cli:run_web" -run-all = "byte_bot.cli:run_all" - -[build-system] -requires = ["uv_build>=0.9.11,<0.10.0"] -build-backend = "uv_build" - -[tool.uv.build-backend] -module-root = "" # Flat layout: byte_bot/ at root, not src/byte_bot/ +# Byte Bot Workspace Configuration +# This is a uv workspace root - individual packages are in services/ and packages/ +# No [project] section needed at workspace level [tool.uv.workspace] members = [ @@ -113,7 +47,7 @@ ignore-words-list = "selectin, alog, thirdparty" skip = "uv.lock, package-lock.json, *.svg, docs/changelog.rst" [tool.coverage.run] -source = ["byte_bot", "packages/byte-common/src/byte_common"] +source = ["packages/byte-common/src/byte_common"] omit = [ "*/tests/*", "*/__init__.py", @@ -159,7 +93,6 @@ addopts = [ "--strict-markers", "--tb=short", "--import-mode=importlib", - "--cov=byte_bot", "--cov=byte_common", "--cov-report=term-missing", "--cov-report=html", @@ -178,19 +111,10 @@ filterwarnings = [ [tool.ty] [tool.ty.environment] -extra-paths = ["byte_bot/", "tests/", "packages/byte-common/src/", "packages/byte-common/tests/", "services/api/src/", "services/bot/src/"] +extra-paths = ["tests/", "packages/byte-common/src/", "packages/byte-common/tests/", "services/api/src/", "services/bot/src/"] -# Disable checking for old monolith code that will be removed in Phase 1.2 -# and API service which has separate import issues [tool.ty.src] exclude = [ - "byte_bot/__init__.py", - "byte_bot/app.py", - "byte_bot/cli.py", - "byte_bot/utils.py", - "byte_bot/__metadata__.py", - "byte_bot/byte/**/*.py", - "byte_bot/server/**/*.py", "services/api/**/*.py", "docs/conf.py", ] @@ -200,7 +124,7 @@ strict-imports = false [tool.ruff] line-length = 120 -src = ["byte_bot", "tests"] +src = ["packages/byte-common/src", "services/api/src", "services/bot/src", "tests"] target-version = "py311" [tool.ruff.lint] @@ -229,7 +153,7 @@ classmethod-decorators = [ ] [tool.ruff.lint.isort] -known-first-party = ["byte_bot", "byte_common", "tests"] +known-first-party = ["byte_common", "byte_api", "tests"] [tool.ruff.lint.per-file-ignores] # Tests can use magic values, assertions, and relative imports @@ -245,9 +169,6 @@ known-first-party = ["byte_bot", "byte_common", "tests"] "FBT003", # Boolean Boolean default positional argument in function definition "B008", # Do not perform function calls in argument defaults ] -"byte_bot/**/*.*" = ["PLR0913", "SLF001", "PLC0415"] # Imports inside functions in monolith -"byte_bot/server/lib/db/base.py" = ["E501"] -"byte_bot/server/lib/db/migrations/versions/*.*" = ["D", "INP", "PGH", "PLC"] "services/api/src/byte_api/app.py" = ["PLC0415"] # Imports inside create_app() to avoid circular imports "services/api/src/byte_api/lib/db/migrations/env.py" = ["SLF001"] # Alembic workaround "services/api/src/byte_api/lib/db/migrations/versions/*.*" = ["D", "INP", "PGH", "PLC"] # Alembic-generated diff --git a/services/api/src/byte_api/lib/schema.py b/services/api/src/byte_api/lib/schema.py index 7926e4d3..f91665f5 100644 --- a/services/api/src/byte_api/lib/schema.py +++ b/services/api/src/byte_api/lib/schema.py @@ -5,7 +5,7 @@ from pydantic import BaseModel as _BaseModel from pydantic import ConfigDict -from byte_bot.utils import camel_case +from byte_common.utils.strings import camel_case __all__ = ["BaseModel", "CamelizedBaseModel"] diff --git a/services/bot/pyproject.toml b/services/bot/pyproject.toml index b606b050..2da94b27 100644 --- a/services/bot/pyproject.toml +++ b/services/bot/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "byte-bot-service" +name = "byte-bot" version = "0.2.0" description = "Discord bot service for Byte Bot" authors = [ @@ -11,6 +11,7 @@ dependencies = [ "httpx>=0.25.0", "pydantic>=2.5.2", "pydantic-settings>=2.1.0", + "python-dateutil>=2.9.0.post0", "python-dotenv>=1.0.0", "anyio>=4.1.0", ]