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:
141
apps/bot/src/modules/feeds/commands.ts
Normal file
141
apps/bot/src/modules/feeds/commands.ts
Normal 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];
|
||||
Reference in New Issue
Block a user