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:
138
apps/bot/src/modules/stats/commands.ts
Normal file
138
apps/bot/src/modules/stats/commands.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
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];
|
||||
Reference in New Issue
Block a user