(rewrite): Began organising commands and polished /name-sync

This commit is contained in:
2026-08-11 17:33:35 +08:00
parent efd5df89ca
commit d2b30ad060
6 changed files with 134 additions and 218 deletions
@@ -1,67 +1,11 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, Guild, GuildMember, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder, time, TimestampStyles, User } from 'discord.js'; import { ChatInputCommandInteraction, Client, Guild, GuildMember, time, TimestampStyles, User } from 'discord.js';
import { Database } from '../shared/Database'; import { Database } from '../../shared/Database';
import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../utils'; import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../../utils';
export default { export default {
name: 'ban',
data: new SlashCommandBuilder()
.setName('ban')
.setDescription('Bans a user.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall)
.setContexts(InteractionContextType.Guild)
.setDefaultMemberPermissions(PermissionFlagsBits.BanMembers)
.addStringOption(option =>
option
.setName('user')
.setDescription('The discord user mention, or ID, to ban.')
.setRequired(true)
)
.addStringOption(option =>
option
.setName('reason')
.setDescription('The reason for the ban')
.setRequired(false)
.setMaxLength(400)
)
.addNumberOption(option =>
option
.setName('hours')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('minutes')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('seconds')
.setDescription('The duration of the ban, added with other options (0 for permanent).')
.setRequired(false)
)
.addNumberOption(option =>
option
.setName('delete-message-days')
.setDescription('How far back to delete messages (in days, default: 0 days).')
.setRequired(false)
.setMinValue(0)
.setMaxValue(7)
)
.addBooleanOption(option =>
option
.setName('full-ban')
.setDescription('Whether or not to prevent the user from joining on known alts (and ban all existing alts).')
.setRequired(false)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) { handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
await deferInteraction(interaction); await deferInteraction(interaction);
if (!interaction.guild) return interaction.editReply('This command must be used in a server');
if (!interaction.guild.members.me) return interaction.editReply('An error has occurred. Please try again later.');
const input = interaction.options.getString('user', true); const input = interaction.options.getString('user', true);
const idToUse = getIdFromInput(input); const idToUse = getIdFromInput(input);
@@ -134,3 +78,38 @@ async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User,
await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt); await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt);
} }
} }
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// ---------------
@SlashCommand(
new CommandBuilder('ban', 'Ban a user.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.setMemberPermission(Constants.PermissionFlagsBits.BanMembers)
.addUserOption('user', 'The user to ban.', true)
.addStringOption({ name: 'duration', description: 'The ban duration.', required: false })
.addStringOption({ name: 'reason', description: 'The ban reason.', required: false })
.addNumberOption({ name: 'cleanup', description: 'Number of days worth of messages to delete.', required: false })
)
class BanCommand extends Command<CommandClient> {
cooldown = 5;
userPermissions = [Constants.PermissionFlagsBits.BanMembers];
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, fetchedData?: any) {
if (!interaction.inGuild() || !interaction.member.highestRole) return;
await interaction.defer();
const user = interaction.getRequiredMember('user');
const duration = interaction.getString('duration');
const reason = interaction.getString('reason') || 'No reason specified.';
const messageDays = interaction.getNumber('cleanup') || 0;
await context.banGuildMember(interaction.guild.id, user.id, messageDays, reason);
}
}
export default new BanCommand('ban');
+33
View File
@@ -0,0 +1,33 @@
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, SlashCommand } from 'athena-prime';
// ---------------
@SlashCommand(
new CommandBuilder('hard-ban', 'Permanently bans a user, and all known alternate accounts.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.setMemberPermission(Constants.PermissionFlagsBits.BanMembers)
.addUserOption('user', 'The user to ban.', true)
.addStringOption({ name: 'reason', description: 'The ban reason.', required: false })
.addNumberOption({ name: 'cleanup', description: 'Number of days worth of messages to delete.', required: false })
)
class HardBanCommand extends Command<CommandClient> {
cooldown = 5;
userPermissions = [Constants.PermissionFlagsBits.BanMembers];
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, fetchedData?: any) {
if (!interaction.inGuild() || !interaction.member.highestRole) return;
await interaction.defer();
const user = interaction.getRequiredMember('user');
const duration = interaction.getString('duration');
const reason = interaction.getString('reason') || 'No reason specified.';
const messageDays = interaction.getNumber('cleanup') || 0;
// Nix -> Tarrgon: Please implement the hard-ban here :3
}
}
export default new HardBanCommand('hardban');
-36
View File
@@ -1,36 +0,0 @@
import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, InteractionContextType, MessageFlags, SlashCommandBuilder } from 'discord.js';
import { config } from '../config';
import { syncName } from '../utils';
export default {
name: 'name-sync',
data: new SlashCommandBuilder()
.setName('name-sync')
.setDescription('Sync your discord nickname to your e621 name.')
.setIntegrationTypes(ApplicationIntegrationType.GuildInstall, ApplicationIntegrationType.UserInstall)
.setContexts(InteractionContextType.Guild, InteractionContextType.BotDM)
.addIntegerOption(option =>
option
.setName('id')
.setDescription('The id of the e621 user to sync your nickname to.')
.setRequired(false)
),
handler: async function (client: Client, interaction: ChatInputCommandInteraction) {
await interaction.deferReply({ flags: [MessageFlags.Ephemeral] });
const id = interaction.options.getInteger('id');
const guild = await interaction.client.guilds.fetch(config.DISCORD_GUILD_ID!);
if (!guild) {
return interaction.editReply('An error has occurred. Please try again later.');
}
const member = await guild.members.fetch(interaction.user.id);
if (!member || !guild.members.me) {
return interaction.editReply('An error has occurred. Please try again later.');
}
await syncName(interaction, member, id);
}
};
+62
View File
@@ -0,0 +1,62 @@
import { Database } from '../../shared/Database';
import { E621User } from '../../types';
import { CreateDefaultEmbed, getE621User, SetSeverity } from '../../utils';
// External Imports
import { Command, CommandBuilder, CommandClient, CommandInteraction, Constants, Member, SlashCommand } from 'athena-prime';
// ---------------
async function fetchUser(member: Member, id: number | null): Promise<E621User | null> {
const syncedIds = await Database.getE621Ids(member.id);
if (!id || !syncedIds.includes(id))
return await getE621User(syncedIds[0]);
else
return await getE621User(id);
}
@SlashCommand(
new CommandBuilder('name-sync', 'Sync your Discord username with your e621 username.')
.setIntegrationTypes(Constants.ApplicationIntegrationType.GuildInstall)
.setContexts(Constants.InteractionContextType.Guild)
.setCommandType(Constants.ApplicationCommandType.ChatInput)
.addNumberOption({ name: 'id', description: 'The ID of the user to sync your name to.', required: false, max_value: 16 })
)
class NameSyncCommand extends Command<CommandClient> {
cooldown = 60;
async handleCommand(context: CommandClient<any, any>, interaction: CommandInteraction, data: any) {
if (!interaction.inGuild()) return;
await interaction.defer();
const id = interaction.getNumber('id');
const user = await fetchUser(interaction.member, id);
if (!user) {
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('error'),
description: 'I was unable to get a user entry for your account or provided id. Please try again, or contact an administrator.'
}]
});
return;
}
await interaction.member.edit({ nick: user.name }, 'Member requested a name-sync.');
await interaction.createMessage({
embeds: [{
...CreateDefaultEmbed(context),
...SetSeverity('success'),
description: `Successfully updated your display name. Hello \`${user.name}\`!`
}]
});
}
}
export default new NameSyncCommand('name-sync');
-94
View File
@@ -1,94 +0,0 @@
import { APIEmbedField, APIRole, AuditLogEvent, EmbedBuilder, Guild, GuildAuditLogsEntry, RoleFlags, SnowflakeUtil } from 'discord.js';
import { Database } from '../shared/Database';
import { formatChanges, formatExtras, formatSnowflake, getTargetType } from '../utils';
const IGNORED_ACTIONS = [
AuditLogEvent.MemberMove,
// Handled by automod.
AuditLogEvent.AutoModerationFlagToChannel
];
export async function handleAuditLogCreate(entry: GuildAuditLogsEntry, guild: Guild) {
if (!await shouldLog(entry, guild)) return;
const settings = await Database.getGuildSettings(guild.id);
if (!settings || !settings.audit_logs_channel_id) return;
const channel = await guild.channels.fetch(settings.audit_logs_channel_id);
if (!channel || !channel.isSendable()) return;
const fields: APIEmbedField[] = [
{
name: 'Actor',
value: `<@${entry.executorId}>`,
inline: true
}
];
if (entry.targetId) {
const targetType = getTargetType(entry.action);
fields.push({
name: 'Target',
value: formatSnowflake(entry.targetId, targetType),
inline: true
});
}
if (entry.reason) {
fields.push({
name: 'Reason',
value: entry.reason,
inline: true
});
}
if (entry.changes && entry.changes.length > 0) {
fields.push({
name: 'Changes',
value: formatChanges(entry),
inline: false
});
}
if (entry.extra) {
fields.push({
name: 'Options',
value: formatExtras(entry, guild),
inline: false
});
}
const embed = new EmbedBuilder()
.setTitle(Object.keys(AuditLogEvent)[Object.values(AuditLogEvent).indexOf(entry.action)])
.setTimestamp(Number(SnowflakeUtil.decode(entry.id).timestamp))
.addFields(...fields);
channel.send({ embeds: [embed] });
}
async function shouldLog(entry: GuildAuditLogsEntry, guild: Guild): Promise<boolean> {
if (!entry.executorId) return true;
if (IGNORED_ACTIONS.some(a => entry.action == a)) return false;
if (entry.action == AuditLogEvent.MemberRoleUpdate) {
return await shouldLogRoleChanges(entry as GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild);
}
return true;
}
async function shouldLogRoleChanges(entry: GuildAuditLogsEntry<AuditLogEvent.MemberRoleUpdate>, guild: Guild): Promise<boolean> {
for (const change of entry.changes) {
// Get role changes from the log.
const roles = (await Promise.all((change.new! as Pick<APIRole, 'id' | 'name'>[]).map(c => guild.roles.fetch(c.id))));
// Check if role is part of onboarding.
for (const role of roles) {
if (role && !role.flags.has(RoleFlags.InPrompt)) return true;
}
}
return false;
}
-28
View File
@@ -1,28 +0,0 @@
import { ChatInputCommandInteraction, GuildMember, ModalSubmitInteraction, UserContextMenuCommandInteraction } from 'discord.js';
import { Database } from '../shared/Database';
import { E621User } from '../types';
import { getE621User } from './e621-utils';
export async function syncName(interaction: ChatInputCommandInteraction | UserContextMenuCommandInteraction | ModalSubmitInteraction, member: GuildMember, id: number | null) {
if (member.roles.highest.comparePositionTo(member.guild.members.me!.roles.highest) > 0) {
const res = interaction.user.id == member.id ? 'your' : 'their';
return interaction.editReply(`I am unable to set ${res} nickname as ${res} role is higher than mine.`);
}
const availableIds = await Database.getE621Ids(interaction.user.id);
let e621User: E621User | null;
if (!id || !availableIds.includes(id)) {
e621User = await getE621User(availableIds[0]);
} else {
e621User = await getE621User(id);
}
if (!e621User) {
return interaction.editReply("Couldn't figure out what your name was. Please contact an administrator.");
}
await member.setNickname(e621User.name);
interaction.editReply(`Nickname set to: ${e621User.name}`);
}