diff options
author | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-10-03 22:57:40 -0400 |
---|---|---|
committer | IRONM00N <64110067+IRONM00N@users.noreply.github.com> | 2022-10-03 22:57:40 -0400 |
commit | 612ed820a0600ec11ed642005377cd7f5a8a8b77 (patch) | |
tree | 6bca4e7268fd0063ff53cf64fa44df62a23dba50 /src | |
parent | ed98ff7e2679f362f2657e77a6cf8dd3ce9b3d43 (diff) | |
download | tanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.tar.gz tanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.tar.bz2 tanzanite-612ed820a0600ec11ed642005377cd7f5a8a8b77.zip |
wip
Diffstat (limited to 'src')
69 files changed, 779 insertions, 687 deletions
@@ -1,26 +1,38 @@ +import { performance } from 'node:perf_hooks'; +performance.mark('processStart'); + console.log('Tanzanite is Starting'); -import { init } from '../lib/utils/Logger.js'; +import { init } from '#lib/utils/Logger.js'; // creates proxies on console.log and console.warn // also starts a REPL session init(); import { config } from '#config'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { Sentry } from '../lib/common/Sentry.js'; -import { TanzaniteClient } from '../lib/extensions/discord-akairo/TanzaniteClient.js'; +import { Sentry } from '#lib/common/Sentry.js'; +import { TanzaniteClient } from '#lib/extensions/discord-akairo/TanzaniteClient.js'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; const isDry = process.argv.includes('dry'); -if (!isDry && config.credentials.sentryDsn !== null) new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config); + +if (!isDry && config.credentials.sentryDsn !== null) { + new Sentry(dirname(fileURLToPath(import.meta.url)) || process.cwd(), config); +} + TanzaniteClient.extendStructures(); + const client = new TanzaniteClient(config); -// @ts-ignore: for debugging purposes +// @ts-ignore: I don't want to add this to the global typings, this is only for debugging purposes global.client = client; -if (!isDry) await client.dbPreInit(); +if (!isDry) { + await client.dbPreInit(); +} + await client.init(); + if (isDry) { process.exit(0); } else { diff --git a/src/commands/config/config.ts b/src/commands/config/config.ts index adc41d8..4d38c54 100644 --- a/src/commands/config/config.ts +++ b/src/commands/config/config.ts @@ -13,8 +13,8 @@ import { type GuildSettingType, type SlashMessage } from '#lib'; +import { ExtSub, type ArgumentGeneratorReturn, type SlashOption } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo'; import { ActionRowBuilder, ApplicationCommandOptionType, @@ -32,10 +32,9 @@ import { User, type Message, type MessageComponentInteraction, - type MessageOptions + type MessageCreateOptions } from 'discord.js'; -import _ from 'lodash'; -const { camelCase, snakeCase } = _; +import { camelCase, snakeCase } from 'lodash-es'; export const arrayActions = ['view' as const, 'add' as const, 'remove' as const, 'clear' as const]; export type ArrayActions = typeof arrayActions[number]; @@ -79,7 +78,7 @@ export default class ConfigCommand extends BotCommand { description: `Manage the server's ${loweredName}`, type: ApplicationCommandOptionType.SubcommandGroup, options: isArray - ? [ + ? ([ { name: 'view', description: `View the server's ${loweredName}.`, @@ -118,8 +117,8 @@ export default class ConfigCommand extends BotCommand { description: `Remove all values from a server's ${loweredName}.`, type: ApplicationCommandOptionType.Subcommand } - ] - : [ + ] as ExtSub[]) + : ([ { name: 'view', description: `View the server's ${loweredName}.`, @@ -144,7 +143,7 @@ export default class ConfigCommand extends BotCommand { description: `Delete the server's ${loweredName}.`, type: ApplicationCommandOptionType.Subcommand } - ] + ] as ExtSub[]) }; }), channel: 'guild', @@ -309,7 +308,7 @@ export default class ConfigCommand extends BotCommand { public async generateMessageOptions( message: CommandMessage | SlashMessage, setting?: undefined | keyof typeof guildSettingsObj - ): Promise<MessageOptions & InteractionUpdateOptions> { + ): Promise<MessageCreateOptions & InteractionUpdateOptions> { assert(message.inGuild()); const settingsEmbed = new EmbedBuilder().setColor(colors.default); diff --git a/src/commands/config/disable.ts b/src/commands/config/disable.ts index 6dd94a6..776ecf0 100644 --- a/src/commands/config/disable.ts +++ b/src/commands/config/disable.ts @@ -10,9 +10,9 @@ import { } from '#lib'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, AutocompleteInteraction } from 'discord.js'; -import { default as Fuse } from 'fuse.js'; -assert(Fuse); +// todo: remove this bullshit once typescript gets its shit together +const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default; export default class DisableCommand extends BotCommand { private static blacklistedCommands = ['eval', 'disable']; diff --git a/src/commands/config/log.ts b/src/commands/config/log.ts index 0c74ce7..6d0f594 100644 --- a/src/commands/config/log.ts +++ b/src/commands/config/log.ts @@ -8,8 +8,8 @@ import { type GuildLogType, type SlashMessage } from '#lib'; +import { ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { ArgumentGeneratorReturn } from 'discord-akairo'; import { ApplicationCommandOptionType, ChannelType } from 'discord.js'; export default class LogCommand extends BotCommand { @@ -38,10 +38,10 @@ export default class LogCommand extends BotCommand { slashType: ApplicationCommandOptionType.Channel, channelTypes: [ ChannelType.GuildText, - ChannelType.GuildNews, - ChannelType.GuildNewsThread, - ChannelType.GuildPublicThread, - ChannelType.GuildPrivateThread + ChannelType.GuildAnnouncement, + ChannelType.AnnouncementThread, + ChannelType.PublicThread, + ChannelType.PrivateThread ], only: 'slash' } diff --git a/src/commands/dev/eval.ts b/src/commands/dev/eval.ts index 83168e0..5fe21c0 100644 --- a/src/commands/dev/eval.ts +++ b/src/commands/dev/eval.ts @@ -283,7 +283,7 @@ export default class EvalCommand extends BotCommand { if (!err && proto) embed.addFields({ name: ':gear: Proto', value: proto }); if (!silent || message.util.isSlashMessage(message)) { - await message.util.reply({ content: null, embeds: [embed] }); + await message.util.reply({ content: '', embeds: [embed] }); } else { const success = await message.author.send({ embeds: [embed] }).catch(() => false); if (!deleteMsg) await message.react(success ? emojis.successFull : emojis.errorFull).catch(() => {}); diff --git a/src/commands/dev/superUser.ts b/src/commands/dev/superUser.ts index fc7fcbf..54332d6 100644 --- a/src/commands/dev/superUser.ts +++ b/src/commands/dev/superUser.ts @@ -1,5 +1,5 @@ import { BotCommand, emojis, format, type ArgType, type CommandMessage } from '#lib'; -import { type ArgumentGeneratorReturn, type ArgumentTypeCasterReturn } from 'discord-akairo'; +import { type ArgumentGeneratorReturn, type ArgumentTypeCasterReturn } from '@notenoughupdates/discord-akairo'; export default class SuperUserCommand extends BotCommand { public constructor() { diff --git a/src/commands/dev/test.ts b/src/commands/dev/test.ts index e1f3b73..994b76f 100644 --- a/src/commands/dev/test.ts +++ b/src/commands/dev/test.ts @@ -1,13 +1,14 @@ -import { BotCommand, ButtonPaginator, colors, emojis, OptArgType, Shared, type CommandMessage } from '#lib'; +import { BotCommand, ButtonPaginator, chunk, colors, emojis, OptArgType, Shared, type CommandMessage } from '#lib'; import { ActionRowBuilder, + APIEmbed, ButtonBuilder, ButtonStyle, + Collection, EmbedBuilder, - GatewayDispatchEvents, + Message, Routes, - type ApplicationCommand, - type Collection + type ApplicationCommand } from 'discord.js'; import badLinksSecretArray from '../../../lib/badlinks-secret.js'; import badLinksArray from '../../../lib/badlinks.js'; @@ -52,15 +53,38 @@ export default class TestCommand extends BotCommand { return await message.util.reply(responses[Math.floor(Math.random() * responses.length)]); } + console.dir(args); + if (args.feature) { if (['button', 'buttons'].includes(args.feature?.toLowerCase())) { const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents( - new ButtonBuilder({ style: ButtonStyle.Primary, customId: 'primaryButton', label: 'Primary' }), - new ButtonBuilder({ style: ButtonStyle.Secondary, customId: 'secondaryButton', label: 'Secondary' }), - new ButtonBuilder({ style: ButtonStyle.Success, customId: 'successButton', label: 'Success' }), - new ButtonBuilder({ style: ButtonStyle.Danger, customId: 'dangerButton', label: 'Danger' }), - new ButtonBuilder({ style: ButtonStyle.Link, label: 'Link', url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' }) + new ButtonBuilder({ + style: ButtonStyle.Primary, + customId: 'test;button;primary', + label: 'Primary' + }), + new ButtonBuilder({ + style: ButtonStyle.Secondary, + customId: 'test;button;secondary', + label: 'Secondary' + }), + new ButtonBuilder({ + style: ButtonStyle.Success, + customId: 'test;button;success', + label: 'Success' + }), + new ButtonBuilder({ + style: ButtonStyle.Danger, + customId: 'test;button;danger', + label: 'Danger' + }), + new ButtonBuilder({ + style: ButtonStyle.Link, + label: 'Link', + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' + }) ); + return await message.util.reply({ content: 'buttons', components: [buttonRow] }); } else if (['embed', 'button embed'].includes(args.feature?.toLowerCase())) { const embed = new EmbedBuilder() @@ -80,18 +104,23 @@ export default class TestCommand extends BotCommand { const buttonRow = new ActionRowBuilder<ButtonBuilder>().addComponents( new ButtonBuilder({ style: ButtonStyle.Link, label: 'Link', url: 'https://google.com/' }) ); + return await message.util.reply({ content: 'Test', embeds: [embed], components: [buttonRow] }); } else if (['lots of buttons'].includes(args.feature?.toLowerCase())) { const buttonRows: ActionRowBuilder<ButtonBuilder>[] = []; + for (let a = 1; a <= 5; a++) { const row = new ActionRowBuilder<ButtonBuilder>(); + for (let b = 1; b <= 5; b++) { - const id = (a + 5 * (b - 1)).toString(); + const id = `test;lots;${a + 5 * (b - 1)}`; const button = new ButtonBuilder({ style: ButtonStyle.Primary, customId: id, label: id }); row.addComponents(button); } + buttonRows.push(row); } + return await message.util.reply({ content: 'buttons', components: buttonRows }); } else if (['paginate'].includes(args.feature?.toLowerCase())) { const embeds = []; @@ -142,13 +171,16 @@ export default class TestCommand extends BotCommand { return message.util.reply(`${emojis.error} no`); } else if (['sync automod'].includes(args.feature?.toLowerCase())) { const row = (await Shared.findByPk(0))!; + row.badLinks = badLinksArray; row.badLinksSecret = badLinksSecretArray; row.badWords = badWords; + await row.save(); + return await message.util.reply(`${emojis.success} Synced automod.`); } else if (['modal'].includes(args.feature?.toLowerCase())) { - const m = await message.util.reply({ + return await message.util.reply({ content: 'Click for modal', components: [ new ActionRowBuilder<ButtonBuilder>().addComponents( @@ -156,63 +188,57 @@ export default class TestCommand extends BotCommand { ) ] }); + } else if (args.feature.includes('backlog experiments')) { + this.client.logger.debug('backlog experiments'); + + if (message.channelId !== '1019830755658055691') { + return await message.util.reply(`${emojis.error} This only works in <#1019830755658055691>.`); + } - // eslint-disable-next-line @typescript-eslint/no-misused-promises - this.client.ws.on(GatewayDispatchEvents.InteractionCreate, async (i: any) => { - if (i?.data?.custom_id !== 'test;modal' || i?.data?.component_type !== 2) return; - if (i?.message?.id !== m.id) return; - - const text = { type: 4, style: 1, min_length: 1, max_length: 4000, required: true }; - - await this.client.rest.post(Routes.interactionCallback(i.id, i.token), { - body: { - type: 9, - data: { - custom_id: 'test;login', - title: 'Login (real)', - components: [ - { - type: 1, - components: [ - { - ...text, - custom_id: 'test;login;email', - label: 'Email', - placeholder: 'Email' - } - ] - }, - { - type: 1, - components: [ - { - ...text, - custom_id: 'test;login;password', - label: 'Password', - placeholder: 'Password' - } - ] - }, - { - type: 1, - components: [ - { - ...text, - custom_id: 'test;login;2fa', - label: 'Enter Discord Auth Code', - min_length: 6, - max_length: 6, - placeholder: '6-digit authentication code' - } - ] - } - ] - } - } + let messages = new Collection<string, Message>(); + let lastID: string | undefined; + + // eslint-disable-next-line no-constant-condition + while (true) { + const fetchedMessages = await message.channel.messages.fetch({ + limit: 100, + ...(lastID && { before: lastID }) }); - }); - return; + if (fetchedMessages.size === 0) { + break; + } + + messages = messages.concat(fetchedMessages); + lastID = fetchedMessages.lastKey(); + + this.client.logger.debug(messages.size); + this.client.logger.debug(lastID); + } + + const embeds = messages + .sort((a, b) => a.createdTimestamp - b.createdTimestamp) + .filter((m) => m.embeds.length > 0 && (m.embeds[0].title?.includes('Guild Experiment') ?? false)) + .map( + (m): APIEmbed => ({ + ...m.embeds[0]!.toJSON(), + timestamp: new Date(m.createdTimestamp).toISOString() + }) + ); + + const chunked = chunk(embeds, 10); + + let i = 0; + for (const chunk of chunked) { + this.client.logger.debug(i); + this.client.logger.debug(chunk, 1); + await this.client.rest.post(Routes.channelMessages('795356494261911553'), { + body: { embeds: chunk } + }); + i++; + } + + return await message.util.reply(`${emojis.success} Done.`); } } return await message.util.reply(responses[Math.floor(Math.random() * responses.length)]); diff --git a/src/commands/fun/minesweeper.ts b/src/commands/fun/minesweeper.ts index 85945c7..b0528ac 100644 --- a/src/commands/fun/minesweeper.ts +++ b/src/commands/fun/minesweeper.ts @@ -1,8 +1,6 @@ import { BotCommand, emojis, OptArgType, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; -import { Minesweeper } from '@notenoughupdates/discord.js-minesweeper'; -import assert from 'assert/strict'; +import { Minesweeper } from '@tanzanite/discord.js-minesweeper'; import { ApplicationCommandOptionType } from 'discord.js'; -assert(Minesweeper); export default class MinesweeperCommand extends BotCommand { public constructor() { diff --git a/src/commands/info/guildInfo.ts b/src/commands/info/guildInfo.ts index e364a89..acd5b86 100644 --- a/src/commands/info/guildInfo.ts +++ b/src/commands/info/guildInfo.ts @@ -11,6 +11,7 @@ import { type OptArgType, type SlashMessage } from '#lib'; +import { embedField } from '#lib/common/tags.js'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, @@ -26,7 +27,6 @@ import { PermissionFlagsBits, type BaseGuildVoiceChannel, type GuildPreview, - type Snowflake, type Vanity } from 'discord.js'; @@ -66,9 +66,13 @@ export default class GuildInfoCommand extends BotCommand { let guild: ArgType<'guild' | 'snowflake'> | GuildPreview = args.guild ?? message.guild!; if (typeof guild === 'string') { - const preview = await this.client.fetchGuildPreview(`${args.guild}` as Snowflake).catch(() => undefined); - if (preview) guild = preview; - else return await message.util.reply(`${emojis.error} That guild is not discoverable or does not exist.`); + const preview = await this.client.fetchGuildPreview(`${args.guild}`).catch(() => {}); + + if (preview) { + guild = preview; + } else { + return await message.util.reply(`${emojis.error} That guild is not discoverable or does not exist.`); + } } assert(guild); @@ -96,10 +100,13 @@ export default class GuildInfoCommand extends BotCommand { const otherEmojis = mappings.otherEmojis; const verifiedGuilds = Object.values(mappings.guilds); - if (verifiedGuilds.includes(guild.id as typeof verifiedGuilds[number])) description.push(otherEmojis.BushVerified); - if (guild instanceof Guild) { - if (guild.premiumTier !== GuildPremiumTier.None) description.push(otherEmojis[`BoostTier${guild.premiumTier}`]); + if (verifiedGuilds.includes(guild.id as typeof verifiedGuilds[number])) { + description.push(otherEmojis.BushVerified); + } + + if (guild instanceof Guild && guild.premiumTier !== GuildPremiumTier.None) { + description.push(otherEmojis[`BoostTier${guild.premiumTier}`]); } const features = mappings.features; @@ -138,52 +145,65 @@ export default class GuildInfoCommand extends BotCommand { ) ] as RTCRegion[]; + const members = guild.memberCount; + const online = guild.approximatePresenceCount ?? 0; + const offline = members - online; + guildAbout.push( - `**Owner:** ${escapeMarkdown(guild.members.cache.get(guild.ownerId)?.user.tag ?? 'ยฏ\\_(ใ)_/ยฏ')}`, - `**Created** ${timestampAndDelta(guild.createdAt, 'd')}`, - `**Members:** ${guild.memberCount.toLocaleString() ?? 0} (${emojis.onlineCircle} ${ - guild.approximatePresenceCount?.toLocaleString() ?? 0 - }, ${emojis.offlineCircle} ${(guild.memberCount - (guild.approximatePresenceCount ?? 0)).toLocaleString() ?? 0})`, - `**Regions:** ${guildRegions.map((region) => mappings.regions[region] || region).join(', ')}` + embedField` + Owner ${escapeMarkdown(guild.members.cache.get(guild.ownerId)?.user.tag ?? 'ยฏ\\_(ใ)_/ยฏ')} + Created ${timestampAndDelta(guild.createdAt, 'd')} + Members ${members} (${emojis.onlineCircle} ${online}, ${emojis.offlineCircle} ${offline}) + Regions ${guildRegions.map((region) => mappings.regions[region] || region).join(', ')} + Boosts ${guild.premiumSubscriptionCount && `Level ${guild.premiumTier} with ${guild.premiumSubscriptionCount} boosts`}` ); - if (guild.premiumSubscriptionCount) - guildAbout.push(`**Boosts:** Level ${guild.premiumTier} with ${guild.premiumSubscriptionCount ?? 0} boosts`); + if (guild.members.me?.permissions.has(PermissionFlagsBits.ManageGuild) && guild.vanityURLCode) { const vanityInfo: Vanity = await guild.fetchVanityData(); - guildAbout.push(`**Vanity URL:** discord.gg/${vanityInfo.code}`, `**Vanity Uses:** ${vanityInfo.uses?.toLocaleString()}`); + guildAbout.push( + embedField` + Vanity URL ${`discord.gg/${vanityInfo.code}`} + Vanity Uses ${vanityInfo.uses}` + ); } - if (guild.icon) guildAbout.push(`**Icon:** [link](${guild.iconURL({ size: 4096, extension: 'png' })})`); - if (guild.banner) guildAbout.push(`**Banner:** [link](${guild.bannerURL({ size: 4096, extension: 'png' })})`); - if (guild.splash) guildAbout.push(`**Splash:** [link](${guild.splashURL({ size: 4096, extension: 'png' })})`); + guildAbout.push( + embedField` + Icon ${guild.icon && `[link](${guild.iconURL({ size: 4096, extension: 'png' })})`} + Banner ${guild.banner && `[link](${guild.bannerURL({ size: 4096, extension: 'png' })})`} + Splash ${guild.splash && `[link](${guild.splashURL({ size: 4096, extension: 'png' })})`}` + ); } else { + const members = guild.approximateMemberCount; + const online = guild.approximatePresenceCount; + const offline = members - online; + guildAbout.push( - `**Members:** ${guild.approximateMemberCount?.toLocaleString() ?? 0} (${emojis.onlineCircle} ${ - guild.approximatePresenceCount?.toLocaleString() ?? 0 - }, ${emojis.offlineCircle} ${( - (guild.approximateMemberCount ?? 0) - (guild.approximatePresenceCount ?? 0) - ).toLocaleString()})`, - `**Emojis:** ${(guild as GuildPreview).emojis.size?.toLocaleString() ?? 0}`, - `**Stickers:** ${(guild as GuildPreview).stickers.size}` + embedField` + Members ${members} (${emojis.onlineCircle} ${online}, ${emojis.offlineCircle} ${offline}) + Emojis ${guild.emojis.size} + Stickers ${guild.stickers.size}` ); } - embed.addFields({ name: 'ยป About', value: guildAbout.join('\n') }); + embed.addFields({ + name: 'ยป About', + // filter out anything that is undefined + value: guildAbout.filter((v) => v !== undefined).join('\n') + }); } private generateStatsField(embed: EmbedBuilder, guild: Guild | GuildPreview) { if (!(guild instanceof Guild)) return; - const guildStats: string[] = []; - const channelTypes = ( [ ['Text', [ChannelType.GuildText]], ['Voice', [ChannelType.GuildVoice]], - ['News', [ChannelType.GuildNews]], + ['News', [ChannelType.GuildAnnouncement]], ['Stage', [ChannelType.GuildStageVoice]], ['Category', [ChannelType.GuildCategory]], - ['Thread', [ChannelType.GuildNewsThread, ChannelType.GuildPrivateThread, ChannelType.GuildPublicThread]] + ['Thread', [ChannelType.AnnouncementThread, ChannelType.PrivateThread, ChannelType.PublicThread]] ] as const ).map( (type) => @@ -205,30 +225,25 @@ export default class GuildInfoCommand extends BotCommand { [GuildPremiumTier.None]: 0 } as const; - guildStats.push( - `**Channels:** ${guild.channels.cache.size.toLocaleString()} / 500 (${channelTypes.join(', ')})`, - // subtract 1 for @everyone role - `**Roles:** ${((guild.roles.cache.size ?? 0) - 1).toLocaleString()} / 250`, - `**Emojis:** ${guild.emojis.cache.size?.toLocaleString() ?? 0} / ${EmojiTierMap[guild.premiumTier]}`, - `**Stickers:** ${guild.stickers.cache.size?.toLocaleString() ?? 0} / ${StickerTierMap[guild.premiumTier]}` - ); + const guildStats = embedField` + Channels ${guild.channels.cache.size} / 500 (${channelTypes.join(', ')}) + Roles ${guild.roles.cache.size - 1 /* account for @everyone role */} / 250 + Emojis ${guild.emojis.cache.size} / ${EmojiTierMap[guild.premiumTier]} + Stickers ${guild.stickers.cache.size} / ${StickerTierMap[guild.premiumTier]}`; - embed.addFields({ name: 'ยป Stats', value: guildStats.join('\n') }); + embed.addFields({ name: 'ยป Stats', value: guildStats }); } private generateSecurityField(embed: EmbedBuilder, guild: Guild | GuildPreview) { if (!(guild instanceof Guild)) return; - const guildSecurity: string[] = []; - - guildSecurity.push( - `**Verification Level:** ${MappedGuildVerificationLevel[guild.verificationLevel]}`, - `**Explicit Content Filter:** ${MappedGuildExplicitContentFilter[guild.explicitContentFilter]}`, - `**Default Message Notifications:** ${MappedGuildDefaultMessageNotifications[guild.defaultMessageNotifications]}`, - `**2FA Required:** ${guild.mfaLevel === GuildMFALevel.Elevated ? 'True' : 'False'}` - ); + const guildSecurity = embedField` + Verification Level ${MappedGuildVerificationLevel[guild.verificationLevel]} + Explicit Content Filter ${MappedGuildExplicitContentFilter[guild.explicitContentFilter]} + Default Message Notifications ${MappedGuildDefaultMessageNotifications[guild.defaultMessageNotifications]} + 2FA Required ${guild.mfaLevel === GuildMFALevel.Elevated ? 'True' : 'False'}`; - embed.addFields({ name: 'ยป Security', value: guildSecurity.join('\n') }); + embed.addFields({ name: 'ยป Security', value: guildSecurity }); } } diff --git a/src/commands/info/help.ts b/src/commands/info/help.ts index 1680b75..565fc25 100644 --- a/src/commands/info/help.ts +++ b/src/commands/info/help.ts @@ -20,9 +20,11 @@ import { ButtonStyle, EmbedBuilder } from 'discord.js'; -import { default as Fuse } from 'fuse.js'; import packageDotJSON from '../../../package.json' assert { type: 'json' }; +// todo: remove this bullshit once typescript gets its shit together +const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default; + assert(Fuse); assert(packageDotJSON); diff --git a/src/commands/info/inviteInfo.ts b/src/commands/info/inviteInfo.ts index bf66a4c..123063d 100644 --- a/src/commands/info/inviteInfo.ts +++ b/src/commands/info/inviteInfo.ts @@ -1,4 +1,5 @@ import { Arg, ArgType, BotCommand, colors, type CommandMessage, type SlashMessage } from '#lib'; +import { embedField } from '#lib/common/tags.js'; import { ApplicationCommandOptionType, EmbedBuilder, Invite } from 'discord.js'; export default class InviteInfoCommand extends BotCommand { @@ -38,8 +39,10 @@ export default class InviteInfoCommand extends BotCommand { } private generateAboutField(embed: EmbedBuilder, invite: Invite) { - const about = [`**code:** ${invite.code}`, `**channel:** ${invite.channel!.name}`]; + const about = embedField` + Code ${invite.code} + Channel ${invite.channel!.name}`; - embed.addFields({ name: 'ยป About', value: about.join('\n') }); + embed.addFields({ name: 'ยป About', value: about }); } } diff --git a/src/commands/info/ping.ts b/src/commands/info/ping.ts index ad58cc0..66cdf01 100644 --- a/src/commands/info/ping.ts +++ b/src/commands/info/ping.ts @@ -43,7 +43,7 @@ export default class PingCommand extends BotCommand { .setColor(colors.default) .setTimestamp(); return message.util.reply({ - content: null, + content: '', embeds: [embed] }); } diff --git a/src/commands/info/snowflake.ts b/src/commands/info/snowflake.ts index ba93611..1d3533e 100644 --- a/src/commands/info/snowflake.ts +++ b/src/commands/info/snowflake.ts @@ -1,5 +1,5 @@ import { BotCommand, colors, timestamp, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; -import { stripIndent } from '#tags'; +import { embedField } from '#tags'; import { ApplicationCommandOptionType, ChannelType, @@ -50,7 +50,7 @@ export default class SnowflakeCommand extends BotCommand { snowflakeEmbed.setTitle(`:snowflake: DM with ${escapeMarkdown(channel.recipient?.tag ?? 'ยฏ\\_(ใ)_/ยฏ')} \`[Channel]\``); } else if ( channel.type === ChannelType.GuildCategory || - channel.type === ChannelType.GuildNews || + channel.type === ChannelType.GuildAnnouncement || channel.type === ChannelType.GuildText || channel.type === ChannelType.GuildVoice || channel.type === ChannelType.GuildStageVoice || @@ -68,10 +68,10 @@ export default class SnowflakeCommand extends BotCommand { // Guild if (this.client.guilds.cache.has(snowflake)) { const guild = this.client.guilds.cache.get(snowflake)!; - const guildInfo = stripIndent` - **Name:** ${escapeMarkdown(guild.name)} - **Owner:** ${escapeMarkdown(this.client.users.cache.get(guild.ownerId)?.tag ?? 'ยฏ\\_(ใ)_/ยฏ')} (${guild.ownerId}) - **Members:** ${guild.memberCount?.toLocaleString()}`; + const guildInfo = embedField` + Name ${escapeMarkdown(guild.name)} + Owner ${escapeMarkdown(this.client.users.cache.get(guild.ownerId)?.tag ?? 'ยฏ\\_(ใ)_/ยฏ')} (${guild.ownerId}) + Members ${guild.memberCount?.toLocaleString()}`; if (guild.icon) snowflakeEmbed.setThumbnail(guild.iconURL({ size: 2048 })!); snowflakeEmbed.addFields({ name: 'ยป Server Info', value: guildInfo }); snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(guild.name)} \`[Server]\``); @@ -81,8 +81,8 @@ export default class SnowflakeCommand extends BotCommand { const fetchedUser = await this.client.users.fetch(`${snowflake}`).catch(() => undefined); if (this.client.users.cache.has(snowflake) || fetchedUser) { const user = (this.client.users.cache.get(snowflake) ?? fetchedUser)!; - const userInfo = stripIndent` - **Name:** <@${user.id}> (${escapeMarkdown(user.tag)})`; + const userInfo = embedField` + Name ${`<@${user.id}> (${escapeMarkdown(user.tag)})`}`; if (user.avatar) snowflakeEmbed.setThumbnail(user.avatarURL({ size: 2048 })!); snowflakeEmbed.addFields({ name: 'ยป User Info', value: userInfo }); snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(user.tag)} \`[User]\``); @@ -91,9 +91,9 @@ export default class SnowflakeCommand extends BotCommand { // Emoji if (this.client.emojis.cache.has(snowflake)) { const emoji = this.client.emojis.cache.get(snowflake)!; - const emojiInfo = stripIndent` - **Name:** ${escapeMarkdown(emoji.name ?? 'ยฏ\\_(ใ)_/ยฏ')} - **Animated:** ${emoji.animated}`; + const emojiInfo = embedField` + Name ${escapeMarkdown(emoji.name ?? 'ยฏ\\_(ใ)_/ยฏ')} + Animated ${emoji.animated}`; if (emoji.url) snowflakeEmbed.setThumbnail(emoji.url); snowflakeEmbed.addFields({ name: 'ยป Emoji Info', value: emojiInfo }); snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(emoji.name ?? 'ยฏ\\_(ใ)_/ยฏ')} \`[Emoji]\``); @@ -102,13 +102,13 @@ export default class SnowflakeCommand extends BotCommand { // Role if (message.guild && message.guild.roles.cache.has(snowflake)) { const role = message.guild.roles.cache.get(snowflake)!; - const roleInfo = stripIndent` - **Name:** <@&${role.id}> (${escapeMarkdown(role.name)}) - **Members:** ${role.members.size} - **Hoisted:** ${role.hoist} - **Managed:** ${role.managed} - **Position:** ${role.position} - **Hex Color:** ${role.hexColor}`; + const roleInfo = embedField` + Name ${`<@&${role.id}> (${escapeMarkdown(role.name)})`} + Members ${role.members.size} + Hoisted ${role.hoist} + Managed ${role.managed} + Position ${role.position} + Hex Color ${role.hexColor}`; if (role.color) snowflakeEmbed.setColor(role.color); snowflakeEmbed.addFields({ name: 'ยป Role Info', value: roleInfo }); snowflakeEmbed.setTitle(`:snowflake: ${escapeMarkdown(role.name)} \`[Role]\``); @@ -116,12 +116,12 @@ export default class SnowflakeCommand extends BotCommand { // SnowflakeInfo const deconstructedSnowflake: DeconstructedSnowflake = SnowflakeUtil.deconstruct(snowflake); - const snowflakeInfo = stripIndent` - **Timestamp:** ${deconstructedSnowflake.timestamp} - **Created:** ${timestamp(new Date(Number(deconstructedSnowflake.timestamp)))} - **Worker ID:** ${deconstructedSnowflake.workerId} - **Process ID:** ${deconstructedSnowflake.processId} - **Increment:** ${deconstructedSnowflake.increment}`; + const snowflakeInfo = embedField` + Timestamp ${deconstructedSnowflake.timestamp} + Created ${timestamp(new Date(Number(deconstructedSnowflake.timestamp)))} + Worker ID ${deconstructedSnowflake.workerId} + Process ID ${deconstructedSnowflake.processId} + Increment ${deconstructedSnowflake.increment}`; snowflakeEmbed.addFields({ name: 'ยป Snowflake Info', value: snowflakeInfo }); return await message.util.reply({ embeds: [snowflakeEmbed] }); diff --git a/src/commands/info/userInfo.ts b/src/commands/info/userInfo.ts index 25621fa..22088fe 100644 --- a/src/commands/info/userInfo.ts +++ b/src/commands/info/userInfo.ts @@ -13,6 +13,7 @@ import { type OptArgType, type SlashMessage } from '#lib'; +import { embedField } from '#lib/common/tags.js'; import { ActivityType, ApplicationCommandOptionType, @@ -20,7 +21,6 @@ import { EmbedBuilder, escapeMarkdown, PermissionFlagsBits, - TeamMemberMembershipState, UserFlags, type APIApplication, type ApplicationFlagsString, @@ -128,62 +128,69 @@ export default class UserInfoCommand extends BotCommand { await this.generateBotField(userEmbed, user); - if (emojis) + if (emojis) { userEmbed.setDescription( `\u200B${emojis.filter((e) => e).join(' ')}${ userEmbed.data.description?.length ? `\n\n${userEmbed.data.description}` : '' }` ); // zero width space + } + return userEmbed; } public static async generateGeneralInfoField(embed: EmbedBuilder, user: User, title = 'ยป General Information') { - // General Info - const generalInfo = [ - `**Mention:** <@${user.id}>`, - `**ID:** ${user.id}`, - `**Created:** ${timestampAndDelta(user.createdAt, 'd')}` - ]; - if (user.accentColor !== null) generalInfo.push(`**Accent Color:** ${user.hexAccentColor}`); - if (user.banner) generalInfo.push(`**Banner:** [link](${user.bannerURL({ extension: 'png', size: 4096 })})`); - - const pronouns = await Promise.race([user.client.utils.getPronounsOf(user), sleep(2 * Time.Second)]); // cut off request after 2 seconds - - if (pronouns && typeof pronouns === 'string' && pronouns !== 'Unspecified') generalInfo.push(`**Pronouns:** ${pronouns}`); - - embed.addFields({ name: title, value: generalInfo.join('\n') }); + const pronouns = await Promise.race([ + user.client.utils.getPronounsOf(user), + // cut off request after 2 seconds + sleep(2 * Time.Second) + ]); + + const generalInfo = embedField` + Mention ${`<@${user.id}>`} + ID ${user.id} + Created ${timestampAndDelta(user.createdAt, 'd')} + Accent Color ${user.hexAccentColor} + Banner ${user.banner && `[link](${user.bannerURL({ extension: 'png', size: 4096 })})`} + Pronouns ${typeof pronouns === 'string' && pronouns !== 'Unspecified' && pronouns}`; + + embed.addFields({ name: title, value: generalInfo }); } public static generateServerInfoField(embed: EmbedBuilder, member?: GuildMember | undefined, title = 'ยป Server Information') { if (!member) return; - // Server User Info - const serverUserInfo = []; - if (member.joinedTimestamp) - serverUserInfo.push( - `**${member.guild!.ownerId == member.user.id ? 'Created Server' : 'Joined'}:** ${timestampAndDelta( - member.joinedAt!, - 'd' - )}` - ); - if (member.premiumSince) serverUserInfo.push(`**Booster Since:** ${timestampAndDelta(member.premiumSince, 'd')}`); - if (member.displayHexColor) serverUserInfo.push(`**Display Color:** ${member.displayHexColor}`); - if (member.user.id == mappings.users['IRONM00N'] && member.guild?.id == mappings.guilds["Moulberry's Bush"]) - serverUserInfo.push(`**General Deletions:** 1โ
`); - if ( - ([mappings.users['nopo'], mappings.users['Bestower']] as const).includes(member.user.id) && - member.guild.id == mappings.guilds["Moulberry's Bush"] - ) - serverUserInfo.push(`**General Deletions:** โ
`); - if (member?.nickname) serverUserInfo.push(`**Nickname:** ${escapeMarkdown(member?.nickname)}`); - if (serverUserInfo.length) embed.addFields({ name: title, value: serverUserInfo.join('\n') }); + const isGuildOwner = member.guild.ownerId === member.id; + + const deletions = (() => { + if (member.guild.id !== mappings.guilds["Moulberry's Bush"]) return null; + + switch (member.id) { + case mappings.users['IRONM00N']: + return '1โ
'; + case mappings.users['nopo']: + case mappings.users['Bestower']: + return 'โ
'; + default: + return null; + } + })(); + + const serverUserInfo = embedField` + Created Server ${member.joinedAt && isGuildOwner && timestampAndDelta(member.joinedAt!, 'd')} + Joined ${member.joinedAt && !isGuildOwner && timestampAndDelta(member.joinedAt!, 'd')} + Booster Since ${member.premiumSince && timestampAndDelta(member.premiumSince, 'd')} + Display Color ${member.displayHexColor} + #general Deletions ${deletions} + Nickname ${member.nickname && escapeMarkdown(member.nickname)}`; + + if (serverUserInfo.length) embed.addFields({ name: title, value: serverUserInfo }); } public static generatePresenceField(embed: EmbedBuilder, member?: GuildMember | undefined, title = 'ยป Presence') { if (!member || !member.presence) return; if (!member.presence.status && !member.presence.clientStatus && !member.presence.activities) return; - // User Presence Info let customStatus = ''; const activitiesNames: string[] = []; if (member.presence.activities) { @@ -231,7 +238,7 @@ export default class UserInfoCommand extends BotCommand { const joined = roles.join(', '); embed.addFields({ name: `ยป Role${roles.length - 1 ? 's' : ''} [${roles.length}]`, - value: joined.length > 1024 ? 'Too Many Roles to Display' + '...' : joined + value: joined.length > 1024 ? 'Too Many Roles to Display...' : joined }); } @@ -242,7 +249,6 @@ export default class UserInfoCommand extends BotCommand { ) { if (!member) return; - // Important Perms const perms = this.getImportantPermissions(member); if (perms.length) embed.addFields({ name: title, value: perms.join(' ') }); @@ -282,27 +288,13 @@ export default class UserInfoCommand extends BotCommand { return emojis.cross; }; - const botInfo = [ - `**Publicity:** ${applicationInfo.bot_public ? 'Public' : 'Private'}`, - `**Requires Code Grant:** ${applicationInfo.bot_require_code_grant ? emojis.check : emojis.cross}`, - `**Server Members Intent:** ${intent('GatewayGuildMembers', 'GatewayGuildMembersLimited')}`, - `**Presence Intent:** ${intent('GatewayPresence', 'GatewayPresenceLimited')}`, - `**Message Content Intent:** ${intent('GatewayMessageContent', 'GatewayMessageContentLimited')}` - ]; - - if (applicationInfo.owner || applicationInfo.team) { - const teamMembers = applicationInfo.owner - ? [applicationInfo.owner] - : applicationInfo - .team!.members.filter((tm) => tm.membership_state === TeamMemberMembershipState.Accepted) - .map((tm) => tm.user); - botInfo.push( - `**Developer${teamMembers.length > 1 ? 's' : ''}:** ${teamMembers - .map((m) => `${m.username}#${m.discriminator}`) - .join(', ')}` - ); - } + const botInfo = embedField` + Publicity ${applicationInfo.bot_public ? 'Public' : 'Private'} + Code Grant ${applicationInfo.bot_require_code_grant ? 'Required' : 'Not Required'} + Server Members Intent ${intent('GatewayGuildMembers', 'GatewayGuildMembersLimited')} + Presence Intent ${intent('GatewayPresence', 'GatewayPresenceLimited')} + Message Content Intent ${intent('GatewayMessageContent', 'GatewayMessageContentLimited')}`; - if (botInfo.length) embed.addFields({ name: title, value: botInfo.join('\n') }); + embed.addFields({ name: title, value: botInfo }); } } diff --git a/src/commands/leveling/level.ts b/src/commands/leveling/level.ts index 869140d..bf4ca9b 100644 --- a/src/commands/leveling/level.ts +++ b/src/commands/leveling/level.ts @@ -9,11 +9,11 @@ import { type SlashMessage } from '#lib'; import canvas from '@napi-rs/canvas'; -import { SimplifyNumber } from '@notenoughupdates/simplify-number'; +import { simplifyNumber } from '@tanzanite/simplify-number'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, AttachmentBuilder, Guild, PermissionFlagsBits, User } from 'discord.js'; + assert(canvas); -assert(SimplifyNumber); export default class LevelCommand extends BotCommand { public constructor() { @@ -119,9 +119,9 @@ export default class LevelCommand extends BotCommand { // Draw level data text ctx.fillStyle = white; - const xpTxt = `${SimplifyNumber(currentLevelXpProgress)}/${SimplifyNumber(xpForNextLevel)}`; + const xpTxt = `${simplifyNumber(currentLevelXpProgress)}/${simplifyNumber(xpForNextLevel)}`; - const rankTxt = SimplifyNumber(rank.indexOf(rank.find((x) => x.user === user.id)!) + 1); + const rankTxt = simplifyNumber(rank.indexOf(rank.find((x) => x.user === user.id)!) + 1); ctx.fillText(`Level: ${userLevel} XP: ${xpTxt} Rank: ${rankTxt}`, AVATAR_SIZE + 70, AVATAR_SIZE - 20); // Return image in buffer form diff --git a/src/commands/leveling/setLevel.ts b/src/commands/leveling/setLevel.ts index 6f6f69e..3a995a2 100644 --- a/src/commands/leveling/setLevel.ts +++ b/src/commands/leveling/setLevel.ts @@ -1,4 +1,5 @@ -import { AllowedMentions, BotCommand, emojis, format, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; +import { AllowedMentions, BotCommand, emojis, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; +import { commas } from '#lib/common/tags.js'; import assert from 'assert/strict'; import { ApplicationCommandOptionType } from 'discord.js'; @@ -42,20 +43,27 @@ export default class SetLevelCommand extends BotCommand { assert(message.inGuild()); assert(user.id); - if (isNaN(level) || !Number.isInteger(level)) + if (isNaN(level) || !Number.isInteger(level)) { return await message.util.reply(`${emojis.error} Provide a valid number to set the user's level to.`); - if (level > 6553 || level < 0) - return await message.util.reply(`${emojis.error} You cannot set a level higher than **6,553**.`); + } + + if (level > Level.MAX_LEVEL || level < 0) { + return await message.util.reply(commas`${emojis.error} You cannot set a level higher than **${Level.MAX_LEVEL}**.`); + } const [levelEntry] = await Level.findOrBuild({ - where: { user: user.id, guild: message.guild.id }, - defaults: { user: user.id, guild: message.guild.id, xp: 0 } + where: { + user: user.id, + guild: message.guild.id + } }); - await levelEntry.update({ xp: Level.convertLevelToXp(level), user: user.id, guild: message.guild.id }); + + const xp = Level.convertLevelToXp(level); + + await levelEntry.update({ xp, user: user.id, guild: message.guild.id }); + return await message.util.send({ - content: `Successfully set level of <@${user.id}> to ${format.input(level.toLocaleString())} (${format.input( - levelEntry.xp.toLocaleString() - )} XP)`, + content: commas`Successfully set level of <@${user.id}> to **${level}** (**${xp}** xp)`, allowedMentions: AllowedMentions.none() }); } diff --git a/src/commands/leveling/setXp.ts b/src/commands/leveling/setXp.ts index 8c3b86f..270ad68 100644 --- a/src/commands/leveling/setXp.ts +++ b/src/commands/leveling/setXp.ts @@ -1,4 +1,6 @@ -import { AllowedMentions, BotCommand, emojis, format, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; +import { AllowedMentions, BotCommand, emojis, Level, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; +import { commas } from '#lib/common/tags.js'; +import { input } from '#lib/utils/Format.js'; import assert from 'assert/strict'; import { ApplicationCommandOptionType } from 'discord.js'; @@ -44,22 +46,36 @@ export default class SetXpCommand extends BotCommand { assert(user.id); if (isNaN(xp)) return await message.util.reply(`${emojis.error} Provide a valid number.`); - if (xp > 2147483647 || xp < 0) + + if (xp > Level.MAX_XP || xp < 0) { return await message.util.reply( - `${emojis.error} Provide an positive integer under **2,147,483,647** to set the user's xp to.` + commas`${emojis.error} Provide an positive integer under **${Level.MAX_XP}** to set the user's xp to.` ); + } const [levelEntry] = await Level.findOrBuild({ - where: { user: user.id, guild: message.guild.id }, - defaults: { user: user.id, guild: message.guild.id } + where: { + user: user.id, + guild: message.guild.id + } }); - await levelEntry.update({ xp: xp, user: user.id, guild: message.guild.id }); + const res = await levelEntry + .update({ xp: xp, user: user.id, guild: message.guild.id }) + .catch((e) => (e instanceof Error ? e : null)); + + xp = levelEntry.xp; + const level = Level.convertXpToLevel(xp); + + if (res instanceof Error || res == null) { + return await message.util.reply({ + content: commas`Unable to set <@${user.id}>'s xp to **${xp}** with error ${input(res?.message ?? 'ยฏ\\_(ใ)_/ยฏ')}.`, + allowedMentions: AllowedMentions.none() + }); + } return await message.util.send({ - content: `Successfully set <@${user.id}>'s xp to ${format.input(levelEntry.xp.toLocaleString())} (level ${format.input( - Level.convertXpToLevel(levelEntry.xp).toLocaleString() - )}).`, + content: commas`${emojis.success} Successfully set <@${user.id}>'s xp to **${xp}** (level **${level}**).`, allowedMentions: AllowedMentions.none() }); } diff --git a/src/commands/moderation/ban.ts b/src/commands/moderation/ban.ts index aee8805..ae77cde 100644 --- a/src/commands/moderation/ban.ts +++ b/src/commands/moderation/ban.ts @@ -96,7 +96,9 @@ export default class BanCommand extends BotCommand { if (!user) return message.util.reply(`${emojis.error} Invalid user.`); const useForce = args.force && message.author.isOwner(); - const canModerateResponse = member ? await Moderation.permissionCheck(message.member, member, 'ban', true, useForce) : true; + const canModerateResponse = member + ? await Moderation.permissionCheck(message.member, member, Moderation.Action.Ban, true, useForce) + : true; if (canModerateResponse !== true) { return await message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/block.ts b/src/commands/moderation/block.ts index a5ad31d..da1dec8 100644 --- a/src/commands/moderation/block.ts +++ b/src/commands/moderation/block.ts @@ -83,7 +83,7 @@ export default class BlockCommand extends BotCommand { return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`); const useForce = args.force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'block', true, useForce); + const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Block, true, useForce); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/evidence.ts b/src/commands/moderation/evidence.ts index 9a5e70f..b7c020a 100644 --- a/src/commands/moderation/evidence.ts +++ b/src/commands/moderation/evidence.ts @@ -10,8 +10,8 @@ import { type CommandMessage, type SlashMessage } from '#lib'; +import { Argument, ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { Argument, ArgumentGeneratorReturn } from 'discord-akairo'; import { ApplicationCommandOptionType, type Message } from 'discord.js'; export default class EvidenceCommand extends BotCommand { diff --git a/src/commands/moderation/kick.ts b/src/commands/moderation/kick.ts index 82ddce4..d757e91 100644 --- a/src/commands/moderation/kick.ts +++ b/src/commands/moderation/kick.ts @@ -70,7 +70,7 @@ export default class KickCommand extends BotCommand { if (!member) return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`); const useForce = force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'kick', true, useForce); + const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Kick, true, useForce); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/modlog.ts b/src/commands/moderation/modlog.ts index dcab9ef..649e44f 100644 --- a/src/commands/moderation/modlog.ts +++ b/src/commands/moderation/modlog.ts @@ -12,12 +12,11 @@ import { type CommandMessage, type SlashMessage } from '#lib'; +import { embedField } from '#lib/common/tags.js'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, escapeMarkdown, User } from 'discord.js'; export default class ModlogCommand extends BotCommand { - public static separator = '\nโโโโโโโโโโโโโโโ\n'; - public constructor() { super('modlog', { aliases: ['modlog', 'modlogs'], @@ -62,44 +61,63 @@ export default class ModlogCommand extends BotCommand { const logs = await ModLog.findAll({ where: { guild: message.guild.id, - user: foundUser.id + user: foundUser.id, + pseudo: false }, order: [['createdAt', 'ASC']] }); - const niceLogs = logs - .filter((log) => !log.pseudo && !(!hidden && log.hidden)) - .map((log) => ModlogCommand.generateModlogInfo(log, false, false)); - if (niceLogs.length < 1) return message.util.reply(`${emojis.error} **${foundUser.tag}** does not have any modlogs.`); + const niceLogs = logs.filter((log) => !log.hidden || hidden).map((log) => generateModlogInfo(log, false, false)); + + if (niceLogs.length < 1) { + return message.util.reply(`${emojis.error} **${foundUser.tag}** does not have any modlogs.`); + } + const chunked: string[][] = chunk(niceLogs, 4); const embedPages = chunked.map((chunk) => ({ title: `${foundUser.tag}'s Modlogs`, - description: chunk.join(ModlogCommand.separator), + description: chunk.join(modlogSeparator), color: colors.default })); return await ButtonPaginator.send(message, embedPages, undefined, true); } else if (search) { const entry = await ModLog.findByPk(search as string); - if (!entry || entry.pseudo || (entry.hidden && !hidden)) + + if (!entry || entry.pseudo || (entry.hidden && !hidden)) { return message.util.send(`${emojis.error} That modlog does not exist.`); - if (entry.guild !== message.guild.id) return message.util.reply(`${emojis.error} This modlog is from another server.`); + } + + if (entry.guild !== message.guild.id) { + return message.util.reply(`${emojis.error} This modlog is from another server.`); + } + const embed = { title: `Case ${entry.id}`, - description: ModlogCommand.generateModlogInfo(entry, true, false), + description: generateModlogInfo(entry, true, false), color: colors.default }; return await ButtonPaginator.send(message, [embed]); } } +} - public static generateModlogInfo(log: ModLog, showUser: boolean, userFacing: boolean): string { - const trim = (str: string): string => (str.endsWith('\n') ? str.substring(0, str.length - 1).trim() : str.trim()); - const modLog = [`**Case ID:** ${escapeMarkdown(log.id)}`, `**Type:** ${log.type.toLowerCase()}`]; - if (showUser) modLog.push(`**User:** <@!${log.user}>`); - if (!userFacing) modLog.push(`**Moderator:** <@!${log.moderator}>`); - if (log.duration) modLog.push(`**Duration:** ${humanizeDuration(log.duration)}`); - modLog.push(`**Reason:** ${trim(log.reason ?? 'No Reason Specified.')}`); - modLog.push(`**Date:** ${timestamp(log.createdAt)}`); - if (log.evidence && !userFacing) modLog.push(`**Evidence:** ${trim(log.evidence)}`); - return modLog.join(`\n`); +export const modlogSeparator = '\nโโโโโโโโโโโโโโโ\n'; + +const trim = (str: string): string => { + if (str.endsWith('\n')) { + return str.substring(0, str.length - 1).trim(); + } else { + return str.trim(); } +}; + +export function generateModlogInfo(log: ModLog, showUser: boolean, userFacing: boolean): string { + return embedField` + Case ID ${escapeMarkdown(log.id)} + Type ${log.type.toLowerCase()} + User ${showUser && `<@!${log.user}>`} + Moderator ${!userFacing && `<@!${log.moderator}>`} + Duration ${log.duration && humanizeDuration(log.duration)} + Reason ${trim(log.reason ?? 'No Reason Specified.')} + Date ${timestamp(log.createdAt)} + Evidence ${log.evidence && !userFacing && trim(log.evidence)}`; } diff --git a/src/commands/moderation/mute.ts b/src/commands/moderation/mute.ts index 9ffaf8d..a64dc99 100644 --- a/src/commands/moderation/mute.ts +++ b/src/commands/moderation/mute.ts @@ -78,7 +78,7 @@ export default class MuteCommand extends BotCommand { return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`); const useForce = args.force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'mute', true, useForce); + const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Mute, true, useForce); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/myLogs.ts b/src/commands/moderation/myLogs.ts index 8faca8c..e3f5f10 100644 --- a/src/commands/moderation/myLogs.ts +++ b/src/commands/moderation/myLogs.ts @@ -12,7 +12,7 @@ import { import { ApplicationCommandOptionType } from 'discord.js'; import { input, sanitizeInputForDiscord } from '../../../lib/utils/Format.js'; -import ModlogCommand from './modlog.js'; +import { generateModlogInfo, modlogSeparator } from './modlog.js'; export default class MyLogsCommand extends BotCommand { public constructor() { super('myLogs', { @@ -50,14 +50,14 @@ export default class MyLogsCommand extends BotCommand { const logs = await ModLog.findAll({ where: { guild: guild.id, - user: message.author.id + user: message.author.id, + pseudo: false, + hidden: false }, order: [['createdAt', 'ASC']] }); - const niceLogs = logs - .filter((log) => !log.pseudo && !log.hidden) - .map((log) => ModlogCommand.generateModlogInfo(log, false, true)); + const niceLogs = logs.map((log) => generateModlogInfo(log, false, true)); if (niceLogs.length < 1) return message.util.reply(`${emojis.error} You don't have any modlogs in ${input(guild.name)}.`); @@ -65,7 +65,7 @@ export default class MyLogsCommand extends BotCommand { const embedPages = chunked.map((chunk) => ({ title: `Your Modlogs in ${sanitizeInputForDiscord(guild.name)}`, - description: chunk.join(ModlogCommand.separator), + description: chunk.join(modlogSeparator), color: colors.default })); diff --git a/src/commands/moderation/role.ts b/src/commands/moderation/role.ts index 565f214..a664aa4 100644 --- a/src/commands/moderation/role.ts +++ b/src/commands/moderation/role.ts @@ -12,8 +12,8 @@ import { type OptArgType, type SlashMessage } from '#lib'; +import { type ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { type ArgumentGeneratorReturn } from 'discord-akairo'; import { ApplicationCommandOptionType, PermissionFlagsBits, type Snowflake } from 'discord.js'; export default class RoleCommand extends BotCommand { diff --git a/src/commands/moderation/slowmode.ts b/src/commands/moderation/slowmode.ts index 82d0264..1256d1f 100644 --- a/src/commands/moderation/slowmode.ts +++ b/src/commands/moderation/slowmode.ts @@ -1,6 +1,6 @@ import { Arg, BotCommand, emojis, format, humanizeDuration, type CommandMessage, type OptArgType, type SlashMessage } from '#lib'; +import { Argument } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { Argument } from 'discord-akairo'; import { ApplicationCommandOptionType, ChannelType } from 'discord.js'; export default class SlowmodeCommand extends BotCommand { @@ -30,7 +30,7 @@ export default class SlowmodeCommand extends BotCommand { retry: '{error} Choose a valid channel.', optional: true, slashType: ApplicationCommandOptionType.Channel, - channelTypes: [ChannelType.GuildText, ChannelType.GuildPrivateThread, ChannelType.GuildPublicThread] + channelTypes: [ChannelType.GuildText, ChannelType.PrivateThread, ChannelType.PublicThread] } ], slash: true, @@ -55,7 +55,7 @@ export default class SlowmodeCommand extends BotCommand { if ( args.channel.type !== ChannelType.GuildText && - args.channel.type !== ChannelType.GuildNews && + args.channel.type !== ChannelType.GuildAnnouncement && args.channel.type !== ChannelType.GuildVoice && !args.channel.isThread() ) diff --git a/src/commands/moderation/timeout.ts b/src/commands/moderation/timeout.ts index 7bb02f7..db6ab56 100644 --- a/src/commands/moderation/timeout.ts +++ b/src/commands/moderation/timeout.ts @@ -73,7 +73,13 @@ export default class TimeoutCommand extends BotCommand { return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`); const useForce = args.force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'timeout', true, useForce); + const canModerateResponse = await Moderation.permissionCheck( + message.member, + member, + Moderation.Action.Timeout, + true, + useForce + ); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/unblock.ts b/src/commands/moderation/unblock.ts index 4838392..4fdfc28 100644 --- a/src/commands/moderation/unblock.ts +++ b/src/commands/moderation/unblock.ts @@ -75,7 +75,13 @@ export default class UnblockCommand extends BotCommand { return await message.util.reply(`${emojis.error} The user you selected is not in the server or is not a valid user.`); const useForce = args.force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'unblock', true, useForce); + const canModerateResponse = await Moderation.permissionCheck( + message.member, + member, + Moderation.Action.Unblock, + true, + useForce + ); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/unmute.ts b/src/commands/moderation/unmute.ts index a4d348d..c8fccc8 100644 --- a/src/commands/moderation/unmute.ts +++ b/src/commands/moderation/unmute.ts @@ -66,7 +66,13 @@ export default class UnmuteCommand extends BotCommand { const member = message.guild.members.cache.get(user.id)!; const useForce = force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'unmute', true, useForce); + const canModerateResponse = await Moderation.permissionCheck( + message.member, + member, + Moderation.Action.Unmute, + true, + useForce + ); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/untimeout.ts b/src/commands/moderation/untimeout.ts index 3775c65..64364e5 100644 --- a/src/commands/moderation/untimeout.ts +++ b/src/commands/moderation/untimeout.ts @@ -73,7 +73,13 @@ export default class UntimeoutCommand extends BotCommand { if (!member.isCommunicationDisabled()) return message.util.reply(`${emojis.error} That user is not timed out.`); const useForce = args.force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'timeout', true, useForce); + const canModerateResponse = await Moderation.permissionCheck( + message.member, + member, + Moderation.Action.Untimeout, + true, + useForce + ); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moderation/warn.ts b/src/commands/moderation/warn.ts index 4bc7f13..a7ed814 100644 --- a/src/commands/moderation/warn.ts +++ b/src/commands/moderation/warn.ts @@ -69,7 +69,7 @@ export default class WarnCommand extends BotCommand { const member = message.guild.members.cache.get(user.id); if (!member) return message.util.reply(`${emojis.error} I cannot warn users that are not in the server.`); const useForce = force && message.author.isOwner(); - const canModerateResponse = await Moderation.permissionCheck(message.member, member, 'warn', true, useForce); + const canModerateResponse = await Moderation.permissionCheck(message.member, member, Moderation.Action.Warn, true, useForce); if (canModerateResponse !== true) { return message.util.reply(canModerateResponse); diff --git a/src/commands/moulberry-bush/capes.ts b/src/commands/moulberry-bush/capes.ts index b292f24..a67cf46 100644 --- a/src/commands/moulberry-bush/capes.ts +++ b/src/commands/moulberry-bush/capes.ts @@ -13,7 +13,9 @@ import { } from '#lib'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, type APIEmbed, type AutocompleteInteraction } from 'discord.js'; -import { default as Fuse } from 'fuse.js'; + +// todo: remove this bullshit once typescript gets its shit together +const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default; assert(Fuse); @@ -85,7 +87,7 @@ export default class CapesCommand extends BotCommand { } } else { const embeds: APIEmbed[] = capes.map(this.makeEmbed); - await ButtonPaginator.send(message, embeds, null); + await ButtonPaginator.send(message, embeds, ''); } } diff --git a/src/commands/tickets/ticket-!.ts b/src/commands/tickets/ticket-!.ts index c5c59f2..b98ec1f 100644 --- a/src/commands/tickets/ticket-!.ts +++ b/src/commands/tickets/ticket-!.ts @@ -1,5 +1,5 @@ import { BotCommand, deepWriteable, type SlashMessage } from '#lib'; -import { Flag, type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo'; +import { Flag, type ArgumentGeneratorReturn, type SlashOption } from '@notenoughupdates/discord-akairo'; import { ApplicationCommandOptionType } from 'discord.js'; export const ticketSubcommands = deepWriteable({ diff --git a/src/commands/utilities/activity.ts b/src/commands/utilities/activity.ts index 89ca53e..3f17d0a 100644 --- a/src/commands/utilities/activity.ts +++ b/src/commands/utilities/activity.ts @@ -7,7 +7,7 @@ import { type CommandMessage, type SlashMessage } from '#lib'; -import { type ArgumentGeneratorReturn, type ArgumentTypeCaster } from 'discord-akairo'; +import { type ArgumentGeneratorReturn, type ArgumentTypeCaster } from '@notenoughupdates/discord-akairo'; import { ApplicationCommandOptionType, ChannelType, type DiscordAPIError, type Snowflake } from 'discord.js'; const activityMap = { diff --git a/src/commands/utilities/highlight-!.ts b/src/commands/utilities/highlight-!.ts index 7716887..0e2db33 100644 --- a/src/commands/utilities/highlight-!.ts +++ b/src/commands/utilities/highlight-!.ts @@ -1,5 +1,5 @@ import { BotCommand, deepWriteable, Highlight, HighlightWord, type SlashMessage } from '#lib'; -import { Flag, type ArgumentGeneratorReturn, type SlashOption } from 'discord-akairo'; +import { Flag, type ArgumentGeneratorReturn, type SlashOption } from '@notenoughupdates/discord-akairo'; import { ApplicationCommandOptionType, Constants, type AutocompleteInteraction, type CacheType } from 'discord.js'; export const highlightSubcommands = deepWriteable({ diff --git a/src/commands/utilities/highlight-block.ts b/src/commands/utilities/highlight-block.ts index b16852e..d1dec1e 100644 --- a/src/commands/utilities/highlight-block.ts +++ b/src/commands/utilities/highlight-block.ts @@ -1,6 +1,6 @@ import { AllowedMentions, BotCommand, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; +import { Argument, ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { Argument, ArgumentGeneratorReturn } from 'discord-akairo'; import { BaseChannel, GuildMember, User } from 'discord.js'; import { HighlightBlockResult } from '../../../lib/common/HighlightManager.js'; import { highlightSubcommands } from './highlight-!.js'; diff --git a/src/commands/utilities/highlight-matches.ts b/src/commands/utilities/highlight-matches.ts index d54fd4a..0665b37 100644 --- a/src/commands/utilities/highlight-matches.ts +++ b/src/commands/utilities/highlight-matches.ts @@ -1,6 +1,6 @@ import { BotCommand, ButtonPaginator, chunk, colors, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; +import { type ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { type ArgumentGeneratorReturn } from 'discord-akairo'; import { type APIEmbed } from 'discord.js'; import { highlightSubcommands } from './highlight-!.js'; diff --git a/src/commands/utilities/highlight-unblock.ts b/src/commands/utilities/highlight-unblock.ts index 0f2dd78..f9cc806 100644 --- a/src/commands/utilities/highlight-unblock.ts +++ b/src/commands/utilities/highlight-unblock.ts @@ -1,6 +1,6 @@ import { AllowedMentions, BotCommand, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; +import { Argument, ArgumentGeneratorReturn } from '@notenoughupdates/discord-akairo'; import assert from 'assert'; -import { Argument, ArgumentGeneratorReturn } from 'discord-akairo'; import { BaseChannel, GuildMember, User } from 'discord.js'; import { HighlightUnblockResult } from '../../../lib/common/HighlightManager.js'; import { highlightSubcommands } from './highlight-!.js'; diff --git a/src/commands/utilities/price.ts b/src/commands/utilities/price.ts index 6f08aaa..0d5d4b3 100644 --- a/src/commands/utilities/price.ts +++ b/src/commands/utilities/price.ts @@ -1,7 +1,9 @@ import { ArgType, BotCommand, colors, emojis, format, formatList, type CommandMessage } from '#lib'; import assert from 'assert/strict'; import { ApplicationCommandOptionType, AutocompleteInteraction, EmbedBuilder } from 'discord.js'; -import { default as Fuse } from 'fuse.js'; + +// todo: remove this bullshit once typescript gets its shit together +const Fuse = (await import('fuse.js')).default as unknown as typeof import('fuse.js').default; assert(Fuse); diff --git a/src/commands/utilities/steal.ts b/src/commands/utilities/steal.ts index a208920..bd2976a 100644 --- a/src/commands/utilities/steal.ts +++ b/src/commands/utilities/steal.ts @@ -1,13 +1,11 @@ import { Arg, BotCommand, emojis, format, OptArgType, regex, type CommandMessage, type SlashMessage } from '#lib'; +import { type ArgumentGeneratorReturn, type ArgumentType, type ArgumentTypeCaster } from '@notenoughupdates/discord-akairo'; import assert from 'assert/strict'; -import { type ArgumentGeneratorReturn, type ArgumentType, type ArgumentTypeCaster } from 'discord-akairo'; import { ApplicationCommandOptionType, Attachment } from 'discord.js'; -import _ from 'lodash'; +import { snakeCase } from 'lodash-es'; import { Stream } from 'stream'; import { URL } from 'url'; -assert(_); - // so I don't have to retype things const enum lang { emojiStart = 'What emoji would you like to steal?', @@ -53,7 +51,7 @@ export default class StealCommand extends BotCommand { const name = yield { prompt: { start: lang.nameStart, retry: lang.nameRetry, optional: true }, - default: hasImage && message.attachments.first()!.name ? _.snakeCase(message.attachments.first()!.name!) : 'unnamed_emoji' + default: hasImage && message.attachments.first()!.name ? snakeCase(message.attachments.first()!.name!) : 'unnamed_emoji' }; return { emoji, name }; diff --git a/src/commands/utilities/whoHasRole.ts b/src/commands/utilities/whoHasRole.ts index c01a0c3..31e413d 100644 --- a/src/commands/utilities/whoHasRole.ts +++ b/src/commands/utilities/whoHasRole.ts @@ -77,7 +77,7 @@ export default class WhoHasRoleCommand extends BotCommand { return await message.util.reply(`${emojis.error} No members found matching the given roles.`); } - return await ButtonPaginator.send(message, embedPages, null, true); + return await ButtonPaginator.send(message, embedPages, '', true); } } diff --git a/src/commands/utilities/wolframAlpha.ts b/src/commands/utilities/wolframAlpha.ts index 503af87..48739cf 100644 --- a/src/commands/utilities/wolframAlpha.ts +++ b/src/commands/utilities/wolframAlpha.ts @@ -1,7 +1,7 @@ import { AllowedMentions, BotCommand, colors, emojis, type ArgType, type CommandMessage, type SlashMessage } from '#lib'; -import { initializeClass as WolframAlphaAPI } from '@notenoughupdates/wolfram-alpha-api'; +import { initializeClass as WolframAlphaAPI } from '@tanzanite/wolfram-alpha'; import assert from 'assert/strict'; -import { ApplicationCommandOptionType, EmbedBuilder, type MessageOptions } from 'discord.js'; +import { ApplicationCommandOptionType, EmbedBuilder, type MessageCreateOptions } from 'discord.js'; assert(WolframAlphaAPI); @@ -62,7 +62,7 @@ export default class WolframAlphaCommand extends BotCommand { name: '๐ฅ Input', value: await this.client.utils.inspectCleanRedactCodeblock(args.expression) }); - const sendOptions: MessageOptions = { content: null, allowedMentions: AllowedMentions.none() }; + const sendOptions: MessageCreateOptions = { content: '', allowedMentions: AllowedMentions.none() }; try { const calculated = await (args.image ? waApi.getSimple({ i: args.expression, timeout: 1, background: '2C2F33', foreground: 'white' }) diff --git a/src/context-menu-commands/message/viewRaw.ts b/src/context-menu-commands/message/viewRaw.ts index 0a8fcfc..08a421d 100644 --- a/src/context-menu-commands/message/viewRaw.ts +++ b/src/context-menu-commands/message/viewRaw.ts @@ -1,4 +1,4 @@ -import { ContextMenuCommand } from 'discord-akairo'; +import { ContextMenuCommand } from '@notenoughupdates/discord-akairo'; import { ApplicationCommandType, type ContextMenuCommandInteraction, type Message } from 'discord.js'; import { getRawData } from '../../commands/utilities/viewRaw.js'; diff --git a/src/context-menu-commands/user/modlog.ts b/src/context-menu-commands/user/modlog.ts index c78396e..b68a7e9 100644 --- a/src/context-menu-commands/user/modlog.ts +++ b/src/context-menu-commands/user/modlog.ts @@ -1,6 +1,6 @@ import { ModlogCommand } from '#commands'; import { emojis, SlashMessage } from '#lib'; -import { CommandUtil, ContextMenuCommand } from 'discord-akairo'; +import { CommandUtil, ContextMenuCommand } from '@notenoughupdates/discord-akairo'; import { ApplicationCommandType, type ContextMenuCommandInteraction } from 'discord.js'; export default class ModlogContextMenuCommand extends ContextMenuCommand { @@ -8,7 +8,8 @@ export default class ModlogContextMenuCommand extends ContextMenuCommand { super('modlog', { name: "Users's Modlogs", type: ApplicationCommandType.User, - category: 'user' + category: 'user', + dmPermission: false }); } @@ -28,6 +29,8 @@ export default class ModlogContextMenuCommand extends ContextMenuCommand { const pseudoMessage = new SlashMessage(this.client, interaction as any); pseudoMessage.util = new CommandUtil(this.client.commandHandler, pseudoMessage); - void new ModlogCommand().exec(pseudoMessage, { search: interaction.targetId, hidden: false }); + const command = this.client.commandHandler.modules.get('modlog') as ModlogCommand; + + void command.exec(pseudoMessage, { search: interaction.targetId, hidden: false }); } } diff --git a/src/context-menu-commands/user/userInfo.ts b/src/context-menu-commands/user/userInfo.ts index 6d7f3b6..283e4a0 100644 --- a/src/context-menu-commands/user/userInfo.ts +++ b/src/context-menu-commands/user/userInfo.ts @@ -1,27 +1,35 @@ import { UserInfoCommand } from '#commands'; -import { format } from '#lib'; -import { ContextMenuCommand } from 'discord-akairo'; -import { ApplicationCommandType, type ContextMenuCommandInteraction, type Guild } from 'discord.js'; +import { emojis } from '#lib'; +import { ContextMenuCommand } from '@notenoughupdates/discord-akairo'; +import assert from 'assert'; +import { ApplicationCommandType, GuildMember, UserContextMenuCommandInteraction } from 'discord.js'; export default class UserInfoContextMenuCommand extends ContextMenuCommand { public constructor() { super('userInfo', { name: 'User Info', type: ApplicationCommandType.User, - category: 'user' + category: 'user', + dmPermission: false }); } - public override async exec(interaction: ContextMenuCommandInteraction) { + public override async exec(interaction: UserContextMenuCommandInteraction) { + if (!interaction.inCachedGuild()) + return interaction.reply({ + content: `${emojis.error} You can't use this command outside of a server.`, + ephemeral: true + }); + await interaction.deferReply({ ephemeral: true }); - const user = await this.client.users.fetch(interaction.targetId).catch(() => null); - if (!user) return interaction.reply(`โ I couldn't find that user`); + const user = interaction.targetUser; + + const guild = interaction.guild ?? undefined; - const guild = interaction.guild as Guild; + const member = interaction.targetMember ?? undefined; - const member = await guild.members.fetch(interaction.targetId).catch(() => null); - if (!member) return interaction.reply(`${format.input(user.tag)} doesn't appear to be a member of this server anymore.`); + assert(member instanceof GuildMember || member === undefined); const userEmbed = await UserInfoCommand.makeUserInfoEmbed(user, member, guild); diff --git a/src/listeners/bush/appealListener.ts b/src/listeners/bush/appealListener.ts index 46859d1..99f1505 100644 --- a/src/listeners/bush/appealListener.ts +++ b/src/listeners/bush/appealListener.ts @@ -2,7 +2,7 @@ import { BotListener, colors, Emitter, mappings, ModLog, type BotClientEvents } import assert from 'assert/strict'; import { EmbedBuilder, Events } from 'discord.js'; import UserInfoCommand from '../../commands/info/userInfo.js'; -import ModlogCommand from '../../commands/moderation/modlog.js'; +import { generateModlogInfo, modlogSeparator } from '../../commands/moderation/modlog.js'; export default class AppealListener extends BotListener { public constructor() { @@ -42,7 +42,9 @@ export default class AppealListener extends BotListener { await ModLog.findAll({ where: { user: user.id, - guild: message.guildId + guild: message.guildId, + pseudo: false, + hidden: false }, order: [['createdAt', 'DESC']] }) @@ -60,15 +62,19 @@ export default class AppealListener extends BotListener { member: { if (!message.guild.members.cache.has(user.id)) break member; + const member = message.guild.members.cache.get(user.id)!; + UserInfoCommand.generateServerInfoField(embed, member); - if (member.roles.cache.size > 1) UserInfoCommand.generateRolesField(embed, member); + if (member.roles.cache.size > 1) { + UserInfoCommand.generateRolesField(embed, member); + } } embed.addFields({ name: 'ยป Latest Modlogs', value: latestModlogs.length - ? latestModlogs.map((ml) => ModlogCommand.generateModlogInfo(ml, false, false)).join(ModlogCommand.separator) + ? latestModlogs.map((ml) => generateModlogInfo(ml, false, false)).join(modlogSeparator) : 'No Modlogs Found' }); diff --git a/src/listeners/bush/experimentYoink.ts b/src/listeners/bush/experimentYoink.ts new file mode 100644 index 0000000..5b7e526 --- /dev/null +++ b/src/listeners/bush/experimentYoink.ts @@ -0,0 +1,28 @@ +import { BotClientEvents, BotListener, Emitter, mappings } from '#lib'; +import { Events, Routes } from 'discord.js'; + +export default class ExperimentYoink extends BotListener { + public constructor() { + super('experimentYoink', { + emitter: Emitter.Client, + event: Events.MessageCreate + }); + } + + public async exec(...[message]: BotClientEvents[Events.MessageCreate]): Promise<any> { + if (message.channelId !== '1019830755658055691') return; + if (message.embeds.length < 1) return; + if (!message.embeds[0].title?.includes('Guild Experiment')) return; + + const guild = this.client.guilds.cache.get(mappings.guilds["Moulberry's Bush"]); + + if (guild == null) return; + + return await this.client.rest.post(Routes.channelMessages('795356494261911553'), { + body: { + content: message.content, + embeds: message.embeds.map((embed) => embed.toJSON()) + } + }); + } +} diff --git a/src/listeners/client/ready.ts b/src/listeners/client/ready.ts index b74c132..e19c4eb 100644 --- a/src/listeners/client/ready.ts +++ b/src/listeners/client/ready.ts @@ -1,6 +1,9 @@ import { BotClientEvents, BotListener, Emitter, Guild } from '#lib'; +import { commas } from '#lib/common/tags.js'; +import { humanizeDuration } from '@notenoughupdates/humanize-duration'; import chalk from 'chalk'; import { Events } from 'discord.js'; +import { performance } from 'perf_hooks'; export default class ReadyListener extends BotListener { public constructor() { @@ -12,19 +15,28 @@ export default class ReadyListener extends BotListener { // eslint-disable-next-line no-empty-pattern public async exec(...[]: BotClientEvents[Events.ClientReady]) { + performance.mark('clientReady'); + process.emit('ready' as any); const tag = `<<${this.client.user?.tag}>>`, - guildCount = `<<${this.client.guilds.cache.size.toLocaleString()}>>`, - userCount = `<<${this.client.users.cache.size.toLocaleString()}>>`; + guildCount = commas`<<${this.client.guilds.cache.size}>>`, + userCount = commas`<<${this.client.users.cache.size}>>`; void this.client.logger.success('ready', `Logged in to ${tag} serving ${guildCount} guilds and ${userCount} users.`); - console.log( - chalk.blue( - `------------------------------------------------------------------------------${ - this.client.config.isDevelopment ? '---' : this.client.config.isBeta ? '----' : '' - }` - ) + + console.log(chalk.blue('-'.repeat(84 + (this.client.config.isDevelopment ? 3 : this.client.config.isBeta ? 4 : 0)))); + + const measure = performance.measure('start', 'processStart', 'clientReady'); + + void this.client.logger.info( + 'ready', + `Took <<${humanizeDuration(measure.duration, { + language: 'en', + largest: 3, + round: false, + maxDecimalPoints: 3 + })}>> to start.` ); const guilds = await Guild.findAll(); diff --git a/src/listeners/commands/commandBlocked.ts b/src/listeners/commands/commandBlocked.ts index c81857c..7795241 100644 --- a/src/listeners/commands/commandBlocked.ts +++ b/src/listeners/commands/commandBlocked.ts @@ -9,7 +9,7 @@ import { type BotCommandHandlerEvents, type CommandMessage } from '#lib'; -import { type Client, type InteractionReplyOptions, type ReplyMessageOptions } from 'discord.js'; +import { type Client, type InteractionReplyOptions, type MessageReplyOptions } from 'discord.js'; export default class CommandBlockedListener extends BotListener { public constructor() { @@ -124,7 +124,7 @@ export default class CommandBlockedListener extends BotListener { } // some inhibitors do not have message.util yet - function respond(content: string | (ReplyMessageOptions & InteractionReplyOptions)) { + function respond(content: string | (MessageReplyOptions & InteractionReplyOptions)) { return message.util ? message.util.reply(content) : message.reply(content); } } diff --git a/src/listeners/contextCommands/contextCommandBlocked.ts b/src/listeners/contextCommands/contextCommandBlocked.ts index 93b53c7..80c7a34 100644 --- a/src/listeners/contextCommands/contextCommandBlocked.ts +++ b/src/listeners/contextCommands/contextCommandBlocked.ts @@ -1,6 +1,6 @@ import { BotListener, ContextCommandHandlerEvent, Emitter, emojis, format } from '#lib'; -import { type ContextMenuCommandHandlerEvents } from 'discord-akairo'; -import { BuiltInReasons } from 'discord-akairo/dist/src/util/Constants.js'; +import { type ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo'; +import { BuiltInReasons } from '@notenoughupdates/discord-akairo/dist/src/util/Constants.js'; export default class ContextCommandBlockedListener extends BotListener { public constructor() { diff --git a/src/listeners/contextCommands/contextCommandError.ts b/src/listeners/contextCommands/contextCommandError.ts index 24e5cef..5043bae 100644 --- a/src/listeners/contextCommands/contextCommandError.ts +++ b/src/listeners/contextCommands/contextCommandError.ts @@ -9,7 +9,7 @@ import { getErrorStack, IFuckedUpError } from '#lib'; -import { type ContextMenuCommand, type ContextMenuCommandHandlerEvents } from 'discord-akairo'; +import { type ContextMenuCommand, type ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo'; import { ChannelType, Client, ContextMenuCommandInteraction, EmbedBuilder, GuildTextBasedChannel } from 'discord.js'; export default class ContextCommandErrorListener extends BotListener { diff --git a/src/listeners/contextCommands/contextCommandNotFound.ts b/src/listeners/contextCommands/contextCommandNotFound.ts index da364ed..e7538ee 100644 --- a/src/listeners/contextCommands/contextCommandNotFound.ts +++ b/src/listeners/contextCommands/contextCommandNotFound.ts @@ -1,5 +1,5 @@ import { BotListener, ContextCommandHandlerEvent, Emitter } from '#lib'; -import { type ContextMenuCommandHandlerEvents } from 'discord-akairo'; +import { type ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo'; export default class ContextCommandNotFoundListener extends BotListener { public constructor() { diff --git a/src/listeners/contextCommands/contextCommandStarted.ts b/src/listeners/contextCommands/contextCommandStarted.ts index bf7cc58..a820d3e 100644 --- a/src/listeners/contextCommands/contextCommandStarted.ts +++ b/src/listeners/contextCommands/contextCommandStarted.ts @@ -1,5 +1,5 @@ import { BotListener, ContextCommandHandlerEvent, Emitter } from '#lib'; -import { ContextMenuCommandHandlerEvents } from 'discord-akairo'; +import { ContextMenuCommandHandlerEvents } from '@notenoughupdates/discord-akairo'; import { ApplicationCommandType, ChannelType } from 'discord.js'; export default class ContextCommandStartedListener extends BotListener { diff --git a/src/listeners/guild/syncUnbanPunishmentModel.ts b/src/listeners/guild/syncUnbanPunishmentModel.ts index eac3aa2..c6de768 100644 --- a/src/listeners/guild/syncUnbanPunishmentModel.ts +++ b/src/listeners/guild/syncUnbanPunishmentModel.ts @@ -13,7 +13,7 @@ export default class SyncUnbanListener extends BotListener { where: { user: ban.user.id, guild: ban.guild.id, - type: ActivePunishmentType.BAN + type: ActivePunishmentType.Ban } }); for (const dbBan of bans) { diff --git a/src/listeners/interaction/$interactionCreate.ts b/src/listeners/interaction/$interactionCreate.ts new file mode 100644 index 0000000..86aa5e2 --- /dev/null +++ b/src/listeners/interaction/$interactionCreate.ts @@ -0,0 +1,31 @@ +import { BotListener, Emitter, TanzaniteEvent, type BotClientEvents } from '#lib'; +import { Events, InteractionType } from 'discord.js'; + +export default class InteractionCreateListener extends BotListener { + public constructor() { + super('interactionCreate', { + emitter: Emitter.Client, + event: Events.InteractionCreate + }); + } + + public async exec(...[interaction]: BotClientEvents[Events.InteractionCreate]) { + if (!interaction) return; + + void this.client.console.verbose( + 'interactionVerbose', + `An interaction of type <<${InteractionType[interaction.type]}>> was received from <<${interaction.user.tag}>>.` + ); + + if (interaction.isCommand() || interaction.isAutocomplete()) { + // handled by the command handler + return; + } else if (interaction.isButton()) { + this.client.emit(TanzaniteEvent.Button, interaction); + } else if (interaction.isModalSubmit()) { + this.client.emit(TanzaniteEvent.ModalSubmit, interaction); + } else if (interaction.isSelectMenu()) { + this.client.emit(TanzaniteEvent.SelectMenu, interaction); + } + } +} diff --git a/src/listeners/interaction/button.ts b/src/listeners/interaction/button.ts index e69de29..d0730d5 100644 --- a/src/listeners/interaction/button.ts +++ b/src/listeners/interaction/button.ts @@ -0,0 +1,129 @@ +import { + BotClientEvents, + BotListener, + Emitter, + emojis, + handleAppealAttempt, + handleAppealDecision, + handleAutomodInteraction, + TanzaniteEvent +} from '#lib'; +import { + ActionRowData, + ButtonInteraction, + ComponentType, + ModalActionRowComponentData, + TextInputComponentData, + TextInputStyle +} from 'discord.js'; + +export default class ButtonListener extends BotListener { + public constructor() { + super(TanzaniteEvent.Button, { + emitter: Emitter.Client, + event: TanzaniteEvent.Button + }); + } + + public async exec(...[interaction]: BotClientEvents[TanzaniteEvent.Button]) { + const { customId } = interaction; + + if (customId.startsWith('automod;')) { + return void handleAutomodInteraction(interaction); + } else if (customId.startsWith('button-role;')) { + return void this.handleButtonRoles(interaction); + } else if (customId === 'test;modal') { + return this.handleTestModal(interaction); + } else if (customId.startsWith('test;lots;') || customId.startsWith('test;button;')) { + return await interaction.reply({ + content: 'Buttons go brrr', + ephemeral: true + }); + } else if (customId.startsWith('appeal_attempt;')) { + return handleAppealAttempt(interaction); + } else if (customId.startsWith('appeal_accept;') || customId.startsWith('appeal_deny;')) { + return handleAppealDecision(interaction); + } + } + + private async handleButtonRoles(interaction: ButtonInteraction) { + if (!interaction.inCachedGuild()) return; + + const [, roleId] = interaction.customId.split(';'); + const role = interaction.guild.roles.cache.get(roleId); + if (!role) { + return interaction.reply({ + content: `${emojis.error} That role does not exist.`, + ephemeral: true + }); + } + const has = interaction.member.roles.cache.has(roleId); + await interaction.deferReply({ ephemeral: true }); + if (has) { + const success = await interaction.member.roles.remove(roleId).catch(() => false); + if (success) { + return interaction.editReply({ + content: `${emojis.success} Removed the ${role} role from you.`, + allowedMentions: {} + }); + } else { + return interaction.editReply({ + content: `${emojis.error} Failed to remove ${role} from you.`, + allowedMentions: {} + }); + } + } else { + const success = await interaction.member.roles.add(roleId).catch(() => false); + if (success) { + return interaction.editReply({ + content: `${emojis.success} Added the ${role} role to you.`, + allowedMentions: {} + }); + } else { + return interaction.editReply({ + content: `${emojis.error} Failed to add ${role} to you.`, + allowedMentions: {} + }); + } + } + } + + private async handleTestModal(interaction: ButtonInteraction) { + const shortText = ( + options: Pick<TextInputComponentData, 'customId' | 'label' | 'placeholder'> & Partial<TextInputComponentData> + ): ActionRowData<ModalActionRowComponentData> => ({ + type: ComponentType.ActionRow as const, + components: [ + { + type: ComponentType.TextInput as const, + style: TextInputStyle.Short as const, + ...options + } + ] + }); + + return interaction.showModal({ + customId: 'test;login', + title: 'Login (real)', + components: [ + shortText({ + customId: 'test;login;email', + label: 'Email', + placeholder: 'Email' + }), + shortText({ + customId: 'test;login;password', + label: 'Password', + placeholder: 'Password' + }), + shortText({ + customId: 'test;login;2fa', + label: 'Enter Discord Auth Code', + minLength: 6, + maxLength: 6, + placeholder: '6-digit authentication code' + }) + ] + }); + } +} diff --git a/src/listeners/interaction/interactionCreate.ts b/src/listeners/interaction/interactionCreate.ts deleted file mode 100644 index ced359c..0000000 --- a/src/listeners/interaction/interactionCreate.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { - BotListener, - Emitter, - emojis, - format, - formatList, - handleAutomodInteraction, - surroundEach, - type BotClientEvents -} from '#lib'; -import { Events, InteractionType } from 'discord.js'; - -export default class InteractionCreateListener extends BotListener { - public constructor() { - super('interactionCreate', { - emitter: Emitter.Client, - event: Events.InteractionCreate - }); - } - - public async exec(...[interaction]: BotClientEvents[Events.InteractionCreate]) { - if (!interaction) return; - if ('customId' in interaction && (interaction as any)['customId'].startsWith('test')) return; - void this.client.console.verbose( - 'interactionVerbose', - `An interaction of type <<${InteractionType[interaction.type]}>> was received from <<${interaction.user.tag}>>.` - ); - if (interaction.type === InteractionType.ApplicationCommand) { - return; - } else if (interaction.isButton()) { - const id = interaction.customId; - if (['paginate_', 'command_', 'confirmationPrompt_', 'appeal'].some((s) => id.startsWith(s))) return; - else if (id.startsWith('automod;')) void handleAutomodInteraction(interaction); - else if (id.startsWith('button-role;') && interaction.inCachedGuild()) { - const [, roleId] = id.split(';'); - const role = interaction.guild.roles.cache.get(roleId); - if (!role) return interaction.reply({ content: `${emojis.error} That role does not exist.`, ephemeral: true }); - const has = interaction.member.roles.cache.has(roleId); - await interaction.deferReply({ ephemeral: true }); - if (has) { - const success = await interaction.member.roles.remove(roleId).catch(() => false); - if (success) - return interaction.editReply({ - content: `${emojis.success} Removed the ${role} role from you.`, - allowedMentions: {} - }); - else - return interaction.editReply({ - content: `${emojis.error} Failed to remove ${role} from you.`, - allowedMentions: {} - }); - } else { - const success = await interaction.member.roles.add(roleId).catch(() => false); - if (success) - return interaction.editReply({ - content: `${emojis.success} Added the ${role} role to you.`, - allowedMentions: {} - }); - else - return interaction.editReply({ - content: `${emojis.error} Failed to add ${role} to you.`, - allowedMentions: {} - }); - } - } else return await interaction.reply({ content: 'Buttons go brrr', ephemeral: true }); - } else if (interaction.isSelectMenu()) { - if (interaction.customId.startsWith('command_')) return; - return await interaction.reply({ - content: `You selected ${ - Array.isArray(interaction.values) - ? formatList(surroundEach(interaction.values, '`'), 'and') - : format.input(interaction.values) - }.`, - ephemeral: true - }); - } - } -} diff --git a/src/listeners/interaction/modalSubmit.ts b/src/listeners/interaction/modalSubmit.ts new file mode 100644 index 0000000..8cf93f4 --- /dev/null +++ b/src/listeners/interaction/modalSubmit.ts @@ -0,0 +1,20 @@ +import { BotClientEvents, BotListener, Emitter, emojis, handleAppealSubmit, TanzaniteEvent } from '#lib'; + +export default class ModalSubmitListener extends BotListener { + public constructor() { + super(TanzaniteEvent.ModalSubmit, { + emitter: Emitter.Client, + event: TanzaniteEvent.ModalSubmit + }); + } + + public async exec(...[interaction]: BotClientEvents[TanzaniteEvent.ModalSubmit]) { + const { customId } = interaction; + + if (customId === 'test;login') { + return interaction.reply({ content: `${emojis.loading} Selling your account information to Facebook...`, ephemeral: true }); + } else if (customId.startsWith('appeal_submit;')) { + return handleAppealSubmit(interaction); + } + } +} diff --git a/src/listeners/interaction/selectMenu.ts b/src/listeners/interaction/selectMenu.ts new file mode 100644 index 0000000..112f303 --- /dev/null +++ b/src/listeners/interaction/selectMenu.ts @@ -0,0 +1,23 @@ +import { BotClientEvents, BotListener, Emitter, format, formatList, surroundEach, TanzaniteEvent } from '#lib'; + +export default class SelectMenuListener extends BotListener { + public constructor() { + super(TanzaniteEvent.SelectMenu, { + emitter: Emitter.Client, + event: TanzaniteEvent.SelectMenu + }); + } + + public async exec(...[interaction]: BotClientEvents[TanzaniteEvent.SelectMenu]) { + if (interaction.customId.startsWith('command_')) return; + + return await interaction.reply({ + content: `You selected ${ + Array.isArray(interaction.values) + ? formatList(surroundEach(interaction.values, '`'), 'and') + : format.input(interaction.values) + }.`, + ephemeral: true + }); + } +} diff --git a/src/listeners/member-custom/levelUpdate.ts b/src/listeners/member-custom/levelUpdate.ts index 53734fd..7d3cee7 100644 --- a/src/listeners/member-custom/levelUpdate.ts +++ b/src/listeners/member-custom/levelUpdate.ts @@ -29,11 +29,12 @@ export default class LevelUpdateListener extends BotListener { .send(`${format.input(member.user.tag)} leveled up to level ${format.input(`${newLevel}`)}.`) .catch(() => null); - if (!success) + if (!success) { await message.guild.error( 'LevelUpdate', `Could not send level up message for ${member.user.tag} in <#${message.channel.id}>.` ); + } } private async assignLevelRoles(member: Args[0], newLevel: Args[2], message: Args[4]) { diff --git a/src/listeners/message/autoPublisher.ts b/src/listeners/message/autoPublisher.ts index 36c448a..46577e4 100644 --- a/src/listeners/message/autoPublisher.ts +++ b/src/listeners/message/autoPublisher.ts @@ -13,7 +13,10 @@ export default class autoPublisherListener extends BotListener { if (!message.guild || !(await message.guild.hasFeature('autoPublish'))) return; const autoPublishChannels = await message.guild.getSetting('autoPublishChannels'); if (autoPublishChannels) { - if (message.channel.type === ChannelType.GuildNews && autoPublishChannels.some((x) => message.channel.id.includes(x))) { + if ( + message.channel.type === ChannelType.GuildAnnouncement && + autoPublishChannels.some((x) => message.channel.id.includes(x)) + ) { await message .crosspost() .then( diff --git a/src/listeners/message/level.ts b/src/listeners/message/level.ts index a445d1e..60742ab 100644 --- a/src/listeners/message/level.ts +++ b/src/listeners/message/level.ts @@ -3,6 +3,7 @@ import { MessageType } from 'discord.js'; export default class LevelListener extends BotListener { #levelCooldowns: Set<string> = new Set(); + public constructor() { super('level', { emitter: Emitter.CommandHandler, @@ -14,34 +15,52 @@ export default class LevelListener extends BotListener { public async exec(...[message]: BotCommandHandlerEvents[CommandHandlerEvent.MessageInvalid]) { if (message.author.bot || !message.author || !message.inGuild()) return; if (!(await message.guild.hasFeature('leveling'))) return; - if (this.#levelCooldowns.has(`${message.guildId}-${message.author.id}`)) return; + + const lock = `${message.guildId}-${message.author.id}`; + if (this.#levelCooldowns.has(lock)) return; if ((await message.guild.getSetting('noXpChannels')).includes(message.channel.id)) return; - if (message.type !== MessageType.Default && message.type !== MessageType.Reply) return; //checks for join messages, slash commands, booster messages etc - const [user] = await Level.findOrBuild({ + + // checks for join messages, slash commands, booster messages etc + if (![MessageType.Default, MessageType.Reply, MessageType.ThreadStarterMessage].includes(message.type)) { + return; + } + + const [levelEntry] = await Level.findOrBuild({ where: { user: message.author.id, guild: message.guildId - }, - defaults: { - user: message.author.id, - guild: message.guildId, - xp: 0 } }); - const previousLevel = Level.convertXpToLevel(user.xp); + + const previousLevel = Level.convertXpToLevel(levelEntry.xp); const xpToGive = Level.genRandomizedXp(); - user.xp = user.xp + xpToGive; - const success = await user.save().catch((e) => { - void this.client.utils.handleError('level', e); + + let xp = levelEntry.xp + xpToGive; + + if (xp > Level.MAX_XP) { + xp = Level.MAX_XP; + } + + const success = await levelEntry.update({ xp, user: message.author.id, guild: message.guild.id }).catch((e) => { + void this.client.utils.handleError('LevelListener', e); + return false; }); - const newLevel = Level.convertXpToLevel(user.xp); - if (previousLevel !== newLevel) - this.client.emit(TanzaniteEvent.LevelUpdate, message.member!, previousLevel, newLevel, user.xp, message); - if (success) - void this.client.logger.verbose(`level`, `Gave <<${xpToGive}>> XP to <<${message.author.tag}>> in <<${message.guild}>>.`); - this.#levelCooldowns.add(`${message.guildId}-${message.author.id}`); - setTimeout(() => this.#levelCooldowns.delete(`${message.guildId}-${message.author.id}`), 60_000); + + const newLevel = Level.convertXpToLevel(levelEntry.xp); + + if (success) { + if (previousLevel !== newLevel) { + // level up messages and level roles + this.client.emit(TanzaniteEvent.LevelUpdate, message.member!, previousLevel, newLevel, levelEntry.xp, message); + } + + void this.client.logger.verbose(`level`, `Gave <<${xpToGive}>> xp to <<${message.author.tag}>> in <<${message.guild}>>.`); + } + + this.#levelCooldowns.add(lock); + + setTimeout(() => this.#levelCooldowns.delete(lock), 60_000); } } diff --git a/src/listeners/message/quoteCreate.ts b/src/listeners/message/quoteCreate.ts index 1c3130c..fca6c9f 100644 --- a/src/listeners/message/quoteCreate.ts +++ b/src/listeners/message/quoteCreate.ts @@ -10,7 +10,7 @@ export default class QuoteCreateListener extends BotListener { } public async exec(...[message]: BotClientEvents[Events.MessageCreate]) { - if (message.author.id !== mappings.users['IRONM00N'] || !this.client.config.isProduction) return; + if (message.author.id !== mappings.users['IRONM00N'] /* || !this.client.config.isProduction */) return; if (!message.inGuild()) return; const messages = await this.client.utils.resolveMessagesFromLinks(message.content); diff --git a/src/listeners/track-manual-punishments/modlogSyncBan.ts b/src/listeners/track-manual-punishments/modlogSyncBan.ts index 83f6dd4..5a4d768 100644 --- a/src/listeners/track-manual-punishments/modlogSyncBan.ts +++ b/src/listeners/track-manual-punishments/modlogSyncBan.ts @@ -37,7 +37,7 @@ export default class ModlogSyncBanListener extends BotListener { const { log } = await Moderation.createModLogEntry({ client: this.client, - type: ModLogType.PERM_BAN, + type: ModLogType.PermBan, user: ban.user, moderator: first.executor, reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`, diff --git a/src/listeners/track-manual-punishments/modlogSyncKick.ts b/src/listeners/track-manual-punishments/modlogSyncKick.ts index e8b2433..a05e666 100644 --- a/src/listeners/track-manual-punishments/modlogSyncKick.ts +++ b/src/listeners/track-manual-punishments/modlogSyncKick.ts @@ -37,7 +37,7 @@ export default class ModlogSyncKickListener extends BotListener { const { log } = await Moderation.createModLogEntry({ client: this.client, - type: ModLogType.KICK, + type: ModLogType.Kick, user: member.user, moderator: first.executor, reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`, diff --git a/src/listeners/track-manual-punishments/modlogSyncTimeout.ts b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts index 2a4e0bb..f6abbab 100644 --- a/src/listeners/track-manual-punishments/modlogSyncTimeout.ts +++ b/src/listeners/track-manual-punishments/modlogSyncTimeout.ts @@ -41,7 +41,7 @@ export default class ModlogSyncTimeoutListener extends BotListener { const { log } = await Moderation.createModLogEntry({ client: this.client, - type: newTime ? ModLogType.TIMEOUT : ModLogType.REMOVE_TIMEOUT, + type: newTime ? ModLogType.Timeout : ModLogType.RemoveTimeout, user: newMember.user, moderator: first.executor, reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`, diff --git a/src/listeners/track-manual-punishments/modlogSyncUnban.ts b/src/listeners/track-manual-punishments/modlogSyncUnban.ts index 4738066..35ee3d2 100644 --- a/src/listeners/track-manual-punishments/modlogSyncUnban.ts +++ b/src/listeners/track-manual-punishments/modlogSyncUnban.ts @@ -36,7 +36,7 @@ export default class ModlogSyncUnbanListener extends BotListener { const { log } = await Moderation.createModLogEntry({ client: this.client, - type: ModLogType.UNBAN, + type: ModLogType.Unban, user: ban.user, moderator: first.executor, reason: `[Manual] ${first.reason ? first.reason : 'No reason given'}`, diff --git a/src/listeners/ws/INTERACTION_CREATE.ts b/src/listeners/ws/INTERACTION_CREATE.ts index d0327df..49f8bdf 100644 --- a/src/listeners/ws/INTERACTION_CREATE.ts +++ b/src/listeners/ws/INTERACTION_CREATE.ts @@ -1,25 +1,5 @@ -import { BotListener, capitalize, colors, Emitter, emojis, Moderation, PunishmentTypePresent } from '#lib'; -import assert from 'assert/strict'; -import { - ActionRowBuilder, - ButtonBuilder, - ButtonStyle, - ComponentType, - EmbedBuilder, - GatewayDispatchEvents, - InteractionResponseType, - InteractionType, - Routes, - Snowflake, - TextInputStyle, - User, - type APIEmbed, - type APIInteraction, - type APIInteractionResponseChannelMessageWithSource, - type APIInteractionResponseDeferredMessageUpdate, - type APIInteractionResponseUpdateMessage, - type APIModalInteractionResponse -} from 'discord.js'; +import { BotListener, Emitter } from '#lib'; +import { GatewayDispatchEvents, type APIInteraction } from 'discord.js'; export default class WsInteractionCreateListener extends BotListener { public constructor() { @@ -29,215 +9,5 @@ export default class WsInteractionCreateListener extends BotListener { }); } - public async exec(interaction: APIInteraction) { - // console.dir(interaction); - - const respond = ( - options: - | APIModalInteractionResponse - | APIInteractionResponseDeferredMessageUpdate - | APIInteractionResponseChannelMessageWithSource - | APIInteractionResponseUpdateMessage - ) => { - return this.client.rest.post( - Routes.interactionCallback(interaction.id, interaction.token), - options ? { body: options } : undefined - ); - }; - - const deferredMessageUpdate = () => { - return respond({ - type: InteractionResponseType.DeferredMessageUpdate - }); - }; - - if (interaction.type === InteractionType.MessageComponent) { - if (interaction.data.custom_id.startsWith('appeal;')) { - const [, punishment, guildId, userId, modlogCase] = interaction.data.custom_id.split(';') as [ - 'appeal', - PunishmentTypePresent, - Snowflake, - Snowflake, - string - ]; - - const guild = this.client.guilds.resolve(guildId); - if (!guild) - return respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: `${emojis.error} I am no longer in that server.` - } - }); - - const modal: APIModalInteractionResponse = { - type: InteractionResponseType.Modal, - data: { - custom_id: `appeal_submit;${punishment};${guildId};${userId};${modlogCase}`, - title: `${capitalize(punishment)} Appeal`, - components: [ - { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.TextInput, - style: TextInputStyle.Paragraph, - max_length: 1024, - required: true, - label: `Why were you ${Moderation.punishmentToPastTense(punishment)}?`, - placeholder: `Why do you think you received a ${punishment}?`, - custom_id: 'appeal_reason' - } - ] - }, - { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.TextInput, - style: TextInputStyle.Paragraph, - max_length: 1024, - required: true, - label: 'Do you believe it was fair?', - placeholder: `Why do you think you received a ${punishment}?`, - custom_id: 'appeal_fair' - } - ] - }, - { - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.TextInput, - style: TextInputStyle.Paragraph, - max_length: 1024, - required: true, - label: `Why should your ${punishment} be removed?`, - placeholder: `Why should your ${punishment} be removed?`, - custom_id: 'appeal_why' - } - ] - } - ] - } - }; - - return respond(modal); - } else if ( - interaction.data.custom_id.startsWith('appeal_accept;') || - interaction.data.custom_id.startsWith('appeal_deny;') - ) { - const [action, punishment, guildId, userId /* modlogCase */] = interaction.data.custom_id.split(';') as [ - 'appeal_accept' | 'appeal_deny', - PunishmentTypePresent, - Snowflake, - Snowflake, - string - ]; - - if (action === 'appeal_deny') { - await this.client.users - .send(userId, `Your ${punishment} appeal has been denied in ${this.client.guilds.resolve(guildId)!}.`) - .catch(() => {}); - - void respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - components: [ - { - type: 1, - components: [ - { - type: ComponentType.Button, - style: ButtonStyle.Danger, - label: 'Close', - custom_id: 'appeal_denied' - } - ] - } - ] - } - }); - } - } - } else if (interaction.type === InteractionType.ModalSubmit) { - if (interaction.data.custom_id.startsWith('appeal_submit;')) { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [, punishment, guildId, userId, modlogCase] = interaction.data.custom_id.split(';') as [ - 'appeal_submit', - PunishmentTypePresent, - Snowflake, - Snowflake, - string - ]; - - const guild = this.client.guilds.resolve(guildId); - if (!guild) - return respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: `${emojis.error} I am no longer in that server.` - } - }); - - const channel = await guild.getLogChannel('appeals'); - if (!channel) - return respond({ - type: InteractionResponseType.ChannelMessageWithSource, - data: { - content: `${emojis.error} ${guild.name} has misconfigured their appeals channel.` - } - }); - - assert(interaction.user); - const user = new User(this.client, interaction.user); - assert(user); - - // const caseId = await ModLog.findOne({ where: { user: userId, guild: guildId, id: modlogCase } }); - - const embed = new EmbedBuilder() - .setTitle(`${capitalize(punishment)} Appeal`) - .setColor(colors.newBlurple) - .setTimestamp() - .setFooter({ text: `CaseID: ${modlogCase}` }) - .setAuthor({ name: user.tag, iconURL: user.displayAvatarURL() }) - .addFields( - { - name: `Why were you ${Moderation.punishmentToPastTense(punishment)}?`, - value: interaction.data.components![0].components[0]!.value.substring(0, 1024) - }, - { - name: 'Do you believe it was fair?', - value: interaction.data.components![1].components[0]!.value.substring(0, 1024) - }, - { - name: `Why should your ${punishment} be removed?`, - value: interaction.data.components![2].components[0]!.value.substring(0, 1024) - } - ) - .toJSON() as APIEmbed; - - const components = [ - new ActionRowBuilder<ButtonBuilder>({ - components: [ - new ButtonBuilder({ - customId: `appeal_accept;${punishment};${guildId};${userId};${modlogCase}`, - label: 'Accept', - style: ButtonStyle.Success - }).toJSON(), - new ButtonBuilder({ - customId: `appeal_deny;${punishment};${guildId};${userId};${modlogCase}`, - label: 'Deny', - style: ButtonStyle.Danger - }).toJSON() - ] - }) - ]; - - await channel.send({ embeds: [embed], components }); - } else { - return deferredMessageUpdate(); - } - } - } + public async exec(interaction: APIInteraction) {} } diff --git a/src/tasks/feature/removeExpiredPunishements.ts b/src/tasks/feature/removeExpiredPunishements.ts index eac325a..5d5d5ab 100644 --- a/src/tasks/feature/removeExpiredPunishements.ts +++ b/src/tasks/feature/removeExpiredPunishements.ts @@ -35,7 +35,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask { assert(guild); switch (entry.type) { - case ActivePunishmentType.BAN: { + case ActivePunishmentType.Ban: { assert(user); const result = await guild.customUnban({ user: user, reason: 'Punishment expired' }); if (['success', 'user not banned', 'cannot resolve user'].includes(result)) await entry.destroy(); @@ -43,7 +43,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask { void this.client.logger.verbose(`removeExpiredPunishments`, `Unbanned ${entry.user}.`); break; } - case ActivePunishmentType.BLOCK: { + case ActivePunishmentType.Block: { if (!member) { await entry.destroy(); // channel overrides are removed when the member leaves the guild return; @@ -54,7 +54,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask { void this.client.logger.verbose(`removeExpiredPunishments`, `Unblocked ${entry.user}.`); break; } - case ActivePunishmentType.MUTE: { + case ActivePunishmentType.Mute: { if (!member) return; const result = await member.customUnmute({ reason: 'Punishment expired' }); if (['success', 'failed to dm'].includes(result)) await entry.destroy(); @@ -62,7 +62,7 @@ export default class RemoveExpiredPunishmentsTask extends BotTask { void this.client.logger.verbose(`removeExpiredPunishments`, `Unmuted ${entry.user}.`); break; } - case ActivePunishmentType.ROLE: { + case ActivePunishmentType.Role: { if (!member) return; const role = guild?.roles?.cache?.get(entry.extraInfo); if (!role) throw new Error(`Cannot unmute ${member.user.tag} because I cannot find the mute role.`); |