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:
@@ -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 (1–32 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 (1–100 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 (5–6 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 (1–32 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 (1–100 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 (5–6 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 };
|
||||
|
||||
Reference in New Issue
Block a user