1
Fork 0
mirror of https://github.com/wlinator/luminara.git synced 2024-10-02 18:03:12 +00:00

Merge branch 'main' into moderation

This commit is contained in:
wlinator 2024-08-01 09:53:09 -04:00
commit 39559d54f1
18 changed files with 245 additions and 117 deletions

60
.github/workflows/docker-image.yml vendored Normal file
View file

@ -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 }}

1
.gitignore vendored
View file

@ -3,6 +3,7 @@ venv/
__pycache__/
.run/
.vscode/
data/
*.db
.env

View file

@ -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()}")

View file

@ -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" ]

31
LICENSE
View file

@ -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 <http://www.gnu.org/licenses/>.
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
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.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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 <http://www.gnu.org/licenses/>.
along with this program. If not, see <https://www.gnu.org/licenses/>.
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
<http://www.gnu.org/licenses/>.
<https://www.gnu.org/licenses/>.
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
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -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])

View file

@ -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/).

View file

@ -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]

View file

@ -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.")

View file

@ -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

28
docker-compose.prod.yml Normal file
View file

@ -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

View file

@ -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()

View file

@ -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

View file

@ -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"

View file

@ -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()

View file

@ -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,
)

6
renovate.json Normal file
View file

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}

View file

@ -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):