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:
@@ -10,6 +10,11 @@ DISCORD_CLIENT_ID=
|
|||||||
# Optional: wenn gesetzt, sind Slash-Commands sofort verfügbar statt nach bis zu 1h
|
# Optional: wenn gesetzt, sind Slash-Commands sofort verfügbar statt nach bis zu 1h
|
||||||
DISCORD_GUILD_ID=
|
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) ---
|
# --- Commit-Feed (Feature 2) ---
|
||||||
|
|
||||||
# Rechtsklick auf den Commit-Kanal → "Kanal-ID kopieren"
|
# Rechtsklick auf den Commit-Kanal → "Kanal-ID kopieren"
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Discord-Bot + Webinterface für die EcoGame-Community.
|
|||||||
**Features (Roadmap):**
|
**Features (Roadmap):**
|
||||||
1. ✅ Bot online + `/ping` Slash-Command
|
1. ✅ Bot online + `/ping` Slash-Command
|
||||||
2. ✅ Commit-Feed: Gitea-Push-Webhooks → Discord-Embeds + SQLite-Archiv
|
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
|
4. ⬜ Webinterface: Discord-Login, Devlog- & Commit-Seiten
|
||||||
|
|
||||||
**Stack:** Node.js 20+, discord.js v14, Fastify, React (ab Feature 4), SQLite (better-sqlite3), Docker
|
**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)
|
## Lokal starten (Entwicklung)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -162,10 +181,12 @@ ecobot/
|
|||||||
│ ├── config.js # Env-Konfiguration mit Validierung
|
│ ├── config.js # Env-Konfiguration mit Validierung
|
||||||
│ ├── db.js # SQLite (better-sqlite3), Schema + Queries
|
│ ├── db.js # SQLite (better-sqlite3), Schema + Queries
|
||||||
│ ├── bot/
|
│ ├── bot/
|
||||||
│ │ ├── client.js # Discord-Client, Command-Registry, Interactions
|
│ │ ├── client.js # Discord-Client, Command-Registry, Interactions, Devlog-Listener
|
||||||
│ │ ├── commit-feed.js # Push → Discord-Embed
|
│ │ ├── commit-feed.js # Push → Discord-Embed
|
||||||
|
│ │ ├── devlog-archive.js # Webhook-Nachricht → SQLite
|
||||||
│ │ └── commands/
|
│ │ └── commands/
|
||||||
│ │ └── ping.js # /ping — Lebenszeichen
|
│ │ ├── ping.js # /ping — Lebenszeichen
|
||||||
|
│ │ └── devlog-backfill.js # /devlog-backfill — Historie archivieren (Admin)
|
||||||
│ └── web/
|
│ └── web/
|
||||||
│ └── server.js # Fastify: /health, /webhooks/gitea (HMAC-geprüft)
|
│ └── server.js # Fastify: /health, /webhooks/gitea (HMAC-geprüft)
|
||||||
├── .env.example # Vorlage für Secrets (nach .env kopieren)
|
├── .env.example # Vorlage für Secrets (nach .env kopieren)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ services:
|
|||||||
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID}
|
DISCORD_CLIENT_ID: ${DISCORD_CLIENT_ID}
|
||||||
DISCORD_GUILD_ID: ${DISCORD_GUILD_ID:-}
|
DISCORD_GUILD_ID: ${DISCORD_GUILD_ID:-}
|
||||||
COMMIT_CHANNEL_ID: ${COMMIT_CHANNEL_ID}
|
COMMIT_CHANNEL_ID: ${COMMIT_CHANNEL_ID}
|
||||||
|
DEVLOG_CHANNEL_ID: ${DEVLOG_CHANNEL_ID}
|
||||||
GITEA_WEBHOOK_SECRET: ${GITEA_WEBHOOK_SECRET}
|
GITEA_WEBHOOK_SECRET: ${GITEA_WEBHOOK_SECRET}
|
||||||
DB_PATH: /app/data/ecobot.db
|
DB_PATH: /app/data/ecobot.db
|
||||||
# HTTP-Endpoint für Gitea-Webhooks + Webinterface
|
# HTTP-Endpoint für Gitea-Webhooks + Webinterface
|
||||||
|
|||||||
+22
-2
@@ -1,14 +1,22 @@
|
|||||||
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
|
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
|
||||||
import { Client, Collection, Events, GatewayIntentBits, REST, Routes } from 'discord.js';
|
import { Client, Collection, Events, GatewayIntentBits, REST, Routes } from 'discord.js';
|
||||||
import { config } from '../config.js';
|
import { config } from '../config.js';
|
||||||
|
import { archiveDevlogMessage } from './devlog-archive.js';
|
||||||
import * as ping from './commands/ping.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 }
|
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
|
||||||
const commandModules = [ping];
|
const commandModules = [ping, devlogBackfill];
|
||||||
|
|
||||||
export async function startBot() {
|
export async function startBot() {
|
||||||
const client = new Client({
|
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
|
// Commands in Collection ablegen für schnellen Zugriff im Interaction-Handler
|
||||||
@@ -22,6 +30,18 @@ export async function startBot() {
|
|||||||
await registerCommands();
|
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) => {
|
client.on(Events.InteractionCreate, async (interaction) => {
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
|
|||||||
@@ -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**.`
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -18,6 +18,10 @@ export const config = {
|
|||||||
// Optional: Guild-ID für sofortige Slash-Command-Registrierung (global dauert bis zu 1h)
|
// Optional: Guild-ID für sofortige Slash-Command-Registrierung (global dauert bis zu 1h)
|
||||||
discordGuildId: process.env.DISCORD_GUILD_ID || null,
|
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)
|
// Commit-Feed (Feature 2)
|
||||||
// Kanal, in den Push-Embeds gepostet werden
|
// Kanal, in den Push-Embeds gepostet werden
|
||||||
commitChannelId: required('COMMIT_CHANNEL_ID'),
|
commitChannelId: required('COMMIT_CHANNEL_ID'),
|
||||||
|
|||||||
@@ -23,6 +23,16 @@ db.exec(`
|
|||||||
received_at TEXT NOT NULL DEFAULT (datetime('now'))
|
received_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_commits_repo_time ON commits (repo, committed_at DESC);
|
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(`
|
const insertCommit = db.prepare(`
|
||||||
@@ -38,3 +48,13 @@ export const saveCommits = db.transaction((commits) => {
|
|||||||
}
|
}
|
||||||
return inserted;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user