Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ api:
port: 8040
log_level: "error"
cors_origins: ["*"]
localhost_bypass: true # set on true for localhost testing, on false for production (will need token).

# Translation Settings
translation:
Expand Down
2 changes: 1 addition & 1 deletion config/example.env
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ DB_HOST=https://database.my-super-discord-bot.com
DB_PORT=3306
DB_USER=managerx
DB_PASSWORD=abc123
DB_DATABASE=managerx
DB_NAME=managerx
6 changes: 6 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from fastapi import FastAPI, Response
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import PlainTextResponse
from fastapi.staticfiles import StaticFiles
from uvicorn import Server, Config

# Logger (muss existieren!)
Expand Down Expand Up @@ -91,6 +92,11 @@
app.include_router(dashboard_main_router)
app.include_router(router_public)

# CMS Media Uploads als statische Dateien bereitstellen
_uploads_dir = BASEDIR / "public" / "uploads" / "cms"
_uploads_dir.mkdir(parents=True, exist_ok=True)
app.mount("/uploads/cms", StaticFiles(directory=str(_uploads_dir)), name="cms_uploads")

@app.get("/robots.txt", response_class=PlainTextResponse)
async def robots():
"""Disallow all crawlers for the API."""
Expand Down
2 changes: 2 additions & 0 deletions mxmariadb/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@
from .welcome_db import WelcomeDatabase
from .profile_db import ProfileDB
from .economy_db import EconomyDatabase
from .management_db import ManagementDatabase
from .cms_db import CMSDatabase
267 changes: 267 additions & 0 deletions mxmariadb/cms_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
# Copyright (c) 2025 OPPRO.NET Network
import aiomysql
import logging
from mxmariadb.connector import MariaConnector

logger = logging.getLogger(__name__)

class CMSDatabase(MariaConnector):
"""MariaDB class for the CMS (Dev Blog, Tutorials, Changelog, News, Announcements)."""

def __init__(self):
super().__init__()

async def init_db(self):
"""Initialize CMS tables."""
async with self.pool.acquire() as conn:
async with conn.cursor() as cur:
# Main posts table
await cur.execute("""
CREATE TABLE IF NOT EXISTS cms_posts (
id INT AUTO_INCREMENT PRIMARY KEY,
post_type VARCHAR(20) NOT NULL DEFAULT 'dev',
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) UNIQUE NOT NULL,
content LONGTEXT NOT NULL,
excerpt TEXT NULL,
cover_image VARCHAR(500) NULL,
author_id BIGINT NOT NULL,
author_name VARCHAR(100),
tags VARCHAR(500),
is_published BOOLEAN DEFAULT FALSE,
scheduled_at TIMESTAMP NULL DEFAULT NULL,
view_count INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX(post_type, is_published),
INDEX(slug)
)
""")

# Add new columns to existing table (safe migration)
for col_def in [
"ALTER TABLE cms_posts ADD COLUMN IF NOT EXISTS excerpt TEXT NULL",
"ALTER TABLE cms_posts ADD COLUMN IF NOT EXISTS cover_image VARCHAR(500) NULL",
"ALTER TABLE cms_posts ADD COLUMN IF NOT EXISTS view_count INT DEFAULT 0",
]:
try:
await cur.execute(col_def)
except Exception:
pass # Column already exists or unsupported syntax

Check notice on line 50 in mxmariadb/cms_db.py

View check run for this annotation

codefactor.io / CodeFactor

mxmariadb/cms_db.py#L49-L50

Try, Except, Pass detected. (B110)

# Media/uploads table
await cur.execute("""
CREATE TABLE IF NOT EXISTS cms_media (
id INT AUTO_INCREMENT PRIMARY KEY,
filename VARCHAR(255) NOT NULL UNIQUE,
original_name VARCHAR(255) NOT NULL,
mime_type VARCHAR(100) NOT NULL,
size_bytes INT NOT NULL,
uploader_id BIGINT NOT NULL,
uploader_name VARCHAR(100),
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX(uploader_id)
)
""")

# Revision history table
await cur.execute("""
CREATE TABLE IF NOT EXISTS cms_revisions (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id INT NOT NULL,
title VARCHAR(255) NOT NULL,
content LONGTEXT NOT NULL,
excerpt TEXT NULL,
cover_image VARCHAR(500) NULL,
tags VARCHAR(500),
changed_by_id BIGINT NOT NULL,
changed_by_name VARCHAR(100),
change_note VARCHAR(255),
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (post_id) REFERENCES cms_posts(id) ON DELETE CASCADE,
INDEX(post_id)
)
""")

await conn.commit()
logger.info("CMS tables initialized in MariaDB")

# ─────────────────────────────────────────
# POSTS
# ─────────────────────────────────────────

async def create_post(self, post_type: str, title: str, slug: str, content: str,
author_id: int, author_name: str, tags: str = "",
is_published: bool = False, scheduled_at: str = None,
excerpt: str = None, cover_image: str = None):
async with self.pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("""
INSERT INTO cms_posts
(post_type, title, slug, content, excerpt, cover_image,
author_id, author_name, tags, is_published, scheduled_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (post_type, title, slug, content, excerpt, cover_image,
author_id, author_name, tags, is_published, scheduled_at))
await conn.commit()
return True

async def get_posts(self, post_type: str = None, published_only: bool = True):
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
query = "SELECT * FROM cms_posts"
conditions = []
params = []

if post_type:
conditions.append("post_type = %s")
params.append(post_type)

if published_only:
conditions.append("(is_published = TRUE AND (scheduled_at IS NULL OR scheduled_at <= CURRENT_TIMESTAMP))")

if conditions:
query += " WHERE " + " AND ".join(conditions)

query += " ORDER BY created_at DESC"

await cur.execute(query, tuple(params))
return await cur.fetchall()

async def get_post_by_slug(self, slug: str):
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("SELECT * FROM cms_posts WHERE slug = %s", (slug,))
return await cur.fetchone()

async def get_post_by_id(self, post_id: int):
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("SELECT * FROM cms_posts WHERE id = %s", (post_id,))
return await cur.fetchone()

async def increment_view_count(self, post_id: int):
async with self.pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("UPDATE cms_posts SET view_count = view_count + 1 WHERE id = %s", (post_id,))
await conn.commit()

async def update_post(self, post_id: int, **kwargs):
if not kwargs:
return False

# Felder die nicht direkt gesetzt werden dürfen
protected = {'id', 'created_at', 'updated_at'}
kwargs = {k: v for k, v in kwargs.items() if k not in protected}

async with self.pool.acquire() as conn:
async with conn.cursor() as cur:
fields = []
params = []
for key, value in kwargs.items():
fields.append(f"{key} = %s")
params.append(value)

params.append(post_id)
query = f"UPDATE cms_posts SET {', '.join(fields)} WHERE id = %s"
await cur.execute(query, tuple(params))
await conn.commit()
return True

async def delete_post(self, post_id: int):
async with self.pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("DELETE FROM cms_posts WHERE id = %s", (post_id,))
await conn.commit()
return True

# ─────────────────────────────────────────
# REVISIONS
# ─────────────────────────────────────────

async def save_revision(self, post_id: int, title: str, content: str,
tags: str, cover_image: str, excerpt: str,
changed_by_id: int, changed_by_name: str,
change_note: str = None):
async with self.pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("""
INSERT INTO cms_revisions
(post_id, title, content, excerpt, cover_image, tags,
changed_by_id, changed_by_name, change_note)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (post_id, title, content, excerpt, cover_image, tags,
changed_by_id, changed_by_name, change_note))
await conn.commit()

async def get_revisions(self, post_id: int, limit: int = 20):
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("""
SELECT id, post_id, title, changed_by_name, change_note, changed_at
FROM cms_revisions
WHERE post_id = %s
ORDER BY changed_at DESC
LIMIT %s
""", (post_id, limit))
return await cur.fetchall()

async def get_revision_by_id(self, revision_id: int):
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("SELECT * FROM cms_revisions WHERE id = %s", (revision_id,))
return await cur.fetchone()

# ─────────────────────────────────────────
# MEDIA
# ─────────────────────────────────────────

async def create_media(self, filename: str, original_name: str, mime_type: str,
size_bytes: int, uploader_id: int, uploader_name: str):
async with self.pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("""
INSERT INTO cms_media
(filename, original_name, mime_type, size_bytes, uploader_id, uploader_name)
VALUES (%s, %s, %s, %s, %s, %s)
""", (filename, original_name, mime_type, size_bytes, uploader_id, uploader_name))
await conn.commit()
return True

async def get_media(self, limit: int = 100):
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("""
SELECT * FROM cms_media ORDER BY uploaded_at DESC LIMIT %s
""", (limit,))
return await cur.fetchall()

async def delete_media(self, media_id: int):
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("SELECT filename FROM cms_media WHERE id = %s", (media_id,))
row = await cur.fetchone()
async with conn.cursor() as cur:
await cur.execute("DELETE FROM cms_media WHERE id = %s", (media_id,))
await conn.commit()
return row["filename"] if row else None

# ─────────────────────────────────────────
# CHANGELOG
# ─────────────────────────────────────────

async def get_changelog(self, limit: int = 50):
"""Get published changelog entries, sorted by date."""
async with self.pool.acquire() as conn:
async with conn.cursor(aiomysql.DictCursor) as cur:
await cur.execute("""
SELECT id, title, slug, excerpt, content, tags, author_name,
cover_image, created_at, updated_at
FROM cms_posts
WHERE post_type = 'changelog'
AND is_published = TRUE
AND (scheduled_at IS NULL OR scheduled_at <= CURRENT_TIMESTAMP)
ORDER BY created_at DESC
LIMIT %s
""", (limit,))
return await cur.fetchall()
4 changes: 3 additions & 1 deletion mxmariadb/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from dotenv import load_dotenv
from pathlib import Path

env_path = Path(__file__).parent.parent / 'config' / '.env'
env_path = (Path(__file__).parent.parent / 'config' / '.env').resolve()
if not env_path.exists():
print(f"[CRITICAL] .env file not found at {env_path}")
load_dotenv(dotenv_path=env_path)

logger = logging.getLogger(__name__)
Expand Down
Loading
Loading