Files
Nexumi/apps/bot/src/modules/stats/commands.ts
smueller 12066befa8 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.
2026-07-22 13:36:39 +02:00

139 lines
4.7 KiB
TypeScript

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 { invitesCommandData, statsCommandData } from './command-definitions.js';
import { getInviteLeaderboard, getInviteStatsForUser } from './invites.js';
import {
getStatsConfig,
StatsSetupSchema,
updateStatsChannels,
upsertStatsConfig
} from './service.js';
const statsCommand: SlashCommand = {
data: statsCommandData,
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 guildId = interaction.guildId!;
const sub = interaction.options.getSubcommand();
if (sub === 'setup') {
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale))) {
return;
}
const membersChannel = interaction.options.getChannel('members_channel', true);
const onlineChannel = interaction.options.getChannel('online_channel', true);
const boostsChannel = interaction.options.getChannel('boosts_channel', true);
const parsed = StatsSetupSchema.safeParse({
membersChannelId: membersChannel.id,
onlineChannelId: onlineChannel.id,
boostsChannelId: boostsChannel.id,
membersTemplate: interaction.options.getString('members_template') ?? undefined,
onlineTemplate: interaction.options.getString('online_template') ?? undefined,
boostsTemplate: interaction.options.getString('boosts_template') ?? undefined
});
if (!parsed.success) {
await interaction.reply({ content: t(locale, 'stats.error.invalidSetup'), ephemeral: true });
return;
}
await upsertStatsConfig(context.prisma, guildId, parsed.data);
await updateStatsChannels(context);
await interaction.reply({ content: t(locale, 'stats.setup.done'), ephemeral: true });
return;
}
if (sub === 'refresh') {
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale))) {
return;
}
const config = await getStatsConfig(context.prisma, guildId);
if (!config?.enabled) {
await interaction.reply({ content: t(locale, 'stats.notConfigured'), ephemeral: true });
return;
}
await updateStatsChannels(context);
await interaction.reply({ content: t(locale, 'stats.refresh.done'), ephemeral: true });
}
}
};
const invitesCommand: SlashCommand = {
data: invitesCommandData,
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 guildId = interaction.guildId!;
const sub = interaction.options.getSubcommand();
if (sub === 'view') {
const targetUser = interaction.options.getUser('user') ?? interaction.user;
const stats = await getInviteStatsForUser(context.prisma, guildId, targetUser.id);
if (!stats || stats.joins === 0) {
await interaction.reply({
content: tf(locale, 'invites.view.empty', { user: `<@${targetUser.id}>` }),
ephemeral: true
});
return;
}
const net = stats.joins - stats.leaves - stats.fakes;
await interaction.reply({
content: tf(locale, 'invites.view.result', {
user: `<@${targetUser.id}>`,
joins: stats.joins,
leaves: stats.leaves,
fakes: stats.fakes,
net
}),
ephemeral: true
});
return;
}
if (sub === 'leaderboard') {
const entries = await getInviteLeaderboard(context.prisma, guildId, 10);
if (entries.length === 0) {
await interaction.reply({ content: t(locale, 'invites.leaderboard.empty'), ephemeral: true });
return;
}
const lines = entries.map((entry, index) => {
const net = entry.joins - entry.leaves - entry.fakes;
return tf(locale, 'invites.leaderboard.line', {
rank: index + 1,
user: `<@${entry.inviterId}>`,
joins: entry.joins,
leaves: entry.leaves,
fakes: entry.fakes,
net
});
});
await interaction.reply({
content: `${t(locale, 'invites.leaderboard.header')}\n${lines.join('\n')}`,
ephemeral: true
});
}
}
};
export const statsCommands: SlashCommand[] = [statsCommand, invitesCommand];