From 8f0cda359e3fdb1a3735b1e7b48fee55c18859ba Mon Sep 17 00:00:00 2001 From: wlinator Date: Wed, 11 Sep 2024 09:13:26 -0400 Subject: [PATCH 01/17] Move to local postgres DB --- .env.example | 14 ++++++-------- docker-compose.dev.yml | 44 ++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 15 -------------- tux/utils/constants.py | 38 +++++++++++++++++------------------- 4 files changed, 68 insertions(+), 43 deletions(-) create mode 100644 docker-compose.dev.yml delete mode 100644 docker-compose.yml diff --git a/.env.example b/.env.example index ab715c73..b49b59dd 100644 --- a/.env.example +++ b/.env.example @@ -2,17 +2,15 @@ # Required # -# If in production mode: +# Bot token and dev/prod mode: -# DEV=False -PROD_DATABASE_URL="" -PROD_TOKEN="" +DEV=False +TUX_TOKEN="" -# If in development mode: +# Database credentials: -# DEV=True -DEV_DATABASE_URL="" -DEV_TOKEN="" +POSTGRES_PASSWORD="" +POSTGRES_PORT = "5432" # # Optional diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..1faefc71 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,44 @@ +services: + tux: + build: . + container_name: tux + restart: always + develop: + watch: + - action: sync + path: . + target: /app/ + ignore: + - .venv/ + env_file: + - path: ./.env + required: true + depends_on: + db: + condition: service_healthy + + db: + image: postgres + container_name: tux-db + restart: always + shm_size: 128mb + env_file: + - path: ./.env + required: true + volumes: + - tux_data:/var/lib/postgresql/data + healthcheck: + test: pg_isready -U postgres + interval: 5s + timeout: 10s + retries: 5 + + adminer: + image: adminer + container_name: tux-adminer + restart: always + ports: + - 8080:8080 + +volumes: + tux_data: 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/tux/utils/constants.py b/tux/utils/constants.py index 1dada67a..df1de17e 100644 --- a/tux/utils/constants.py +++ b/tux/utils/constants.py @@ -17,36 +17,34 @@ class 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 + # env constants DEV: Final[str | None] = os.getenv("DEV") - DEV_TOKEN: Final[str] = os.getenv("DEV_TOKEN", "") + TOKEN: Final[str] = os.getenv("TUX_TOKEN", "") + + # Prefix constants + DEFAULT_PROD_PREFIX: Final[str] = config["DEFAULT_PREFIX"]["PROD"] DEFAULT_DEV_PREFIX: Final[str] = config["DEFAULT_PREFIX"]["DEV"] + DEFAULT_PREFIX: Final[str] = DEFAULT_DEV_PREFIX if DEV and DEV.lower() == "true" else DEFAULT_PROD_PREFIX + + # Cog ignore list constants + PROD_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("PROD_COG_IGNORE_LIST", "").split(",")) DEV_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("DEV_COG_IGNORE_LIST", "").split(",")) + COG_IGNORE_LIST: Final[set[str]] = DEV_COG_IGNORE_LIST if DEV and DEV.lower() == "true" else PROD_COG_IGNORE_LIST + + # Database constants + POSTGRES_PASSWORD: Final[str] = os.getenv("POSTGRES_PASSWORD", "tux") + POSTGRES_PORT: Final[str] = os.getenv("POSTGRES_PORT", "5432") + + # Set final database string in .env + DATABASE_URL: Final[str] = f"postgresql://postgres:{POSTGRES_PASSWORD}@db:{POSTGRES_PORT}/postgres" + set_key(".env", "DATABASE_URL", DATABASE_URL) # 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", "") From be0e1b2ded007206fdf6aa6337ef1a8e64284de2 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 11 Sep 2024 13:54:33 -0400 Subject: [PATCH 02/17] feat: add .env.dev.example and .env.prod.example files for better environment configuration refactor: simplify .env.example by removing unnecessary variables and adding TUX_ENV variable chore: update .gitignore to ignore new environment files refactor: simplify settings.yml.example by removing unnecessary variables --- .env.dev.example | 4 ++++ .env.example | 8 +------- .env.prod.example | 4 ++++ .gitignore | 6 ++++++ config/settings.yml.example | 30 +----------------------------- 5 files changed, 16 insertions(+), 36 deletions(-) create mode 100644 .env.dev.example create mode 100644 .env.prod.example 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 b49b59dd..c114a02c 100644 --- a/.env.example +++ b/.env.example @@ -4,13 +4,7 @@ # Bot token and dev/prod mode: -DEV=False -TUX_TOKEN="" - -# Database credentials: - -POSTGRES_PASSWORD="" -POSTGRES_PORT = "5432" +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..376ca901 100644 --- a/.gitignore +++ b/.gitignore @@ -157,6 +157,12 @@ ENV/ env.bak/ venv.bak/ .env.old +.env.dev +.env.development +.env.test +.env.local +.env.production +.env.prod # Sensitive files github-private-key.pem 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 From 9a5e886369034231731a5c8ca9d400d362a09fb9 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 11 Sep 2024 13:55:02 -0400 Subject: [PATCH 03/17] feat(utils/config.py): add new configuration file to manage environment variables and settings This new configuration file will load environment variables from .env files based on the TUX_ENV variable. It also loads settings from a YAML file and provides a Config class to access these settings. This change was made to centralize configuration management and improve code readability. --- tux/utils/config.py | 77 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tux/utils/config.py diff --git a/tux/utils/config.py b/tux/utils/config.py new file mode 100644 index 00000000..2bbef606 --- /dev/null +++ b/tux/utils/config.py @@ -0,0 +1,77 @@ +import base64 +import os +from pathlib import Path +from typing import ClassVar, Final + +import yaml +from dotenv import load_dotenv, set_key + +# Load shared environment variables from .env +load_dotenv(dotenv_path=".env") + +# Determine which environment file to load based on TUX_ENV +env = os.getenv("TUX_ENV", "dev").lower() + +# Load the environment-specific .env file +env_file = ".env.prod" if env == "prod" else ".env.dev" +load_dotenv(dotenv_path=env_file) + +# 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_PASSWORD: Final[str] = os.getenv("POSTGRES_PASSWORD", "tux") + POSTGRES_PORT: Final[str] = os.getenv("POSTGRES_PORT", "5432") + + # Derived settings + DATABASE_URL: Final[str] = f"postgresql://postgres:{POSTGRES_PASSWORD}@db:{POSTGRES_PORT}/postgres" + + set_key(".env", "DATABASE_URL", 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"] + + +CONFIG = Config() From 7c7080a0182faae1228a3b6eea4b6d232a1ced45 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 11 Sep 2024 13:55:18 -0400 Subject: [PATCH 04/17] refactor(constants.py): remove unused imports and configuration loading feat(constants.py): hardcode EMBED_COLORS and EMBED_ICONS for simplicity and to avoid unnecessary config file dependencies --- tux/utils/constants.py | 97 ++++++++++++------------------------------ 1 file changed, 28 insertions(+), 69 deletions(-) diff --git a/tux/utils/constants.py b/tux/utils/constants.py index df1de17e..fa44a83e 100644 --- a/tux/utils/constants.py +++ b/tux/utils/constants.py @@ -1,79 +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"] - - # env constants - DEV: Final[str | None] = os.getenv("DEV") - TOKEN: Final[str] = os.getenv("TUX_TOKEN", "") - - # Prefix constants - DEFAULT_PROD_PREFIX: Final[str] = config["DEFAULT_PREFIX"]["PROD"] - DEFAULT_DEV_PREFIX: Final[str] = config["DEFAULT_PREFIX"]["DEV"] - DEFAULT_PREFIX: Final[str] = DEFAULT_DEV_PREFIX if DEV and DEV.lower() == "true" else DEFAULT_PROD_PREFIX - - # Cog ignore list constants - PROD_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("PROD_COG_IGNORE_LIST", "").split(",")) - DEV_COG_IGNORE_LIST: Final[set[str]] = set(os.getenv("DEV_COG_IGNORE_LIST", "").split(",")) - COG_IGNORE_LIST: Final[set[str]] = DEV_COG_IGNORE_LIST if DEV and DEV.lower() == "true" else PROD_COG_IGNORE_LIST - - # Database constants - POSTGRES_PASSWORD: Final[str] = os.getenv("POSTGRES_PASSWORD", "tux") - POSTGRES_PORT: Final[str] = os.getenv("POSTGRES_PORT", "5432") - - # Set final database string in .env - DATABASE_URL: Final[str] = f"postgresql://postgres:{POSTGRES_PASSWORD}@db:{POSTGRES_PORT}/postgres" - set_key(".env", "DATABASE_URL", DATABASE_URL) - - # Debug env constants - DEBUG: Final[bool] = bool(os.getenv("DEBUG", "True")) - - # Sentry-related constants - SENTRY_URL: Final[str | None] = os.getenv("SENTRY_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 From 450c1887fb22c15c2fa2d8f90a8c055fa5ef7b06 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 11 Sep 2024 13:55:45 -0400 Subject: [PATCH 05/17] refactor: replace Constants with Config in multiple files for better configuration management The Constants module was replaced with a Config module in multiple files. This change was made to improve the management of configuration variables, making it easier to modify and maintain them. The Config module provides a more flexible and scalable way to handle configuration settings. --- tux/cog_loader.py | 4 +-- tux/cogs/admin/git.py | 4 +-- tux/cogs/admin/mail.py | 8 +++--- tux/cogs/guild/config.py | 4 +-- tux/cogs/services/temp_vc.py | 6 ++--- tux/help.py | 5 ++-- tux/main.py | 14 +++++----- tux/utils/checks.py | 6 ++--- tux/wrappers/github.py | 52 ++++++++++++++++++------------------ 9 files changed, 52 insertions(+), 51 deletions(-) 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/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, ) From 1d08ac4b5559b830929f6f77388b7bd0594ddf02 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 11 Sep 2024 13:56:12 -0400 Subject: [PATCH 06/17] feat(prisma): initialize database schema and migration lock This commit introduces the initial database schema for the application. It includes the creation of several tables such as "Guild", "GuildConfig", "Case", "Snippet", "Note", "Reminder", "AFKModel", "Starboard", and "StarboardMessage". Each table has its own set of columns, constraints, and indices. In addition, the commit also introduces a migration lock file for Prisma, which is crucial for managing database migrations in a team environment. This file should not be edited manually and should be included in the version control system. --- .../20240911144909_init/migration.sql | 209 ++++++++++++++++++ prisma/migrations/migration_lock.toml | 3 + 2 files changed, 212 insertions(+) create mode 100644 prisma/migrations/20240911144909_init/migration.sql create mode 100644 prisma/migrations/migration_lock.toml 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 From bb707f2017159f32a6e3818736463628b3b4568f Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Wed, 11 Sep 2024 13:57:13 -0400 Subject: [PATCH 07/17] feat(docker-compose.dev.yml): rename service 'tux' to 'bot' and container name to 'tux-bot' for better clarity feat(docker-compose.dev.yml): add environment variable support for development environment feat(docker-compose.dev.yml): change .env path to .env.dev for development environment feat(docker-compose.dev.yml): add volume mapping for application code and postgres initialization script feat(docker-compose.dev.yml): add entrypoint for postgres service to run initialization script feat(docker-compose.dev.yml): change healthcheck command for postgres service chore(docker-compose.dev.yml): comment out adminer service as it's not needed in development environment feat(scripts/postgres-init.sh): add new script for postgres initialization with dynamic password setup --- docker-compose.dev.yml | 30 +++++++++++++++++++----------- scripts/postgres-init.sh | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 11 deletions(-) create mode 100755 scripts/postgres-init.sh diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1faefc71..15e0c5ae 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,8 +1,12 @@ +name: tux + services: - tux: + bot: build: . - container_name: tux + container_name: tux-bot restart: always + # environment: + # - TUX_ENV=dev develop: watch: - action: sync @@ -11,8 +15,10 @@ services: ignore: - .venv/ env_file: - - path: ./.env + - path: ./.env.dev required: true + volumes: + - .:/app depends_on: db: condition: service_healthy @@ -23,22 +29,24 @@ services: restart: always shm_size: 128mb env_file: - - path: ./.env + - path: ./.env.dev required: true volumes: - tux_data:/var/lib/postgresql/data + - ./scripts/postgres-init.sh:/docker-entrypoint-initdb.d/postgres-init.sh + entrypoint: ["/docker-entrypoint-initdb.d/postgres-init.sh"] healthcheck: - test: pg_isready -U postgres + 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 + # adminer: + # image: adminer + # container_name: tux-adminer + # restart: always + # ports: + # - 8080:8080 volumes: tux_data: diff --git a/scripts/postgres-init.sh b/scripts/postgres-init.sh new file mode 100755 index 00000000..cb99dfb5 --- /dev/null +++ b/scripts/postgres-init.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -e + +# Ensure POSTGRES_PASSWORD environment variable is set +if [ -z "$POSTGRES_PASSWORD" ]; then + echo "Error: POSTGRES_PASSWORD environment variable is not set." + exit 1 +fi + +# Log that we are setting up the init.sql file +echo "Creating init.sql with POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" + +# Generate init.sql dynamically based on POSTGRES_PASSWORD +cat </docker-entrypoint-initdb.d/init.sql +DO \$\$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_roles + WHERE rolname = 'postgres' + ) THEN + CREATE USER postgres WITH PASSWORD '${POSTGRES_PASSWORD}'; + ALTER USER postgres WITH SUPERUSER; + END IF; +END +\$$; +EOF + +# Set permissions for the SQL file +chmod 644 /docker-entrypoint-initdb.d/init.sql # Read permissions for everyone, write permission for owner only + +# Execute the original entrypoint script from the Postgres image +exec docker-entrypoint.sh postgres From 0371e57eccb8104f91b37408e7deac5993767f64 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 12 Sep 2024 06:20:52 -0400 Subject: [PATCH 08/17] feat(pyproject.toml): capitalize project name to 'Tux' for better branding feat(pyproject.toml): add 'package-mode' set to false to disable package mode feat(pyproject.toml): add 'emojis' package to the dependencies for emoji support chore(pyproject.toml): move 'emojis' package from the bottom to the correct alphabetical order in the dependencies feat(pyproject.toml): add 'cache-dir' in 'tool.ruff' for caching ruff files feat(pyproject.toml): add 'tool.prisma' configuration for Prisma ORM settings --- pyproject.toml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) 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 From eca382a34212fe549b901b5355b3014f8305a1d5 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 12 Sep 2024 06:21:23 -0400 Subject: [PATCH 09/17] chore(Dockerfile): remove unnecessary mkdir and chmod commands for /app/.cache/prisma refactor(Dockerfile): remove 'prisma py fetch' from CMD as it's no longer needed for the application to run --- Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 From 64c4b47d566ae0849d5ad2b5d0756ba76471bfca Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 12 Sep 2024 06:21:35 -0400 Subject: [PATCH 10/17] chore(.dockerignore): add .cache/ to .dockerignore to prevent Docker from including cache files in the build context --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 21d0b898..0fea4f73 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,2 @@ .venv/ +.cache/ From 0acd313b6d232dff28de46153ef2810097280068 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 12 Sep 2024 06:22:26 -0400 Subject: [PATCH 11/17] feat(docker-compose.dev.yml): enable TUX_ENV environment variable for development environment chore(docker-compose.dev.yml): add .cache/ to watch ignore list to prevent unnecessary rebuilds feat(docker-compose.dev.yml): trigger rebuild on changes to pyproject.toml and poetry.lock for dependency updates refactor(docker-compose.dev.yml): remove leading ./ from .env.dev path for consistency feat(docker-compose.dev.yml): add command to bot service to install dependencies and run prisma commands before starting the app feat(docker-compose.dev.yml): uncomment adminer service for database management feat(docker-compose.dev.yml): add environment variables and command to adminer service for automatic login feat(docker-compose.dev.yml): add adminer-index.php config for automatic login script style(docker-compose.dev.yml): add newline at end of file for POSIX compliance --- docker-compose.dev.yml | 68 ++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 15e0c5ae..1526cbe2 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,12 +1,13 @@ name: tux services: + bot: build: . container_name: tux-bot restart: always - # environment: - # - TUX_ENV=dev + environment: + - TUX_ENV=dev develop: watch: - action: sync @@ -14,14 +15,18 @@ services: target: /app/ ignore: - .venv/ + - .cache/ + - action: rebuild + path: pyproject.toml + - action: rebuild + path: poetry.lock env_file: - - path: ./.env.dev + - path: .env.dev required: true - volumes: - - .:/app 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 @@ -29,24 +34,57 @@ services: restart: always shm_size: 128mb env_file: - - path: ./.env.dev + - path: .env.dev required: true volumes: - tux_data:/var/lib/postgresql/data - - ./scripts/postgres-init.sh:/docker-entrypoint-initdb.d/postgres-init.sh - entrypoint: ["/docker-entrypoint-initdb.d/postgres-init.sh"] 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 + adminer: + image: adminer + container_name: tux-adminer + restart: always + ports: + - 8080:8080 + env_file: + - path: .env.dev + required: true + environment: + ADMINER_DEFAULT_DRIVER: "pgsql" + ADMINER_DEFAULT_SERVER: "db" + ADMINER_DEFAULT_DB: postgres + ADMINER_DEFAULT_USERNAME: postgres + ADMINER_DEFAULT_PASSWORD: tux + ADMINER_DESIGN: "hydra" + 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: - tux_data: + tux_data: \ No newline at end of file From 601b648a00bad373a8ba4b8aff4b77b446453dbc Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Thu, 12 Sep 2024 06:22:39 -0400 Subject: [PATCH 12/17] refactor(config.py): remove hardcoded dotenv path to allow dotenv to find .env file automatically feat(config.py): add POSTGRES_DB and POSTGRES_USER environment variables for more flexible database configuration fix(config.py): update DATABASE_URL to include new POSTGRES_DB and POSTGRES_USER variables for accurate database connection string --- tux/utils/config.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tux/utils/config.py b/tux/utils/config.py index 2bbef606..fde81e0d 100644 --- a/tux/utils/config.py +++ b/tux/utils/config.py @@ -7,7 +7,7 @@ from dotenv import load_dotenv, set_key # Load shared environment variables from .env -load_dotenv(dotenv_path=".env") +load_dotenv() # Determine which environment file to load based on TUX_ENV env = os.getenv("TUX_ENV", "dev").lower() @@ -26,12 +26,13 @@ class Config: TUX_ENV: Final[str] = os.getenv("TUX_ENV", "dev") TUX_TOKEN: Final[str] = os.getenv("TUX_TOKEN", "") - POSTGRES_PASSWORD: Final[str] = os.getenv("POSTGRES_PASSWORD", "tux") + 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] = f"postgresql://postgres:{POSTGRES_PASSWORD}@db:{POSTGRES_PORT}/postgres" - + DATABASE_URL: Final[str] = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@db:{POSTGRES_PORT}/{POSTGRES_DB}" set_key(".env", "DATABASE_URL", DATABASE_URL) # Cog ignore list constants From 3cb1c5c00f4c6ce456bf49f95fb38ae7e17d5b1b Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 14 Sep 2024 01:59:43 -0400 Subject: [PATCH 13/17] chore(postgres-init.sh): remove unused initialization script for PostgreSQL --- scripts/postgres-init.sh | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100755 scripts/postgres-init.sh diff --git a/scripts/postgres-init.sh b/scripts/postgres-init.sh deleted file mode 100755 index cb99dfb5..00000000 --- a/scripts/postgres-init.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash -set -e - -# Ensure POSTGRES_PASSWORD environment variable is set -if [ -z "$POSTGRES_PASSWORD" ]; then - echo "Error: POSTGRES_PASSWORD environment variable is not set." - exit 1 -fi - -# Log that we are setting up the init.sql file -echo "Creating init.sql with POSTGRES_PASSWORD=${POSTGRES_PASSWORD}" - -# Generate init.sql dynamically based on POSTGRES_PASSWORD -cat </docker-entrypoint-initdb.d/init.sql -DO \$\$ -BEGIN - IF NOT EXISTS ( - SELECT 1 - FROM pg_roles - WHERE rolname = 'postgres' - ) THEN - CREATE USER postgres WITH PASSWORD '${POSTGRES_PASSWORD}'; - ALTER USER postgres WITH SUPERUSER; - END IF; -END -\$$; -EOF - -# Set permissions for the SQL file -chmod 644 /docker-entrypoint-initdb.d/init.sql # Read permissions for everyone, write permission for owner only - -# Execute the original entrypoint script from the Postgres image -exec docker-entrypoint.sh postgres From dff814015330ad186dc2990328d689b3cbfcc639 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 14 Sep 2024 02:00:19 -0400 Subject: [PATCH 14/17] feat: add set_env.sh script to manage environment variables This script allows to set environment variables for different environments (dev, prod) by sourcing respective .env files. It also backs up existing .env file, and exports additional dynamically computed variables. This change is done to simplify and automate the process of environment configuration. --- set_env.sh | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100755 set_env.sh 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 From e628a7672f965ed58f88e32cca233303db0cac46 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 14 Sep 2024 02:00:38 -0400 Subject: [PATCH 15/17] refactor(config.py): simplify environment variable loading process - Removed the logic for loading environment-specific .env files based on TUX_ENV. Now, only a single .env file is loaded. - Removed the generation of DATABASE_URL from other environment variables. Now, it is directly fetched from the environment variables. - These changes were made to simplify the configuration process and reduce potential points of failure. --- tux/utils/config.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/tux/utils/config.py b/tux/utils/config.py index fde81e0d..c4ad74f4 100644 --- a/tux/utils/config.py +++ b/tux/utils/config.py @@ -4,18 +4,11 @@ from typing import ClassVar, Final import yaml -from dotenv import load_dotenv, set_key +from dotenv import load_dotenv -# Load shared environment variables from .env +# Load environment variables from the single .env file load_dotenv() -# Determine which environment file to load based on TUX_ENV -env = os.getenv("TUX_ENV", "dev").lower() - -# Load the environment-specific .env file -env_file = ".env.prod" if env == "prod" else ".env.dev" -load_dotenv(dotenv_path=env_file) - # Load YAML configuration settings_file_path = Path("config/settings.yml") settings = yaml.safe_load(settings_file_path.read_text()) @@ -32,8 +25,7 @@ class Config: POSTGRES_PASSWORD: Final[str] = os.getenv("POSTGRES_PASSWORD", "tux") # Derived settings - DATABASE_URL: Final[str] = f"postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@db:{POSTGRES_PORT}/{POSTGRES_DB}" - set_key(".env", "DATABASE_URL", DATABASE_URL) + 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(",")) @@ -75,4 +67,5 @@ class Config: TEMPVC_CHANNEL_ID: Final[str | None] = settings["TEMPVC_CHANNEL_ID"] +# Load the updated environment variables into the current environment CONFIG = Config() From 5e912bdb6f824bbeed65790f0ca0532088959701 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 14 Sep 2024 02:00:52 -0400 Subject: [PATCH 16/17] refactor(docker-compose.dev.yml): rename container names to follow naming convention fix(docker-compose.dev.yml): change .env.dev to .env for environment file path to match production environment refactor(docker-compose.dev.yml): rename volume from tux_data to data for consistency feat(docker-compose.dev.yml): use environment variables for adminer default settings for better security and configurability --- docker-compose.dev.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1526cbe2..f418e404 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -4,10 +4,8 @@ services: bot: build: . - container_name: tux-bot + container_name: tux_bot restart: always - environment: - - TUX_ENV=dev develop: watch: - action: sync @@ -21,7 +19,7 @@ services: - action: rebuild path: poetry.lock env_file: - - path: .env.dev + - path: .env required: true depends_on: db: @@ -30,14 +28,14 @@ services: db: image: postgres - container_name: tux-db + container_name: tux_db restart: always shm_size: 128mb env_file: - - path: .env.dev + - path: .env required: true volumes: - - tux_data:/var/lib/postgresql/data + - data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s @@ -46,20 +44,19 @@ services: adminer: image: adminer - container_name: tux-adminer + container_name: tux_adminer restart: always ports: - 8080:8080 env_file: - - path: .env.dev + - path: .env required: true environment: ADMINER_DEFAULT_DRIVER: "pgsql" ADMINER_DEFAULT_SERVER: "db" - ADMINER_DEFAULT_DB: postgres - ADMINER_DEFAULT_USERNAME: postgres - ADMINER_DEFAULT_PASSWORD: tux - ADMINER_DESIGN: "hydra" + 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 @@ -87,4 +84,4 @@ configs: ?> volumes: - tux_data: \ No newline at end of file + data: \ No newline at end of file From 70a6f1fd7704c4ad33ffcf3ae6569506f0460ce7 Mon Sep 17 00:00:00 2001 From: kzndotsh Date: Sat, 14 Sep 2024 02:01:04 -0400 Subject: [PATCH 17/17] chore(.gitignore): remove venv.bak/ and modify env.bak/ to .env.bak to correctly ignore backup environment files --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 376ca901..d3c7a0b6 100644 --- a/.gitignore +++ b/.gitignore @@ -154,8 +154,7 @@ prisma/database.db env/ venv/ ENV/ -env.bak/ -venv.bak/ +.env.bak .env.old .env.dev .env.development