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:
@@ -963,6 +963,58 @@ export const commandLocales = {
|
||||
de: 'Nachrichtenvorlage ({user}, {age})',
|
||||
en: 'Message template ({user}, {age})'
|
||||
},
|
||||
'stats.description': {
|
||||
de: 'Server-Statistik-Kanäle verwalten',
|
||||
en: 'Manage server statistics channels'
|
||||
},
|
||||
'stats.setup.description': {
|
||||
de: 'Statistik-Kanäle einrichten',
|
||||
en: 'Configure statistics channels'
|
||||
},
|
||||
'stats.setup.options.members_channel': {
|
||||
de: 'Kanal für Mitgliederzahl',
|
||||
en: 'Channel for member count'
|
||||
},
|
||||
'stats.setup.options.online_channel': {
|
||||
de: 'Kanal für Online-Anzahl',
|
||||
en: 'Channel for online count'
|
||||
},
|
||||
'stats.setup.options.boosts_channel': {
|
||||
de: 'Kanal für Boost-Anzahl',
|
||||
en: 'Channel for boost count'
|
||||
},
|
||||
'stats.setup.options.members_template': {
|
||||
de: 'Vorlage Mitglieder ({count})',
|
||||
en: 'Members template ({count})'
|
||||
},
|
||||
'stats.setup.options.online_template': {
|
||||
de: 'Vorlage Online ({count})',
|
||||
en: 'Online template ({count})'
|
||||
},
|
||||
'stats.setup.options.boosts_template': {
|
||||
de: 'Vorlage Boosts ({count})',
|
||||
en: 'Boosts template ({count})'
|
||||
},
|
||||
'stats.refresh.description': {
|
||||
de: 'Statistik-Kanäle jetzt aktualisieren',
|
||||
en: 'Refresh statistics channels now'
|
||||
},
|
||||
'invites.description': {
|
||||
de: 'Invite-Statistiken anzeigen',
|
||||
en: 'Show invite statistics'
|
||||
},
|
||||
'invites.view.description': {
|
||||
de: 'Invite-Statistik eines Nutzers anzeigen',
|
||||
en: 'Show invite stats for a user'
|
||||
},
|
||||
'invites.view.options.user': {
|
||||
de: 'Nutzer (Standard: du)',
|
||||
en: 'User (defaults to you)'
|
||||
},
|
||||
'invites.leaderboard.description': {
|
||||
de: 'Top-Einlader anzeigen',
|
||||
en: 'Show top inviters'
|
||||
},
|
||||
'tempvoice.description': {
|
||||
de: 'Temp-Voice (Join to Create) steuern',
|
||||
en: 'Control temp voice (join to create)'
|
||||
@@ -1194,6 +1246,158 @@ export const commandLocales = {
|
||||
'tag.choice.response_type.embed': {
|
||||
de: 'Embed',
|
||||
en: 'Embed'
|
||||
},
|
||||
'feeds.description': {
|
||||
de: 'Social-Feeds verwalten',
|
||||
en: 'Manage social feeds'
|
||||
},
|
||||
'feeds.add.description': {
|
||||
de: 'Feed hinzufügen',
|
||||
en: 'Add feed'
|
||||
},
|
||||
'feeds.add.options.type': {
|
||||
de: 'Feed-Typ',
|
||||
en: 'Feed type'
|
||||
},
|
||||
'feeds.add.options.source': {
|
||||
de: 'Quelle (Kanal, URL oder Subreddit)',
|
||||
en: 'Source (channel, URL, or subreddit)'
|
||||
},
|
||||
'feeds.add.options.channel': {
|
||||
de: 'Ziel-Kanal',
|
||||
en: 'Target channel'
|
||||
},
|
||||
'feeds.add.options.role': {
|
||||
de: 'Rolle zum Pingen',
|
||||
en: 'Role to ping'
|
||||
},
|
||||
'feeds.add.options.template': {
|
||||
de: 'Nachrichtenvorlage ({title}, {url})',
|
||||
en: 'Message template ({title}, {url})'
|
||||
},
|
||||
'feeds.remove.description': {
|
||||
de: 'Feed entfernen',
|
||||
en: 'Remove feed'
|
||||
},
|
||||
'feeds.remove.options.id': {
|
||||
de: 'Feed-ID',
|
||||
en: 'Feed ID'
|
||||
},
|
||||
'feeds.list.description': {
|
||||
de: 'Feeds auflisten',
|
||||
en: 'List feeds'
|
||||
},
|
||||
'feeds.test.description': {
|
||||
de: 'Feed einmal abrufen und posten',
|
||||
en: 'Fetch once and post'
|
||||
},
|
||||
'feeds.test.options.id': {
|
||||
de: 'Feed-ID',
|
||||
en: 'Feed ID'
|
||||
},
|
||||
'feeds.choice.type.twitch': {
|
||||
de: 'Twitch',
|
||||
en: 'Twitch'
|
||||
},
|
||||
'feeds.choice.type.youtube': {
|
||||
de: 'YouTube',
|
||||
en: 'YouTube'
|
||||
},
|
||||
'feeds.choice.type.rss': {
|
||||
de: 'RSS',
|
||||
en: 'RSS'
|
||||
},
|
||||
'feeds.choice.type.reddit': {
|
||||
de: 'Reddit',
|
||||
en: 'Reddit'
|
||||
},
|
||||
'schedule.description': {
|
||||
de: 'Geplante Nachrichten verwalten',
|
||||
en: 'Manage scheduled messages'
|
||||
},
|
||||
'schedule.create.description': {
|
||||
de: 'Nachricht planen',
|
||||
en: 'Schedule a message'
|
||||
},
|
||||
'schedule.list.description': {
|
||||
de: 'Geplante Nachrichten anzeigen',
|
||||
en: 'List scheduled messages'
|
||||
},
|
||||
'schedule.delete.description': {
|
||||
de: 'Geplante Nachricht löschen',
|
||||
en: 'Delete a scheduled message'
|
||||
},
|
||||
'schedule.create.options.when': {
|
||||
de: 'Einmalig: Dauer (z. B. 1h) oder ISO-Datum',
|
||||
en: 'One-shot: duration (e.g. 1h) or ISO date'
|
||||
},
|
||||
'schedule.create.options.cron': {
|
||||
de: 'Wiederkehrend: Cron-Ausdruck',
|
||||
en: 'Recurring: cron expression'
|
||||
},
|
||||
'schedule.common.options.channel': {
|
||||
de: 'Zielkanal',
|
||||
en: 'Target channel'
|
||||
},
|
||||
'schedule.common.options.content': {
|
||||
de: 'Nachrichtentext',
|
||||
en: 'Message text'
|
||||
},
|
||||
'schedule.common.options.embed_title': {
|
||||
de: 'Embed-Titel',
|
||||
en: 'Embed title'
|
||||
},
|
||||
'schedule.common.options.embed_description': {
|
||||
de: 'Embed-Beschreibung',
|
||||
en: 'Embed description'
|
||||
},
|
||||
'schedule.common.options.embed_color': {
|
||||
de: 'Embed-Farbe (Dezimal)',
|
||||
en: 'Embed color (decimal)'
|
||||
},
|
||||
'schedule.common.options.role': {
|
||||
de: 'Zu pingende Rolle',
|
||||
en: 'Role to ping'
|
||||
},
|
||||
'schedule.common.options.id': {
|
||||
de: 'ID der geplanten Nachricht',
|
||||
en: 'Scheduled message ID'
|
||||
},
|
||||
'announce.description': {
|
||||
de: 'Sofortige Ankündigung senden',
|
||||
en: 'Send an immediate announcement'
|
||||
},
|
||||
'guildbackup.description': {
|
||||
de: 'Server-Backups erstellen und wiederherstellen',
|
||||
en: 'Create and restore server backups'
|
||||
},
|
||||
'guildbackup.create.description': {
|
||||
de: 'Snapshot von Rollen, Kanälen und Einstellungen erstellen',
|
||||
en: 'Create a snapshot of roles, channels, and settings'
|
||||
},
|
||||
'guildbackup.create.options.name': {
|
||||
de: 'Name des Backups',
|
||||
en: 'Backup name'
|
||||
},
|
||||
'guildbackup.list.description': {
|
||||
de: 'Backups dieses Servers auflisten',
|
||||
en: 'List backups for this server'
|
||||
},
|
||||
'guildbackup.info.description': {
|
||||
de: 'Details zu einem Backup anzeigen',
|
||||
en: 'Show backup details'
|
||||
},
|
||||
'guildbackup.delete.description': {
|
||||
de: 'Backup löschen',
|
||||
en: 'Delete a backup'
|
||||
},
|
||||
'guildbackup.restore.description': {
|
||||
de: 'Backup wiederherstellen (nur Owner)',
|
||||
en: 'Restore a backup (owner only)'
|
||||
},
|
||||
'guildbackup.common.options.id': {
|
||||
de: 'Backup-ID',
|
||||
en: 'Backup ID'
|
||||
}
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
14
packages/shared/src/phase5.test.ts
Normal file
14
packages/shared/src/phase5.test.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { applyCountTemplate, applyFeedTemplate } from './phase5.js';
|
||||
|
||||
describe('phase5 helpers', () => {
|
||||
it('applies count template', () => {
|
||||
expect(applyCountTemplate('Members: {count}', 42)).toBe('Members: 42');
|
||||
});
|
||||
|
||||
it('applies feed template', () => {
|
||||
expect(applyFeedTemplate('{title} - {url}', { title: 'Hi', url: 'https://x.test' })).toBe(
|
||||
'Hi - https://x.test'
|
||||
);
|
||||
});
|
||||
});
|
||||
66
packages/shared/src/phase5.ts
Normal file
66
packages/shared/src/phase5.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const SocialFeedTypeSchema = z.enum(['TWITCH', 'YOUTUBE', 'RSS', 'REDDIT']);
|
||||
export type SocialFeedType = z.infer<typeof SocialFeedTypeSchema>;
|
||||
|
||||
export const GuildBackupPayloadSchema = z.object({
|
||||
version: z.literal(1),
|
||||
guildName: z.string(),
|
||||
roles: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
color: z.number(),
|
||||
hoist: z.boolean(),
|
||||
mentionable: z.boolean(),
|
||||
permissions: z.string(),
|
||||
position: z.number()
|
||||
})
|
||||
),
|
||||
channels: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.number(),
|
||||
parentId: z.string().nullable(),
|
||||
position: z.number(),
|
||||
topic: z.string().nullable().optional(),
|
||||
nsfw: z.boolean().optional(),
|
||||
rateLimitPerUser: z.number().optional(),
|
||||
bitrate: z.number().optional(),
|
||||
userLimit: z.number().optional(),
|
||||
permissionOverwrites: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
type: z.number(),
|
||||
allow: z.string(),
|
||||
deny: z.string()
|
||||
})
|
||||
)
|
||||
})
|
||||
),
|
||||
settings: z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
verificationLevel: z.number().optional(),
|
||||
explicitContentFilter: z.number().optional(),
|
||||
defaultMessageNotifications: z.number().optional(),
|
||||
afkTimeout: z.number().optional()
|
||||
})
|
||||
.optional()
|
||||
});
|
||||
export type GuildBackupPayload = z.infer<typeof GuildBackupPayloadSchema>;
|
||||
|
||||
export function applyCountTemplate(template: string, count: number): string {
|
||||
return template.replaceAll('{count}', String(count));
|
||||
}
|
||||
|
||||
export function applyFeedTemplate(
|
||||
template: string,
|
||||
vars: Record<string, string>
|
||||
): string {
|
||||
return Object.entries(vars).reduce(
|
||||
(acc, [key, value]) => acc.replaceAll(`{${key}}`, value),
|
||||
template
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user