diff --git a/.dockerignore b/.dockerignore index 21d0b898..0fea4f73 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ .venv/ +.cache/ diff --git a/.env.dev.example b/.env.dev.example new file mode 100644 index 00000000..c6542346 --- /dev/null +++ b/.env.dev.example @@ -0,0 +1,4 @@ +TUX_TOKEN="" +POSTGRES_PASSWORD="" +POSTGRES_PORT="" +COG_IGNORE_LIST="" diff --git a/.env.example b/.env.example index ab715c73..c114a02c 100644 --- a/.env.example +++ b/.env.example @@ -2,17 +2,9 @@ # Required # -# If in production mode: +# Bot token and dev/prod mode: -# DEV=False -PROD_DATABASE_URL="" -PROD_TOKEN="" - -# If in development mode: - -# DEV=True -DEV_DATABASE_URL="" -DEV_TOKEN="" +TUX_ENV=dev # # Optional diff --git a/.env.prod.example b/.env.prod.example new file mode 100644 index 00000000..c6542346 --- /dev/null +++ b/.env.prod.example @@ -0,0 +1,4 @@ +TUX_TOKEN="" +POSTGRES_PASSWORD="" +POSTGRES_PORT="" +COG_IGNORE_LIST="" diff --git a/.gitignore b/.gitignore index efe1365e..d3c7a0b6 100644 --- a/.gitignore +++ b/.gitignore @@ -154,9 +154,14 @@ prisma/database.db env/ venv/ ENV/ -env.bak/ -venv.bak/ +.env.bak .env.old +.env.dev +.env.development +.env.test +.env.local +.env.production +.env.prod # Sensitive files github-private-key.pem diff --git a/Dockerfile b/Dockerfile index aa3819b8..99079ed5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,9 +34,7 @@ RUN pip install poetry && \ # Copy the remaining project files COPY . /app -RUN mkdir -p /app/.cache/prisma && chmod +x /app/.cache/prisma - # Set PYTHONPATH environment variable to /app ENV PYTHONPATH=/app -CMD ["sh", "-c", "ls && poetry run prisma py fetch && poetry run prisma generate && poetry run python tux/main.py"] +CMD ["sh", "-c", "poetry run prisma generate && poetry run python tux/main.py"] \ No newline at end of file diff --git a/config/settings.yml.example b/config/settings.yml.example index 1557b46d..6b9073ae 100644 --- a/config/settings.yml.example +++ b/config/settings.yml.example @@ -11,32 +11,4 @@ USER_IDS: BOT_OWNER: 123456789012345679 TEMPVC_CATEGORY_ID: 123456789012345679 -TEMPVC_CHANNEL_ID: 123456789012345679 - -EMBED_COLORS: - DEFAULT: 16044058 - INFO: 12634869 - WARNING: 16634507 - ERROR: 16067173 - SUCCESS: 10407530 - POLL: 14724968 - CASE: 16217742 - NOTE: 16752228 - -EMBED_ICONS: - DEFAULT: "https://i.imgur.com/owW4EZk.png" - INFO: "https://i.imgur.com/8GRtR2G.png" - SUCCESS: "https://i.imgur.com/JsNbN7D.png" - ERROR: "https://i.imgur.com/zZjuWaU.png" - CASE: "https://i.imgur.com/c43cwnV.png" - NOTE: "https://i.imgur.com/VqPFbil.png" - POLL: "https://i.imgur.com/pkPeG5q.png" - ACTIVE_CASE: "https://github.com/allthingslinux/tux/blob/main/assets/embeds/active_case.png?raw=true" - INACTIVE_CASE: "https://github.com/allthingslinux/tux/blob/main/assets/embeds/inactive_case.png?raw=true" - ADD: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/added.png?raw=true" - REMOVE: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/removed.png?raw=true" - BAN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/ban.png?raw=true" - JAIL: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/jail.png?raw=true" - KICK: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true" - TIMEOUT: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true" - WARN: "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true" +TEMPVC_CHANNEL_ID: 123456789012345679 \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..f418e404 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,87 @@ +name: tux + +services: + + bot: + build: . + container_name: tux_bot + restart: always + develop: + watch: + - action: sync + path: . + target: /app/ + ignore: + - .venv/ + - .cache/ + - action: rebuild + path: pyproject.toml + - action: rebuild + path: poetry.lock + env_file: + - path: .env + required: true + depends_on: + db: + condition: service_healthy + command: ["sh", "-c", "poetry install --no-root && poetry run prisma db push && poetry run prisma generate && poetry run python tux/main.py"] + + db: + image: postgres + container_name: tux_db + restart: always + shm_size: 128mb + env_file: + - path: .env + required: true + volumes: + - data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 10s + retries: 5 + + adminer: + image: adminer + container_name: tux_adminer + restart: always + ports: + - 8080:8080 + env_file: + - path: .env + required: true + environment: + ADMINER_DEFAULT_DRIVER: "pgsql" + ADMINER_DEFAULT_SERVER: "db" + ADMINER_DEFAULT_DB: ${POSTGRES_DB} + ADMINER_DEFAULT_USERNAME: ${POSTGRES_USER} + ADMINER_DEFAULT_PASSWORD: ${POSTGRES_PASSWORD} + command: ["sh", "-c", "php -S 0.0.0.0:8080 -t /var/www/html"] + configs: + - source: adminer-index.php + target: /var/www/html/index.php + depends_on: + db: + condition: service_healthy + bot: + condition: service_started + +configs: + adminer-index.php: + content: | + $$_ENV['ADMINER_DEFAULT_SERVER'], + 'username' => $$_ENV['ADMINER_DEFAULT_USERNAME'], + 'password' => $$_ENV['ADMINER_DEFAULT_PASSWORD'], + 'driver' => $$_ENV['ADMINER_DEFAULT_DRIVER'], + 'db' => $$_ENV['ADMINER_DEFAULT_DB'], + ]; + } + include './adminer.php'; + ?> + +volumes: + data: \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 7d794d12..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,15 +0,0 @@ -services: - tux: - build: . - image: allthingslinux/tux:latest - container_name: tux - restart: always - develop: - watch: - - action: sync - path: . - target: /app/ - ignore: - - .venv/ - env_file: - - .env \ No newline at end of file diff --git a/prisma/migrations/20240911144909_init/migration.sql b/prisma/migrations/20240911144909_init/migration.sql new file mode 100644 index 00000000..1174e064 --- /dev/null +++ b/prisma/migrations/20240911144909_init/migration.sql @@ -0,0 +1,209 @@ +-- CreateEnum +CREATE TYPE "CaseType" AS ENUM ('BAN', 'UNBAN', 'HACKBAN', 'TEMPBAN', 'KICK', 'SNIPPETBAN', 'TIMEOUT', 'UNTIMEOUT', 'WARN', 'JAIL', 'UNJAIL', 'SNIPPETUNBAN', 'UNTEMPBAN'); + +-- CreateTable +CREATE TABLE "Guild" ( + "guild_id" BIGINT NOT NULL, + "guild_joined_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "case_count" BIGINT NOT NULL DEFAULT 0, + + CONSTRAINT "Guild_pkey" PRIMARY KEY ("guild_id") +); + +-- CreateTable +CREATE TABLE "GuildConfig" ( + "prefix" TEXT, + "mod_log_id" BIGINT, + "audit_log_id" BIGINT, + "join_log_id" BIGINT, + "private_log_id" BIGINT, + "report_log_id" BIGINT, + "dev_log_id" BIGINT, + "jail_channel_id" BIGINT, + "general_channel_id" BIGINT, + "starboard_channel_id" BIGINT, + "perm_level_0_role_id" BIGINT, + "perm_level_1_role_id" BIGINT, + "perm_level_2_role_id" BIGINT, + "perm_level_3_role_id" BIGINT, + "perm_level_4_role_id" BIGINT, + "perm_level_5_role_id" BIGINT, + "perm_level_6_role_id" BIGINT, + "perm_level_7_role_id" BIGINT, + "base_staff_role_id" BIGINT, + "base_member_role_id" BIGINT, + "jail_role_id" BIGINT, + "quarantine_role_id" BIGINT, + "guild_id" BIGINT NOT NULL, + + CONSTRAINT "GuildConfig_pkey" PRIMARY KEY ("guild_id") +); + +-- CreateTable +CREATE TABLE "Case" ( + "case_id" BIGSERIAL NOT NULL, + "case_status" BOOLEAN DEFAULT true, + "case_type" "CaseType" NOT NULL, + "case_reason" TEXT NOT NULL, + "case_moderator_id" BIGINT NOT NULL, + "case_user_id" BIGINT NOT NULL, + "case_user_roles" BIGINT[] DEFAULT ARRAY[]::BIGINT[], + "case_number" BIGINT, + "case_created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "case_expires_at" TIMESTAMP(3), + "case_tempban_expired" BOOLEAN DEFAULT false, + "guild_id" BIGINT NOT NULL, + + CONSTRAINT "Case_pkey" PRIMARY KEY ("case_id") +); + +-- CreateTable +CREATE TABLE "Snippet" ( + "snippet_id" BIGSERIAL NOT NULL, + "snippet_name" TEXT NOT NULL, + "snippet_content" TEXT NOT NULL, + "snippet_user_id" BIGINT NOT NULL, + "snippet_created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guild_id" BIGINT NOT NULL, + "uses" BIGINT NOT NULL DEFAULT 0, + "locked" BOOLEAN NOT NULL DEFAULT false, + + CONSTRAINT "Snippet_pkey" PRIMARY KEY ("snippet_id") +); + +-- CreateTable +CREATE TABLE "Note" ( + "note_id" BIGSERIAL NOT NULL, + "note_content" TEXT NOT NULL, + "note_created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "note_moderator_id" BIGINT NOT NULL, + "note_user_id" BIGINT NOT NULL, + "note_number" BIGINT, + "guild_id" BIGINT NOT NULL, + + CONSTRAINT "Note_pkey" PRIMARY KEY ("note_id") +); + +-- CreateTable +CREATE TABLE "Reminder" ( + "reminder_id" BIGSERIAL NOT NULL, + "reminder_content" TEXT NOT NULL, + "reminder_created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "reminder_expires_at" TIMESTAMP(3) NOT NULL, + "reminder_channel_id" BIGINT NOT NULL, + "reminder_user_id" BIGINT NOT NULL, + "guild_id" BIGINT NOT NULL, + + CONSTRAINT "Reminder_pkey" PRIMARY KEY ("reminder_id") +); + +-- CreateTable +CREATE TABLE "AFKModel" ( + "member_id" BIGINT NOT NULL, + "nickname" TEXT NOT NULL, + "reason" TEXT NOT NULL, + "since" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "guild_id" BIGINT NOT NULL, + + CONSTRAINT "AFKModel_pkey" PRIMARY KEY ("member_id") +); + +-- CreateTable +CREATE TABLE "Starboard" ( + "guild_id" BIGINT NOT NULL, + "starboard_channel_id" BIGINT NOT NULL, + "starboard_emoji" TEXT NOT NULL, + "starboard_threshold" INTEGER NOT NULL, + + CONSTRAINT "Starboard_pkey" PRIMARY KEY ("guild_id") +); + +-- CreateTable +CREATE TABLE "StarboardMessage" ( + "message_id" BIGINT NOT NULL, + "message_content" TEXT NOT NULL, + "message_created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "message_expires_at" TIMESTAMP(3) NOT NULL, + "message_channel_id" BIGINT NOT NULL, + "message_user_id" BIGINT NOT NULL, + "message_guild_id" BIGINT NOT NULL, + "star_count" INTEGER NOT NULL DEFAULT 0, + "starboard_message_id" BIGINT NOT NULL, + + CONSTRAINT "StarboardMessage_pkey" PRIMARY KEY ("message_id") +); + +-- CreateIndex +CREATE INDEX "Guild_guild_id_idx" ON "Guild"("guild_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "GuildConfig_guild_id_key" ON "GuildConfig"("guild_id"); + +-- CreateIndex +CREATE INDEX "GuildConfig_guild_id_idx" ON "GuildConfig"("guild_id"); + +-- CreateIndex +CREATE INDEX "Case_case_number_guild_id_idx" ON "Case"("case_number", "guild_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Case_case_number_guild_id_key" ON "Case"("case_number", "guild_id"); + +-- CreateIndex +CREATE INDEX "Snippet_snippet_name_guild_id_idx" ON "Snippet"("snippet_name", "guild_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Snippet_snippet_name_guild_id_key" ON "Snippet"("snippet_name", "guild_id"); + +-- CreateIndex +CREATE INDEX "Note_note_number_guild_id_idx" ON "Note"("note_number", "guild_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Note_note_number_guild_id_key" ON "Note"("note_number", "guild_id"); + +-- CreateIndex +CREATE INDEX "Reminder_reminder_id_guild_id_idx" ON "Reminder"("reminder_id", "guild_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Reminder_reminder_id_guild_id_key" ON "Reminder"("reminder_id", "guild_id"); + +-- CreateIndex +CREATE INDEX "AFKModel_member_id_idx" ON "AFKModel"("member_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "AFKModel_member_id_guild_id_key" ON "AFKModel"("member_id", "guild_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Starboard_guild_id_key" ON "Starboard"("guild_id"); + +-- CreateIndex +CREATE INDEX "Starboard_guild_id_idx" ON "Starboard"("guild_id"); + +-- CreateIndex +CREATE INDEX "StarboardMessage_message_id_message_guild_id_idx" ON "StarboardMessage"("message_id", "message_guild_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "StarboardMessage_message_id_message_guild_id_key" ON "StarboardMessage"("message_id", "message_guild_id"); + +-- AddForeignKey +ALTER TABLE "GuildConfig" ADD CONSTRAINT "GuildConfig_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Case" ADD CONSTRAINT "Case_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Snippet" ADD CONSTRAINT "Snippet_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Note" ADD CONSTRAINT "Note_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Reminder" ADD CONSTRAINT "Reminder_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "AFKModel" ADD CONSTRAINT "AFKModel_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Starboard" ADD CONSTRAINT "Starboard_guild_id_fkey" FOREIGN KEY ("guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "StarboardMessage" ADD CONSTRAINT "StarboardMessage_message_guild_id_fkey" FOREIGN KEY ("message_guild_id") REFERENCES "Guild"("guild_id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..fbffa92c --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 98cb797c..a7491602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,8 @@ [tool.poetry] authors = ["kaizen "] description = "All Things Linux Discord Bot" -name = "tux" +name = "Tux" +package-mode = false readme = "README.md" repository = "https://github.com/allthingslinux/tux" version = "0.1.0" @@ -15,6 +16,7 @@ asynctempfile = "^0.5.0" cairosvg = "^2.7.1" dateparser = "^1.2.0" discord-py = "^2.4.0" +emojis = "^0.7.0" githubkit = {extras = ["auth-app"], version = "^0.11.3"} httpx = "^0.27.0" jishaku = "^2.5.2" @@ -37,7 +39,6 @@ sentry-sdk = {extras = ["httpx", "loguru"], version = "^2.7.0"} types-aiofiles = "^24.1.0.20240626" types-psutil = "^6.0.0.20240621" typing-extensions = "^4.12.2" -emojis = "^0.7.0" [tool.poetry.group.docs.dependencies] mkdocs-material = "^9.5.30" @@ -48,6 +49,7 @@ requires = ["poetry-core"] # Ruff Configuration [tool.ruff] +cache-dir = ".cache/ruff" exclude = [ ".bzr", ".direnv", @@ -171,3 +173,8 @@ reportShadowedImports = false typeCheckingMode = "strict" venv = ".venv" venvPath = "." + +[tool.prisma] +binary_cache_dir = '.cache/prisma' +nodeenv_cache_dir = '.cache/nodeenv' +use_global_node = false diff --git a/set_env.sh b/set_env.sh new file mode 100755 index 00000000..1bc65e5e --- /dev/null +++ b/set_env.sh @@ -0,0 +1,81 @@ +#!/bin/bash + +# Function to print usage instructions +usage() { + echo "Usage: $0 {dev|prod}" + exit 1 +} + +# Check if the correct number of arguments is provided +if [ "$#" -ne 1 ]; then + usage +fi + +# Set the environment based on the argument +TUX_ENV=$1 + +# Determine which .env file to source +if [ "$TUX_ENV" == "prod" ]; then + ENV_FILE=".env.prod" +elif [ "$TUX_ENV" == "dev" ]; then + ENV_FILE=".env.dev" +else + usage +fi + +# Check if the environment file exists +if [ ! -f "$ENV_FILE" ]; then + echo "Environment file $ENV_FILE does not exist!" + exit 1 +fi + +# Backup existing .env file if it exists +if [ -f ".env" ]; then + cp .env .env.bak + echo "Existing .env file backed up to .env.bak" +fi + +# Function to add or replace environment variables in the .env file +add_or_replace_var() { + local var_name + var_name=$(echo "$1" | cut -d '=' -f 1) + local var_value + var_value=$(echo "$1" | cut -d '=' -f 2-) + + # Escape special characters except for '/' + var_value=${var_value//&/\\&} + + # Properly quote the value if not already quoted + if [[ $var_value != \"*\" ]]; then + var_value="\"$var_value\"" + fi + + # If the variable already exists, replace it + if grep -q "^$var_name=" .env; then + sed -i "s|^$var_name=.*|$var_name=$var_value|" .env + else + echo "$var_name=$var_value" >>.env + fi +} + +# Set the environment at the top +add_or_replace_var "TUX_ENV=$TUX_ENV" + +# Parse the environment file and export variables +while IFS= read -r line; do + # Ignore comments and empty lines + if [[ ! "$line" =~ ^# && "$line" =~ .*=.* ]]; then + # Export the variable + eval "export $line" + add_or_replace_var "$line" + fi +done <"$ENV_FILE" + +# Optionally: Export additional dynamically computed variables +DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:${POSTGRES_PORT}/${POSTGRES_DB}" + +add_or_replace_var "DATABASE_URL=$DATABASE_URL" + +# Print the environment variables for verification +echo "Environment ($TUX_ENV) variables updated in .env file:" +cat .env diff --git a/tux/cog_loader.py b/tux/cog_loader.py index 46249ba3..75df2c61 100644 --- a/tux/cog_loader.py +++ b/tux/cog_loader.py @@ -6,13 +6,13 @@ from discord.ext import commands from loguru import logger -from tux.utils.constants import Constants as CONST +from tux.utils.config import CONFIG class CogLoader(commands.Cog): def __init__(self, bot: commands.Bot) -> None: self.bot = bot - self.cog_ignore_list: set[str] = CONST.COG_IGNORE_LIST + self.cog_ignore_list: set[str] = CONFIG.COG_IGNORE_LIST async def is_cog_eligible(self, filepath: Path) -> bool: """ diff --git a/tux/cogs/admin/git.py b/tux/cogs/admin/git.py index 6291b97e..e56eee5c 100644 --- a/tux/cogs/admin/git.py +++ b/tux/cogs/admin/git.py @@ -5,7 +5,7 @@ from tux.ui.buttons import GithubButton from tux.ui.embeds import EmbedCreator from tux.utils import checks -from tux.utils.constants import Constants as CONST +from tux.utils.config import CONFIG from tux.utils.flags import generate_usage from tux.wrappers.github import GithubService @@ -16,7 +16,7 @@ class Git(commands.Cog): def __init__(self, bot: Tux) -> None: self.bot = bot self.github = GithubService() - self.repo_url = CONST.GITHUB_REPO_URL + self.repo_url = CONFIG.GITHUB_REPO_URL self.git.usage = generate_usage(self.git) self.get_repo.usage = generate_usage(self.get_repo) self.create_issue.usage = generate_usage(self.create_issue) diff --git a/tux/cogs/admin/mail.py b/tux/cogs/admin/mail.py index 112aa7ef..56bc82b0 100644 --- a/tux/cogs/admin/mail.py +++ b/tux/cogs/admin/mail.py @@ -8,7 +8,7 @@ from tux.bot import Tux from tux.utils import checks -from tux.utils.constants import Constants as CONST +from tux.utils.config import CONFIG MailboxData = dict[str, str | list[str]] @@ -16,12 +16,12 @@ class Mail(commands.Cog): def __init__(self, bot: Tux) -> None: self.bot = bot - self.api_url = CONST.MAILCOW_API_URL + self.api_url = CONFIG.MAILCOW_API_URL self.headers = { "Content-Type": "application/json", "Accept": "application/json", - "X-API-Key": CONST.MAILCOW_API_KEY, - "Authorization": f"Bearer {CONST.MAILCOW_API_KEY}", + "X-API-Key": CONFIG.MAILCOW_API_KEY, + "Authorization": f"Bearer {CONFIG.MAILCOW_API_KEY}", } self.default_options: dict[str, str | list[str]] = { "active": "1", diff --git a/tux/cogs/guild/config.py b/tux/cogs/guild/config.py index 404fa499..b53286e2 100644 --- a/tux/cogs/guild/config.py +++ b/tux/cogs/guild/config.py @@ -8,7 +8,7 @@ from tux.database.controllers import DatabaseController from tux.ui.embeds import EmbedCreator, EmbedType from tux.ui.views.config import ConfigSetChannels, ConfigSetPrivateLogs, ConfigSetPublicLogs -from tux.utils.constants import CONST +from tux.utils.config import CONFIG # TODO: Add onboarding setup to ensure all required channels, logs, and roles are set up # TODO: Figure out how to handle using our custom checks because the current checks would result in a lock out @@ -380,7 +380,7 @@ async def config_clear_prefix( user_display_avatar=interaction.user.display_avatar.url, embed_type=EmbedCreator.SUCCESS, title="Guild Config", - description=f"The prefix was reset to `{CONST.DEFAULT_PREFIX}`", + description=f"The prefix was reset to `{CONFIG.DEFAULT_PREFIX}`", ), ) diff --git a/tux/cogs/services/temp_vc.py b/tux/cogs/services/temp_vc.py index 5360c552..d90e42a0 100644 --- a/tux/cogs/services/temp_vc.py +++ b/tux/cogs/services/temp_vc.py @@ -2,7 +2,7 @@ from discord.ext import commands from tux.bot import Tux -from tux.utils.constants import Constants as CONST +from tux.utils.config import CONFIG class TempVc(commands.Cog): @@ -32,8 +32,8 @@ async def on_voice_state_update( """ # Ensure constants are set correctly - temp_channel_id = int(CONST.TEMPVC_CHANNEL_ID or "0") - temp_category_id = int(CONST.TEMPVC_CATEGORY_ID or "0") + temp_channel_id = int(CONFIG.TEMPVC_CHANNEL_ID or "0") + temp_category_id = int(CONFIG.TEMPVC_CATEGORY_ID or "0") if temp_channel_id == 0 or temp_category_id == 0: return diff --git a/tux/help.py b/tux/help.py index b854b399..4bf8ea99 100644 --- a/tux/help.py +++ b/tux/help.py @@ -12,7 +12,8 @@ from reactionmenu.views_menu import ViewSelect from tux.ui.embeds import EmbedCreator -from tux.utils.constants import Constants as CONST +from tux.utils.config import CONFIG +from tux.utils.constants import CONST class TuxHelp(commands.HelpCommand): @@ -36,7 +37,7 @@ async def _get_prefix(self) -> str: The prefix used to invoke the bot. """ - return self.context.clean_prefix or CONST.DEFAULT_PREFIX + return self.context.clean_prefix or CONFIG.DEFAULT_PREFIX def _embed_base(self, title: str, description: str | None = None) -> discord.Embed: """ diff --git a/tux/main.py b/tux/main.py index a7aad94c..4550ced1 100644 --- a/tux/main.py +++ b/tux/main.py @@ -10,9 +10,9 @@ from tux.bot import Tux from tux.database.controllers.guild_config import GuildConfigController from tux.help import TuxHelp +from tux.utils.config import CONFIG # from tux.utils.console import Console -from tux.utils.constants import Constants as CONST async def get_prefix(bot: Tux, message: discord.Message) -> list[str]: @@ -21,19 +21,19 @@ async def get_prefix(bot: Tux, message: discord.Message) -> list[str]: if message.guild: prefix = await GuildConfigController().get_guild_prefix(message.guild.id) - return commands.when_mentioned_or(prefix or CONST.DEFAULT_PREFIX)(bot, message) + return commands.when_mentioned_or(prefix or CONFIG.DEFAULT_PREFIX)(bot, message) async def main() -> None: - if not CONST.TOKEN: + if not CONFIG.TUX_TOKEN: logger.critical("No token provided, exiting.") return logger.info("Setting up Sentry...") sentry_sdk.init( - dsn=CONST.SENTRY_URL, - environment="dev" if CONST.DEV == "True" else "prod", + dsn=CONFIG.SENTRY_URL, + environment="dev" if CONFIG.TUX_ENV == "dev" else "prod", traces_sample_rate=1.0, profiles_sample_rate=1.0, enable_tracing=True, @@ -47,7 +47,7 @@ async def main() -> None: strip_after_prefix=True, case_insensitive=True, intents=discord.Intents.all(), - owner_ids=[*CONST.SYSADMIN_IDS, CONST.BOT_OWNER_ID], + owner_ids=[*CONFIG.SYSADMIN_IDS, CONFIG.BOT_OWNER_ID], allowed_mentions=discord.AllowedMentions(everyone=False), help_command=TuxHelp(), ) @@ -59,7 +59,7 @@ async def main() -> None: # console = Console(bot) # console_task = asyncio.create_task(console.run_console()) - await bot.start(token=CONST.TOKEN, reconnect=True) + await bot.start(token=CONFIG.TUX_TOKEN, reconnect=True) except KeyboardInterrupt: logger.info("KeyboardInterrupt received, shutting down.") diff --git a/tux/utils/checks.py b/tux/utils/checks.py index f9280bbd..98767288 100644 --- a/tux/utils/checks.py +++ b/tux/utils/checks.py @@ -8,7 +8,7 @@ from tux.bot import Tux from tux.database.controllers import DatabaseController -from tux.utils.constants import CONST +from tux.utils.config import CONFIG from tux.utils.exceptions import AppCommandPermissionLevelError, PermissionLevelError db = DatabaseController().guild_config @@ -42,8 +42,8 @@ async def has_permission( if isinstance(author, discord.Member) and any(role in [r.id for r in author.roles] for role in roles): return True - return (8 in range(lower_bound, higher_bound + 1) and author.id in CONST.SYSADMIN_IDS) or ( - 9 in range(lower_bound, higher_bound + 1) and author.id == CONST.BOT_OWNER_ID + return (8 in range(lower_bound, higher_bound + 1) and author.id in CONFIG.SYSADMIN_IDS) or ( + 9 in range(lower_bound, higher_bound + 1) and author.id == CONFIG.BOT_OWNER_ID ) diff --git a/tux/utils/config.py b/tux/utils/config.py new file mode 100644 index 00000000..c4ad74f4 --- /dev/null +++ b/tux/utils/config.py @@ -0,0 +1,71 @@ +import base64 +import os +from pathlib import Path +from typing import ClassVar, Final + +import yaml +from dotenv import load_dotenv + +# Load environment variables from the single .env file +load_dotenv() + +# Load YAML configuration +settings_file_path = Path("config/settings.yml") +settings = yaml.safe_load(settings_file_path.read_text()) + + +class Config: + # Environment-specific constants + TUX_ENV: Final[str] = os.getenv("TUX_ENV", "dev") + TUX_TOKEN: Final[str] = os.getenv("TUX_TOKEN", "") + + POSTGRES_DB: Final[str] = os.getenv("POSTGRES_DB", "postgres") + POSTGRES_PORT: Final[str] = os.getenv("POSTGRES_PORT", "5432") + POSTGRES_USER: Final[str] = os.getenv("POSTGRES_USER", "postgres") + POSTGRES_PASSWORD: Final[str] = os.getenv("POSTGRES_PASSWORD", "tux") + + # Derived settings + DATABASE_URL: Final[str] = os.getenv("DATABASE_URL", "") + + # Cog ignore list constants + COG_IGNORE_LIST: ClassVar[set[str]] = set(os.getenv("COG_IGNORE_LIST", "").split(",")) + + # Sentry constants + SENTRY_URL: Final[str] = os.getenv("SENTRY_URL", "") + + # Default command prefixes based on TUX_ENV + DEFAULT_PREFIX: Final[str] = ( + settings["DEFAULT_PREFIX"]["DEV"] if TUX_ENV == "dev" else settings["DEFAULT_PREFIX"]["PROD"] + ) + + # Non-environment-specific GitHub constants + GITHUB_REPO_URL: Final[str] = os.getenv("GITHUB_REPO_URL", "") + GITHUB_REPO_OWNER: Final[str] = os.getenv("GITHUB_REPO_OWNER", "") + GITHUB_REPO: Final[str] = os.getenv("GITHUB_REPO", "") + GITHUB_TOKEN: Final[str] = os.getenv("GITHUB_TOKEN", "") + GITHUB_APP_ID: Final[int] = int(os.getenv("GITHUB_APP_ID", "0")) + GITHUB_CLIENT_ID: Final[str] = os.getenv("GITHUB_CLIENT_ID", "") + GITHUB_CLIENT_SECRET: Final[str] = os.getenv("GITHUB_CLIENT_SECRET", "") + GITHUB_PUBLIC_KEY: Final[str] = os.getenv("GITHUB_PUBLIC_KEY", "") + GITHUB_INSTALLATION_ID: Final[str] = os.getenv("GITHUB_INSTALLATION_ID", "0") + GITHUB_PRIVATE_KEY: Final[str] = ( + base64.b64decode(os.getenv("GITHUB_PRIVATE_KEY_BASE64", "")).decode("utf-8") + if os.getenv("GITHUB_PRIVATE_KEY_BASE64") + else "" + ) + + # Non-environment-specific Mailcow constants + MAILCOW_API_KEY: Final[str] = os.getenv("MAILCOW_API_KEY", "") + MAILCOW_API_URL: Final[str] = os.getenv("MAILCOW_API_URL", "") + + # Permission constants via config/settings.yml + BOT_OWNER_ID: Final[int] = settings["USER_IDS"]["BOT_OWNER"] + SYSADMIN_IDS: Final[list[int]] = settings["USER_IDS"]["SYSADMINS"] + + # Temp VC constants via config/settings.yml + TEMPVC_CATEGORY_ID: Final[str | None] = settings["TEMPVC_CATEGORY_ID"] + TEMPVC_CHANNEL_ID: Final[str | None] = settings["TEMPVC_CHANNEL_ID"] + + +# Load the updated environment variables into the current environment +CONFIG = Config() diff --git a/tux/utils/constants.py b/tux/utils/constants.py index 1dada67a..fa44a83e 100644 --- a/tux/utils/constants.py +++ b/tux/utils/constants.py @@ -1,81 +1,38 @@ -import base64 -import os -from pathlib import Path from typing import Final -import yaml -from dotenv import load_dotenv, set_key - -load_dotenv(verbose=True) - -config_file = Path("config/settings.yml") -config = yaml.safe_load(config_file.read_text()) - class Constants: - # Permission constants - BOT_OWNER_ID: Final[int] = config["USER_IDS"]["BOT_OWNER"] - SYSADMIN_IDS: Final[list[int]] = config["USER_IDS"]["SYSADMINS"] - - # Production env constants - PROD_TOKEN: Final[str] = os.getenv("PROD_TOKEN", "") - DEFAULT_PROD_PREFIX: Final[str] = config["DEFAULT_PREFIX"]["PROD"] - PROD_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("PROD_COG_IGNORE_LIST", "").split(",")) - - # Dev env constants - DEV: Final[str | None] = os.getenv("DEV") - DEV_TOKEN: Final[str] = os.getenv("DEV_TOKEN", "") - DEFAULT_DEV_PREFIX: Final[str] = config["DEFAULT_PREFIX"]["DEV"] - DEV_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("DEV_COG_IGNORE_LIST", "").split(",")) - - # Debug env constants - DEBUG: Final[bool] = bool(os.getenv("DEBUG", "True")) - - # Final env constants - TOKEN: Final[str] = DEV_TOKEN if DEV and DEV.lower() == "true" else PROD_TOKEN - DEFAULT_PREFIX: Final[str] = DEFAULT_DEV_PREFIX if DEV and DEV.lower() == "true" else DEFAULT_PROD_PREFIX - COG_IGNORE_LIST: Final[set[str]] = DEV_COG_IGNORE_LIST if DEV and DEV.lower() == "true" else PROD_COG_IGNORE_LIST - - # Sentry-related constants - SENTRY_URL: Final[str | None] = os.getenv("SENTRY_URL", "") - - # Database constants - PROD_DATABASE_URL: Final[str] = os.getenv("PROD_DATABASE_URL", "") - DEV_DATABASE_URL: Final[str] = os.getenv("DEV_DATABASE_URL", "") - - DATABASE_URL: Final[str] = DEV_DATABASE_URL if DEV and DEV.lower() == "true" else PROD_DATABASE_URL - - set_key(".env", "DATABASE_URL", DATABASE_URL) - - # GitHub constants - GITHUB_REPO_URL: Final[str] = os.getenv("GITHUB_REPO_URL", "") - GITHUB_REPO_OWNER: Final[str] = os.getenv("GITHUB_REPO_OWNER", "") - GITHUB_REPO: Final[str] = os.getenv("GITHUB_REPO", "") - GITHUB_TOKEN: Final[str] = os.getenv("GITHUB_TOKEN", "") - GITHUB_APP_ID: Final[int] = int(os.getenv("GITHUB_APP_ID") or "0") - GITHUB_CLIENT_ID = os.getenv("GITHUB_CLIENT_ID", "") - GITHUB_CLIENT_SECRET = os.getenv("GITHUB_CLIENT_SECRET", "") - GITHUB_PUBLIC_KEY = os.getenv("GITHUB_PUBLIC_KEY", "") - GITHUB_INSTALLATION_ID: Final[str] = os.getenv("GITHUB_INSTALLATION_ID") or "0" - GITHUB_PRIVATE_KEY: str = ( - base64.b64decode(os.getenv("GITHUB_PRIVATE_KEY_BASE64", "")).decode("utf-8") - if os.getenv("GITHUB_PRIVATE_KEY_BASE64") - else "" - ) - - # Mailcow constants - MAILCOW_API_KEY: Final[str] = os.getenv("MAILCOW_API_KEY", "") - MAILCOW_API_URL: Final[str] = os.getenv("MAILCOW_API_URL", "") - - # Temp VC constants - TEMPVC_CATEGORY_ID: Final[str | None] = config["TEMPVC_CATEGORY_ID"] - TEMPVC_CHANNEL_ID: Final[str | None] = config["TEMPVC_CHANNEL_ID"] - # Color constants - EMBED_COLORS: Final[dict[str, int]] = config["EMBED_COLORS"] + EMBED_COLORS: Final[dict[str, int]] = { + "DEFAULT": 16044058, + "INFO": 12634869, + "WARNING": 16634507, + "ERROR": 16067173, + "SUCCESS": 10407530, + "POLL": 14724968, + "CASE": 16217742, + "NOTE": 16752228, + } # Icon constants - EMBED_ICONS: Final[dict[str, str]] = config["EMBED_ICONS"] + EMBED_ICONS: Final[dict[str, str]] = { + "DEFAULT": "https://i.imgur.com/owW4EZk.png", + "INFO": "https://i.imgur.com/8GRtR2G.png", + "SUCCESS": "https://i.imgur.com/JsNbN7D.png", + "ERROR": "https://i.imgur.com/zZjuWaU.png", + "CASE": "https://i.imgur.com/c43cwnV.png", + "NOTE": "https://i.imgur.com/VqPFbil.png", + "POLL": "https://i.imgur.com/pkPeG5q.png", + "ACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/active_case.png?raw=true", + "INACTIVE_CASE": "https://github.com/allthingslinux/tux/blob/main/assets/embeds/inactive_case.png?raw=true", + "ADD": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/added.png?raw=true", + "REMOVE": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/removed.png?raw=true", + "BAN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/ban.png?raw=true", + "JAIL": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/jail.png?raw=true", + "KICK": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/kick.png?raw=true", + "TIMEOUT": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/timeout.png?raw=true", + "WARN": "https://github.com/allthingslinux/tux/blob/main/assets/emojis/warn.png?raw=true", + } # Embed limit constants EMBED_MAX_NAME_LENGTH = 256 diff --git a/tux/wrappers/github.py b/tux/wrappers/github.py index a9506b5a..793cbdec 100644 --- a/tux/wrappers/github.py +++ b/tux/wrappers/github.py @@ -8,26 +8,26 @@ ) from loguru import logger -from tux.utils.constants import Constants as CONST +from tux.utils.config import CONFIG class GithubService: def __init__(self) -> None: self.github = GitHub( AppInstallationAuthStrategy( - CONST.GITHUB_APP_ID, - CONST.GITHUB_PRIVATE_KEY, - int(CONST.GITHUB_INSTALLATION_ID), - CONST.GITHUB_CLIENT_ID, - CONST.GITHUB_CLIENT_SECRET, + CONFIG.GITHUB_APP_ID, + CONFIG.GITHUB_PRIVATE_KEY, + int(CONFIG.GITHUB_INSTALLATION_ID), + CONFIG.GITHUB_CLIENT_ID, + CONFIG.GITHUB_CLIENT_SECRET, ), ) async def get_repo(self) -> FullRepository: try: response: Response[FullRepository] = await self.github.rest.repos.async_get( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, ) repo: FullRepository = response.parsed_data @@ -42,8 +42,8 @@ async def get_repo(self) -> FullRepository: async def create_issue(self, title: str, body: str) -> Issue: try: response: Response[Issue] = await self.github.rest.issues.async_create( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, title=title, body=body, ) @@ -60,8 +60,8 @@ async def create_issue(self, title: str, body: str) -> Issue: async def create_issue_comment(self, issue_number: int, body: str) -> IssueComment: try: response: Response[IssueComment] = await self.github.rest.issues.async_create_comment( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, issue_number, body=body, ) @@ -78,8 +78,8 @@ async def create_issue_comment(self, issue_number: int, body: str) -> IssueComme async def close_issue(self, issue_number: int) -> Issue: try: response: Response[Issue] = await self.github.rest.issues.async_update( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, issue_number, state="closed", ) @@ -96,8 +96,8 @@ async def close_issue(self, issue_number: int) -> Issue: async def get_issue(self, issue_number: int) -> Issue: try: response: Response[Issue] = await self.github.rest.issues.async_get( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, issue_number, ) @@ -113,8 +113,8 @@ async def get_issue(self, issue_number: int) -> Issue: async def get_open_issues(self) -> list[Issue]: try: response: Response[list[Issue]] = await self.github.rest.issues.async_list_for_repo( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, state="open", ) @@ -130,8 +130,8 @@ async def get_open_issues(self) -> list[Issue]: async def get_closed_issues(self) -> list[Issue]: try: response: Response[list[Issue]] = await self.github.rest.issues.async_list_for_repo( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, state="closed", ) @@ -147,8 +147,8 @@ async def get_closed_issues(self) -> list[Issue]: async def get_open_pulls(self) -> list[PullRequestSimple]: try: response: Response[list[PullRequestSimple]] = await self.github.rest.pulls.async_list( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, state="open", ) @@ -164,8 +164,8 @@ async def get_open_pulls(self) -> list[PullRequestSimple]: async def get_closed_pulls(self) -> list[PullRequestSimple]: try: response: Response[list[PullRequestSimple]] = await self.github.rest.pulls.async_list( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, state="closed", ) @@ -181,8 +181,8 @@ async def get_closed_pulls(self) -> list[PullRequestSimple]: async def get_pull(self, pr_number: int) -> PullRequest: try: response: Response[PullRequest] = await self.github.rest.pulls.async_get( - CONST.GITHUB_REPO_OWNER, - CONST.GITHUB_REPO, + CONFIG.GITHUB_REPO_OWNER, + CONFIG.GITHUB_REPO, pr_number, )