diff --git a/.env.example b/.env.example index bf5b545..7d6fbe5 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,11 @@ DISCORD_CLIENT_ID= # Optional: wenn gesetzt, sind Slash-Commands sofort verfügbar statt nach bis zu 1h DISCORD_GUILD_ID= +# --- Devlog-Archiv (Feature 3) --- + +# Rechtsklick auf den Devlog-Kanal (dort postet tools/devlog.py) → "Kanal-ID kopieren" +DEVLOG_CHANNEL_ID= + # --- Commit-Feed (Feature 2) --- # Rechtsklick auf den Commit-Kanal → "Kanal-ID kopieren" diff --git a/README.md b/README.md index b70c3da..ddb31fe 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Discord-Bot + Webinterface für die EcoGame-Community. **Features (Roadmap):** 1. ✅ Bot online + `/ping` Slash-Command 2. ✅ Commit-Feed: Gitea-Push-Webhooks → Discord-Embeds + SQLite-Archiv -3. ⬜ Devlog-Archiv: tägliche Devlogs archivieren +3. ✅ Devlog-Archiv: Devlog-Kanal live archivieren + `/devlog-backfill` für die Historie 4. ⬜ Webinterface: Discord-Login, Devlog- & Commit-Seiten **Stack:** Node.js 20+, discord.js v14, Fastify, React (ab Feature 4), SQLite (better-sqlite3), Docker @@ -97,6 +97,25 @@ derselben Commits erzeugt keine doppelten DB-Einträge. --- +## Setup: Devlog-Archiv + +`tools/devlog.py` (EcoGame-Repo) postet weiterhin unverändert per Discord-Webhook +in den Devlog-Kanal — der Bot liest dort mit und archiviert jede Webhook-Nachricht +in SQLite (User-Chatter im Kanal wird ignoriert). + +1. **Message Content Intent** muss im + [Developer Portal](https://discord.com/developers/applications) unter **Bot** + aktiviert sein (steht oben schon im Setup — kurz prüfen) +2. `.env` / Portainer-Stack: `DEVLOG_CHANNEL_ID` = Kanal-ID des Devlog-Kanals +3. Bot neu starten → ab jetzt wird jedes neue Devlog automatisch archiviert +4. Einmalig in Discord: **`/devlog-backfill`** (Admin only) — scannt die komplette + Kanal-Historie und archiviert alle bisherigen Devlogs nach + +Dedupe läuft über die Discord-Message-ID — Backfill und Live-Listener +können sich nicht doppeln, der Command ist beliebig oft wiederholbar. + +--- + ## Lokal starten (Entwicklung) ``` @@ -162,10 +181,12 @@ ecobot/ │ ├── config.js # Env-Konfiguration mit Validierung │ ├── db.js # SQLite (better-sqlite3), Schema + Queries │ ├── bot/ -│ │ ├── client.js # Discord-Client, Command-Registry, Interactions +│ │ ├── client.js # Discord-Client, Command-Registry, Interactions, Devlog-Listener │ │ ├── commit-feed.js # Push → Discord-Embed +│ │ ├── devlog-archive.js # Webhook-Nachricht → SQLite │ │ └── commands/ -│ │ └── ping.js # /ping — Lebenszeichen +│ │ ├── ping.js # /ping — Lebenszeichen +│ │ └── devlog-backfill.js # /devlog-backfill — Historie archivieren (Admin) │ └── web/ │ └── server.js # Fastify: /health, /webhooks/gitea (HMAC-geprüft) ├── .env.example # Vorlage für Secrets (nach .env kopieren) diff --git a/docker-compose.yml b/docker-compose.yml index 45a7b63..106c67e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID} DISCORD_GUILD_ID: ${DISCORD_GUILD_ID:-} COMMIT_CHANNEL_ID: ${COMMIT_CHANNEL_ID} + DEVLOG_CHANNEL_ID: ${DEVLOG_CHANNEL_ID} GITEA_WEBHOOK_SECRET: ${GITEA_WEBHOOK_SECRET} DB_PATH: /app/data/ecobot.db # HTTP-Endpoint für Gitea-Webhooks + Webinterface diff --git a/src/bot/client.js b/src/bot/client.js index d095545..fef4558 100644 --- a/src/bot/client.js +++ b/src/bot/client.js @@ -1,14 +1,22 @@ // Discord-Client: Commands laden, registrieren und Interactions verarbeiten import { Client, Collection, Events, GatewayIntentBits, REST, Routes } from 'discord.js'; import { config } from '../config.js'; +import { archiveDevlogMessage } from './devlog-archive.js'; import * as ping from './commands/ping.js'; +import * as devlogBackfill from './commands/devlog-backfill.js'; // Alle Commands hier eintragen — jedes Modul exportiert { data, execute } -const commandModules = [ping]; +const commandModules = [ping, devlogBackfill]; export async function startBot() { const client = new Client({ - intents: [GatewayIntentBits.Guilds], + intents: [ + GatewayIntentBits.Guilds, + // Für das Devlog-Archiv: Nachrichten im Devlog-Kanal mitlesen + // (erfordert aktivierten "Message Content Intent" im Developer Portal!) + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + ], }); // Commands in Collection ablegen für schnellen Zugriff im Interaction-Handler @@ -22,6 +30,18 @@ export async function startBot() { await registerCommands(); }); + // Live-Archivierung: neue Devlogs (Webhook-Posts im Devlog-Kanal) sofort sichern + client.on(Events.MessageCreate, (message) => { + if (message.channelId !== config.devlogChannelId) return; + try { + if (archiveDevlogMessage(message)) { + console.log(`[devlog] Neues Devlog archiviert (${message.id})`); + } + } catch (error) { + console.error('[devlog] Archivierung fehlgeschlagen:', error); + } + }); + client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; diff --git a/src/bot/commands/devlog-backfill.js b/src/bot/commands/devlog-backfill.js new file mode 100644 index 0000000..a8f4c64 --- /dev/null +++ b/src/bot/commands/devlog-backfill.js @@ -0,0 +1,38 @@ +// /devlog-backfill — komplette Kanal-Historie scannen und alte Devlogs nacharchivieren (Admin only) +import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags } from 'discord.js'; +import { config } from '../../config.js'; +import { archiveDevlogMessage } from '../devlog-archive.js'; + +export const data = new SlashCommandBuilder() + .setName('devlog-backfill') + .setDescription('Archiviert alle bisherigen Devlogs aus dem Devlog-Kanal') + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator); + +export async function execute(interaction) { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + const channel = await interaction.client.channels.fetch(config.devlogChannelId); + if (!channel?.isTextBased()) { + await interaction.editReply('❌ Devlog-Kanal nicht gefunden oder kein Textkanal.'); + return; + } + + // Historie rückwärts in 100er-Blöcken durchlaufen (Discord-API-Maximum pro Fetch) + let scanned = 0; + let saved = 0; + let before; + for (;;) { + const batch = await channel.messages.fetch({ limit: 100, before }); + if (batch.size === 0) break; + + for (const message of batch.values()) { + scanned++; + if (archiveDevlogMessage(message)) saved++; + } + before = batch.last().id; + } + + await interaction.editReply( + `✅ Backfill fertig: ${scanned} Nachrichten gescannt, **${saved} Devlogs neu archiviert**.` + ); +} diff --git a/src/bot/devlog-archive.js b/src/bot/devlog-archive.js new file mode 100644 index 0000000..9b89718 --- /dev/null +++ b/src/bot/devlog-archive.js @@ -0,0 +1,38 @@ +// Devlog-Archivierung: Webhook-Nachrichten aus dem Devlog-Kanal in SQLite sichern. +// Wird vom Live-Listener (MessageCreate) und vom /devlog-backfill-Command genutzt. +import { saveDevlog } from '../db.js'; + +/** Text aus einer Nachricht ziehen — Plain-Content plus Embed-Titel/-Beschreibungen */ +function extractContent(message) { + const parts = []; + if (message.content?.trim()) { + parts.push(message.content.trim()); + } + for (const embed of message.embeds) { + if (embed.title?.trim()) parts.push(embed.title.trim()); + if (embed.description?.trim()) parts.push(embed.description.trim()); + } + return parts.join('\n\n'); +} + +/** + * Nachricht archivieren, falls sie ein Devlog ist (Webhook-Post mit Text). + * @param {import('discord.js').Message} message + * @returns {boolean} true, wenn neu gespeichert + */ +export function archiveDevlogMessage(message) { + // Nur Webhook-Nachrichten — devlog.py postet per Discord-Webhook. + // Filtert nebenbei User-Chatter und Bot-Posts im Kanal aus. + if (!message.webhookId) return false; + + const content = extractContent(message); + if (!content) return false; + + return saveDevlog({ + message_id: message.id, + channel_id: message.channelId, + content, + author_name: message.author?.username ?? null, + posted_at: message.createdAt.toISOString(), + }); +} diff --git a/src/config.js b/src/config.js index 9742a69..71b16f7 100644 --- a/src/config.js +++ b/src/config.js @@ -18,6 +18,10 @@ export const config = { // Optional: Guild-ID für sofortige Slash-Command-Registrierung (global dauert bis zu 1h) discordGuildId: process.env.DISCORD_GUILD_ID || null, + // Devlog-Archiv (Feature 3) + // Kanal, in den tools/devlog.py (EcoGame-Repo) per Discord-Webhook postet + devlogChannelId: required('DEVLOG_CHANNEL_ID'), + // Commit-Feed (Feature 2) // Kanal, in den Push-Embeds gepostet werden commitChannelId: required('COMMIT_CHANNEL_ID'), diff --git a/src/db.js b/src/db.js index 1b58d53..2efc133 100644 --- a/src/db.js +++ b/src/db.js @@ -23,6 +23,16 @@ db.exec(` received_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE INDEX IF NOT EXISTS idx_commits_repo_time ON commits (repo, committed_at DESC); + + CREATE TABLE IF NOT EXISTS devlogs ( + message_id TEXT PRIMARY KEY, + channel_id TEXT NOT NULL, + content TEXT NOT NULL, + author_name TEXT, + posted_at TEXT NOT NULL, + archived_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_devlogs_posted ON devlogs (posted_at DESC); `); const insertCommit = db.prepare(` @@ -38,3 +48,13 @@ export const saveCommits = db.transaction((commits) => { } return inserted; }); + +const insertDevlog = db.prepare(` + INSERT OR IGNORE INTO devlogs (message_id, channel_id, content, author_name, posted_at) + VALUES (@message_id, @channel_id, @content, @author_name, @posted_at) +`); + +/** Devlog speichern — bereits bekannte Message-IDs werden ignoriert. Gibt true zurück, wenn neu. */ +export function saveDevlog(devlog) { + return insertDevlog.run(devlog).changes > 0; +}