Add stats, feeds, scheduler, and guild backup features to the bot

- Introduced new models in the Prisma schema for statistics, social feeds, scheduled messages, and guild backups, enhancing the bot's functionality.
- Implemented command modules for managing stats channels, feeds, scheduling messages, and creating/restoring backups, improving user engagement and server management.
- Updated job handling to include dedicated workers for stats updates, feed polling, scheduling, and backup restoration, enhancing automation.
- Enhanced command localization for new features in both German and English.
- Documented the new features and their setup processes in PHASE-TRACKING.md for clarity on implementation steps.
This commit is contained in:
smueller
2026-07-22 13:36:39 +02:00
parent 52b10c9536
commit 12066befa8
36 changed files with 4032 additions and 42 deletions

View File

@@ -6,6 +6,7 @@ export * from './discord-localization.js';
export * from './phase2.js';
export * from './phase3.js';
export * from './phase4.js';
export * from './phase5.js';
export const LocaleSchema = z.enum(['de', 'en']);
export type Locale = z.infer<typeof LocaleSchema>;
@@ -364,6 +365,17 @@ const de: Dictionary = {
'birthdays.list.header': '**Geburtstage (sortiert nach nächstem Datum):**',
'birthdays.next.header': '**Nächste Geburtstage:**',
'birthdays.list.line': '{rank}. {user} — {date} (in {days} T.)',
'stats.notConfigured': 'Server-Statistiken sind noch nicht eingerichtet. Nutze `/stats setup`.',
'stats.setup.done': 'Statistik-Kanäle konfiguriert und aktualisiert.',
'stats.refresh.done': 'Statistik-Kanäle wurden aktualisiert.',
'stats.error.invalidSetup': 'Ungültige Eingabe für die Statistik-Konfiguration.',
'invites.view.empty': '{user} hat noch keine Invite-Statistiken.',
'invites.view.result':
'**Invite-Statistik für {user}**\nJoins: {joins} | Leaves: {leaves} | Fakes: {fakes} | Netto: {net}',
'invites.leaderboard.empty': 'Noch keine Invite-Daten vorhanden.',
'invites.leaderboard.header': '**Top Einlader:**',
'invites.leaderboard.line':
'{rank}. {user} — Joins: {joins}, Leaves: {leaves}, Fakes: {fakes}, Netto: {net}',
'tempvoice.error.disabled': 'Temp-Voice ist auf diesem Server deaktiviert.',
'tempvoice.error.invalidCategory': 'Ungültige Kategorie.',
'tempvoice.error.notOwner': 'Du bist nicht der Besitzer dieses Temp-Voice-Kanals.',
@@ -386,6 +398,7 @@ const de: Dictionary = {
'tempvoice.claim.success': 'Du bist jetzt Besitzer dieses Kanals.',
'tempvoice.panel.title': 'Temp-Voice Steuerung',
'tempvoice.panel.description': 'Verwalte **{channel}** mit den Buttons oder `/voice`-Commands.',
'tempvoice.panel.sent': 'Control-Panel wurde in den Voice-Kanal-Chat gesendet.',
'tempvoice.button.rename': 'Umbenennen',
'tempvoice.button.limit': 'Limit',
'tempvoice.button.lock': 'Sperren',
@@ -436,7 +449,70 @@ const de: Dictionary = {
'tag.error.not_allowed': 'Du darfst diesen Tag hier nicht nutzen.',
'tag.error.invalid_name': 'Ungültiger Tag-Name (132 Zeichen, a-z, 0-9, _, -).',
'tag.error.content_required': 'Text-Tags benötigen Inhalt.',
'tag.error.embed_required': 'Embed-Tags benötigen Titel oder Beschreibung.'
'tag.error.embed_required': 'Embed-Tags benötigen Titel oder Beschreibung.',
'guildbackup.description': 'Server-Backups erstellen und wiederherstellen',
'guildbackup.created': 'Backup **{name}** erstellt (ID: `{id}`).',
'guildbackup.deleted': 'Backup `{id}` gelöscht.',
'guildbackup.list.header': '**Server-Backups**',
'guildbackup.list.empty': 'Keine Backups vorhanden.',
'guildbackup.list.line': '• `{id}` — **{name}** ({date})',
'guildbackup.info.id': 'ID: `{id}`',
'guildbackup.info.name': 'Name: **{name}**',
'guildbackup.info.created': 'Erstellt: {date}',
'guildbackup.info.createdBy': 'Erstellt von: <@{userId}>',
'guildbackup.info.roles': 'Rollen: {count}',
'guildbackup.info.channels': 'Kanäle: {count}',
'guildbackup.info.limitations':
'Hinweis: Backups enthalten keine Nachrichten. Beim Restore werden IDs nicht übernommen; fehlende Rollen/Kanäle werden nach Name ergänzt, bestehende nicht gelöscht.',
'guildbackup.confirm.delete': 'Backup `{id}` wirklich löschen?',
'guildbackup.confirm.restore.step1':
'Backup `{id}` wiederherstellen? Es werden fehlende Rollen/Kanäle ergänzt und Overwrites best-effort angewendet. Bestehende Kanäle werden **nicht** gelöscht.',
'guildbackup.confirm.restore.step2':
'**Letzte Bestätigung:** Backup `{id}` jetzt wiederherstellen? Diese Aktion kann nicht rückgängig gemacht werden.',
'guildbackup.confirm.button.confirm': 'Bestätigen',
'guildbackup.confirm.button.cancel': 'Abbrechen',
'guildbackup.confirm.expired': 'Diese Bestätigung ist abgelaufen.',
'guildbackup.confirm.unauthorized': 'Nur der auslösende Nutzer kann das bestätigen.',
'guildbackup.confirm.cancelled': 'Abgebrochen.',
'guildbackup.restore.done':
'Backup `{id}` wiederhergestellt. Rollen: {rolesCreated} neu, {rolesSkipped} übersprungen. Kanäle: {channelsCreated} neu, {channelsUpdated} aktualisiert. Overwrites: {overwritesApplied}.',
'guildbackup.restore.queued':
'Großes Backup `{id}` — Restore wurde in die Warteschlange gestellt und läuft im Hintergrund.',
'guildbackup.error.not_found': 'Backup nicht gefunden.',
'guildbackup.error.invalid_payload': 'Backup-Daten sind ungültig oder veraltet.',
'guildbackup.error.invalid_name': 'Ungültiger Backup-Name (1100 Zeichen).',
'guildbackup.error.owner_only': 'Nur der Server-Owner kann Backups wiederherstellen.',
'guildbackup.error.bot_missing_permissions':
'Dem Bot fehlen Manage Roles oder Manage Channels für einen Restore.',
'feeds.added': 'Feed `{id}` ({type}) hinzugefügt.',
'feeds.removed': 'Feed `{id}` entfernt.',
'feeds.list.header': '**Social Feeds**',
'feeds.list.empty': 'Keine Feeds konfiguriert.',
'feeds.list.line': '`{id}` — {type}: {source} → {channel}',
'feeds.test.posted': 'Test erfolgreich: {count} Nachricht(en) gesendet.',
'feeds.test.noItems': 'Keine neuen Einträge gefunden.',
'feeds.test.failed': 'Abruf fehlgeschlagen: {reason}',
'feeds.test.skipped': 'Feed-Test übersprungen.',
'feeds.test.twitchSkipped':
'Twitch-Feed übersprungen: TWITCH_CLIENT_ID und TWITCH_CLIENT_SECRET fehlen in der Bot-Konfiguration.',
'feeds.error.not_found': 'Feed nicht gefunden.',
'scheduler.created': 'Geplante Nachricht erstellt (ID: `{id}`). {timing}',
'scheduler.created.at': 'Sendung: {time}',
'scheduler.created.cron': 'Wiederholung: `{cron}`',
'scheduler.deleted': 'Geplante Nachricht gelöscht.',
'scheduler.announce.sent': 'Ankündigung gesendet.',
'scheduler.list.header': '**Geplante Nachrichten**',
'scheduler.list.empty': 'Keine geplanten Nachrichten.',
'scheduler.list.line': '`{id}` — {channel} — {timing} — {preview}',
'scheduler.list.cron': 'Cron `{cron}`',
'scheduler.list.embedPreview': '[Embed]',
'scheduler.error.missing_schedule_time': 'Gib `when` oder `cron` an.',
'scheduler.error.invalid_when': 'Ungültige Zeit. Nutze z. B. 1h, 2d oder ein ISO-Datum.',
'scheduler.error.invalid_cron': 'Ungültiger Cron-Ausdruck (56 Felder).',
'scheduler.error.invalid_channel': 'Ungültiger Kanal.',
'scheduler.error.channel_unavailable': 'Der Bot kann in diesem Kanal nicht senden.',
'scheduler.error.not_found': 'Geplante Nachricht nicht gefunden.',
'scheduler.error.content_required': 'Inhalt oder Embed-Titel/Beschreibung erforderlich.'
};
const en: Dictionary = {
'generic.noPermission': 'You do not have permission for this action.',
@@ -762,6 +838,17 @@ const en: Dictionary = {
'birthdays.list.header': '**Birthdays (sorted by next date):**',
'birthdays.next.header': '**Upcoming birthdays:**',
'birthdays.list.line': '{rank}. {user} — {date} (in {days} d)',
'stats.notConfigured': 'Server statistics are not configured yet. Use `/stats setup`.',
'stats.setup.done': 'Stats channels configured and updated.',
'stats.refresh.done': 'Stats channels have been refreshed.',
'stats.error.invalidSetup': 'Invalid input for stats configuration.',
'invites.view.empty': '{user} has no invite statistics yet.',
'invites.view.result':
'**Invite stats for {user}**\nJoins: {joins} | Leaves: {leaves} | Fakes: {fakes} | Net: {net}',
'invites.leaderboard.empty': 'No invite data available yet.',
'invites.leaderboard.header': '**Top inviters:**',
'invites.leaderboard.line':
'{rank}. {user} — Joins: {joins}, Leaves: {leaves}, Fakes: {fakes}, Net: {net}',
'tempvoice.error.disabled': 'Temp voice is disabled on this server.',
'tempvoice.error.invalidCategory': 'Invalid category.',
'tempvoice.error.notOwner': 'You are not the owner of this temp voice channel.',
@@ -784,6 +871,7 @@ const en: Dictionary = {
'tempvoice.claim.success': 'You are now the owner of this channel.',
'tempvoice.panel.title': 'Temp Voice Control',
'tempvoice.panel.description': 'Manage **{channel}** with the buttons below or `/voice` commands.',
'tempvoice.panel.sent': 'Control panel posted in the voice channel chat.',
'tempvoice.button.rename': 'Rename',
'tempvoice.button.limit': 'Limit',
'tempvoice.button.lock': 'Lock',
@@ -834,7 +922,70 @@ const en: Dictionary = {
'tag.error.not_allowed': 'You are not allowed to use this tag here.',
'tag.error.invalid_name': 'Invalid tag name (132 chars, a-z, 0-9, _, -).',
'tag.error.content_required': 'Text tags require content.',
'tag.error.embed_required': 'Embed tags require a title or description.'
'tag.error.embed_required': 'Embed tags require a title or description.',
'guildbackup.description': 'Create and restore server backups',
'guildbackup.created': 'Created backup **{name}** (ID: `{id}`).',
'guildbackup.deleted': 'Deleted backup `{id}`.',
'guildbackup.list.header': '**Server backups**',
'guildbackup.list.empty': 'No backups found.',
'guildbackup.list.line': '• `{id}` — **{name}** ({date})',
'guildbackup.info.id': 'ID: `{id}`',
'guildbackup.info.name': 'Name: **{name}**',
'guildbackup.info.created': 'Created: {date}',
'guildbackup.info.createdBy': 'Created by: <@{userId}>',
'guildbackup.info.roles': 'Roles: {count}',
'guildbackup.info.channels': 'Channels: {count}',
'guildbackup.info.limitations':
'Note: Backups do not include messages. Restore cannot preserve IDs; missing roles/channels are added by name, existing channels are not deleted.',
'guildbackup.confirm.delete': 'Really delete backup `{id}`?',
'guildbackup.confirm.restore.step1':
'Restore backup `{id}`? Missing roles/channels will be added and overwrites applied best-effort. Existing channels will **not** be deleted.',
'guildbackup.confirm.restore.step2':
'**Final confirmation:** Restore backup `{id}` now? This cannot be undone.',
'guildbackup.confirm.button.confirm': 'Confirm',
'guildbackup.confirm.button.cancel': 'Cancel',
'guildbackup.confirm.expired': 'This confirmation has expired.',
'guildbackup.confirm.unauthorized': 'Only the user who started this action can confirm.',
'guildbackup.confirm.cancelled': 'Cancelled.',
'guildbackup.restore.done':
'Restored backup `{id}`. Roles: {rolesCreated} created, {rolesSkipped} skipped. Channels: {channelsCreated} created, {channelsUpdated} updated. Overwrites: {overwritesApplied}.',
'guildbackup.restore.queued':
'Large backup `{id}` — restore queued and will run in the background.',
'guildbackup.error.not_found': 'Backup not found.',
'guildbackup.error.invalid_payload': 'Backup data is invalid or outdated.',
'guildbackup.error.invalid_name': 'Invalid backup name (1100 characters).',
'guildbackup.error.owner_only': 'Only the server owner can restore backups.',
'guildbackup.error.bot_missing_permissions':
'The bot is missing Manage Roles or Manage Channels for a restore.',
'feeds.added': 'Added feed `{id}` ({type}).',
'feeds.removed': 'Removed feed `{id}`.',
'feeds.list.header': '**Social Feeds**',
'feeds.list.empty': 'No feeds configured.',
'feeds.list.line': '`{id}` — {type}: {source} → {channel}',
'feeds.test.posted': 'Test successful: sent {count} message(s).',
'feeds.test.noItems': 'No new items found.',
'feeds.test.failed': 'Fetch failed: {reason}',
'feeds.test.skipped': 'Feed test skipped.',
'feeds.test.twitchSkipped':
'Twitch feed skipped: TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET are missing in the bot configuration.',
'feeds.error.not_found': 'Feed not found.',
'scheduler.created': 'Scheduled message created (ID: `{id}`). {timing}',
'scheduler.created.at': 'Sends at: {time}',
'scheduler.created.cron': 'Repeats: `{cron}`',
'scheduler.deleted': 'Scheduled message deleted.',
'scheduler.announce.sent': 'Announcement sent.',
'scheduler.list.header': '**Scheduled messages**',
'scheduler.list.empty': 'No scheduled messages.',
'scheduler.list.line': '`{id}` — {channel} — {timing} — {preview}',
'scheduler.list.cron': 'Cron `{cron}`',
'scheduler.list.embedPreview': '[Embed]',
'scheduler.error.missing_schedule_time': 'Provide `when` or `cron`.',
'scheduler.error.invalid_when': 'Invalid time. Use e.g. 1h, 2d, or an ISO date.',
'scheduler.error.invalid_cron': 'Invalid cron expression (56 fields).',
'scheduler.error.invalid_channel': 'Invalid channel.',
'scheduler.error.channel_unavailable': 'The bot cannot send messages in that channel.',
'scheduler.error.not_found': 'Scheduled message not found.',
'scheduler.error.content_required': 'Content or embed title/description is required.'
};
const locales: Record<Locale, Dictionary> = { de, en };