Feature 3: Devlog-Archiv — Live-Listener + /devlog-backfill

- Webhook-Nachrichten im Devlog-Kanal werden automatisch in SQLite archiviert
  (devlog.py im EcoGame-Repo bleibt unverändert)
- /devlog-backfill (Admin): scannt die komplette Kanal-Historie in 100er-Blöcken
- Dedupe über Discord-Message-ID, Embeds werden mit Titel+Beschreibung erfasst
- Neue Intents: GuildMessages + MessageContent, neue Env-Var DEVLOG_CHANNEL_ID

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 23:51:16 +02:00
co-authored by Claude Opus 4.8
parent 716268b1c1
commit 2c63c62e14
8 changed files with 152 additions and 5 deletions
+5
View File
@@ -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"
+24 -3
View File
@@ -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)
+1
View File
@@ -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
+22 -2
View File
@@ -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;
+38
View File
@@ -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**.`
);
}
+38
View File
@@ -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(),
});
}
+4
View File
@@ -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'),
+20
View File
@@ -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;
}