diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml
new file mode 100644
index 0000000..648710e
--- /dev/null
+++ b/.github/workflows/docker-image.yml
@@ -0,0 +1,60 @@
+name: Create and Publish Docker Image CI
+
+
+on:
+ push:
+ branches: [ "main" ]
+ tags: [ "v*.*.*" ]
+ pull_request:
+
+jobs:
+
+ docker:
+
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ -
+ name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ wlinator/luminara
+ ghcr.io/wlinator/luminara
+ tags: |
+ type=raw,value=latest,enable={{is_default_branch}}
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+ -
+ name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+ -
+ name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+ -
+ name: Login to GHCR
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ -
+ name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ push: ${{ github.event_name != 'pull_request' }}
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+
+
diff --git a/.gitignore b/.gitignore
index 7a6ed10..4747dcf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ venv/
__pycache__/
.run/
.vscode/
+data/
*.db
.env
diff --git a/Client.py b/Client.py
index 9e7ad60..e098926 100644
--- a/Client.py
+++ b/Client.py
@@ -7,7 +7,7 @@ from discord.ext import bridge, commands
from discord.ext.commands import EmojiConverter, TextChannelConverter
from loguru import logger
-from lib import metadata
+from lib.constants import CONST
class LumiBot(bridge.Bot):
@@ -18,7 +18,7 @@ class LumiBot(bridge.Bot):
Logs various information about the bot and the environment it is running on.
Note: This function isn't guaranteed to only be called once. The event is called when a RESUME request fails.
"""
- logger.info(f"{metadata.__title__} v{metadata.__version__}")
+ logger.info(f"{CONST.TITLE} v{CONST.VERSION}")
logger.info(f"Logged in with ID {self.user.id if self.user else 'Unknown'}")
logger.info(f"discord.py API version: {discord.__version__}")
logger.info(f"Python version: {platform.python_version()}")
diff --git a/Dockerfile b/Dockerfile
index 76e47fa..801a53b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,21 +1,24 @@
-FROM python:3.12
+FROM python:3.12-slim-bookworm
ARG DEBIAN_FRONTEND=noninteractive
WORKDIR /usr/src/app
RUN apt-get update && \
- apt-get install -y locales mariadb-client && \
+ apt-get install -y --no-install-recommends locales mariadb-client && \
sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
- dpkg-reconfigure locales
+ dpkg-reconfigure locales && \
+ apt-get clean && \
+ rm -rf /var/lib/apt/lists/*
COPY pyproject.toml poetry.lock ./
-RUN pip install poetry && \
+RUN pip install --no-cache-dir poetry && \
poetry config virtualenvs.create false && \
- poetry install --no-interaction --no-ansi
+ poetry install --no-interaction --no-ansi --no-dev && \
+ pip cache purge
COPY . .
-ENV LANG en_US.UTF-8
-ENV LC_ALL en_US.UTF-8
+ENV LANG=en_US.UTF-8
+ENV LC_ALL=en_US.UTF-8
CMD [ "poetry", "run", "python", "./Luminara.py" ]
\ No newline at end of file
diff --git a/LICENSE b/LICENSE
index e01b934..99d60b9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,24 +1,7 @@
-Luminara
-Copyright (C) 2022-2024 wlinator
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program. If not, see .
-
-
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
- Copyright (C) 2007 Free Software Foundation, Inc.
+ Copyright (C) 2007 Free Software Foundation, Inc.
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
@@ -648,8 +631,8 @@ to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
-
- Copyright (C)
+ Luminara - A Discord bot application
+ Copyright (C) 2022-present wlinator (dokimakimaki@gmail.com)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -662,14 +645,14 @@ the "copyright" line and a pointer to where the full notice is found.
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
- along with this program. If not, see .
+ along with this program. If not, see .
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
- Luminara Copyright (C) 2022-2024 wlinator
+ Luminara Copyright (C) 2022-present wlinator (dokimakimaki@gmail.com)
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
@@ -681,11 +664,11 @@ might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
-.
+.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
-.
\ No newline at end of file
+.
diff --git a/Luminara.py b/Luminara.py
index 364514e..88e1309 100644
--- a/Luminara.py
+++ b/Luminara.py
@@ -10,6 +10,7 @@ import services.config_service
import services.help_service
from lib.constants import CONST
from services.blacklist_service import BlacklistUserService
+from db.database import run_migrations
# Remove the default logger configuration
logger.remove()
@@ -42,9 +43,7 @@ client = Client.LumiBot(
@client.check
async def blacklist_check(ctx):
- if BlacklistUserService.is_user_blacklisted(ctx.author.id):
- return False
- return True
+ return not BlacklistUserService.is_user_blacklisted(ctx.author.id)
def load_modules():
@@ -87,6 +86,9 @@ if __name__ == "__main__":
logger.info("LUMI IS BOOTING")
+ # Run database migrations
+ run_migrations()
+
# cache all JSON
[
config.parser.JsonCache.read_json(file[:-5])
diff --git a/README.md b/README.md
index 38c04c8..c830522 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,43 @@
![Lumi art](https://git.wlinator.org/assets/img/logo.png)
+
+## Self-Hosting
+
+Self-hosting refers to running Luminara on your own server or computer, rather than using the publicly hosted version. This approach offers the ability to manage your own instance of the bot and give it a custom name and avatar.
+
+### Requirements
+
+Before you begin, make sure you have the following installed on your system:
+- [Docker](https://docs.docker.com/get-docker/)
+- [Docker Compose](https://docs.docker.com/compose/install/)
+
+Additionally, you'll need to create a Discord bot application and obtain a token:
+
+1. Go to the [Discord Developer Portal](https://discord.com/developers/applications).
+2. Click on "New Application" and give it a name.
+3. Navigate to the "Bot" tab and click "Add Bot".
+4. Under the bot's username, click "Reset Token" to reveal your bot token.
+5. Copy this token; you'll need it for the `.env` file later.
+
+*Note: remember to keep your bot token secret and never share it publicly.*
+
+### Running Luminara:
+
+1. Copy the contents from [docker-compose.prod.yml](docker-compose.prod.yml) to a new file named `docker-compose.yml` in an empty directory.
+
+2. Copy the `.env.example` file to a new file named `.env` in the same directory.
+
+3. Fill out the `.env` file with your specific configuration details.
+
+4. Run the following command in your terminal:
+
+ ```
+ docker compose up -d --build
+ ```
+
+This will build and start Luminara in detached mode.
+
---
Some icons used in Lumi are provided by [Icons8](https://icons8.com/).
diff --git a/config/parser.py b/config/parser.py
index 324a13a..5e4b35c 100644
--- a/config/parser.py
+++ b/config/parser.py
@@ -1,8 +1,5 @@
import json
-import yaml
-from loguru import logger
-
class JsonCache:
_cache = {}
@@ -13,21 +10,5 @@ class JsonCache:
if path not in JsonCache._cache:
with open(f"config/JSON/{path}.json") as file:
JsonCache._cache[path] = json.load(file)
- logger.debug(f"{path}.json was loaded and cached.")
return JsonCache._cache[path]
-
-
-class YamlCache:
- _cache = {}
-
- @staticmethod
- def read_credentialsl():
- """Read and cache the creds.yaml data if not already cached."""
- path = "creds"
- if path not in YamlCache._cache:
- with open(f"{path}.yaml") as file:
- YamlCache._cache[path] = yaml.safe_load(file)
- logger.debug(f"{path}.yaml was loaded and cached.")
-
- return YamlCache._cache[path]
diff --git a/db/database.py b/db/database.py
index c7c6c04..c63b752 100644
--- a/db/database.py
+++ b/db/database.py
@@ -1,6 +1,9 @@
import mysql.connector
from loguru import logger
from mysql.connector import pooling
+import os
+import pathlib
+import re
from lib.constants import CONST
@@ -54,3 +57,57 @@ def select_query_dict(query, values=None):
with conn.cursor(dictionary=True) as cursor:
cursor.execute(query, values)
return cursor.fetchall()
+
+def run_migrations():
+ migrations_dir = "db/migrations"
+ migration_files = sorted(
+ [f for f in os.listdir(migrations_dir) if f.endswith(".sql")],
+ )
+
+ with _cnxpool.get_connection() as conn:
+ with conn.cursor() as cursor:
+ # Create migrations table if it doesn't exist
+ cursor.execute("""
+ CREATE TABLE IF NOT EXISTS migrations (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ filename VARCHAR(255) NOT NULL,
+ applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+ )
+ """)
+
+ for migration_file in migration_files:
+ # Check if migration has already been applied
+ cursor.execute(
+ "SELECT COUNT(*) FROM migrations WHERE filename = %s",
+ (migration_file,),
+ )
+ if cursor.fetchone()[0] > 0:
+ logger.debug(
+ f"Migration {migration_file} already applied, skipping.",
+ )
+ continue
+
+ # Read and execute migration file
+ migration_sql = pathlib.Path(
+ os.path.join(migrations_dir, migration_file),
+ ).read_text()
+ try:
+ # Split the migration file into individual statements
+ statements = re.split(r";\s*$", migration_sql, flags=re.MULTILINE)
+ for statement in statements:
+ if statement.strip():
+ cursor.execute(statement)
+
+ # Record successful migration
+ cursor.execute(
+ "INSERT INTO migrations (filename) VALUES (%s)",
+ (migration_file,),
+ )
+ conn.commit()
+ logger.debug(f"Successfully applied migration: {migration_file}")
+ except mysql.connector.Error as e:
+ conn.rollback()
+ logger.error(f"Error applying migration {migration_file}: {e}")
+ raise
+
+ logger.debug("All migrations completed.")
diff --git a/docker-compose.yml b/docker-compose.dev.yml
similarity index 84%
rename from docker-compose.yml
rename to docker-compose.dev.yml
index f5c609c..11a3706 100644
--- a/docker-compose.yml
+++ b/docker-compose.dev.yml
@@ -3,6 +3,9 @@ services:
build: .
container_name: lumi-core
restart: always
+ env_file:
+ - path: ./.env
+ required: true
depends_on:
db:
condition: service_healthy
@@ -17,8 +20,7 @@ services:
MARIADB_PASSWORD: ${MARIADB_PASSWORD}
MARIADB_DATABASE: ${MARIADB_DATABASE}
volumes:
- - ./db/migrations:/docker-entrypoint-initdb.d/
- - database:/var/lib/mysql/
+ - ./data:/var/lib/mysql/
healthcheck:
test: [ "CMD", "mariadb", "-h", "localhost", "-u", "${MARIADB_USER}", "-p${MARIADB_PASSWORD}", "-e", "SELECT 1" ]
interval: 5s
@@ -30,7 +32,4 @@ services:
container_name: lumi-adminer
restart: always
ports:
- - 8080:8080
-
-volumes:
- database:
+ - 8080:8080
\ No newline at end of file
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..c24272d
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,28 @@
+services:
+ core:
+ image: ghcr.io/wlinator/luminara:2 # Remove "ghcr.io/" if you want to use the Docker Hub image.
+ container_name: lumi-core
+ restart: always
+ env_file:
+ - path: ./.env
+ required: true
+ depends_on:
+ db:
+ condition: service_healthy
+
+ db:
+ image: mariadb
+ container_name: lumi-db
+ restart: always
+ environment:
+ MARIADB_ROOT_PASSWORD: ${MARIADB_ROOT_PASSWORD}
+ MARIADB_USER: ${MARIADB_USER}
+ MARIADB_PASSWORD: ${MARIADB_PASSWORD}
+ MARIADB_DATABASE: ${MARIADB_DATABASE}
+ volumes:
+ - ./data:/var/lib/mysql/
+ healthcheck:
+ test: [ "CMD", "mariadb", "-h", "localhost", "-u", "${MARIADB_USER}", "-p${MARIADB_PASSWORD}", "-e", "SELECT 1" ]
+ interval: 5s
+ timeout: 10s
+ retries: 5
diff --git a/lib/constants.py b/lib/constants.py
index 9fd031a..b37c4b0 100644
--- a/lib/constants.py
+++ b/lib/constants.py
@@ -1,18 +1,19 @@
import os
from typing import Optional, Set
-from dotenv import load_dotenv
-
from config.parser import JsonCache
-# Load environment variables from .env file
-load_dotenv()
-
art = JsonCache.read_json("art")
resources = JsonCache.read_json("resources")
class Constants:
+ # metadata
+ TITLE = "Luminara"
+ AUTHOR = "wlinator"
+ LICENSE = "GNU General Public License v3.0"
+ VERSION = "2.7.0"
+
# bot credentials
TOKEN: Optional[str] = os.environ.get("TOKEN", None)
INSTANCE: Optional[str] = os.environ.get("INSTANCE", None)
@@ -39,9 +40,9 @@ class Constants:
EMOTES_GUILD_ID = 1038051105642401812
# color scheme
- COLOR_DEFAULT = int(0xFF8C00)
- COLOR_WARNING = int(0xFF7600)
- COLOR_ERROR = int(0xFF4500)
+ COLOR_DEFAULT = 0xFF8C00
+ COLOR_WARNING = 0xFF7600
+ COLOR_ERROR = 0xFF4500
# strings
STRINGS = JsonCache.read_json("strings")
@@ -84,3 +85,6 @@ class Constants:
CONST = Constants()
+
+
+CONST = Constants()
diff --git a/lib/interaction.py b/lib/interaction.py
index 581fb6d..d71134a 100644
--- a/lib/interaction.py
+++ b/lib/interaction.py
@@ -36,14 +36,13 @@ class BlackJackButtons(View):
self.stop()
async def interaction_check(self, interaction) -> bool:
- if interaction.user != self.ctx.author:
- await interaction.response.send_message(
- "You can't use these buttons, they're someone else's!",
- ephemeral=True,
- )
- return False
- else:
+ if interaction.user == self.ctx.author:
return True
+ await interaction.response.send_message(
+ "You can't use these buttons, they're someone else's!",
+ ephemeral=True,
+ )
+ return False
class ExchangeConfirmation(View):
@@ -69,11 +68,10 @@ class ExchangeConfirmation(View):
self.stop()
async def interaction_check(self, interaction) -> bool:
- if interaction.user != self.ctx.author:
- await interaction.response.send_message(
- "You can't use these buttons, they're someone else's!",
- ephemeral=True,
- )
- return False
- else:
+ if interaction.user == self.ctx.author:
return True
+ await interaction.response.send_message(
+ "You can't use these buttons, they're someone else's!",
+ ephemeral=True,
+ )
+ return False
diff --git a/lib/metadata.py b/lib/metadata.py
deleted file mode 100644
index 6d5c804..0000000
--- a/lib/metadata.py
+++ /dev/null
@@ -1,19 +0,0 @@
-import subprocess
-
-
-def get_latest_git_tag():
- """
- Retrieves the latest git tag.
- """
- try:
- command = ["git", "describe", "--abbrev=0", "--tags"]
- return subprocess.check_output(command).decode().strip()
- except subprocess.CalledProcessError as e:
- print(f"Error: {e}")
- return "BETA"
-
-
-__title__ = "Luminara"
-__version__ = get_latest_git_tag()
-__author__ = "wlinator"
-__license__ = "MIT License"
diff --git a/lib/time.py b/lib/time.py
index e553cd8..9d0b2f5 100644
--- a/lib/time.py
+++ b/lib/time.py
@@ -16,7 +16,4 @@ def seconds_until(hours, minutes):
if future_exec < now:
future_exec += datetime.timedelta(days=1)
- # Calculate the time difference in seconds
- seconds_until_execution = (future_exec - now).total_seconds()
-
- return seconds_until_execution
+ return (future_exec - now).total_seconds()
diff --git a/modules/misc/info.py b/modules/misc/info.py
index 5a9fd95..3d991af 100644
--- a/modules/misc/info.py
+++ b/modules/misc/info.py
@@ -5,7 +5,6 @@ import discord
import psutil
from discord.ext import bridge
-from lib import metadata
from lib.constants import CONST
from lib.embed_builder import EmbedBuilder
from services.currency_service import Currency
@@ -34,7 +33,7 @@ async def cmd(self, ctx: bridge.Context, unix_timestamp: int) -> None:
show_name=False,
)
embed.set_author(
- name=f"{metadata.__title__} v{metadata.__version__}",
+ name=f"{CONST.TITLE} v{CONST.VERSION}",
url=CONST.REPO_URL,
icon_url=CONST.CHECK_ICON,
)
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..5db72dd
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended"
+ ]
+}
diff --git a/services/currency_service.py b/services/currency_service.py
index 45c7866..314fd5d 100644
--- a/services/currency_service.py
+++ b/services/currency_service.py
@@ -57,15 +57,7 @@ class Currency:
query = "SELECT user_id, balance FROM currency ORDER BY balance DESC"
data = database.select_query(query)
- leaderboard = []
- rank = 1
- for row in data:
- row_user_id = row[0]
- balance = row[1]
- leaderboard.append((row_user_id, balance, rank))
- rank += 1
-
- return leaderboard
+ return [(row[0], row[1], rank) for rank, row in enumerate(data, start=1)]
@staticmethod
def format(num):