diff --git a/.gitea/workflows/check-code_quality.yaml b/.gitea/workflows/check-code_quality.yaml new file mode 100644 index 0000000..9962c55 --- /dev/null +++ b/.gitea/workflows/check-code_quality.yaml @@ -0,0 +1,97 @@ +name: Code Quality Check + +on: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + quality-check: + runs-on: ubuntu-latest + env: + GITHUB: ${{ secrets.GITHUB }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install dependencies + run: | + pip install -r src/requirements.txt + pip install black isort pylint flake8 mypy bandit safety pytest coverage radon + + - name: Create results folder + run: mkdir -p /tmp/results + + # Tools ausführen + - name: Run Black + run: black --check --diff . > /tmp/results/black.txt 2>&1 || echo "BLACK_FAILED=true" >> $GITHUB_ENV + + - name: Run isort + run: isort --check-only --diff . > /tmp/results/isort.txt 2>&1 || echo "ISORT_FAILED=true" >> $GITHUB_ENV + + - name: Run pylint with optional rcfile + run: | + if [ -f .gitea/linters/pylintrc ]; then + echo "Using custom pylintrc file." + pylint --rcfile=.gitea/linters/pylintrc . > /tmp/results/pylint.txt 2>&1 || echo "PYLINT_FAILED=true" >> $GITHUB_ENV + else + echo "No custom pylintrc file found. Using default settings." + pylint . > /tmp/results/pylint.txt 2>&1 || echo "PYLINT_FAILED=true" >> $GITHUB_ENV + fi + + - name: Run flake8 with optional config + run: | + if [ -f .gitea/linters/flake8 ]; then + echo "Using custom flake8 config." + flake8 --config=.gitea/linters/flake8 . > /tmp/results/flake8.txt 2>&1 || echo "FLAKE8_FAILED=true" >> $GITHUB_ENV + else + echo "No custom flake8 config found. Using default settings." + flake8 . > /tmp/results/flake8.txt 2>&1 || echo "FLAKE8_FAILED=true" >> $GITHUB_ENV + fi + + # Ergebnisse zusammenfassen + - name: Combine all results + run: | + echo "### 🛡️ Code Quality Report" > /tmp/results/summary.md + + for tool in black isort pylint flake8; do + echo "#### 🔍 ${tool}" >> /tmp/results/summary.md + echo '```' >> /tmp/results/summary.md + cat /tmp/results/${tool}.txt >> /tmp/results/summary.md + echo '```' >> /tmp/results/summary.md + echo "" >> /tmp/results/summary.md + done + + # PR-Kommentar hinzufügen + - name: Add comment to PR + if: ${{ env.GITHUB_EVENT_NAME == 'pull_request' }} + uses: mshick/add-pr-comment@v2 + with: + message-path: /tmp/results/summary.md + + - name: Create black diff patch + run: black --diff . > /tmp/results/black_diff.patch + + - name: Create isort diff patch + run: isort . --diff > /tmp/results/isort.patch + + # Ergebnisse als Artefakte speichern + - name: Upload results as artifacts + uses: actions/upload-artifact@v3 + with: + name: quality-reports + path: /tmp/results + + - name: Check for issues + run: | + if [[ "$BLACK_FAILED" == "true" || "$ISORT_FAILED" == "true" || "$PYLINT_FAILED" == "true" || "$FLAKE8_FAILED" == "true" ]]; then + echo "❌ Code quality issues found! Please fix the issues before merging." && exit 1 + fi diff --git a/.gitea/workflows/check-safety.yaml b/.gitea/workflows/check-safety.yaml new file mode 100644 index 0000000..6f91a13 --- /dev/null +++ b/.gitea/workflows/check-safety.yaml @@ -0,0 +1,24 @@ +name: Safety Check + +on: + pull_request: + branches: + - main + push: + branches: + - main + schedule: + - cron: "@daily" + +jobs: + safety-check: + runs-on: ubuntu-latest + + steps: + - name: Create results folder + run: mkdir -p results + + - name: Run Safety check + uses: pyupio/safety-action@v1 + with: + api-key: ${{ secrets.SAFETY }} diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml new file mode 100644 index 0000000..d84190c --- /dev/null +++ b/.gitea/workflows/release.yaml @@ -0,0 +1,26 @@ +name: Build and Push Docker Image + +on: + push: + tags: + - 'v*' + branches: + - main + +jobs: + build: + runs-on: docker + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Log in to Gitea Container Registry + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.0day.agency -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + + - name: Build Docker Image + run: | + docker build -f src/Dockerfile -t git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-discord-bot:${{ gitea.ref_name }} src/ + + - name: Push Docker Image + run: | + docker push git.0day.agency/${{ secrets.REGISTRY_USERNAME }}/novelai-discord-bot:${{ gitea.ref_name }} diff --git a/src/Dockerfile b/src/Dockerfile new file mode 100644 index 0000000..47f49c3 --- /dev/null +++ b/src/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.13.3-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + build-essential \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt requirements.txt +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["python", "main.py"] diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..99f6b99 --- /dev/null +++ b/src/main.py @@ -0,0 +1,185 @@ +import io +import os +import random +import zipfile +import discord +from discord import app_commands +from discord.ext import commands +import requests + +NOVELAI_API_TOKEN = os.getenv("NOVELAI_API_TOKEN") +BOT_TOKEN = os.getenv("DISCORD_BOT_TOKEN") +ALLOWED_CHANNEL_ID = int(os.getenv("DISCORD_CHANNEL_ID", "0")) # falls nicht gesetzt → 0 + +if not all([NOVELAI_API_TOKEN, BOT_TOKEN, ALLOWED_CHANNEL_ID]): + raise ValueError("Environment-Variablen NOVELAI_API_TOKEN, DISCORD_BOT_TOKEN und DISCORD_CHANNEL_ID müssen gesetzt sein.") + +intents = discord.Intents.default() +bot = commands.Bot(command_prefix=commands.when_mentioned, intents=intents) + +@bot.event +async def on_ready(): + print(f"Bot gestartet als {bot.user}") + try: + synced = await bot.tree.sync() + print(f"{len(synced)} Slash-Commands synchronisiert.") + except Exception as e: + print(e) + +@bot.tree.command(name="generate", description="Generiere ein Bild mit NovelAI v4") +@app_commands.describe( + prompt="Was soll dargestellt werden?", + undesired_prompt="Was soll vermieden werden? (optional)", + orientation="portrait oder landscape (Standard: portrait)" +) +async def generate( + interaction: discord.Interaction, + prompt: str, + undesired_prompt: str = "", + orientation: str = "portrait" +): + if interaction.channel.id != ALLOWED_CHANNEL_ID: + await interaction.response.send_message( + "Dieser Befehl ist in diesem Kanal nicht erlaubt.", + ephemeral=True + ) + return + + await interaction.response.defer(thinking=True) + + # Auflösung wählen + if orientation.lower() == "landscape": + width, height = 1216, 832 + else: + width, height = 832, 1216 + + default_negative = ( + "blurry, lowres, error, film grain, scan artifacts, worst quality, bad quality, jpeg artifacts, " + "very displeasing, chromatic aberration, multiple views, logo, too many watermarks, white blank page, " + "blank page, " + ) + negative = undesired_prompt.strip() or ( + "blurry, lowres, error, film grain, scan artifacts, worst quality, bad quality, jpeg artifacts, " + "very displeasing, chromatic aberration, multiple views, logo, too many watermarks, white blank page, " + "blank page" + ) + + seed = random.randint(0, 2**32 - 1) + + # API-Payload zusammenbauen + payload = { + "input": prompt, + "model": "nai-diffusion-4-full", + "action": "generate", + "parameters": { + "params_version": 3, + "width": width, + "height": height, + "scale": 3.7, + "sampler": "k_euler_ancestral", + "steps": 28, + "n_samples": 1, + "ucPreset": 0, + "qualityToggle": True, + "autoSmea": False, + "dynamic_thresholding": False, + "controlnet_strength": 1, + "legacy": False, + "add_original_image": True, + "cfg_rescale": 0, + "noise_schedule": "karras", + "legacy_v3_extend": False, + "skip_cfg_above_sigma": None, + "use_coords": False, + "legacy_uc": False, + "normalize_reference_strength_multiple": True, + #"seed": seed, + "characterPrompts": [], + "v4_prompt": { + "caption": { + "base_caption": prompt, + "char_captions": [] + }, + "use_coords": False, + "use_order": True + }, + "v4_negative_prompt": { + "caption": { + "base_caption": default_negative + undesired_prompt.strip(), + "char_captions": [] + }, + "legacy_uc": False + }, + "negative_prompt": default_negative + undesired_prompt.strip(), + "deliberate_euler_ancestral_bug": False, + "prefer_brownian": True + } + } + + headers = { + "Authorization": f"Bearer {NOVELAI_API_TOKEN}", + "Content-Type": "application/json" + } + + response = requests.post( + "https://image.novelai.net/ai/generate-image", + json=payload, + headers=headers, + timeout=120 + ) + + if response.status_code == 200: + response_bytes = response.content + + param_text = f"""```Prompt: {prompt} +Negativer Prompt: {negative} +Seed: {seed} +Auflösung: {width}x{height} +Sampler: k_euler_ancestral +Steps: 28 +Scale: 3.7 +Model: nai-diffusion-4-full```""" + + # Prüfen ob ZIP (PK am Anfang) + if response_bytes[:2] == b'PK': + # ZIP-Archiv entpacken + with zipfile.ZipFile(io.BytesIO(response_bytes)) as zip_file: + namelist = zip_file.namelist() + # Erstes PNG suchen + image_name = next((name for name in namelist if name.endswith(".png")), None) + + if image_name: + image_bytes = zip_file.read(image_name) + filename = "novelai.png" + file = discord.File(io.BytesIO(image_bytes), filename=filename) + await interaction.followup.send(content=param_text, file=file) + else: + await interaction.followup.send( + "ZIP erhalten, aber kein PNG-Bild gefunden." + ) + + elif response_bytes[:4] == b'\x89PNG': + # Direkte PNG-Rückgabe + filename = "novelai.png" + file = discord.File(io.BytesIO(response_bytes), filename=filename) + await interaction.followup.send(content=param_text, file=file) + + else: + await interaction.followup.send( + "Die API hat Daten gesendet, aber es war weder ein Bild noch ein gültiges ZIP-Archiv." + ) + + else: + try: + error_data = response.json() + except Exception: + error_data = {"error": response.text} + + error_message = f"Fehler {response.status_code} bei der API-Anfrage.\n" + for key, value in error_data.items(): + error_message += f"**{key}**: {value}\n" + + await interaction.followup.send(error_message) + + +bot.run(BOT_TOKEN) diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..a19c97b --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,2 @@ +discord +requests \ No newline at end of file