- 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.
139 lines
4.7 KiB
TypeScript
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];
|