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

@@ -0,0 +1,141 @@
import { PermissionFlagsBits } from 'discord.js';
import { SocialFeedTypeSchema, t, tf } from '@nexumi/shared';
import type { SlashCommand } from '../../types.js';
import { getGuildLocale } from '../../i18n.js';
import { requirePermission } from '../../permissions.js';
import { feedsCommandData } from './command-definitions.js';
import {
addFeed,
FeedAddSchema,
FeedError,
getFeedById,
listFeeds,
removeFeed,
testFeed
} from './service.js';
const feedsCommand: SlashCommand = {
data: feedsCommandData,
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(true);
const guildId = interaction.guildId!;
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale))) {
return;
}
try {
if (sub === 'add') {
const input = FeedAddSchema.parse({
type: SocialFeedTypeSchema.parse(interaction.options.getString('type', true)),
source: interaction.options.getString('source', true),
channelId: interaction.options.getChannel('channel', true).id,
rolePingId: interaction.options.getRole('role')?.id,
template: interaction.options.getString('template') ?? undefined
});
const feed = await addFeed(context.prisma, guildId, input);
await interaction.reply({
content: tf(locale, 'feeds.added', { id: feed.id, type: feed.type }),
ephemeral: true
});
return;
}
if (sub === 'remove') {
const feedId = interaction.options.getString('id', true);
const removed = await removeFeed(context.prisma, guildId, feedId);
if (!removed) {
await interaction.reply({ content: t(locale, 'feeds.error.not_found'), ephemeral: true });
return;
}
await interaction.reply({
content: tf(locale, 'feeds.removed', { id: removed.id }),
ephemeral: true
});
return;
}
if (sub === 'list') {
const feeds = await listFeeds(context.prisma, guildId);
if (feeds.length === 0) {
await interaction.reply({ content: t(locale, 'feeds.list.empty'), ephemeral: true });
return;
}
const lines = feeds.map((feed) =>
tf(locale, 'feeds.list.line', {
id: feed.id,
type: feed.type,
source: feed.sourceId,
channel: `<#${feed.channelId}>`
})
);
await interaction.reply({
content: `${t(locale, 'feeds.list.header')}\n${lines.join('\n')}`,
ephemeral: true
});
return;
}
if (sub === 'test') {
const feedId = interaction.options.getString('id', true);
const feed = await getFeedById(context.prisma, guildId, feedId);
if (!feed) {
await interaction.reply({ content: t(locale, 'feeds.error.not_found'), ephemeral: true });
return;
}
const result = await testFeed(context, feed);
if (result.skipped) {
const skipKey =
result.message === 'twitch_credentials_missing'
? 'feeds.test.twitchSkipped'
: 'feeds.test.skipped';
await interaction.reply({
content: t(locale, skipKey),
ephemeral: true
});
return;
}
if (result.message && result.posted === 0) {
await interaction.reply({
content: tf(locale, 'feeds.test.failed', { reason: result.message }),
ephemeral: true
});
return;
}
if (result.posted === 0) {
await interaction.reply({ content: t(locale, 'feeds.test.noItems'), ephemeral: true });
return;
}
await interaction.reply({
content: tf(locale, 'feeds.test.posted', { count: result.posted }),
ephemeral: true
});
}
} catch (error) {
if (error instanceof FeedError) {
await interaction.reply({
content: t(locale, `feeds.error.${error.code}`),
ephemeral: true
});
return;
}
throw error;
}
}
};
export const feedsCommands: SlashCommand[] = [feedsCommand];