diff --git a/src/commands/ban.ts b/src/commands/infractions/ban.ts similarity index 58% rename from src/commands/ban.ts rename to src/commands/infractions/ban.ts index 2136850..2e53645 100644 --- a/src/commands/ban.ts +++ b/src/commands/infractions/ban.ts @@ -1,67 +1,11 @@ -import { ApplicationIntegrationType, ChatInputCommandInteraction, Client, Guild, GuildMember, InteractionContextType, PermissionFlagsBits, SlashCommandBuilder, time, TimestampStyles, User } from 'discord.js'; -import { Database } from '../shared/Database'; -import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../utils'; +import { ChatInputCommandInteraction, Client, Guild, GuildMember, time, TimestampStyles, User } from 'discord.js'; +import { Database } from '../../shared/Database'; +import { AltData, comprehensiveAltLookupFromDiscord, deferInteraction, getIdFromInput } from '../../utils'; 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) { 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 idToUse = getIdFromInput(input); @@ -133,4 +77,39 @@ async function removeAllAlts(altData: AltData[], guild: Guild, moderator: User, await removeAllAlts(data.alts, guild, moderator, fullBan, reason, deleteMessageDays, duration, expiresAt); } -} \ No newline at end of file +} + +// 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 { + cooldown = 5; + userPermissions = [Constants.PermissionFlagsBits.BanMembers]; + + async handleCommand(context: CommandClient, 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'); \ No newline at end of file diff --git a/src/commands/infractions/hardban.ts b/src/commands/infractions/hardban.ts new file mode 100644 index 0000000..35d67fc --- /dev/null +++ b/src/commands/infractions/hardban.ts @@ -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 { + cooldown = 5; + userPermissions = [Constants.PermissionFlagsBits.BanMembers]; + + async handleCommand(context: CommandClient, 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'); \ No newline at end of file diff --git a/src/commands/name-sync.ts b/src/commands/name-sync.ts deleted file mode 100644 index 63ffeea..0000000 --- a/src/commands/name-sync.ts +++ /dev/null @@ -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); - } -}; \ No newline at end of file diff --git a/src/commands/utilities/name-sync.ts b/src/commands/utilities/name-sync.ts new file mode 100644 index 0000000..042ddf9 --- /dev/null +++ b/src/commands/utilities/name-sync.ts @@ -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 { + 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 { + cooldown = 60; + + async handleCommand(context: CommandClient, 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'); \ No newline at end of file diff --git a/src/events/handle-audit-log-create.ts b/src/events/handle-audit-log-create.ts deleted file mode 100644 index f875e7e..0000000 --- a/src/events/handle-audit-log-create.ts +++ /dev/null @@ -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 { - 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, guild); - } - - return true; -} - -async function shouldLogRoleChanges(entry: GuildAuditLogsEntry, guild: Guild): Promise { - for (const change of entry.changes) { - // Get role changes from the log. - const roles = (await Promise.all((change.new! as Pick[]).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; -} \ No newline at end of file diff --git a/src/utils/name-sync.ts b/src/utils/name-sync.ts deleted file mode 100644 index 842957d..0000000 --- a/src/utils/name-sync.ts +++ /dev/null @@ -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}`); -} \ No newline at end of file