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:
203
apps/bot/src/modules/guildbackup/commands.ts
Normal file
203
apps/bot/src/modules/guildbackup/commands.ts
Normal file
@@ -0,0 +1,203 @@
|
||||
import { PermissionFlagsBits } from 'discord.js';
|
||||
import { t, tf } from '@nexumi/shared';
|
||||
import type { SlashCommand } from '../../types.js';
|
||||
import { getGuildLocale } from '../../i18n.js';
|
||||
import { requirePermission } from '../../permissions.js';
|
||||
import { backupCommandData } from './command-definitions.js';
|
||||
import {
|
||||
createGuildBackup,
|
||||
ensureBotCanRestore,
|
||||
getGuildBackup,
|
||||
GuildBackupError,
|
||||
listGuildBackups,
|
||||
parseBackupPayload
|
||||
} from './service.js';
|
||||
import { promptDeleteConfirmation, promptRestoreConfirmation } from './confirmations.js';
|
||||
|
||||
async function ensureManageGuild(
|
||||
interaction: Parameters<SlashCommand['execute']>[0],
|
||||
locale: 'de' | 'en'
|
||||
): Promise<boolean> {
|
||||
if (!interaction.inGuild()) {
|
||||
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
||||
return false;
|
||||
}
|
||||
return requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale);
|
||||
}
|
||||
|
||||
function defaultBackupName(): string {
|
||||
return `backup-${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}`;
|
||||
}
|
||||
|
||||
const backupCommand: SlashCommand = {
|
||||
data: backupCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!interaction.inGuild()) {
|
||||
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const sub = interaction.options.getSubcommand();
|
||||
const guild = interaction.guild!;
|
||||
|
||||
if (sub === 'create') {
|
||||
if (!(await ensureManageGuild(interaction, locale))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = interaction.options.getString('name')?.trim() || defaultBackupName();
|
||||
if (name.length < 1 || name.length > 100) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.invalid_name'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const backup = await createGuildBackup(context, guild, name, interaction.user.id);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'guildbackup.created', { id: backup.id, name: backup.name }),
|
||||
ephemeral: true
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof GuildBackupError) {
|
||||
await interaction.reply({
|
||||
content: t(locale, `guildbackup.error.${error.code}`),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'list') {
|
||||
const backups = await listGuildBackups(context, guild.id);
|
||||
if (backups.length === 0) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.list.empty'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = backups.map((backup) =>
|
||||
tf(locale, 'guildbackup.list.line', {
|
||||
id: backup.id,
|
||||
name: backup.name,
|
||||
date: backup.createdAt.toISOString().slice(0, 10)
|
||||
})
|
||||
);
|
||||
|
||||
await interaction.reply({
|
||||
content: `${t(locale, 'guildbackup.list.header')}\n${lines.join('\n')}`,
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'info') {
|
||||
const backupId = interaction.options.getString('id', true);
|
||||
const backup = await getGuildBackup(context, guild.id, backupId);
|
||||
if (!backup) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.not_found'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let roleCount = 0;
|
||||
let channelCount = 0;
|
||||
try {
|
||||
const payload = parseBackupPayload(backup.payload);
|
||||
roleCount = payload.roles.length;
|
||||
channelCount = payload.channels.length;
|
||||
} catch {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.invalid_payload'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await interaction.reply({
|
||||
content: [
|
||||
tf(locale, 'guildbackup.info.id', { id: backup.id }),
|
||||
tf(locale, 'guildbackup.info.name', { name: backup.name }),
|
||||
tf(locale, 'guildbackup.info.created', { date: backup.createdAt.toISOString() }),
|
||||
tf(locale, 'guildbackup.info.createdBy', { userId: backup.createdById }),
|
||||
tf(locale, 'guildbackup.info.roles', { count: roleCount }),
|
||||
tf(locale, 'guildbackup.info.channels', { count: channelCount }),
|
||||
t(locale, 'guildbackup.info.limitations')
|
||||
].join('\n'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'delete') {
|
||||
if (!(await ensureManageGuild(interaction, locale))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backupId = interaction.options.getString('id', true);
|
||||
const backup = await getGuildBackup(context, guild.id, backupId);
|
||||
if (!backup) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.not_found'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await promptDeleteConfirmation(interaction, locale, backupId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'restore') {
|
||||
if (guild.ownerId !== interaction.user.id) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.owner_only'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ensureBotCanRestore(guild)) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.bot_missing_permissions'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const backupId = interaction.options.getString('id', true);
|
||||
const backup = await getGuildBackup(context, guild.id, backupId);
|
||||
if (!backup) {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.not_found'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
parseBackupPayload(backup.payload);
|
||||
} catch {
|
||||
await interaction.reply({
|
||||
content: t(locale, 'guildbackup.error.invalid_payload'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await promptRestoreConfirmation(interaction, locale, backupId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const guildBackupCommands = [backupCommand];
|
||||
Reference in New Issue
Block a user