;
13 | }
14 | 
--------------------------------------------------------------------------------
/src/lib/plugins/symbols.ts:
--------------------------------------------------------------------------------
1 | export const preGenericsInitialization: unique symbol = Symbol('SapphireFrameworkPluginsPreGenericsInitialization');
2 | export const preInitialization: unique symbol = Symbol('SapphireFrameworkPluginsPreInitialization');
3 | export const postInitialization: unique symbol = Symbol('SapphireFrameworkPluginsPostInitialization');
4 | 
5 | export const preLogin: unique symbol = Symbol('SapphireFrameworkPluginsPreLogin');
6 | export const postLogin: unique symbol = Symbol('SapphireFrameworkPluginsPostLogin');
7 | 
--------------------------------------------------------------------------------
/src/lib/precondition-resolvers/clientPermissions.ts:
--------------------------------------------------------------------------------
 1 | import { PermissionsBitField, type PermissionResolvable } from 'discord.js';
 2 | import { CommandPreConditions } from '../types/Enums';
 3 | import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';
 4 | 
 5 | /**
 6 |  * Appends the `ClientPermissions` precondition when {@link Command.Options.requiredClientPermissions} resolves to a
 7 |  * non-zero bitfield.
 8 |  * @param requiredClientPermissions The required client permissions.
 9 |  * @param preconditionContainerArray The precondition container array to append the precondition to.
10 |  */
11 | export function parseConstructorPreConditionsRequiredClientPermissions(
12 | 	requiredClientPermissions: PermissionResolvable | undefined,
13 | 	preconditionContainerArray: PreconditionContainerArray
14 | ) {
15 | 	const permissions = new PermissionsBitField(requiredClientPermissions);
16 | 	if (permissions.bitfield !== 0n) {
17 | 		preconditionContainerArray.append({ name: CommandPreConditions.ClientPermissions, context: { permissions } });
18 | 	}
19 | }
20 | 
--------------------------------------------------------------------------------
/src/lib/precondition-resolvers/cooldown.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { Command } from '../structures/Command';
 3 | import { BucketScope, CommandPreConditions } from '../types/Enums';
 4 | import { type PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';
 5 | 
 6 | /**
 7 |  * Appends the `Cooldown` precondition when {@link Command.Options.cooldownLimit} and
 8 |  * {@link Command.Options.cooldownDelay} are both non-zero.
 9 |  *
10 |  * @param command The command to parse cooldowns for.
11 |  * @param cooldownLimit The cooldown limit to use.
12 |  * @param cooldownDelay The cooldown delay to use.
13 |  * @param cooldownScope The cooldown scope to use.
14 |  * @param cooldownFilteredUsers The cooldown filtered users to use.
15 |  * @param preconditionContainerArray The precondition container array to append the precondition to.
16 |  */
17 | export function parseConstructorPreConditionsCooldown(
18 | 	command: Command
,
19 | 	cooldownLimit: number | undefined,
20 | 	cooldownDelay: number | undefined,
21 | 	cooldownScope: BucketScope | undefined,
22 | 	cooldownFilteredUsers: string[] | undefined,
23 | 	preconditionContainerArray: PreconditionContainerArray
24 | ) {
25 | 	const { defaultCooldown } = container.client.options;
26 | 
27 | 	// We will check for whether the command is filtered from the defaults, but we will allow overridden values to
28 | 	// be set. If an overridden value is passed, it will have priority. Otherwise, it will default to 0 if filtered
29 | 	// (causing the precondition to not be registered) or the default value with a fallback to a single-use cooldown.
30 | 	const filtered = defaultCooldown?.filteredCommands?.includes(command.name) ?? false;
31 | 	const limit = cooldownLimit ?? (filtered ? 0 : (defaultCooldown?.limit ?? 1));
32 | 	const delay = cooldownDelay ?? (filtered ? 0 : (defaultCooldown?.delay ?? 0));
33 | 
34 | 	if (limit && delay) {
35 | 		const scope = cooldownScope ?? defaultCooldown?.scope ?? BucketScope.User;
36 | 		const filteredUsers = cooldownFilteredUsers ?? defaultCooldown?.filteredUsers;
37 | 		preconditionContainerArray.append({
38 | 			name: CommandPreConditions.Cooldown,
39 | 			context: { scope, limit, delay, filteredUsers }
40 | 		});
41 | 	}
42 | }
43 | 
--------------------------------------------------------------------------------
/src/lib/precondition-resolvers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './clientPermissions';
2 | export * from './cooldown';
3 | export * from './nsfw';
4 | export * from './runIn';
5 | export * from './userPermissions';
6 | 
--------------------------------------------------------------------------------
/src/lib/precondition-resolvers/nsfw.ts:
--------------------------------------------------------------------------------
 1 | import { CommandPreConditions } from '../types/Enums';
 2 | import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';
 3 | 
 4 | /**
 5 |  * Appends the `NSFW` precondition if {@link SubcommandMappingMethod.nsfw} is set to true.
 6 |  * @param nsfw Whether this command is NSFW or not.
 7 |  * @param preconditionContainerArray The precondition container array to append the precondition to.
 8 |  */
 9 | export function parseConstructorPreConditionsNsfw(nsfw: boolean | undefined, preconditionContainerArray: PreconditionContainerArray) {
10 | 	if (nsfw) preconditionContainerArray.append(CommandPreConditions.NotSafeForWork);
11 | }
12 | 
--------------------------------------------------------------------------------
/src/lib/precondition-resolvers/runIn.ts:
--------------------------------------------------------------------------------
 1 | import { isNullish } from '@sapphire/utilities';
 2 | import type { ChannelType } from 'discord.js';
 3 | import { Command } from '../structures/Command';
 4 | import type { CommandRunInUnion, CommandSpecificRunIn } from '../types/CommandTypes';
 5 | import { CommandPreConditions } from '../types/Enums';
 6 | import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';
 7 | 
 8 | /**
 9 |  * Appends the `RunIn` precondition based on the values passed, defaulting to `null`, which doesn't add a
10 |  * precondition.
11 |  * @param runIn The command's `runIn` option field from the constructor.
12 |  * @param resolveConstructorPreConditionsRunType The function to resolve the run type from the constructor.
13 |  * @param preconditionContainerArray The precondition container array to append the precondition to.
14 |  */
15 | export function parseConstructorPreConditionsRunIn(
16 | 	runIn: CommandRunInUnion | CommandSpecificRunIn,
17 | 	resolveConstructorPreConditionsRunType: (types: CommandRunInUnion) => readonly ChannelType[] | null,
18 | 	preconditionContainerArray: PreconditionContainerArray
19 | ) {
20 | 	// Early return if there's no runIn option:
21 | 	if (isNullish(runIn)) return;
22 | 
23 | 	if (Command.runInTypeIsSpecificsObject(runIn)) {
24 | 		const messageRunTypes = resolveConstructorPreConditionsRunType(runIn.messageRun);
25 | 		const chatInputRunTypes = resolveConstructorPreConditionsRunType(runIn.chatInputRun);
26 | 		const contextMenuRunTypes = resolveConstructorPreConditionsRunType(runIn.contextMenuRun);
27 | 
28 | 		if (messageRunTypes !== null || chatInputRunTypes !== null || contextMenuRunTypes !== null) {
29 | 			preconditionContainerArray.append({
30 | 				name: CommandPreConditions.RunIn,
31 | 				context: {
32 | 					types: {
33 | 						messageRun: messageRunTypes ?? [],
34 | 						chatInputRun: chatInputRunTypes ?? [],
35 | 						contextMenuRun: contextMenuRunTypes ?? []
36 | 					}
37 | 				}
38 | 			});
39 | 		}
40 | 	} else {
41 | 		const types = resolveConstructorPreConditionsRunType(runIn);
42 | 		if (types !== null) {
43 | 			preconditionContainerArray.append({ name: CommandPreConditions.RunIn, context: { types } });
44 | 		}
45 | 	}
46 | }
47 | 
--------------------------------------------------------------------------------
/src/lib/precondition-resolvers/userPermissions.ts:
--------------------------------------------------------------------------------
 1 | import { PermissionsBitField, type PermissionResolvable } from 'discord.js';
 2 | import { CommandPreConditions } from '../types/Enums';
 3 | import type { PreconditionContainerArray } from '../utils/preconditions/PreconditionContainerArray';
 4 | 
 5 | /**
 6 |  * Appends the `UserPermissions` precondition when {@link Command.Options.requiredUserPermissions} resolves to a
 7 |  * non-zero bitfield.
 8 |  * @param requiredUserPermissions The required user permissions.
 9 |  * @param preconditionContainerArray The precondition container array to append the precondition to.
10 |  */
11 | export function parseConstructorPreConditionsRequiredUserPermissions(
12 | 	requiredUserPermissions: PermissionResolvable | undefined,
13 | 	preconditionContainerArray: PreconditionContainerArray
14 | ) {
15 | 	const permissions = new PermissionsBitField(requiredUserPermissions);
16 | 	if (permissions.bitfield !== 0n) {
17 | 		preconditionContainerArray.append({ name: CommandPreConditions.UserPermissions, context: { permissions } });
18 | 	}
19 | }
20 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/boolean.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../errors/Identifiers';
 3 | 
 4 | const baseTruths = ['1', 'true', '+', 't', 'yes', 'y'] as const;
 5 | const baseFalses = ['0', 'false', '-', 'f', 'no', 'n'] as const;
 6 | 
 7 | export function resolveBoolean(
 8 | 	parameter: string,
 9 | 	customs?: { truths?: readonly string[]; falses?: readonly string[] }
10 | ): Result {
11 | 	const boolean = parameter.toLowerCase();
12 | 
13 | 	if ([...baseTruths, ...(customs?.truths ?? [])].includes(boolean)) {
14 | 		return Result.ok(true);
15 | 	}
16 | 
17 | 	if ([...baseFalses, ...(customs?.falses ?? [])].includes(boolean)) {
18 | 		return Result.ok(false);
19 | 	}
20 | 
21 | 	return Result.err(Identifiers.ArgumentBooleanError);
22 | }
23 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/channel.ts:
--------------------------------------------------------------------------------
 1 | import { ChannelMentionRegex, type ChannelTypes } from '@sapphire/discord.js-utilities';
 2 | import { container } from '@sapphire/pieces';
 3 | import { Result } from '@sapphire/result';
 4 | import type { CommandInteraction, Message, Snowflake } from 'discord.js';
 5 | import { Identifiers } from '../errors/Identifiers';
 6 | 
 7 | export function resolveChannel(
 8 | 	parameter: string,
 9 | 	messageOrInteraction: Message | CommandInteraction
10 | ): Result {
11 | 	const channelId = (ChannelMentionRegex.exec(parameter)?.[1] ?? parameter) as Snowflake;
12 | 	const channel = (messageOrInteraction.guild ? messageOrInteraction.guild.channels : container.client.channels).cache.get(channelId);
13 | 
14 | 	if (channel) {
15 | 		return Result.ok(channel as ChannelTypes);
16 | 	}
17 | 
18 | 	return Result.err(Identifiers.ArgumentChannelError);
19 | }
20 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/date.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../errors/Identifiers';
 3 | 
 4 | export function resolveDate(
 5 | 	parameter: string,
 6 | 	options?: { minimum?: number; maximum?: number }
 7 | ): Result {
 8 | 	const parsed = new Date(parameter);
 9 | 
10 | 	const time = parsed.getTime();
11 | 
12 | 	if (Number.isNaN(time)) {
13 | 		return Result.err(Identifiers.ArgumentDateError);
14 | 	}
15 | 
16 | 	if (typeof options?.minimum === 'number' && time < options.minimum) {
17 | 		return Result.err(Identifiers.ArgumentDateTooEarly);
18 | 	}
19 | 
20 | 	if (typeof options?.maximum === 'number' && time > options.maximum) {
21 | 		return Result.err(Identifiers.ArgumentDateTooFar);
22 | 	}
23 | 
24 | 	return Result.ok(parsed);
25 | }
26 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/dmChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isDMChannel } from '@sapphire/discord.js-utilities';
 2 | import { Result } from '@sapphire/result';
 3 | import type { CommandInteraction, DMChannel, Message } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveChannel } from './channel';
 6 | 
 7 | export function resolveDMChannel(
 8 | 	parameter: string,
 9 | 	messageOrInteraction: Message | CommandInteraction
10 | ): Result {
11 | 	const result = resolveChannel(parameter, messageOrInteraction);
12 | 	return result.mapInto((value) => {
13 | 		if (isDMChannel(value) && !value.partial) {
14 | 			return Result.ok(value);
15 | 		}
16 | 
17 | 		return Result.err(Identifiers.ArgumentDMChannelError);
18 | 	});
19 | }
20 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/emoji.ts:
--------------------------------------------------------------------------------
 1 | import { EmojiRegex, createTwemojiRegex } from '@sapphire/discord-utilities';
 2 | import { Result } from '@sapphire/result';
 3 | import { parseEmoji } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | 
 6 | const TwemojiRegex = createTwemojiRegex();
 7 | 
 8 | export function resolveEmoji(parameter: string): Result {
 9 | 	const twemoji = TwemojiRegex.exec(parameter)?.[0] ?? null;
10 | 
11 | 	TwemojiRegex.lastIndex = 0;
12 | 
13 | 	if (twemoji) {
14 | 		return Result.ok({
15 | 			name: twemoji,
16 | 			id: null
17 | 		});
18 | 	}
19 | 
20 | 	const emojiId = EmojiRegex.test(parameter);
21 | 
22 | 	if (emojiId) {
23 | 		const resolved = parseEmoji(parameter) as EmojiObject | null;
24 | 
25 | 		if (resolved) {
26 | 			return Result.ok(resolved);
27 | 		}
28 | 	}
29 | 
30 | 	return Result.err(Identifiers.ArgumentEmojiError);
31 | }
32 | 
33 | export interface EmojiObject {
34 | 	name: string | null;
35 | 	id: string | null;
36 | 	animated?: boolean;
37 | }
38 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/enum.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../errors/Identifiers';
 3 | 
 4 | export function resolveEnum(
 5 | 	parameter: string,
 6 | 	options?: { enum?: string[]; caseInsensitive?: boolean }
 7 | ): Result {
 8 | 	if (!options?.enum?.length) {
 9 | 		return Result.err(Identifiers.ArgumentEnumEmptyError);
10 | 	}
11 | 
12 | 	if (!options.caseInsensitive && !options.enum.includes(parameter)) {
13 | 		return Result.err(Identifiers.ArgumentEnumError);
14 | 	}
15 | 
16 | 	if (options.caseInsensitive && !options.enum.some((v) => v.toLowerCase() === parameter.toLowerCase())) {
17 | 		return Result.err(Identifiers.ArgumentEnumError);
18 | 	}
19 | 
20 | 	return Result.ok(parameter);
21 | }
22 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/float.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../errors/Identifiers';
 3 | 
 4 | export function resolveFloat(
 5 | 	parameter: string,
 6 | 	options?: { minimum?: number; maximum?: number }
 7 | ): Result {
 8 | 	const parsed = Number(parameter);
 9 | 
10 | 	if (Number.isNaN(parsed)) {
11 | 		return Result.err(Identifiers.ArgumentFloatError);
12 | 	}
13 | 
14 | 	if (typeof options?.minimum === 'number' && parsed < options.minimum) {
15 | 		return Result.err(Identifiers.ArgumentFloatTooSmall);
16 | 	}
17 | 
18 | 	if (typeof options?.maximum === 'number' && parsed > options.maximum) {
19 | 		return Result.err(Identifiers.ArgumentFloatTooLarge);
20 | 	}
21 | 
22 | 	return Result.ok(parsed);
23 | }
24 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guild.ts:
--------------------------------------------------------------------------------
 1 | import { SnowflakeRegex } from '@sapphire/discord-utilities';
 2 | import { container } from '@sapphire/pieces';
 3 | import { Result } from '@sapphire/result';
 4 | import type { Guild } from 'discord.js';
 5 | import { Identifiers } from '../errors/Identifiers';
 6 | 
 7 | export async function resolveGuild(parameter: string): Promise> {
 8 | 	const guildId = SnowflakeRegex.exec(parameter)?.groups?.id;
 9 | 	const guild = guildId ? await container.client.guilds.fetch(guildId).catch(() => null) : null;
10 | 
11 | 	if (guild) {
12 | 		return Result.ok(guild);
13 | 	}
14 | 
15 | 	return Result.err(Identifiers.ArgumentGuildError);
16 | }
17 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildCategoryChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isCategoryChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { CategoryChannel, Guild } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildCategoryChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result {
11 | 	return resolveGuildChannelPredicate(parameter, guild, isCategoryChannel, Identifiers.ArgumentGuildCategoryChannelError);
12 | }
13 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildChannel.ts:
--------------------------------------------------------------------------------
 1 | import { ChannelMentionRegex, SnowflakeRegex } from '@sapphire/discord-utilities';
 2 | import type { GuildBasedChannelTypes } from '@sapphire/discord.js-utilities';
 3 | import { Result } from '@sapphire/result';
 4 | import type { Guild, Snowflake } from 'discord.js';
 5 | import { Identifiers } from '../errors/Identifiers';
 6 | 
 7 | export function resolveGuildChannel(parameter: string, guild: Guild): Result {
 8 | 	const channel = resolveById(parameter, guild) ?? resolveByQuery(parameter, guild);
 9 | 
10 | 	if (channel) {
11 | 		return Result.ok(channel);
12 | 	}
13 | 
14 | 	return Result.err(Identifiers.ArgumentGuildChannelError);
15 | }
16 | 
17 | function resolveById(argument: string, guild: Guild): GuildBasedChannelTypes | null {
18 | 	const channelId = ChannelMentionRegex.exec(argument) ?? SnowflakeRegex.exec(argument);
19 | 	return channelId ? ((guild.channels.cache.get(channelId[1] as Snowflake) as GuildBasedChannelTypes) ?? null) : null;
20 | }
21 | 
22 | function resolveByQuery(argument: string, guild: Guild): GuildBasedChannelTypes | null {
23 | 	const lowerCaseArgument = argument.toLowerCase();
24 | 	return (guild.channels.cache.find((channel) => channel.name.toLowerCase() === lowerCaseArgument) as GuildBasedChannelTypes) ?? null;
25 | }
26 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildNewsChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isNewsChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, NewsChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildNewsChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result {
11 | 	return resolveGuildChannelPredicate(parameter, guild, isNewsChannel, Identifiers.ArgumentGuildNewsChannelError);
12 | }
13 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildNewsThreadChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isNewsThreadChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, ThreadChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildNewsThreadChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result<
11 | 	ThreadChannel,
12 | 	Identifiers.ArgumentGuildChannelError | Identifiers.ArgumentGuildThreadChannelError | Identifiers.ArgumentGuildNewsThreadChannelError
13 | > {
14 | 	return resolveGuildChannelPredicate(parameter, guild, isNewsThreadChannel, Identifiers.ArgumentGuildNewsThreadChannelError);
15 | }
16 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildPrivateThreadChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isPrivateThreadChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, ThreadChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildPrivateThreadChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result<
11 | 	ThreadChannel,
12 | 	Identifiers.ArgumentGuildChannelError | Identifiers.ArgumentGuildThreadChannelError | Identifiers.ArgumentGuildPrivateThreadChannelError
13 | > {
14 | 	return resolveGuildChannelPredicate(parameter, guild, isPrivateThreadChannel, Identifiers.ArgumentGuildPrivateThreadChannelError);
15 | }
16 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildPublicThreadChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isPublicThreadChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, ThreadChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildPublicThreadChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result<
11 | 	ThreadChannel,
12 | 	Identifiers.ArgumentGuildChannelError | Identifiers.ArgumentGuildThreadChannelError | Identifiers.ArgumentGuildPublicThreadChannelError
13 | > {
14 | 	return resolveGuildChannelPredicate(parameter, guild, isPublicThreadChannel, Identifiers.ArgumentGuildPublicThreadChannelError);
15 | }
16 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildStageVoiceChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isStageChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, StageChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildStageVoiceChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result {
11 | 	return resolveGuildChannelPredicate(parameter, guild, isStageChannel, Identifiers.ArgumentGuildStageVoiceChannelError);
12 | }
13 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildTextChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isTextChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, TextChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildTextChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result {
11 | 	return resolveGuildChannelPredicate(parameter, guild, isTextChannel, Identifiers.ArgumentGuildTextChannelError);
12 | }
13 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildThreadChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isThreadChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, ThreadChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildThreadChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result {
11 | 	return resolveGuildChannelPredicate(parameter, guild, isThreadChannel, Identifiers.ArgumentGuildThreadChannelError);
12 | }
13 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/guildVoiceChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isVoiceChannel } from '@sapphire/discord.js-utilities';
 2 | import type { Result } from '@sapphire/result';
 3 | import type { Guild, VoiceChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveGuildChannelPredicate } from '../utils/resolvers/resolveGuildChannelPredicate';
 6 | 
 7 | export function resolveGuildVoiceChannel(
 8 | 	parameter: string,
 9 | 	guild: Guild
10 | ): Result {
11 | 	return resolveGuildChannelPredicate(parameter, guild, isVoiceChannel, Identifiers.ArgumentGuildVoiceChannelError);
12 | }
13 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/hyperlink.ts:
--------------------------------------------------------------------------------
1 | import { Result } from '@sapphire/result';
2 | import { URL } from 'node:url';
3 | import { Identifiers } from '../errors/Identifiers';
4 | 
5 | export function resolveHyperlink(parameter: string): Result {
6 | 	const result = Result.from(() => new URL(parameter));
7 | 	return result.mapErr(() => Identifiers.ArgumentHyperlinkError) as Result;
8 | }
9 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/index.ts:
--------------------------------------------------------------------------------
 1 | export * from './boolean';
 2 | export * from './channel';
 3 | export * from './date';
 4 | export * from './dmChannel';
 5 | export { resolveEmoji } from './emoji';
 6 | export * from './enum';
 7 | export * from './float';
 8 | export * from './guild';
 9 | export * from './guildCategoryChannel';
10 | export * from './guildChannel';
11 | export * from './guildNewsChannel';
12 | export * from './guildNewsThreadChannel';
13 | export * from './guildPrivateThreadChannel';
14 | export * from './guildPublicThreadChannel';
15 | export * from './guildStageVoiceChannel';
16 | export * from './guildTextChannel';
17 | export * from './guildThreadChannel';
18 | export * from './guildVoiceChannel';
19 | export * from './hyperlink';
20 | export * from './integer';
21 | export * from './member';
22 | export { resolveMessage } from './message';
23 | export * from './number';
24 | export * from './partialDMChannel';
25 | export * from './role';
26 | export * from './string';
27 | export * from './user';
28 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/integer.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../errors/Identifiers';
 3 | 
 4 | export function resolveInteger(
 5 | 	parameter: string,
 6 | 	options?: { minimum?: number; maximum?: number }
 7 | ): Result {
 8 | 	const parsed = Number(parameter);
 9 | 
10 | 	if (!Number.isInteger(parsed)) {
11 | 		return Result.err(Identifiers.ArgumentIntegerError);
12 | 	}
13 | 
14 | 	if (typeof options?.minimum === 'number' && parsed < options.minimum) {
15 | 		return Result.err(Identifiers.ArgumentIntegerTooSmall);
16 | 	}
17 | 
18 | 	if (typeof options?.maximum === 'number' && parsed > options.maximum) {
19 | 		return Result.err(Identifiers.ArgumentIntegerTooLarge);
20 | 	}
21 | 
22 | 	return Result.ok(parsed);
23 | }
24 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/member.ts:
--------------------------------------------------------------------------------
 1 | import { SnowflakeRegex, UserOrMemberMentionRegex } from '@sapphire/discord-utilities';
 2 | import { Result } from '@sapphire/result';
 3 | import { isNullish } from '@sapphire/utilities';
 4 | import type { Guild, GuildMember, Snowflake } from 'discord.js';
 5 | import { Identifiers } from '../errors/Identifiers';
 6 | 
 7 | export async function resolveMember(
 8 | 	parameter: string,
 9 | 	guild: Guild,
10 | 	performFuzzySearch?: boolean
11 | ): Promise> {
12 | 	let member = await resolveById(parameter, guild);
13 | 
14 | 	if (isNullish(member) && performFuzzySearch) {
15 | 		member = await resolveByQuery(parameter, guild);
16 | 	}
17 | 
18 | 	if (member) {
19 | 		return Result.ok(member);
20 | 	}
21 | 
22 | 	return Result.err(Identifiers.ArgumentMemberError);
23 | }
24 | 
25 | async function resolveById(argument: string, guild: Guild): Promise {
26 | 	const memberId = UserOrMemberMentionRegex.exec(argument) ?? SnowflakeRegex.exec(argument);
27 | 	return memberId ? guild.members.fetch(memberId[1] as Snowflake).catch(() => null) : null;
28 | }
29 | 
30 | async function resolveByQuery(argument: string, guild: Guild): Promise {
31 | 	argument = argument.length > 5 && argument.at(-5) === '#' ? argument.slice(0, -5) : argument;
32 | 
33 | 	const members = await guild.members.fetch({ query: argument, limit: 1 }).catch(() => null);
34 | 	return members?.first() ?? null;
35 | }
36 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/number.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../errors/Identifiers';
 3 | 
 4 | export function resolveNumber(
 5 | 	parameter: string,
 6 | 	options?: { minimum?: number; maximum?: number }
 7 | ): Result {
 8 | 	const parsed = Number(parameter);
 9 | 	if (Number.isNaN(parsed)) {
10 | 		return Result.err(Identifiers.ArgumentNumberError);
11 | 	}
12 | 
13 | 	if (typeof options?.minimum === 'number' && parsed < options.minimum) {
14 | 		return Result.err(Identifiers.ArgumentNumberTooSmall);
15 | 	}
16 | 
17 | 	if (typeof options?.maximum === 'number' && parsed > options.maximum) {
18 | 		return Result.err(Identifiers.ArgumentNumberTooLarge);
19 | 	}
20 | 
21 | 	return Result.ok(parsed);
22 | }
23 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/partialDMChannel.ts:
--------------------------------------------------------------------------------
 1 | import { isDMChannel } from '@sapphire/discord.js-utilities';
 2 | import { Result } from '@sapphire/result';
 3 | import type { DMChannel, Message, PartialDMChannel } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | import { resolveChannel } from './channel';
 6 | 
 7 | export function resolvePartialDMChannel(
 8 | 	parameter: string,
 9 | 	message: Message
10 | ): Result {
11 | 	const result = resolveChannel(parameter, message);
12 | 	return result.mapInto((channel) => {
13 | 		if (isDMChannel(channel)) {
14 | 			return Result.ok(channel);
15 | 		}
16 | 
17 | 		return Result.err(Identifiers.ArgumentDMChannelError);
18 | 	});
19 | }
20 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/role.ts:
--------------------------------------------------------------------------------
 1 | import { RoleMentionRegex, SnowflakeRegex } from '@sapphire/discord-utilities';
 2 | import { Result } from '@sapphire/result';
 3 | import type { Guild, Role, Snowflake } from 'discord.js';
 4 | import { Identifiers } from '../errors/Identifiers';
 5 | 
 6 | export async function resolveRole(parameter: string, guild: Guild): Promise> {
 7 | 	const role = (await resolveById(parameter, guild)) ?? resolveByQuery(parameter, guild);
 8 | 
 9 | 	if (role) {
10 | 		return Result.ok(role);
11 | 	}
12 | 
13 | 	return Result.err(Identifiers.ArgumentRoleError);
14 | }
15 | 
16 | async function resolveById(argument: string, guild: Guild): Promise {
17 | 	const roleId = RoleMentionRegex.exec(argument) ?? SnowflakeRegex.exec(argument);
18 | 	return roleId ? guild.roles.fetch(roleId[1] as Snowflake) : null;
19 | }
20 | 
21 | function resolveByQuery(argument: string, guild: Guild): Role | null {
22 | 	const lowerCaseArgument = argument.toLowerCase();
23 | 	return guild.roles.cache.find((role) => role.name.toLowerCase() === lowerCaseArgument) ?? null;
24 | }
25 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/string.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../errors/Identifiers';
 3 | 
 4 | export function resolveString(
 5 | 	parameter: string,
 6 | 	options?: { minimum?: number; maximum?: number }
 7 | ): Result {
 8 | 	if (typeof options?.minimum === 'number' && parameter.length < options.minimum) {
 9 | 		return Result.err(Identifiers.ArgumentStringTooShort);
10 | 	}
11 | 
12 | 	if (typeof options?.maximum === 'number' && parameter.length > options.maximum) {
13 | 		return Result.err(Identifiers.ArgumentStringTooLong);
14 | 	}
15 | 
16 | 	return Result.ok(parameter);
17 | }
18 | 
--------------------------------------------------------------------------------
/src/lib/resolvers/user.ts:
--------------------------------------------------------------------------------
 1 | import { SnowflakeRegex, UserOrMemberMentionRegex } from '@sapphire/discord-utilities';
 2 | import { container } from '@sapphire/pieces';
 3 | import { Result } from '@sapphire/result';
 4 | import type { Snowflake, User } from 'discord.js';
 5 | import { Identifiers } from '../errors/Identifiers';
 6 | 
 7 | export async function resolveUser(parameter: string): Promise> {
 8 | 	const userId = UserOrMemberMentionRegex.exec(parameter) ?? SnowflakeRegex.exec(parameter);
 9 | 	const user = userId ? await container.client.users.fetch(userId[1] as Snowflake).catch(() => null) : null;
10 | 
11 | 	if (user) {
12 | 		return Result.ok(user);
13 | 	}
14 | 
15 | 	return Result.err(Identifiers.ArgumentUserError);
16 | }
17 | 
--------------------------------------------------------------------------------
/src/lib/structures/ArgumentStore.ts:
--------------------------------------------------------------------------------
1 | import { AliasStore } from '@sapphire/pieces';
2 | import { Argument } from './Argument';
3 | 
4 | export class ArgumentStore extends AliasStore {
5 | 	public constructor() {
6 | 		super(Argument, { name: 'arguments' });
7 | 	}
8 | }
9 | 
--------------------------------------------------------------------------------
/src/lib/structures/ListenerLoaderStrategy.ts:
--------------------------------------------------------------------------------
 1 | import { LoaderStrategy } from '@sapphire/pieces';
 2 | import type { Listener } from './Listener';
 3 | import type { ListenerStore } from './ListenerStore';
 4 | 
 5 | export class ListenerLoaderStrategy extends LoaderStrategy {
 6 | 	public override onLoad(_store: ListenerStore, piece: Listener) {
 7 | 		const listenerCallback = piece['_listener'];
 8 | 		if (listenerCallback) {
 9 | 			const emitter = piece.emitter!;
10 | 
11 | 			// Increment the maximum amount of listeners by one:
12 | 			const maxListeners = emitter.getMaxListeners();
13 | 			if (maxListeners !== 0) emitter.setMaxListeners(maxListeners + 1);
14 | 
15 | 			emitter[piece.once ? 'once' : 'on'](piece.event, listenerCallback);
16 | 		}
17 | 	}
18 | 
19 | 	public override onUnload(_store: ListenerStore, piece: Listener) {
20 | 		const listenerCallback = piece['_listener'];
21 | 		if (!piece.once && listenerCallback) {
22 | 			const emitter = piece.emitter!;
23 | 
24 | 			// Increment the maximum amount of listeners by one:
25 | 			const maxListeners = emitter.getMaxListeners();
26 | 			if (maxListeners !== 0) emitter.setMaxListeners(maxListeners - 1);
27 | 
28 | 			emitter.off(piece.event, listenerCallback);
29 | 			piece['_listener'] = null;
30 | 		}
31 | 	}
32 | }
33 | 
--------------------------------------------------------------------------------
/src/lib/structures/ListenerStore.ts:
--------------------------------------------------------------------------------
 1 | import { Store } from '@sapphire/pieces';
 2 | import { Listener } from './Listener';
 3 | import { ListenerLoaderStrategy } from './ListenerLoaderStrategy';
 4 | 
 5 | export class ListenerStore extends Store {
 6 | 	public constructor() {
 7 | 		super(Listener, { name: 'listeners', strategy: new ListenerLoaderStrategy() });
 8 | 	}
 9 | }
10 | 
--------------------------------------------------------------------------------
/src/lib/types/ArgumentContexts.ts:
--------------------------------------------------------------------------------
 1 | import type { MessageResolverOptions } from '../resolvers/message';
 2 | import type { Argument } from '../structures/Argument';
 3 | 
 4 | /**
 5 |  * The context for the `'enum'` argument.
 6 |  * @since 4.2.0 (🌿)
 7 |  */
 8 | export interface EnumArgumentContext extends Argument.Context {
 9 | 	readonly enum?: string[];
10 | 	readonly caseInsensitive?: boolean;
11 | }
12 | 
13 | /**
14 |  * The context for the `'boolean'` argument.
15 |  * @since 4.2.0 (🌿)
16 |  */
17 | export interface BooleanArgumentContext extends Argument.Context {
18 | 	/**
19 | 	 * The words that resolve to `true`.
20 | 	 * Any words added to this array will be merged with the words:
21 | 	 * ```ts
22 | 	 * ['1', 'true', '+', 't', 'yes', 'y']
23 | 	 * ```
24 | 	 */
25 | 	readonly truths?: string[];
26 | 	/**
27 | 	 * The words that resolve to `false`.
28 | 	 * Any words added to this array will be merged with the words:
29 | 	 * ```ts
30 | 	 * ['0', 'false', '-', 'f', 'no', 'n']
31 | 	 * ```
32 | 	 */
33 | 	readonly falses?: string[];
34 | }
35 | 
36 | /**
37 |  * The context for the `'member'` argument.
38 |  * @since 4.2.0 (🌿)
39 |  */
40 | export interface MemberArgumentContext extends Argument.Context {
41 | 	/**
42 | 	 * Whether to perform a fuzzy search with the given argument.
43 | 	 * This will leverage `FetchMembersOptions.query` to do the fuzzy searching.
44 | 	 * @default true
45 | 	 */
46 | 	readonly performFuzzySearch?: boolean;
47 | }
48 | 
49 | /**
50 |  * The context for the `'message'` argument.
51 |  * @since 4.2.0 (🌿)
52 |  */
53 | export type MessageArgumentContext = Omit & Argument.Context;
54 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/_shared.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 | 	ApplicationCommandOptionType,
 3 | 	ApplicationCommandType,
 4 | 	type APIApplicationCommandChannelOption,
 5 | 	type APIApplicationCommandIntegerOption,
 6 | 	type APIApplicationCommandNumberOption,
 7 | 	type APIApplicationCommandOption,
 8 | 	type APIApplicationCommandStringOption,
 9 | 	type APIApplicationCommandSubcommandGroupOption,
10 | 	type APIApplicationCommandSubcommandOption
11 | } from 'discord-api-types/v10';
12 | 
13 | export const optionTypeToPrettyName = new Map([
14 | 	[ApplicationCommandOptionType.Subcommand, 'subcommand'],
15 | 	[ApplicationCommandOptionType.SubcommandGroup, 'subcommand group'],
16 | 	[ApplicationCommandOptionType.String, 'string option'],
17 | 	[ApplicationCommandOptionType.Integer, 'integer option'],
18 | 	[ApplicationCommandOptionType.Boolean, 'boolean option'],
19 | 	[ApplicationCommandOptionType.User, 'user option'],
20 | 	[ApplicationCommandOptionType.Channel, 'channel option'],
21 | 	[ApplicationCommandOptionType.Role, 'role option'],
22 | 	[ApplicationCommandOptionType.Mentionable, 'mentionable option'],
23 | 	[ApplicationCommandOptionType.Number, 'number option'],
24 | 	[ApplicationCommandOptionType.Attachment, 'attachment option']
25 | ]);
26 | 
27 | export const contextMenuTypes = [ApplicationCommandType.Message, ApplicationCommandType.User];
28 | export const subcommandTypes = [ApplicationCommandOptionType.SubcommandGroup, ApplicationCommandOptionType.Subcommand];
29 | 
30 | export type APIApplicationCommandSubcommandTypes = APIApplicationCommandSubcommandOption | APIApplicationCommandSubcommandGroupOption;
31 | export type APIApplicationCommandMinAndMaxValueTypes = APIApplicationCommandIntegerOption | APIApplicationCommandNumberOption;
32 | export type APIApplicationCommandChoosableAndAutocompletableTypes = APIApplicationCommandMinAndMaxValueTypes | APIApplicationCommandStringOption;
33 | export type APIApplicationCommandMinMaxLengthTypes = APIApplicationCommandStringOption;
34 | 
35 | export function hasMinMaxValueSupport(option: APIApplicationCommandOption): option is APIApplicationCommandMinAndMaxValueTypes {
36 | 	return [ApplicationCommandOptionType.Integer, ApplicationCommandOptionType.Number].includes(option.type);
37 | }
38 | 
39 | export function hasChoicesAndAutocompleteSupport(
40 | 	option: APIApplicationCommandOption
41 | ): option is APIApplicationCommandChoosableAndAutocompletableTypes {
42 | 	return [
43 | 		ApplicationCommandOptionType.Integer, //
44 | 		ApplicationCommandOptionType.Number,
45 | 		ApplicationCommandOptionType.String
46 | 	].includes(option.type);
47 | }
48 | 
49 | export function hasMinMaxLengthSupport(option: APIApplicationCommandOption): option is APIApplicationCommandMinMaxLengthTypes {
50 | 	return option.type === ApplicationCommandOptionType.String;
51 | }
52 | 
53 | export function hasChannelTypesSupport(option: APIApplicationCommandOption): option is APIApplicationCommandChannelOption {
54 | 	return option.type === ApplicationCommandOptionType.Channel;
55 | }
56 | 
57 | export interface CommandDifference {
58 | 	key: string;
59 | 	expected: string;
60 | 	original: string;
61 | }
62 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/contexts.ts:
--------------------------------------------------------------------------------
 1 | import type { InteractionContextType } from 'discord.js';
 2 | import type { CommandDifference } from './_shared';
 3 | 
 4 | export function* checkInteractionContextTypes(
 5 | 	existingContexts?: InteractionContextType[],
 6 | 	newContexts?: InteractionContextType[]
 7 | ): Generator {
 8 | 	// 0. No existing contexts and now we have contexts
 9 | 	if (!existingContexts && newContexts?.length) {
10 | 		yield {
11 | 			key: 'contexts',
12 | 			original: 'no contexts present',
13 | 			expected: 'contexts present'
14 | 		};
15 | 	}
16 | 	// 1. Existing contexts and now we have no contexts
17 | 	else if (existingContexts?.length && !newContexts?.length) {
18 | 		yield {
19 | 			key: 'contexts',
20 | 			original: 'contexts present',
21 | 			expected: 'no contexts present'
22 | 		};
23 | 	}
24 | 	// 2. Maybe changes in order or additions, log
25 | 	else if (newContexts?.length) {
26 | 		let index = 0;
27 | 
28 | 		for (const newContext of newContexts) {
29 | 			const currentIndex = index++;
30 | 
31 | 			if (existingContexts![currentIndex] !== newContext) {
32 | 				yield {
33 | 					key: `contexts[${currentIndex}]`,
34 | 					original: `contexts type ${existingContexts?.[currentIndex]}`,
35 | 					expected: `contexts type ${newContext}`
36 | 				};
37 | 			}
38 | 		}
39 | 
40 | 		if (index < existingContexts!.length) {
41 | 			let type: InteractionContextType;
42 | 
43 | 			while ((type = existingContexts![index]) !== undefined) {
44 | 				yield {
45 | 					key: `contexts[${index}]`,
46 | 					original: `context ${type} present`,
47 | 					expected: `no context present`
48 | 				};
49 | 
50 | 				index++;
51 | 			}
52 | 		}
53 | 	}
54 | }
55 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/default_member_permissions.ts:
--------------------------------------------------------------------------------
 1 | import type { CommandDifference } from './_shared';
 2 | 
 3 | export function* checkDefaultMemberPermissions(oldPermissions?: string | null, newPermissions?: string | null): Generator {
 4 | 	if (oldPermissions !== newPermissions) {
 5 | 		yield {
 6 | 			key: 'defaultMemberPermissions',
 7 | 			original: String(oldPermissions),
 8 | 			expected: String(newPermissions)
 9 | 		};
10 | 	}
11 | }
12 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/description.ts:
--------------------------------------------------------------------------------
 1 | import type { CommandDifference } from './_shared';
 2 | 
 3 | export function* checkDescription({
 4 | 	oldDescription,
 5 | 	newDescription,
 6 | 	key = 'description'
 7 | }: {
 8 | 	oldDescription: string;
 9 | 	newDescription: string;
10 | 	key?: string;
11 | }): Generator {
12 | 	if (oldDescription !== newDescription) {
13 | 		yield {
14 | 			key,
15 | 			original: oldDescription,
16 | 			expected: newDescription
17 | 		};
18 | 	}
19 | }
20 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/dm_permission.ts:
--------------------------------------------------------------------------------
 1 | import type { CommandDifference } from './_shared';
 2 | 
 3 | export function* checkDMPermission(oldDmPermission?: boolean, newDmPermission?: boolean): Generator {
 4 | 	if ((oldDmPermission ?? true) !== (newDmPermission ?? true)) {
 5 | 		yield {
 6 | 			key: 'dmPermission',
 7 | 			original: String(oldDmPermission ?? true),
 8 | 			expected: String(newDmPermission ?? true)
 9 | 		};
10 | 	}
11 | }
12 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/integration_types.ts:
--------------------------------------------------------------------------------
 1 | import type { ApplicationIntegrationType } from 'discord.js';
 2 | import type { CommandDifference } from './_shared';
 3 | 
 4 | export function* checkIntegrationTypes(
 5 | 	existingIntegrationTypes?: ApplicationIntegrationType[],
 6 | 	newIntegrationTypes?: ApplicationIntegrationType[]
 7 | ): Generator {
 8 | 	// 0. No existing integration types and now we have integration types
 9 | 	if (!existingIntegrationTypes?.length && newIntegrationTypes?.length) {
10 | 		yield {
11 | 			key: 'integrationTypes',
12 | 			original: 'no integration types present',
13 | 			expected: 'integration types present'
14 | 		};
15 | 	}
16 | 	// 1. Existing integration types and now we have no integration types
17 | 	else if (existingIntegrationTypes?.length && !newIntegrationTypes?.length) {
18 | 		yield {
19 | 			key: 'integrationTypes',
20 | 			original: 'integration types present',
21 | 			expected: 'no integration types present'
22 | 		};
23 | 	}
24 | 	// 2. Maybe changes in order or additions, log
25 | 	else if (newIntegrationTypes?.length) {
26 | 		let index = 0;
27 | 
28 | 		for (const newIntegrationType of newIntegrationTypes) {
29 | 			const currentIndex = index++;
30 | 
31 | 			if (existingIntegrationTypes![currentIndex] !== newIntegrationType) {
32 | 				yield {
33 | 					key: `integrationTypes[${currentIndex}]`,
34 | 					original: `integration type ${existingIntegrationTypes?.[currentIndex]}`,
35 | 					expected: `integration type ${newIntegrationType}`
36 | 				};
37 | 			}
38 | 		}
39 | 
40 | 		if (index < existingIntegrationTypes!.length) {
41 | 			let type: ApplicationIntegrationType;
42 | 
43 | 			while ((type = existingIntegrationTypes![index]) !== undefined) {
44 | 				yield {
45 | 					key: `integrationTypes[${index}]`,
46 | 					original: `integration type ${type} present`,
47 | 					expected: 'no integration type present'
48 | 				};
49 | 
50 | 				index++;
51 | 			}
52 | 		}
53 | 	}
54 | }
55 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/localizations.ts:
--------------------------------------------------------------------------------
 1 | import type { LocalizationMap } from 'discord-api-types/v10';
 2 | import type { CommandDifference } from './_shared';
 3 | 
 4 | export function* checkLocalizations({
 5 | 	localeMapName,
 6 | 	localePresentMessage,
 7 | 	localeMissingMessage,
 8 | 	originalLocalizedDescriptions,
 9 | 	expectedLocalizedDescriptions
10 | }: {
11 | 	localeMapName: string;
12 | 	localePresentMessage: string;
13 | 	localeMissingMessage: string;
14 | 	originalLocalizedDescriptions?: LocalizationMap | null;
15 | 	expectedLocalizedDescriptions?: LocalizationMap | null;
16 | }) {
17 | 	if (!originalLocalizedDescriptions && expectedLocalizedDescriptions) {
18 | 		yield {
19 | 			key: localeMapName,
20 | 			original: localeMissingMessage,
21 | 			expected: localePresentMessage
22 | 		};
23 | 	} else if (originalLocalizedDescriptions && !expectedLocalizedDescriptions) {
24 | 		yield {
25 | 			key: localeMapName,
26 | 			original: localePresentMessage,
27 | 			expected: localeMissingMessage
28 | 		};
29 | 	} else if (originalLocalizedDescriptions && expectedLocalizedDescriptions) {
30 | 		yield* reportLocalizationMapDifferences(originalLocalizedDescriptions, expectedLocalizedDescriptions, localeMapName);
31 | 	}
32 | }
33 | 
34 | function* reportLocalizationMapDifferences(
35 | 	originalMap: LocalizationMap,
36 | 	expectedMap: LocalizationMap,
37 | 	mapName: string
38 | ): Generator {
39 | 	const originalLocalizations = new Map(Object.entries(originalMap));
40 | 
41 | 	for (const [key, value] of Object.entries(expectedMap)) {
42 | 		const possiblyExistingEntry = originalLocalizations.get(key) as string | undefined;
43 | 		originalLocalizations.delete(key);
44 | 
45 | 		const wasMissingBefore = typeof possiblyExistingEntry === 'undefined';
46 | 		const isResetNow = value === null;
47 | 
48 | 		// Was missing before and now is present
49 | 		if (wasMissingBefore && !isResetNow) {
50 | 			yield {
51 | 				key: `${mapName}.${key}`,
52 | 				original: 'no localization present',
53 | 				expected: value
54 | 			};
55 | 		}
56 | 		// Was present before and now is reset
57 | 		else if (!wasMissingBefore && isResetNow) {
58 | 			yield {
59 | 				key: `${mapName}.${key}`,
60 | 				original: possiblyExistingEntry,
61 | 				expected: 'no localization present'
62 | 			};
63 | 		}
64 | 		// Not equal
65 | 		// eslint-disable-next-line no-negated-condition
66 | 		else if (possiblyExistingEntry !== value) {
67 | 			yield {
68 | 				key: `${mapName}.${key}`,
69 | 				original: String(possiblyExistingEntry),
70 | 				expected: String(value)
71 | 			};
72 | 		}
73 | 	}
74 | 
75 | 	// Report any remaining localizations
76 | 	for (const [key, value] of originalLocalizations) {
77 | 		if (value) {
78 | 			yield {
79 | 				key: `${mapName}.${key}`,
80 | 				original: value,
81 | 				expected: 'no localization present'
82 | 			};
83 | 		}
84 | 	}
85 | }
86 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/name.ts:
--------------------------------------------------------------------------------
 1 | import type { CommandDifference } from './_shared';
 2 | 
 3 | export function* checkName({ oldName, newName, key = 'name' }: { oldName: string; newName: string; key?: string }): Generator {
 4 | 	if (oldName !== newName) {
 5 | 		yield {
 6 | 			key,
 7 | 			original: oldName,
 8 | 			expected: newName
 9 | 		};
10 | 	}
11 | }
12 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/option/minMaxLength.ts:
--------------------------------------------------------------------------------
 1 | import type { APIApplicationCommandStringOption } from 'discord-api-types/v10';
 2 | import type { CommandDifference } from '../_shared';
 3 | 
 4 | export function* handleMinMaxLengthOptions({
 5 | 	currentIndex,
 6 | 	existingOption,
 7 | 	expectedOption,
 8 | 	keyPath
 9 | }: {
10 | 	currentIndex: number;
11 | 	keyPath: (index: number) => string;
12 | 	expectedOption: APIApplicationCommandStringOption;
13 | 	existingOption: APIApplicationCommandStringOption;
14 | }): Generator {
15 | 	// 0. No min_length and now we have min_length
16 | 	if (existingOption.min_length === undefined && expectedOption.min_length !== undefined) {
17 | 		yield {
18 | 			key: `${keyPath(currentIndex)}.min_length`,
19 | 			expected: 'min_length present',
20 | 			original: 'no min_length present'
21 | 		};
22 | 	}
23 | 	// 1. Have min_length and now we don't
24 | 	else if (existingOption.min_length !== undefined && expectedOption.min_length === undefined) {
25 | 		yield {
26 | 			key: `${keyPath(currentIndex)}.min_length`,
27 | 			expected: 'no min_length present',
28 | 			original: 'min_length present'
29 | 		};
30 | 	}
31 | 	// 2. Equality check
32 | 	else if (existingOption.min_length !== expectedOption.min_length) {
33 | 		yield {
34 | 			key: `${keyPath(currentIndex)}.min_length`,
35 | 			original: String(existingOption.min_length),
36 | 			expected: String(expectedOption.min_length)
37 | 		};
38 | 	}
39 | 
40 | 	// 0. No max_length and now we have max_length
41 | 	if (existingOption.max_length === undefined && expectedOption.max_length !== undefined) {
42 | 		yield {
43 | 			key: `${keyPath(currentIndex)}.max_length`,
44 | 			expected: 'max_length present',
45 | 			original: 'no max_length present'
46 | 		};
47 | 	}
48 | 	// 1. Have max_length and now we don't
49 | 	else if (existingOption.max_length !== undefined && expectedOption.max_length === undefined) {
50 | 		yield {
51 | 			key: `${keyPath(currentIndex)}.max_length`,
52 | 			expected: 'no max_length present',
53 | 			original: 'max_length present'
54 | 		};
55 | 	}
56 | 	// 2. Equality check
57 | 	else if (existingOption.max_length !== expectedOption.max_length) {
58 | 		yield {
59 | 			key: `${keyPath(currentIndex)}.max_length`,
60 | 			original: String(existingOption.max_length),
61 | 			expected: String(expectedOption.max_length)
62 | 		};
63 | 	}
64 | }
65 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/option/minMaxValue.ts:
--------------------------------------------------------------------------------
 1 | import type { APIApplicationCommandMinAndMaxValueTypes, CommandDifference } from '../_shared';
 2 | 
 3 | export function* handleMinMaxValueOptions({
 4 | 	currentIndex,
 5 | 	existingOption,
 6 | 	expectedOption,
 7 | 	keyPath
 8 | }: {
 9 | 	currentIndex: number;
10 | 	keyPath: (index: number) => string;
11 | 	expectedOption: APIApplicationCommandMinAndMaxValueTypes;
12 | 	existingOption: APIApplicationCommandMinAndMaxValueTypes;
13 | }): Generator {
14 | 	// 0. No min_value and now we have min_value
15 | 	if (existingOption.min_value === undefined && expectedOption.min_value !== undefined) {
16 | 		yield {
17 | 			key: `${keyPath(currentIndex)}.min_value`,
18 | 			expected: 'min_value present',
19 | 			original: 'no min_value present'
20 | 		};
21 | 	}
22 | 	// 1. Have min_value and now we don't
23 | 	else if (existingOption.min_value !== undefined && expectedOption.min_value === undefined) {
24 | 		yield {
25 | 			key: `${keyPath(currentIndex)}.min_value`,
26 | 			expected: 'no min_value present',
27 | 			original: 'min_value present'
28 | 		};
29 | 	}
30 | 	// 2. Equality check
31 | 	else if (existingOption.min_value !== expectedOption.min_value) {
32 | 		yield {
33 | 			key: `${keyPath(currentIndex)}.min_value`,
34 | 			original: String(existingOption.min_value),
35 | 			expected: String(expectedOption.min_value)
36 | 		};
37 | 	}
38 | 
39 | 	// 0. No max_value and now we have max_value
40 | 	if (existingOption.max_value === undefined && expectedOption.max_value !== undefined) {
41 | 		yield {
42 | 			key: `${keyPath(currentIndex)}.max_value`,
43 | 			expected: 'max_value present',
44 | 			original: 'no max_value present'
45 | 		};
46 | 	}
47 | 	// 1. Have max_value and now we don't
48 | 	else if (existingOption.max_value !== undefined && expectedOption.max_value === undefined) {
49 | 		yield {
50 | 			key: `${keyPath(currentIndex)}.max_value`,
51 | 			expected: 'no max_value present',
52 | 			original: 'max_value present'
53 | 		};
54 | 	}
55 | 	// 2. Equality check
56 | 	else if (existingOption.max_value !== expectedOption.max_value) {
57 | 		yield {
58 | 			key: `${keyPath(currentIndex)}.max_value`,
59 | 			original: String(existingOption.max_value),
60 | 			expected: String(expectedOption.max_value)
61 | 		};
62 | 	}
63 | }
64 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/option/required.ts:
--------------------------------------------------------------------------------
 1 | import type { CommandDifference } from '../_shared';
 2 | 
 3 | export function* checkOptionRequired({
 4 | 	oldRequired,
 5 | 	newRequired,
 6 | 	key
 7 | }: {
 8 | 	oldRequired?: boolean;
 9 | 	newRequired?: boolean;
10 | 	key: string;
11 | }): Generator {
12 | 	if ((oldRequired ?? false) !== (newRequired ?? false)) {
13 | 		yield {
14 | 			key,
15 | 			original: String(oldRequired ?? false),
16 | 			expected: String(newRequired ?? false)
17 | 		};
18 | 	}
19 | }
20 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/compute-differences/option/type.ts:
--------------------------------------------------------------------------------
 1 | import type { ApplicationCommandOptionType } from 'discord-api-types/v10';
 2 | import { optionTypeToPrettyName, type CommandDifference } from '../_shared';
 3 | 
 4 | export function* checkOptionType({
 5 | 	key,
 6 | 	expectedType,
 7 | 	originalType
 8 | }: {
 9 | 	key: string;
10 | 	originalType: ApplicationCommandOptionType;
11 | 	expectedType: ApplicationCommandOptionType;
12 | }): Generator {
13 | 	const expectedTypeString =
14 | 		optionTypeToPrettyName.get(expectedType) ?? `unknown (${expectedType}); please contact Sapphire developers about this!`;
15 | 
16 | 	if (originalType !== expectedType) {
17 | 		yield {
18 | 			key,
19 | 			original: optionTypeToPrettyName.get(originalType) ?? `unknown (${originalType}); please contact Sapphire developers about this!`,
20 | 			expected: expectedTypeString
21 | 		};
22 | 	}
23 | }
24 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/getNeededParameters.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ApplicationCommand, ApplicationCommandManager, Collection } from 'discord.js';
 3 | 
 4 | export async function getNeededRegistryParameters(guildIds: Set = new Set()) {
 5 | 	const { client } = container;
 6 | 
 7 | 	const applicationCommands = client.application!.commands;
 8 | 	const globalCommands = await applicationCommands.fetch({ withLocalizations: true });
 9 | 	const guildCommands = await fetchGuildCommands(applicationCommands, guildIds);
10 | 
11 | 	return {
12 | 		applicationCommands,
13 | 		globalCommands,
14 | 		guildCommands
15 | 	};
16 | }
17 | 
18 | async function fetchGuildCommands(commands: ApplicationCommandManager, guildIds: Set) {
19 | 	const map = new Map>();
20 | 
21 | 	for (const guildId of guildIds) {
22 | 		try {
23 | 			const guildCommands = await commands.fetch({ guildId, withLocalizations: true });
24 | 			map.set(guildId, guildCommands);
25 | 		} catch (err) {
26 | 			const { preventFailedToFetchLogForGuilds } = container.client.options;
27 | 
28 | 			if (preventFailedToFetchLogForGuilds === true) continue;
29 | 
30 | 			if (Array.isArray(preventFailedToFetchLogForGuilds) && !preventFailedToFetchLogForGuilds?.includes(guildId)) {
31 | 				const guild = container.client.guilds.resolve(guildId) ?? { name: 'Guild not in cache' };
32 | 				container.logger.warn(
33 | 					`ApplicationCommandRegistries: Failed to fetch guild commands for guild "${guild.name}" (${guildId}).`,
34 | 					'Make sure to authorize your application with the "applications.commands" scope in that guild.'
35 | 				);
36 | 			}
37 | 		}
38 | 	}
39 | 
40 | 	return map;
41 | }
42 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/registriesErrors.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { Command } from '../../structures/Command';
 3 | import { Events } from '../../types/Events';
 4 | import { bulkOverwriteError } from './registriesLog';
 5 | 
 6 | /**
 7 |  * Opinionatedly logs the encountered registry error.
 8 |  * @param error The emitted error
 9 |  * @param command The command which had the error
10 |  */
11 | export function emitPerRegistryError(error: unknown, command: Command) {
12 | 	const { name, location } = command;
13 | 	const { client, logger } = container;
14 | 
15 | 	if (client.listenerCount(Events.CommandApplicationCommandRegistryError)) {
16 | 		client.emit(Events.CommandApplicationCommandRegistryError, error, command);
17 | 	} else {
18 | 		logger.error(
19 | 			`Encountered error while handling the command application command registry for command "${name}" at path "${location.full}"`,
20 | 			error
21 | 		);
22 | 	}
23 | }
24 | 
25 | /**
26 |  * Opinionatedly logs any bulk overwrite registries error.
27 |  * @param error The emitted error
28 |  * @param guildId The guild id in which the error was caused
29 |  */
30 | export function emitBulkOverwriteError(error: unknown, guildId: string | null) {
31 | 	const { client } = container;
32 | 
33 | 	if (client.listenerCount(Events.ApplicationCommandRegistriesBulkOverwriteError)) {
34 | 		client.emit(Events.ApplicationCommandRegistriesBulkOverwriteError, error, guildId);
35 | 	} else if (guildId) {
36 | 		bulkOverwriteError(`Failed to overwrite guild application commands for guild ${guildId}`, error);
37 | 	} else {
38 | 		bulkOverwriteError(`Failed to overwrite global application commands`, error);
39 | 	}
40 | }
41 | 
--------------------------------------------------------------------------------
/src/lib/utils/application-commands/registriesLog.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | 
 3 | export function bulkOverwriteInfo(message: string, ...other: unknown[]) {
 4 | 	container.logger.info(`ApplicationCommandRegistries(BulkOverwrite) ${message}`, ...other);
 5 | }
 6 | 
 7 | export function bulkOverwriteError(message: string, ...other: unknown[]) {
 8 | 	container.logger.error(`ApplicationCommandRegistries(BulkOverwrite) ${message}`, ...other);
 9 | }
10 | 
11 | export function bulkOverwriteWarn(message: string, ...other: unknown[]) {
12 | 	container.logger.warn(`ApplicationCommandRegistries(BulkOverwrite) ${message}`, ...other);
13 | }
14 | 
15 | export function bulkOverwriteDebug(message: string, ...other: unknown[]) {
16 | 	container.logger.debug(`ApplicationCommandRegistries(BulkOverwrite) ${message}`, ...other);
17 | }
18 | 
--------------------------------------------------------------------------------
/src/lib/utils/logger/ILogger.ts:
--------------------------------------------------------------------------------
 1 | /**
 2 |  * The logger levels for the {@link ILogger}.
 3 |  */
 4 | export enum LogLevel {
 5 | 	/**
 6 | 	 * The lowest log level, used when calling {@link ILogger.trace}.
 7 | 	 */
 8 | 	Trace = 10,
 9 | 
10 | 	/**
11 | 	 * The debug level, used when calling {@link ILogger.debug}.
12 | 	 */
13 | 	Debug = 20,
14 | 
15 | 	/**
16 | 	 * The info level, used when calling {@link ILogger.info}.
17 | 	 */
18 | 	Info = 30,
19 | 
20 | 	/**
21 | 	 * The warning level, used when calling {@link ILogger.warn}.
22 | 	 */
23 | 	Warn = 40,
24 | 
25 | 	/**
26 | 	 * The error level, used when calling {@link ILogger.error}.
27 | 	 */
28 | 	Error = 50,
29 | 
30 | 	/**
31 | 	 * The critical level, used when calling {@link ILogger.fatal}.
32 | 	 */
33 | 	Fatal = 60,
34 | 
35 | 	/**
36 | 	 * An unknown or uncategorized level.
37 | 	 */
38 | 	None = 100
39 | }
40 | 
41 | export interface ILogger {
42 | 	/**
43 | 	 * Checks whether a level is supported.
44 | 	 * @param level The level to check.
45 | 	 */
46 | 	has(level: LogLevel): boolean;
47 | 
48 | 	/**
49 | 	 * Alias of {@link ILogger.write} with {@link LogLevel.Trace} as level.
50 | 	 * @param values The values to log.
51 | 	 */
52 | 	trace(...values: readonly unknown[]): void;
53 | 
54 | 	/**
55 | 	 * Alias of {@link ILogger.write} with {@link LogLevel.Debug} as level.
56 | 	 * @param values The values to log.
57 | 	 */
58 | 	debug(...values: readonly unknown[]): void;
59 | 
60 | 	/**
61 | 	 * Alias of {@link ILogger.write} with {@link LogLevel.Info} as level.
62 | 	 * @param values The values to log.
63 | 	 */
64 | 	info(...values: readonly unknown[]): void;
65 | 
66 | 	/**
67 | 	 * Alias of {@link ILogger.write} with {@link LogLevel.Warn} as level.
68 | 	 * @param values The values to log.
69 | 	 */
70 | 	warn(...values: readonly unknown[]): void;
71 | 
72 | 	/**
73 | 	 * Alias of {@link ILogger.write} with {@link LogLevel.Error} as level.
74 | 	 * @param values The values to log.
75 | 	 */
76 | 	error(...values: readonly unknown[]): void;
77 | 
78 | 	/**
79 | 	 * Alias of {@link ILogger.write} with {@link LogLevel.Fatal} as level.
80 | 	 * @param values The values to log.
81 | 	 */
82 | 	fatal(...values: readonly unknown[]): void;
83 | 
84 | 	/**
85 | 	 * Writes the log message given a level and the value(s).
86 | 	 * @param level The log level.
87 | 	 * @param values The values to log.
88 | 	 */
89 | 	write(level: LogLevel, ...values: readonly unknown[]): void;
90 | }
91 | 
--------------------------------------------------------------------------------
/src/lib/utils/logger/Logger.ts:
--------------------------------------------------------------------------------
 1 | import { LogLevel, type ILogger } from './ILogger';
 2 | 
 3 | export class Logger implements ILogger {
 4 | 	public level: LogLevel;
 5 | 
 6 | 	public constructor(level: LogLevel) {
 7 | 		this.level = level;
 8 | 	}
 9 | 
10 | 	public has(level: LogLevel): boolean {
11 | 		return level >= this.level;
12 | 	}
13 | 
14 | 	public trace(...values: readonly unknown[]): void {
15 | 		this.write(LogLevel.Trace, ...values);
16 | 	}
17 | 
18 | 	public debug(...values: readonly unknown[]): void {
19 | 		this.write(LogLevel.Debug, ...values);
20 | 	}
21 | 
22 | 	public info(...values: readonly unknown[]): void {
23 | 		this.write(LogLevel.Info, ...values);
24 | 	}
25 | 
26 | 	public warn(...values: readonly unknown[]): void {
27 | 		this.write(LogLevel.Warn, ...values);
28 | 	}
29 | 
30 | 	public error(...values: readonly unknown[]): void {
31 | 		this.write(LogLevel.Error, ...values);
32 | 	}
33 | 
34 | 	public fatal(...values: readonly unknown[]): void {
35 | 		this.write(LogLevel.Fatal, ...values);
36 | 	}
37 | 
38 | 	public write(level: LogLevel, ...values: readonly unknown[]): void {
39 | 		if (!this.has(level)) return;
40 | 		const method = Logger.levels.get(level);
41 | 		if (typeof method === 'string') console[method](`[${method.toUpperCase()}]`, ...values);
42 | 	}
43 | 
44 | 	protected static readonly levels = new Map([
45 | 		[LogLevel.Trace, 'trace'],
46 | 		[LogLevel.Debug, 'debug'],
47 | 		[LogLevel.Info, 'info'],
48 | 		[LogLevel.Warn, 'warn'],
49 | 		[LogLevel.Error, 'error'],
50 | 		[LogLevel.Fatal, 'error']
51 | 	]);
52 | }
53 | 
54 | export type LogMethods = 'trace' | 'debug' | 'info' | 'warn' | 'error';
55 | 
--------------------------------------------------------------------------------
/src/lib/utils/preconditions/IPreconditionContainer.ts:
--------------------------------------------------------------------------------
 1 | import type { Result } from '@sapphire/result';
 2 | import type { Awaitable } from '@sapphire/utilities';
 3 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 4 | import type { UserError } from '../../errors/UserError';
 5 | import type { Command } from '../../structures/Command';
 6 | import type { PreconditionContext } from '../../structures/Precondition';
 7 | 
 8 | /**
 9 |  * Defines the result's value for a PreconditionContainer.
10 |  * @since 1.0.0
11 |  */
12 | export type PreconditionContainerResult = Result;
13 | 
14 | /**
15 |  * Defines the return type of the generic {@link IPreconditionContainer.messageRun}.
16 |  * @since 1.0.0
17 |  */
18 | export type PreconditionContainerReturn = Awaitable;
19 | 
20 | /**
21 |  * Async-only version of {@link PreconditionContainerReturn}, to be used when the run method is async.
22 |  * @since 1.0.0
23 |  */
24 | export type AsyncPreconditionContainerReturn = Promise;
25 | 
26 | /**
27 |  * An abstracted precondition container to be implemented by classes.
28 |  * @since 1.0.0
29 |  */
30 | export interface IPreconditionContainer {
31 | 	/**
32 | 	 * Runs a precondition container.
33 | 	 * @since 1.0.0
34 | 	 * @param message The message that ran this precondition.
35 | 	 * @param command The command the message invoked.
36 | 	 * @param context The context for the precondition.
37 | 	 */
38 | 	messageRun(message: Message, command: Command, context?: PreconditionContext): PreconditionContainerReturn;
39 | 	/**
40 | 	 * Runs a precondition container.
41 | 	 * @since 3.0.0
42 | 	 * @param interaction The interaction that ran this precondition.
43 | 	 * @param command The command the interaction invoked.
44 | 	 * @param context The context for the precondition.
45 | 	 */
46 | 	chatInputRun(interaction: ChatInputCommandInteraction, command: Command, context?: PreconditionContext): PreconditionContainerReturn;
47 | 	/**
48 | 	 * Runs a precondition container.
49 | 	 * @since 3.0.0
50 | 	 * @param interaction The interaction that ran this precondition.
51 | 	 * @param command The command the interaction invoked.
52 | 	 * @param context The context for the precondition.
53 | 	 */
54 | 	contextMenuRun(interaction: ContextMenuCommandInteraction, command: Command, context?: PreconditionContext): PreconditionContainerReturn;
55 | }
56 | 
--------------------------------------------------------------------------------
/src/lib/utils/preconditions/conditions/PreconditionConditionAnd.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import type { IPreconditionCondition } from './IPreconditionCondition';
 3 | 
 4 | /**
 5 |  * An {@link IPreconditionCondition} which runs all containers similarly to doing (V0 && V1 [&& V2 [&& V3 ...]]).
 6 |  * @since 1.0.0
 7 |  */
 8 | export const PreconditionConditionAnd: IPreconditionCondition = {
 9 | 	async messageSequential(message, command, entries, context) {
10 | 		for (const child of entries) {
11 | 			const result = await child.messageRun(message, command, context);
12 | 			if (result.isErr()) return result;
13 | 		}
14 | 
15 | 		return Result.ok();
16 | 	},
17 | 	async messageParallel(message, command, entries, context) {
18 | 		const results = await Promise.all(entries.map((entry) => entry.messageRun(message, command, context)));
19 | 		// This is simplified compared to PreconditionContainerAny, because we're looking for the first error.
20 | 		// However, the base implementation short-circuits with the first Ok.
21 | 		return results.find((res) => res.isErr()) ?? Result.ok();
22 | 	},
23 | 	async chatInputSequential(interaction, command, entries, context) {
24 | 		for (const child of entries) {
25 | 			const result = await child.chatInputRun(interaction, command, context);
26 | 			if (result.isErr()) return result;
27 | 		}
28 | 
29 | 		return Result.ok();
30 | 	},
31 | 	async chatInputParallel(interaction, command, entries, context) {
32 | 		const results = await Promise.all(entries.map((entry) => entry.chatInputRun(interaction, command, context)));
33 | 		// This is simplified compared to PreconditionContainerAny, because we're looking for the first error.
34 | 		// However, the base implementation short-circuits with the first Ok.
35 | 		return results.find((res) => res.isErr()) ?? Result.ok();
36 | 	},
37 | 	async contextMenuSequential(interaction, command, entries, context) {
38 | 		for (const child of entries) {
39 | 			const result = await child.contextMenuRun(interaction, command, context);
40 | 			if (result.isErr()) return result;
41 | 		}
42 | 
43 | 		return Result.ok();
44 | 	},
45 | 	async contextMenuParallel(interaction, command, entries, context) {
46 | 		const results = await Promise.all(entries.map((entry) => entry.contextMenuRun(interaction, command, context)));
47 | 		// This is simplified compared to PreconditionContainerAny, because we're looking for the first error.
48 | 		// However, the base implementation short-circuits with the first Ok.
49 | 		return results.find((res) => res.isErr()) ?? Result.ok();
50 | 	}
51 | };
52 | 
--------------------------------------------------------------------------------
/src/lib/utils/preconditions/conditions/PreconditionConditionOr.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import type { PreconditionContainerResult } from '../IPreconditionContainer';
 3 | import type { IPreconditionCondition } from './IPreconditionCondition';
 4 | 
 5 | /**
 6 |  * An {@link IPreconditionCondition} which runs all containers similarly to doing (V0 || V1 [|| V2 [|| V3 ...]]).
 7 |  * @since 1.0.0
 8 |  */
 9 | export const PreconditionConditionOr: IPreconditionCondition = {
10 | 	async messageSequential(message, command, entries, context) {
11 | 		let error: PreconditionContainerResult | null = null;
12 | 		for (const child of entries) {
13 | 			const result = await child.messageRun(message, command, context);
14 | 			if (result.isOk()) return result;
15 | 			error = result;
16 | 		}
17 | 
18 | 		return error ?? Result.ok();
19 | 	},
20 | 	async messageParallel(message, command, entries, context) {
21 | 		const results = await Promise.all(entries.map((entry) => entry.messageRun(message, command, context)));
22 | 
23 | 		let error: PreconditionContainerResult | null = null;
24 | 		for (const result of results) {
25 | 			if (result.isOk()) return result;
26 | 			error = result;
27 | 		}
28 | 
29 | 		return error ?? Result.ok();
30 | 	},
31 | 	async chatInputSequential(interaction, command, entries, context) {
32 | 		let error: PreconditionContainerResult | null = null;
33 | 		for (const child of entries) {
34 | 			const result = await child.chatInputRun(interaction, command, context);
35 | 			if (result.isOk()) return result;
36 | 			error = result;
37 | 		}
38 | 
39 | 		return error ?? Result.ok();
40 | 	},
41 | 	async chatInputParallel(interaction, command, entries, context) {
42 | 		const results = await Promise.all(entries.map((entry) => entry.chatInputRun(interaction, command, context)));
43 | 
44 | 		let error: PreconditionContainerResult | null = null;
45 | 		for (const result of results) {
46 | 			if (result.isOk()) return result;
47 | 			error = result;
48 | 		}
49 | 
50 | 		return error ?? Result.ok();
51 | 	},
52 | 	async contextMenuSequential(interaction, command, entries, context) {
53 | 		let error: PreconditionContainerResult | null = null;
54 | 		for (const child of entries) {
55 | 			const result = await child.contextMenuRun(interaction, command, context);
56 | 			if (result.isOk()) return result;
57 | 			error = result;
58 | 		}
59 | 
60 | 		return error ?? Result.ok();
61 | 	},
62 | 	async contextMenuParallel(interaction, command, entries, context) {
63 | 		const results = await Promise.all(entries.map((entry) => entry.contextMenuRun(interaction, command, context)));
64 | 
65 | 		let error: PreconditionContainerResult | null = null;
66 | 		for (const result of results) {
67 | 			if (result.isOk()) return result;
68 | 			error = result;
69 | 		}
70 | 
71 | 		return error ?? Result.ok();
72 | 	}
73 | };
74 | 
--------------------------------------------------------------------------------
/src/lib/utils/preconditions/containers/ClientPermissionsPrecondition.ts:
--------------------------------------------------------------------------------
 1 | import { PermissionsBitField, type PermissionResolvable } from 'discord.js';
 2 | import type { PreconditionSingleResolvableDetails } from '../PreconditionContainerSingle';
 3 | 
 4 | /**
 5 |  * Constructs a contextful permissions precondition requirement.
 6 |  * @since 1.0.0
 7 |  * @example
 8 |  * ```typescript
 9 |  * export class CoreCommand extends Command {
10 |  *   public constructor(context: Command.Context) {
11 |  *     super(context, {
12 |  *       preconditions: [
13 |  *         'GuildOnly',
14 |  *         new ClientPermissionsPrecondition('ADD_REACTIONS')
15 |  *       ]
16 |  *     });
17 |  *   }
18 |  *
19 |  *   public messageRun(message: Message, args: Args) {
20 |  *     // ...
21 |  *   }
22 |  * }
23 |  * ```
24 |  */
25 | export class ClientPermissionsPrecondition implements PreconditionSingleResolvableDetails<'ClientPermissions'> {
26 | 	public name: 'ClientPermissions';
27 | 	public context: { permissions: PermissionsBitField };
28 | 
29 | 	/**
30 | 	 * Constructs a precondition container entry.
31 | 	 * @param permissions The permissions that will be required by this command.
32 | 	 */
33 | 	public constructor(permissions: PermissionResolvable) {
34 | 		this.name = 'ClientPermissions';
35 | 		this.context = {
36 | 			permissions: new PermissionsBitField(permissions)
37 | 		};
38 | 	}
39 | }
40 | 
--------------------------------------------------------------------------------
/src/lib/utils/preconditions/containers/UserPermissionsPrecondition.ts:
--------------------------------------------------------------------------------
 1 | import { PermissionsBitField, type PermissionResolvable } from 'discord.js';
 2 | import type { PreconditionSingleResolvableDetails } from '../PreconditionContainerSingle';
 3 | 
 4 | /**
 5 |  * Constructs a contextful permissions precondition requirement.
 6 |  * @since 1.0.0
 7 |  * @example
 8 |  * ```typescript
 9 |  * export class CoreCommand extends Command {
10 |  *   public constructor(context: Command.Context) {
11 |  *     super(context, {
12 |  *       preconditions: [
13 |  *         'GuildOnly',
14 |  *         new UserPermissionsPrecondition('ADD_REACTIONS')
15 |  *       ]
16 |  *     });
17 |  *   }
18 |  *
19 |  *   public messageRun(message: Message, args: Args) {
20 |  *     // ...
21 |  *   }
22 |  * }
23 |  * ```
24 |  */
25 | export class UserPermissionsPrecondition implements PreconditionSingleResolvableDetails<'UserPermissions'> {
26 | 	public name: 'UserPermissions';
27 | 	public context: { permissions: PermissionsBitField };
28 | 
29 | 	/**
30 | 	 * Constructs a precondition container entry.
31 | 	 * @param permissions The permissions that will be required by this command.
32 | 	 */
33 | 	public constructor(permissions: PermissionResolvable) {
34 | 		this.name = 'UserPermissions';
35 | 		this.context = {
36 | 			permissions: new PermissionsBitField(permissions)
37 | 		};
38 | 	}
39 | }
40 | 
--------------------------------------------------------------------------------
/src/lib/utils/resolvers/resolveGuildChannelPredicate.ts:
--------------------------------------------------------------------------------
 1 | import type { ChannelTypes, GuildBasedChannelTypes } from '@sapphire/discord.js-utilities';
 2 | import { Result } from '@sapphire/result';
 3 | import type { Nullish } from '@sapphire/utilities';
 4 | import type { Guild } from 'discord.js';
 5 | import type { Identifiers } from '../../errors/Identifiers';
 6 | import { resolveGuildChannel } from '../../resolvers/guildChannel';
 7 | 
 8 | export function resolveGuildChannelPredicate(
 9 | 	parameter: string,
10 | 	guild: Guild,
11 | 	predicate: (channel: ChannelTypes | Nullish) => channel is TChannel,
12 | 	error: TError
13 | ): Result {
14 | 	const result = resolveGuildChannel(parameter, guild);
15 | 	return result.mapInto((channel) => (predicate(channel) ? Result.ok(channel) : Result.err(error)));
16 | }
17 | 
--------------------------------------------------------------------------------
/src/lib/utils/strategies/FlagUnorderedStrategy.ts:
--------------------------------------------------------------------------------
 1 | import { PrefixedStrategy } from '@sapphire/lexure';
 2 | import { Option } from '@sapphire/result';
 3 | 
 4 | /**
 5 |  * The strategy options used in Sapphire.
 6 |  */
 7 | export interface FlagStrategyOptions {
 8 | 	/**
 9 | 	 * The accepted flags. Flags are key-only identifiers that can be placed anywhere in the command. Two different types are accepted:
10 | 	 * * An array of strings, e.g. [`silent`].
11 | 	 * * A boolean defining whether the strategy should accept all keys (`true`) or none at all (`false`).
12 | 	 * @default []
13 | 	 */
14 | 	flags?: readonly string[] | boolean;
15 | 
16 | 	/**
17 | 	 * The accepted options. Options are key-value identifiers that can be placed anywhere in the command. Two different types are accepted:
18 | 	 * * An array of strings, e.g. [`silent`].
19 | 	 * * A boolean defining whether the strategy should accept all keys (`true`) or none at all (`false`).
20 | 	 * @default []
21 | 	 */
22 | 	options?: readonly string[] | boolean;
23 | 
24 | 	/**
25 | 	 * The prefixes for both flags and options.
26 | 	 * @default ['--', '-', '—']
27 | 	 */
28 | 	prefixes?: string[];
29 | 
30 | 	/**
31 | 	 * The flag separators.
32 | 	 * @default ['=', ':']
33 | 	 */
34 | 	separators?: string[];
35 | }
36 | 
37 | const never = () => Option.none;
38 | const always = () => true;
39 | 
40 | export class FlagUnorderedStrategy extends PrefixedStrategy {
41 | 	public readonly flags: readonly string[] | true;
42 | 	public readonly options: readonly string[] | true;
43 | 
44 | 	public constructor({ flags, options, prefixes = ['--', '-', '—'], separators = ['=', ':'] }: FlagStrategyOptions = {}) {
45 | 		super(prefixes, separators);
46 | 		this.flags = flags || [];
47 | 		this.options = options || [];
48 | 
49 | 		if (this.flags === true) this.allowedFlag = always;
50 | 		else if (this.flags.length === 0) this.matchFlag = never;
51 | 
52 | 		if (this.options === true) {
53 | 			this.allowedOption = always;
54 | 		} else if (this.options.length === 0) {
55 | 			this.matchOption = never;
56 | 		}
57 | 	}
58 | 
59 | 	public override matchFlag(s: string): Option {
60 | 		const result = super.matchFlag(s);
61 | 
62 | 		// The flag must be an allowed one.
63 | 		if (result.isSomeAnd((value) => this.allowedFlag(value))) return result;
64 | 
65 | 		// If it did not match a flag, return null.
66 | 		return Option.none;
67 | 	}
68 | 
69 | 	public override matchOption(s: string): Option {
70 | 		const result = super.matchOption(s);
71 | 
72 | 		if (result.isSomeAnd((option) => this.allowedOption(option[0]))) return result;
73 | 
74 | 		return Option.none;
75 | 	}
76 | 
77 | 	private allowedFlag(s: string) {
78 | 		return (this.flags as readonly string[]).includes(s);
79 | 	}
80 | 
81 | 	private allowedOption(s: string) {
82 | 		return (this.options as readonly string[]).includes(s);
83 | 	}
84 | }
85 | 
--------------------------------------------------------------------------------
/src/listeners/CoreInteractionCreate.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { Interaction } from 'discord.js';
 3 | import { Listener } from '../lib/structures/Listener';
 4 | import { Events } from '../lib/types/Events';
 5 | 
 6 | export class CoreListener extends Listener {
 7 | 	public constructor(context: Listener.LoaderContext) {
 8 | 		super(context, { event: Events.InteractionCreate });
 9 | 	}
10 | 
11 | 	public async run(interaction: Interaction) {
12 | 		if (interaction.isChatInputCommand()) {
13 | 			this.container.client.emit(Events.PossibleChatInputCommand, interaction);
14 | 		} else if (interaction.isContextMenuCommand()) {
15 | 			this.container.client.emit(Events.PossibleContextMenuCommand, interaction);
16 | 		} else if (interaction.isAutocomplete()) {
17 | 			this.container.client.emit(Events.PossibleAutocompleteInteraction, interaction);
18 | 		} else if (interaction.isMessageComponent() || interaction.isModalSubmit()) {
19 | 			await this.container.stores.get('interaction-handlers').run(interaction);
20 | 		} else {
21 | 			this.container.logger.warn(`[Sapphire ${this.location.name}] Unhandled interaction type: ${(interaction as any).constructor.name}`);
22 | 		}
23 | 	}
24 | }
25 | 
26 | void container.stores.loadPiece({
27 | 	name: 'CoreInteractionCreate',
28 | 	piece: CoreListener,
29 | 	store: 'listeners'
30 | });
31 | 
--------------------------------------------------------------------------------
/src/listeners/CoreReady.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { Listener } from '../lib/structures/Listener';
 3 | import { Events } from '../lib/types/Events';
 4 | import { handleRegistryAPICalls } from '../lib/utils/application-commands/ApplicationCommandRegistries';
 5 | 
 6 | export class CoreListener extends Listener {
 7 | 	public constructor(context: Listener.LoaderContext) {
 8 | 		super(context, { event: Events.ClientReady, once: true });
 9 | 	}
10 | 
11 | 	public async run() {
12 | 		this.container.client.id ??= this.container.client.user?.id ?? null;
13 | 
14 | 		await handleRegistryAPICalls();
15 | 	}
16 | }
17 | 
18 | void container.stores.loadPiece({
19 | 	name: 'CoreReady',
20 | 	piece: CoreListener,
21 | 	store: 'listeners'
22 | });
23 | 
--------------------------------------------------------------------------------
/src/listeners/_load.ts:
--------------------------------------------------------------------------------
 1 | import './CoreInteractionCreate';
 2 | import './CoreReady';
 3 | import './application-commands/CorePossibleAutocompleteInteraction';
 4 | import './application-commands/chat-input/CoreChatInputCommandAccepted';
 5 | import './application-commands/chat-input/CorePossibleChatInputCommand';
 6 | import './application-commands/chat-input/CorePreChatInputCommandRun';
 7 | import './application-commands/context-menu/CoreContextMenuCommandAccepted';
 8 | import './application-commands/context-menu/CorePossibleContextMenuCommand';
 9 | import './application-commands/context-menu/CorePreContextMenuCommandRun';
10 | 
--------------------------------------------------------------------------------
/src/listeners/application-commands/CorePossibleAutocompleteInteraction.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { AutocompleteInteraction } from 'discord.js';
 3 | import { Listener } from '../../lib/structures/Listener';
 4 | import type { AutocompleteCommand } from '../../lib/types/CommandTypes';
 5 | import { Events } from '../../lib/types/Events';
 6 | 
 7 | export class CoreListener extends Listener {
 8 | 	public constructor(context: Listener.LoaderContext) {
 9 | 		super(context, { event: Events.PossibleAutocompleteInteraction });
10 | 	}
11 | 
12 | 	public async run(interaction: AutocompleteInteraction) {
13 | 		const { stores } = this.container;
14 | 
15 | 		const commandStore = stores.get('commands');
16 | 
17 | 		// Try resolving in command
18 | 		const command = commandStore.get(interaction.commandId) ?? commandStore.get(interaction.commandName);
19 | 
20 | 		if (command?.autocompleteRun) {
21 | 			try {
22 | 				await command.autocompleteRun(interaction);
23 | 				this.container.client.emit(Events.CommandAutocompleteInteractionSuccess, {
24 | 					command: command as AutocompleteCommand,
25 | 					context: { commandId: interaction.commandId, commandName: interaction.commandName },
26 | 					interaction
27 | 				});
28 | 			} catch (err) {
29 | 				this.container.client.emit(Events.CommandAutocompleteInteractionError, err, {
30 | 					command: command as AutocompleteCommand,
31 | 					context: { commandId: interaction.commandId, commandName: interaction.commandName },
32 | 					interaction
33 | 				});
34 | 			}
35 | 			return;
36 | 		}
37 | 
38 | 		// Unless we ran a command handler, always call interaction handlers with the interaction
39 | 		await this.container.stores.get('interaction-handlers').run(interaction);
40 | 	}
41 | }
42 | 
43 | void container.stores.loadPiece({
44 | 	name: 'CorePossibleAutocompleteInteraction',
45 | 	piece: CoreListener,
46 | 	store: 'listeners'
47 | });
48 | 
--------------------------------------------------------------------------------
/src/listeners/application-commands/chat-input/CoreChatInputCommandAccepted.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { Result } from '@sapphire/result';
 3 | import { Stopwatch } from '@sapphire/stopwatch';
 4 | import { Listener } from '../../../lib/structures/Listener';
 5 | import { Events, type ChatInputCommandAcceptedPayload } from '../../../lib/types/Events';
 6 | 
 7 | export class CoreListener extends Listener {
 8 | 	public constructor(context: Listener.LoaderContext) {
 9 | 		super(context, { event: Events.ChatInputCommandAccepted });
10 | 	}
11 | 
12 | 	public async run(payload: ChatInputCommandAcceptedPayload) {
13 | 		const { command, context, interaction } = payload;
14 | 
15 | 		const result = await Result.fromAsync(async () => {
16 | 			this.container.client.emit(Events.ChatInputCommandRun, interaction, command, { ...payload });
17 | 
18 | 			const stopwatch = new Stopwatch();
19 | 			const result = await command.chatInputRun(interaction, context);
20 | 			const { duration } = stopwatch.stop();
21 | 
22 | 			this.container.client.emit(Events.ChatInputCommandSuccess, { ...payload, result, duration });
23 | 
24 | 			return duration;
25 | 		});
26 | 
27 | 		result.inspectErr((error) => this.container.client.emit(Events.ChatInputCommandError, error, { ...payload, duration: -1 }));
28 | 
29 | 		this.container.client.emit(Events.ChatInputCommandFinish, interaction, command, {
30 | 			...payload,
31 | 			success: result.isOk(),
32 | 			duration: result.unwrapOr(-1)
33 | 		});
34 | 	}
35 | }
36 | 
37 | void container.stores.loadPiece({
38 | 	name: 'CoreChatInputCommandAccepted',
39 | 	piece: CoreListener,
40 | 	store: 'listeners'
41 | });
42 | 
--------------------------------------------------------------------------------
/src/listeners/application-commands/chat-input/CorePossibleChatInputCommand.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ChatInputCommandInteraction } from 'discord.js';
 3 | import { Listener } from '../../../lib/structures/Listener';
 4 | import type { ChatInputCommand } from '../../../lib/types/CommandTypes';
 5 | import { Events } from '../../../lib/types/Events';
 6 | 
 7 | export class CoreListener extends Listener {
 8 | 	public constructor(context: Listener.LoaderContext) {
 9 | 		super(context, { event: Events.PossibleChatInputCommand });
10 | 	}
11 | 
12 | 	public run(interaction: ChatInputCommandInteraction) {
13 | 		const { client, stores } = this.container;
14 | 		const commandStore = stores.get('commands');
15 | 
16 | 		const command = commandStore.get(interaction.commandId) ?? commandStore.get(interaction.commandName);
17 | 		if (!command) {
18 | 			client.emit(Events.UnknownChatInputCommand, {
19 | 				interaction,
20 | 				context: { commandId: interaction.commandId, commandName: interaction.commandName }
21 | 			});
22 | 			return;
23 | 		}
24 | 
25 | 		if (!command.chatInputRun) {
26 | 			client.emit(Events.CommandDoesNotHaveChatInputCommandHandler, {
27 | 				command,
28 | 				interaction,
29 | 				context: { commandId: interaction.commandId, commandName: interaction.commandName }
30 | 			});
31 | 			return;
32 | 		}
33 | 
34 | 		client.emit(Events.PreChatInputCommandRun, {
35 | 			command: command as ChatInputCommand,
36 | 			context: { commandId: interaction.commandId, commandName: interaction.commandName },
37 | 			interaction
38 | 		});
39 | 	}
40 | }
41 | 
42 | void container.stores.loadPiece({
43 | 	name: 'CorePossibleChatInputCommand',
44 | 	piece: CoreListener,
45 | 	store: 'listeners'
46 | });
47 | 
--------------------------------------------------------------------------------
/src/listeners/application-commands/chat-input/CorePreChatInputCommandRun.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { Listener } from '../../../lib/structures/Listener';
 3 | import { Events, type PreChatInputCommandRunPayload } from '../../../lib/types/Events';
 4 | 
 5 | export class CoreListener extends Listener {
 6 | 	public constructor(context: Listener.LoaderContext) {
 7 | 		super(context, { event: Events.PreChatInputCommandRun });
 8 | 	}
 9 | 
10 | 	public async run(payload: PreChatInputCommandRunPayload) {
11 | 		const { command, interaction } = payload;
12 | 
13 | 		// Run global preconditions:
14 | 		const globalResult = await this.container.stores.get('preconditions').chatInputRun(interaction, command, payload as any);
15 | 		if (globalResult.isErr()) {
16 | 			this.container.client.emit(Events.ChatInputCommandDenied, globalResult.unwrapErr(), payload);
17 | 			return;
18 | 		}
19 | 
20 | 		// Run command-specific preconditions:
21 | 		const localResult = await command.preconditions.chatInputRun(interaction, command, payload as any);
22 | 		if (localResult.isErr()) {
23 | 			this.container.client.emit(Events.ChatInputCommandDenied, localResult.unwrapErr(), payload);
24 | 			return;
25 | 		}
26 | 
27 | 		this.container.client.emit(Events.ChatInputCommandAccepted, payload);
28 | 	}
29 | }
30 | 
31 | void container.stores.loadPiece({
32 | 	name: 'CorePreChatInputCommandRun',
33 | 	piece: CoreListener,
34 | 	store: 'listeners'
35 | });
36 | 
--------------------------------------------------------------------------------
/src/listeners/application-commands/context-menu/CoreContextMenuCommandAccepted.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { Result } from '@sapphire/result';
 3 | import { Stopwatch } from '@sapphire/stopwatch';
 4 | import { Listener } from '../../../lib/structures/Listener';
 5 | import { Events, type ContextMenuCommandAcceptedPayload } from '../../../lib/types/Events';
 6 | 
 7 | export class CoreListener extends Listener {
 8 | 	public constructor(context: Listener.LoaderContext) {
 9 | 		super(context, { event: Events.ContextMenuCommandAccepted });
10 | 	}
11 | 
12 | 	public async run(payload: ContextMenuCommandAcceptedPayload) {
13 | 		const { command, context, interaction } = payload;
14 | 
15 | 		const result = await Result.fromAsync(async () => {
16 | 			this.container.client.emit(Events.ContextMenuCommandRun, interaction, command, { ...payload });
17 | 
18 | 			const stopwatch = new Stopwatch();
19 | 			const result = await command.contextMenuRun(interaction, context);
20 | 			const { duration } = stopwatch.stop();
21 | 
22 | 			this.container.client.emit(Events.ContextMenuCommandSuccess, { ...payload, result, duration });
23 | 
24 | 			return duration;
25 | 		});
26 | 
27 | 		result.inspectErr((error) => this.container.client.emit(Events.ContextMenuCommandError, error, { ...payload, duration: -1 }));
28 | 
29 | 		this.container.client.emit(Events.ContextMenuCommandFinish, interaction, command, {
30 | 			...payload,
31 | 			success: result.isOk(),
32 | 			duration: result.unwrapOr(-1)
33 | 		});
34 | 	}
35 | }
36 | 
37 | void container.stores.loadPiece({
38 | 	name: 'CoreContextMenuCommandAccepted',
39 | 	piece: CoreListener,
40 | 	store: 'listeners'
41 | });
42 | 
--------------------------------------------------------------------------------
/src/listeners/application-commands/context-menu/CorePossibleContextMenuCommand.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ContextMenuCommandInteraction } from 'discord.js';
 3 | import { Listener } from '../../../lib/structures/Listener';
 4 | import type { ContextMenuCommand } from '../../../lib/types/CommandTypes';
 5 | import { Events } from '../../../lib/types/Events';
 6 | 
 7 | export class CoreListener extends Listener {
 8 | 	public constructor(context: Listener.LoaderContext) {
 9 | 		super(context, { event: Events.PossibleContextMenuCommand });
10 | 	}
11 | 
12 | 	public run(interaction: ContextMenuCommandInteraction) {
13 | 		const { client, stores } = this.container;
14 | 		const commandStore = stores.get('commands');
15 | 
16 | 		const command = commandStore.get(interaction.commandId) ?? commandStore.get(interaction.commandName);
17 | 		if (!command) {
18 | 			client.emit(Events.UnknownContextMenuCommand, {
19 | 				interaction,
20 | 				context: { commandId: interaction.commandId, commandName: interaction.commandName }
21 | 			});
22 | 			return;
23 | 		}
24 | 
25 | 		if (!command.contextMenuRun) {
26 | 			client.emit(Events.CommandDoesNotHaveContextMenuCommandHandler, {
27 | 				command,
28 | 				interaction,
29 | 				context: { commandId: interaction.commandId, commandName: interaction.commandName }
30 | 			});
31 | 			return;
32 | 		}
33 | 
34 | 		client.emit(Events.PreContextMenuCommandRun, {
35 | 			command: command as ContextMenuCommand,
36 | 			context: { commandId: interaction.commandId, commandName: interaction.commandName },
37 | 			interaction
38 | 		});
39 | 	}
40 | }
41 | 
42 | void container.stores.loadPiece({
43 | 	name: 'CorePossibleContextMenuCommand',
44 | 	piece: CoreListener,
45 | 	store: 'listeners'
46 | });
47 | 
--------------------------------------------------------------------------------
/src/listeners/application-commands/context-menu/CorePreContextMenuCommandRun.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { Listener } from '../../../lib/structures/Listener';
 3 | import { Events, type PreContextMenuCommandRunPayload } from '../../../lib/types/Events';
 4 | 
 5 | export class CoreListener extends Listener {
 6 | 	public constructor(context: Listener.LoaderContext) {
 7 | 		super(context, { event: Events.PreContextMenuCommandRun });
 8 | 	}
 9 | 
10 | 	public async run(payload: PreContextMenuCommandRunPayload) {
11 | 		const { command, interaction } = payload;
12 | 
13 | 		// Run global preconditions:
14 | 		const globalResult = await this.container.stores.get('preconditions').contextMenuRun(interaction, command, payload as any);
15 | 		if (globalResult.isErr()) {
16 | 			this.container.client.emit(Events.ContextMenuCommandDenied, globalResult.unwrapErr(), payload);
17 | 			return;
18 | 		}
19 | 
20 | 		// Run command-specific preconditions:
21 | 		const localResult = await command.preconditions.contextMenuRun(interaction, command, payload as any);
22 | 		if (localResult.isErr()) {
23 | 			this.container.client.emit(Events.ContextMenuCommandDenied, localResult.unwrapErr(), payload);
24 | 			return;
25 | 		}
26 | 
27 | 		this.container.client.emit(Events.ContextMenuCommandAccepted, payload);
28 | 	}
29 | }
30 | 
31 | void container.stores.loadPiece({
32 | 	name: 'CorePreContextMenuCommandRun',
33 | 	piece: CoreListener,
34 | 	store: 'listeners'
35 | });
36 | 
--------------------------------------------------------------------------------
/src/optional-listeners/application-command-registries-listeners/CoreApplicationCommandRegistriesInitialising.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.ApplicationCommandRegistriesInitialising, once: true });
 7 | 	}
 8 | 
 9 | 	public run() {
10 | 		this.container.logger.info('ApplicationCommandRegistries: Initializing...');
11 | 	}
12 | }
13 | 
--------------------------------------------------------------------------------
/src/optional-listeners/application-command-registries-listeners/CoreApplicationCommandRegistriesRegistered.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events } from '../../lib/types/Events';
 3 | import type { ApplicationCommandRegistry } from '../../lib/utils/application-commands/ApplicationCommandRegistry';
 4 | 
 5 | export class CoreListener extends Listener {
 6 | 	public constructor(context: Listener.LoaderContext) {
 7 | 		super(context, { event: Events.ApplicationCommandRegistriesRegistered, once: true });
 8 | 	}
 9 | 
10 | 	public run(_registries: Map, timeTaken: number) {
11 | 		this.container.logger.info(`ApplicationCommandRegistries: Took ${timeTaken.toLocaleString()}ms to initialize.`);
12 | 	}
13 | }
14 | 
--------------------------------------------------------------------------------
/src/optional-listeners/application-command-registries-listeners/_load.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { CoreListener as CoreApplicationCommandRegistriesInitialising } from './CoreApplicationCommandRegistriesInitialising';
 3 | import { CoreListener as CoreApplicationCommandRegistriesRegistered } from './CoreApplicationCommandRegistriesRegistered';
 4 | 
 5 | export function loadApplicationCommandRegistriesListeners() {
 6 | 	const store = 'listeners' as const;
 7 | 	void container.stores.loadPiece({
 8 | 		name: 'CoreApplicationCommandRegistriesInitialising',
 9 | 		piece: CoreApplicationCommandRegistriesInitialising,
10 | 		store
11 | 	});
12 | 	void container.stores.loadPiece({
13 | 		name: 'CoreApplicationCommandRegistriesRegistered',
14 | 		piece: CoreApplicationCommandRegistriesRegistered,
15 | 		store
16 | 	});
17 | }
18 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreChatInputCommandError.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type ChatInputCommandErrorPayload } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.ChatInputCommandError });
 7 | 	}
 8 | 
 9 | 	public run(error: unknown, context: ChatInputCommandErrorPayload) {
10 | 		const { name, location } = context.command;
11 | 		this.container.logger.error(`Encountered error on chat input command "${name}" at path "${location.full}"`, error);
12 | 	}
13 | }
14 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreCommandApplicationCommandRegistryError.ts:
--------------------------------------------------------------------------------
 1 | import type { Command } from '../../lib/structures/Command';
 2 | import { Listener } from '../../lib/structures/Listener';
 3 | import { Events } from '../../lib/types/Events';
 4 | 
 5 | export class CoreListener extends Listener {
 6 | 	public constructor(context: Listener.LoaderContext) {
 7 | 		super(context, { event: Events.CommandApplicationCommandRegistryError });
 8 | 	}
 9 | 
10 | 	public run(error: unknown, command: Command) {
11 | 		const { name, location } = command;
12 | 		this.container.logger.error(
13 | 			`Encountered error while handling the command application command registry for command "${name}" at path "${location.full}"`,
14 | 			error
15 | 		);
16 | 	}
17 | }
18 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreCommandAutocompleteInteractionError.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type AutocompleteInteractionPayload } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.CommandAutocompleteInteractionError });
 7 | 	}
 8 | 
 9 | 	public run(error: unknown, context: AutocompleteInteractionPayload) {
10 | 		const { name, location } = context.command;
11 | 		this.container.logger.error(
12 | 			`Encountered error while handling an autocomplete run method on command "${name}" at path "${location.full}"`,
13 | 			error
14 | 		);
15 | 	}
16 | }
17 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreContextMenuCommandError.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type ContextMenuCommandErrorPayload } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.ContextMenuCommandError });
 7 | 	}
 8 | 
 9 | 	public run(error: unknown, context: ContextMenuCommandErrorPayload) {
10 | 		const { name, location } = context.command;
11 | 		this.container.logger.error(`Encountered error on message command "${name}" at path "${location.full}"`, error);
12 | 	}
13 | }
14 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreInteractionHandlerError.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type InteractionHandlerError } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.InteractionHandlerError });
 7 | 	}
 8 | 
 9 | 	public run(error: unknown, context: InteractionHandlerError) {
10 | 		const { name, location } = context.handler;
11 | 		this.container.logger.error(
12 | 			`Encountered error while handling an interaction handler run method for interaction-handler "${name}" at path "${location.full}"`,
13 | 			error
14 | 		);
15 | 	}
16 | }
17 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreInteractionHandlerParseError.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type InteractionHandlerParseError as InteractionHandlerParseErrorPayload } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.InteractionHandlerParseError });
 7 | 	}
 8 | 
 9 | 	public run(error: unknown, context: InteractionHandlerParseErrorPayload) {
10 | 		const { name, location } = context.handler;
11 | 		this.container.logger.error(
12 | 			`Encountered error while handling an interaction handler parse method for interaction-handler "${name}" at path "${location.full}"`,
13 | 			error
14 | 		);
15 | 	}
16 | }
17 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreListenerError.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type ListenerErrorPayload } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.ListenerError });
 7 | 	}
 8 | 
 9 | 	public run(error: unknown, context: ListenerErrorPayload) {
10 | 		const { name, event, location } = context.piece;
11 | 		this.container.logger.error(`Encountered error on event listener "${name}" for event "${String(event)}" at path "${location.full}"`, error);
12 | 	}
13 | }
14 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/CoreMessageCommandError.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type MessageCommandErrorPayload } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.MessageCommandError });
 7 | 	}
 8 | 
 9 | 	public run(error: unknown, context: MessageCommandErrorPayload) {
10 | 		const { name, location } = context.command;
11 | 		this.container.logger.error(`Encountered error on message command "${name}" at path "${location.full}"`, error);
12 | 	}
13 | }
14 | 
--------------------------------------------------------------------------------
/src/optional-listeners/error-listeners/_load.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { CoreListener as CoreChatInputCommandError } from './CoreChatInputCommandError';
 3 | import { CoreListener as CoreCommandApplicationCommandRegistryError } from './CoreCommandApplicationCommandRegistryError';
 4 | import { CoreListener as CoreCommandAutocompleteInteractionError } from './CoreCommandAutocompleteInteractionError';
 5 | import { CoreListener as CoreContextMenuCommandError } from './CoreContextMenuCommandError';
 6 | import { CoreListener as CoreInteractionHandlerError } from './CoreInteractionHandlerError';
 7 | import { CoreListener as CoreInteractionHandlerParseError } from './CoreInteractionHandlerParseError';
 8 | import { CoreListener as CoreListenerError } from './CoreListenerError';
 9 | import { CoreListener as CoreMessageCommandError } from './CoreMessageCommandError';
10 | 
11 | export function loadErrorListeners() {
12 | 	const store = 'listeners' as const;
13 | 	void container.stores.loadPiece({ name: 'CoreChatInputCommandError', piece: CoreChatInputCommandError, store });
14 | 	void container.stores.loadPiece({ name: 'CoreCommandApplicationCommandRegistryError', piece: CoreCommandApplicationCommandRegistryError, store });
15 | 	void container.stores.loadPiece({ name: 'CoreCommandAutocompleteInteractionError', piece: CoreCommandAutocompleteInteractionError, store });
16 | 	void container.stores.loadPiece({ name: 'CoreContextMenuCommandError', piece: CoreContextMenuCommandError, store });
17 | 	void container.stores.loadPiece({ name: 'CoreInteractionHandlerError', piece: CoreInteractionHandlerError, store });
18 | 	void container.stores.loadPiece({ name: 'CoreInteractionHandlerParseError', piece: CoreInteractionHandlerParseError, store });
19 | 	void container.stores.loadPiece({ name: 'CoreListenerError', piece: CoreListenerError, store });
20 | 	void container.stores.loadPiece({ name: 'CoreMessageCommandError', piece: CoreMessageCommandError, store });
21 | }
22 | 
--------------------------------------------------------------------------------
/src/optional-listeners/message-command-listeners/CoreMessageCommandAccepted.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Stopwatch } from '@sapphire/stopwatch';
 3 | import { Listener } from '../../lib/structures/Listener';
 4 | import { Events, type MessageCommandAcceptedPayload } from '../../lib/types/Events';
 5 | 
 6 | export class CoreListener extends Listener {
 7 | 	public constructor(context: Listener.LoaderContext) {
 8 | 		super(context, { event: Events.MessageCommandAccepted });
 9 | 	}
10 | 
11 | 	public async run(payload: MessageCommandAcceptedPayload) {
12 | 		const { message, command, parameters, context } = payload;
13 | 		const args = await command.messagePreParse(message, parameters, context);
14 | 
15 | 		const result = await Result.fromAsync(async () => {
16 | 			message.client.emit(Events.MessageCommandRun, message, command, { ...payload, args });
17 | 
18 | 			const stopwatch = new Stopwatch();
19 | 			const result = await command.messageRun(message, args, context);
20 | 			const { duration } = stopwatch.stop();
21 | 
22 | 			message.client.emit(Events.MessageCommandSuccess, { ...payload, args, result, duration });
23 | 
24 | 			return duration;
25 | 		});
26 | 
27 | 		result.inspectErr((error) => message.client.emit(Events.MessageCommandError, error, { ...payload, args, duration: -1 }));
28 | 
29 | 		message.client.emit(Events.MessageCommandFinish, message, command, {
30 | 			...payload,
31 | 			args,
32 | 			success: result.isOk(),
33 | 			duration: result.unwrapOr(-1)
34 | 		});
35 | 	}
36 | }
37 | 
--------------------------------------------------------------------------------
/src/optional-listeners/message-command-listeners/CoreMessageCommandTyping.ts:
--------------------------------------------------------------------------------
 1 | import { isStageChannel } from '@sapphire/discord.js-utilities';
 2 | import { ChannelType, type Message } from 'discord.js';
 3 | import { Listener } from '../../lib/structures/Listener';
 4 | import type { MessageCommand } from '../../lib/types/CommandTypes';
 5 | import { Events, type MessageCommandRunPayload } from '../../lib/types/Events';
 6 | 
 7 | export class CoreListener extends Listener {
 8 | 	public constructor(context: Listener.LoaderContext) {
 9 | 		super(context, { event: Events.MessageCommandRun });
10 | 		this.enabled = this.container.client.options.typing ?? false;
11 | 	}
12 | 
13 | 	public async run(message: Message, command: MessageCommand, payload: MessageCommandRunPayload) {
14 | 		if (!command.typing || isStageChannel(message.channel)) {
15 | 			return;
16 | 		}
17 | 
18 | 		if (message.channel.type === ChannelType.GroupDM) {
19 | 			return;
20 | 		}
21 | 
22 | 		try {
23 | 			await message.channel.sendTyping();
24 | 		} catch (error) {
25 | 			message.client.emit(Events.MessageCommandTypingError, error as Error, { ...payload, command, message });
26 | 		}
27 | 	}
28 | }
29 | 
--------------------------------------------------------------------------------
/src/optional-listeners/message-command-listeners/CoreMessageCreate.ts:
--------------------------------------------------------------------------------
 1 | import type { Message } from 'discord.js';
 2 | import { Listener } from '../../lib/structures/Listener';
 3 | import { Events } from '../../lib/types/Events';
 4 | 
 5 | export class CoreListener extends Listener {
 6 | 	public constructor(context: Listener.LoaderContext) {
 7 | 		super(context, { event: Events.MessageCreate });
 8 | 	}
 9 | 
10 | 	public run(message: Message) {
11 | 		// Stop bots and webhooks from running commands.
12 | 		if (message.author.bot || message.webhookId) return;
13 | 
14 | 		// Run the message parser.
15 | 		this.container.client.emit(Events.PreMessageParsed, message);
16 | 	}
17 | }
18 | 
--------------------------------------------------------------------------------
/src/optional-listeners/message-command-listeners/CorePreMessageCommandRun.ts:
--------------------------------------------------------------------------------
 1 | import { Listener } from '../../lib/structures/Listener';
 2 | import { Events, type PreMessageCommandRunPayload } from '../../lib/types/Events';
 3 | 
 4 | export class CoreListener extends Listener {
 5 | 	public constructor(context: Listener.LoaderContext) {
 6 | 		super(context, { event: Events.PreMessageCommandRun });
 7 | 	}
 8 | 
 9 | 	public async run(payload: PreMessageCommandRunPayload) {
10 | 		const { message, command } = payload;
11 | 
12 | 		// Run global preconditions:
13 | 		const globalResult = await this.container.stores.get('preconditions').messageRun(message, command, payload as any);
14 | 		if (globalResult.isErr()) {
15 | 			message.client.emit(Events.MessageCommandDenied, globalResult.unwrapErr(), payload);
16 | 			return;
17 | 		}
18 | 
19 | 		// Run command-specific preconditions:
20 | 		const localResult = await command.preconditions.messageRun(message, command, payload as any);
21 | 		if (localResult.isErr()) {
22 | 			message.client.emit(Events.MessageCommandDenied, localResult.unwrapErr(), payload);
23 | 			return;
24 | 		}
25 | 
26 | 		message.client.emit(Events.MessageCommandAccepted, payload);
27 | 	}
28 | }
29 | 
--------------------------------------------------------------------------------
/src/optional-listeners/message-command-listeners/CorePrefixedMessage.ts:
--------------------------------------------------------------------------------
 1 | import type { Message } from 'discord.js';
 2 | import { Listener } from '../../lib/structures/Listener';
 3 | import type { MessageCommand } from '../../lib/types/CommandTypes';
 4 | import { Events } from '../../lib/types/Events';
 5 | 
 6 | export class CoreListener extends Listener {
 7 | 	public constructor(context: Listener.LoaderContext) {
 8 | 		super(context, { event: Events.PrefixedMessage });
 9 | 	}
10 | 
11 | 	public run(message: Message, prefix: string | RegExp) {
12 | 		const { client, stores } = this.container;
13 | 		// Retrieve the command name and validate:
14 | 		const commandPrefix = this.getCommandPrefix(message.content, prefix);
15 | 		const prefixLess = message.content.slice(commandPrefix.length).trim();
16 | 
17 | 		// The character that separates the command name from the arguments, this will return -1 when '[p]command' is
18 | 		// passed, and a non -1 value when '[p]command arg' is passed instead.
19 | 		const spaceIndex = prefixLess.indexOf(' ');
20 | 		const commandName = spaceIndex === -1 ? prefixLess : prefixLess.slice(0, spaceIndex);
21 | 		if (commandName.length === 0) {
22 | 			client.emit(Events.UnknownMessageCommandName, { message, prefix, commandPrefix });
23 | 			return;
24 | 		}
25 | 
26 | 		// Retrieve the command and validate:
27 | 		const command = stores.get('commands').get(client.options.caseInsensitiveCommands ? commandName.toLowerCase() : commandName);
28 | 		if (!command) {
29 | 			client.emit(Events.UnknownMessageCommand, { message, prefix, commandName, commandPrefix });
30 | 			return;
31 | 		}
32 | 
33 | 		// If the command exists but is missing a message handler, emit a different event (maybe an application command variant exists)
34 | 		if (!command.messageRun) {
35 | 			client.emit(Events.CommandDoesNotHaveMessageCommandHandler, { message, prefix, commandPrefix, command });
36 | 			return;
37 | 		}
38 | 
39 | 		// Run the last stage before running the command:
40 | 		const parameters = spaceIndex === -1 ? '' : prefixLess.substring(spaceIndex + 1).trim();
41 | 		client.emit(Events.PreMessageCommandRun, {
42 | 			message,
43 | 			command: command as MessageCommand,
44 | 			parameters,
45 | 			context: { commandName, commandPrefix, prefix }
46 | 		});
47 | 	}
48 | 
49 | 	private getCommandPrefix(content: string, prefix: string | RegExp): string {
50 | 		return typeof prefix === 'string' ? prefix : prefix.exec(content)![0];
51 | 	}
52 | }
53 | 
--------------------------------------------------------------------------------
/src/optional-listeners/message-command-listeners/_load.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { CoreListener as CoreMessageCommandAccepted } from './CoreMessageCommandAccepted';
 3 | import { CoreListener as CoreMessageCommandTyping } from './CoreMessageCommandTyping';
 4 | import { CoreListener as CoreMessageCreate } from './CoreMessageCreate';
 5 | import { CoreListener as CorePreMessageCommandRun } from './CorePreMessageCommandRun';
 6 | import { CoreListener as CorePreMessageParser } from './CorePreMessageParser';
 7 | import { CoreListener as CorePrefixedMessage } from './CorePrefixedMessage';
 8 | 
 9 | export function loadMessageCommandListeners() {
10 | 	const store = 'listeners' as const;
11 | 	void container.stores.loadPiece({ name: 'CoreMessageCommandAccepted', piece: CoreMessageCommandAccepted, store });
12 | 	void container.stores.loadPiece({ name: 'CoreMessageCommandTyping', piece: CoreMessageCommandTyping, store });
13 | 	void container.stores.loadPiece({ name: 'CoreMessageCreate', piece: CoreMessageCreate, store });
14 | 	void container.stores.loadPiece({ name: 'CorePrefixedMessage', piece: CorePrefixedMessage, store });
15 | 	void container.stores.loadPiece({ name: 'CorePreMessageCommandRun', piece: CorePreMessageCommandRun, store });
16 | 	void container.stores.loadPiece({ name: 'CorePreMessageParser', piece: CorePreMessageParser, store });
17 | }
18 | 
--------------------------------------------------------------------------------
/src/preconditions/DMOnly.ts:
--------------------------------------------------------------------------------
 1 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 2 | import { Identifiers } from '../lib/errors/Identifiers';
 3 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 4 | import { container } from '@sapphire/pieces';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 8 | 		return message.guild === null ? this.ok() : this.makeSharedError();
 9 | 	}
10 | 
11 | 	public chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.Result {
12 | 		return interaction.guildId === null ? this.ok() : this.makeSharedError();
13 | 	}
14 | 
15 | 	public contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.Result {
16 | 		return interaction.guildId === null ? this.ok() : this.makeSharedError();
17 | 	}
18 | 
19 | 	private makeSharedError(): AllFlowsPrecondition.Result {
20 | 		return this.error({
21 | 			identifier: Identifiers.PreconditionDMOnly,
22 | 			message: 'You cannot run this command outside DMs.'
23 | 		});
24 | 	}
25 | }
26 | 
27 | void container.stores.loadPiece({
28 | 	name: 'DMOnly',
29 | 	piece: CorePrecondition,
30 | 	store: 'preconditions'
31 | });
32 | 
--------------------------------------------------------------------------------
/src/preconditions/Enabled.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import type { Command } from '../lib/structures/Command';
 5 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 6 | 
 7 | export class CorePrecondition extends AllFlowsPrecondition {
 8 | 	public constructor(context: AllFlowsPrecondition.LoaderContext) {
 9 | 		super(context, { position: 10 });
10 | 	}
11 | 
12 | 	public messageRun(_: Message, command: Command, context: AllFlowsPrecondition.Context): AllFlowsPrecondition.Result {
13 | 		return command.enabled
14 | 			? this.ok()
15 | 			: this.error({ identifier: Identifiers.CommandDisabled, message: 'This message command is disabled.', context });
16 | 	}
17 | 
18 | 	public chatInputRun(_: ChatInputCommandInteraction, command: Command, context: AllFlowsPrecondition.Context): AllFlowsPrecondition.Result {
19 | 		return command.enabled
20 | 			? this.ok()
21 | 			: this.error({ identifier: Identifiers.CommandDisabled, message: 'This chat input command is disabled.', context });
22 | 	}
23 | 
24 | 	public contextMenuRun(_: ContextMenuCommandInteraction, command: Command, context: AllFlowsPrecondition.Context): AllFlowsPrecondition.Result {
25 | 		return command.enabled
26 | 			? this.ok()
27 | 			: this.error({ identifier: Identifiers.CommandDisabled, message: 'This context menu command is disabled.', context });
28 | 	}
29 | }
30 | 
31 | void container.stores.loadPiece({
32 | 	name: 'Enabled',
33 | 	piece: CorePrecondition,
34 | 	store: 'preconditions'
35 | });
36 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildNewsOnly.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { ChannelType, ChatInputCommandInteraction, ContextMenuCommandInteraction, Message, type TextBasedChannelTypes } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	private readonly allowedTypes: TextBasedChannelTypes[] = [ChannelType.GuildAnnouncement, ChannelType.AnnouncementThread];
 8 | 
 9 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
10 | 		return this.allowedTypes.includes(message.channel.type) ? this.ok() : this.makeSharedError();
11 | 	}
12 | 
13 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
14 | 		const channel = await this.fetchChannelFromInteraction(interaction);
15 | 
16 | 		return this.allowedTypes.includes(channel.type) ? this.ok() : this.makeSharedError();
17 | 	}
18 | 
19 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
20 | 		const channel = await this.fetchChannelFromInteraction(interaction);
21 | 
22 | 		return this.allowedTypes.includes(channel.type) ? this.ok() : this.makeSharedError();
23 | 	}
24 | 
25 | 	private makeSharedError(): AllFlowsPrecondition.Result {
26 | 		return this.error({
27 | 			identifier: Identifiers.PreconditionGuildNewsOnly,
28 | 			message: 'You can only run this command in server announcement channels.'
29 | 		});
30 | 	}
31 | }
32 | 
33 | void container.stores.loadPiece({
34 | 	name: 'GuildNewsOnly',
35 | 	piece: CorePrecondition,
36 | 	store: 'preconditions'
37 | });
38 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildNewsThreadOnly.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { ChannelType, ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 8 | 		return message.thread?.type === ChannelType.AnnouncementThread ? this.ok() : this.makeSharedError();
 9 | 	}
10 | 
11 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
12 | 		const channel = await this.fetchChannelFromInteraction(interaction);
13 | 		return channel.type === ChannelType.AnnouncementThread ? this.ok() : this.makeSharedError();
14 | 	}
15 | 
16 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
17 | 		const channel = await this.fetchChannelFromInteraction(interaction);
18 | 		return channel.type === ChannelType.AnnouncementThread ? this.ok() : this.makeSharedError();
19 | 	}
20 | 
21 | 	private makeSharedError(): AllFlowsPrecondition.Result {
22 | 		return this.error({
23 | 			identifier: Identifiers.PreconditionGuildNewsThreadOnly,
24 | 			message: 'You can only run this command in server announcement thread channels.'
25 | 		});
26 | 	}
27 | }
28 | 
29 | void container.stores.loadPiece({
30 | 	name: 'GuildNewsThreadOnly',
31 | 	piece: CorePrecondition,
32 | 	store: 'preconditions'
33 | });
34 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildOnly.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 8 | 		return message.guildId === null ? this.makeSharedError() : this.ok();
 9 | 	}
10 | 
11 | 	public chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.Result {
12 | 		return interaction.guildId === null ? this.makeSharedError() : this.ok();
13 | 	}
14 | 
15 | 	public contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.Result {
16 | 		return interaction.guildId === null ? this.makeSharedError() : this.ok();
17 | 	}
18 | 
19 | 	private makeSharedError(): AllFlowsPrecondition.Result {
20 | 		return this.error({
21 | 			identifier: Identifiers.PreconditionGuildOnly,
22 | 			message: 'You cannot run this command in DMs.'
23 | 		});
24 | 	}
25 | }
26 | 
27 | void container.stores.loadPiece({
28 | 	name: 'GuildOnly',
29 | 	piece: CorePrecondition,
30 | 	store: 'preconditions'
31 | });
32 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildPrivateThreadOnly.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { ChannelType, ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 8 | 		return message.thread?.type === ChannelType.PrivateThread ? this.ok() : this.makeSharedError();
 9 | 	}
10 | 
11 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
12 | 		const channel = await this.fetchChannelFromInteraction(interaction);
13 | 		return channel.type === ChannelType.PrivateThread ? this.ok() : this.makeSharedError();
14 | 	}
15 | 
16 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
17 | 		const channel = await this.fetchChannelFromInteraction(interaction);
18 | 		return channel.type === ChannelType.PrivateThread ? this.ok() : this.makeSharedError();
19 | 	}
20 | 
21 | 	private makeSharedError(): AllFlowsPrecondition.Result {
22 | 		return this.error({
23 | 			identifier: Identifiers.PreconditionGuildPrivateThreadOnly,
24 | 			message: 'You can only run this command in private server thread channels.'
25 | 		});
26 | 	}
27 | }
28 | 
29 | void container.stores.loadPiece({
30 | 	name: 'GuildPrivateThreadOnly',
31 | 	piece: CorePrecondition,
32 | 	store: 'preconditions'
33 | });
34 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildPublicThreadOnly.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { ChannelType, ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 8 | 		return message.thread?.type === ChannelType.PublicThread ? this.ok() : this.makeSharedError();
 9 | 	}
10 | 
11 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
12 | 		const channel = await this.fetchChannelFromInteraction(interaction);
13 | 		return channel.type === ChannelType.PublicThread ? this.ok() : this.makeSharedError();
14 | 	}
15 | 
16 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
17 | 		const channel = await this.fetchChannelFromInteraction(interaction);
18 | 		return channel.type === ChannelType.PublicThread ? this.ok() : this.makeSharedError();
19 | 	}
20 | 
21 | 	private makeSharedError(): AllFlowsPrecondition.Result {
22 | 		return this.error({
23 | 			identifier: Identifiers.PreconditionGuildPublicThreadOnly,
24 | 			message: 'You can only run this command in public server thread channels.'
25 | 		});
26 | 	}
27 | }
28 | 
29 | void container.stores.loadPiece({
30 | 	name: 'GuildPublicThreadOnly',
31 | 	piece: CorePrecondition,
32 | 	store: 'preconditions'
33 | });
34 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildTextOnly.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import { ChannelType, ChatInputCommandInteraction, ContextMenuCommandInteraction, Message, type TextBasedChannelTypes } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	private readonly allowedTypes: TextBasedChannelTypes[] = [ChannelType.GuildText, ChannelType.PublicThread, ChannelType.PrivateThread];
 8 | 
 9 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
10 | 		return this.allowedTypes.includes(message.channel.type) ? this.ok() : this.makeSharedError();
11 | 	}
12 | 
13 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
14 | 		const channel = await this.fetchChannelFromInteraction(interaction);
15 | 		return this.allowedTypes.includes(channel.type) ? this.ok() : this.makeSharedError();
16 | 	}
17 | 
18 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
19 | 		const channel = await this.fetchChannelFromInteraction(interaction);
20 | 		return this.allowedTypes.includes(channel.type) ? this.ok() : this.makeSharedError();
21 | 	}
22 | 
23 | 	private makeSharedError(): AllFlowsPrecondition.Result {
24 | 		return this.error({
25 | 			identifier: Identifiers.PreconditionGuildTextOnly,
26 | 			message: 'You can only run this command in server text channels.'
27 | 		});
28 | 	}
29 | }
30 | 
31 | void container.stores.loadPiece({
32 | 	name: 'GuildTextOnly',
33 | 	piece: CorePrecondition,
34 | 	store: 'preconditions'
35 | });
36 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildThreadOnly.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 8 | 		return message.thread ? this.ok() : this.makeSharedError();
 9 | 	}
10 | 
11 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
12 | 		const channel = await this.fetchChannelFromInteraction(interaction);
13 | 		return channel.isThread() ? this.ok() : this.makeSharedError();
14 | 	}
15 | 
16 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
17 | 		const channel = await this.fetchChannelFromInteraction(interaction);
18 | 		return channel.isThread() ? this.ok() : this.makeSharedError();
19 | 	}
20 | 
21 | 	private makeSharedError(): AllFlowsPrecondition.Result {
22 | 		return this.error({
23 | 			identifier: Identifiers.PreconditionThreadOnly,
24 | 			message: 'You can only run this command in server thread channels.'
25 | 		});
26 | 	}
27 | }
28 | 
29 | void container.stores.loadPiece({
30 | 	name: 'GuildThreadOnly',
31 | 	piece: CorePrecondition,
32 | 	store: 'preconditions'
33 | });
34 | 
--------------------------------------------------------------------------------
/src/preconditions/GuildVoiceOnly.ts:
--------------------------------------------------------------------------------
 1 | import { isVoiceChannel } from '@sapphire/discord.js-utilities';
 2 | import { container } from '@sapphire/pieces';
 3 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 4 | import { Identifiers } from '../lib/errors/Identifiers';
 5 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 6 | 
 7 | export class CorePrecondition extends AllFlowsPrecondition {
 8 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 9 | 		return isVoiceChannel(message.channel) ? this.ok() : this.makeSharedError();
10 | 	}
11 | 
12 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
13 | 		const channel = await this.fetchChannelFromInteraction(interaction);
14 | 		return isVoiceChannel(channel) ? this.ok() : this.makeSharedError();
15 | 	}
16 | 
17 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
18 | 		const channel = await this.fetchChannelFromInteraction(interaction);
19 | 		return isVoiceChannel(channel) ? this.ok() : this.makeSharedError();
20 | 	}
21 | 
22 | 	private makeSharedError(): AllFlowsPrecondition.Result {
23 | 		return this.error({
24 | 			identifier: Identifiers.PreconditionGuildVoiceOnly,
25 | 			message: 'You can only run this command in server voice channels.'
26 | 		});
27 | 	}
28 | }
29 | 
30 | void container.stores.loadPiece({
31 | 	name: 'GuildVoiceOnly',
32 | 	piece: CorePrecondition,
33 | 	store: 'preconditions'
34 | });
35 | 
--------------------------------------------------------------------------------
/src/preconditions/NSFW.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { AllFlowsPrecondition } from '../lib/structures/Precondition';
 5 | 
 6 | export class CorePrecondition extends AllFlowsPrecondition {
 7 | 	public messageRun(message: Message): AllFlowsPrecondition.Result {
 8 | 		// `nsfw` is undefined in DMChannel, doing `=== true`
 9 | 		// will result on it returning `false`.
10 | 		return Reflect.get(message.channel, 'nsfw') === true
11 | 			? this.ok()
12 | 			: this.error({ identifier: Identifiers.PreconditionNSFW, message: 'You cannot run this message command outside NSFW channels.' });
13 | 	}
14 | 
15 | 	public async chatInputRun(interaction: ChatInputCommandInteraction): AllFlowsPrecondition.AsyncResult {
16 | 		const channel = await this.fetchChannelFromInteraction(interaction);
17 | 
18 | 		// `nsfw` is undefined in DMChannel, doing `=== true`
19 | 		// will result on it returning `false`.
20 | 		return Reflect.get(channel, 'nsfw') === true
21 | 			? this.ok()
22 | 			: this.error({ identifier: Identifiers.PreconditionNSFW, message: 'You cannot run this chat input command outside NSFW channels.' });
23 | 	}
24 | 
25 | 	public async contextMenuRun(interaction: ContextMenuCommandInteraction): AllFlowsPrecondition.AsyncResult {
26 | 		const channel = await this.fetchChannelFromInteraction(interaction);
27 | 
28 | 		// `nsfw` is undefined in DMChannel, doing `=== true`
29 | 		// will result on it returning `false`.
30 | 		return Reflect.get(channel, 'nsfw') === true
31 | 			? this.ok()
32 | 			: this.error({ identifier: Identifiers.PreconditionNSFW, message: 'You cannot run this command outside NSFW channels.' });
33 | 	}
34 | }
35 | 
36 | void container.stores.loadPiece({
37 | 	name: 'NSFW',
38 | 	piece: CorePrecondition,
39 | 	store: 'preconditions'
40 | });
41 | 
--------------------------------------------------------------------------------
/src/preconditions/RunIn.ts:
--------------------------------------------------------------------------------
 1 | import { container } from '@sapphire/pieces';
 2 | import type { ChatInputCommandInteraction, ContextMenuCommandInteraction, Message } from 'discord.js';
 3 | import { Identifiers } from '../lib/errors/Identifiers';
 4 | import { Command } from '../lib/structures/Command';
 5 | import { AllFlowsPrecondition, type Preconditions } from '../lib/structures/Precondition';
 6 | import type { ChatInputCommand, ContextMenuCommand, MessageCommand } from '../lib/types/CommandTypes';
 7 | 
 8 | export interface RunInPreconditionContext extends AllFlowsPrecondition.Context {
 9 | 	types?: Preconditions['RunIn']['types'];
10 | }
11 | 
12 | export class CorePrecondition extends AllFlowsPrecondition {
13 | 	public override messageRun(message: Message, _: MessageCommand, context: RunInPreconditionContext): AllFlowsPrecondition.Result {
14 | 		const commandType = 'message';
15 | 		if (!context.types) return this.ok();
16 | 
17 | 		const channelType = message.channel.type;
18 | 
19 | 		if (Command.runInTypeIsSpecificsObject(context.types)) {
20 | 			return context.types.messageRun.includes(channelType) ? this.ok() : this.makeSharedError(context, commandType);
21 | 		}
22 | 
23 | 		return context.types.includes(channelType) ? this.ok() : this.makeSharedError(context, commandType);
24 | 	}
25 | 
26 | 	public override async chatInputRun(
27 | 		interaction: ChatInputCommandInteraction,
28 | 		_: ChatInputCommand,
29 | 		context: RunInPreconditionContext
30 | 	): AllFlowsPrecondition.AsyncResult {
31 | 		const commandType = 'chat input';
32 | 		if (!context.types) return this.ok();
33 | 
34 | 		const channelType = (await this.fetchChannelFromInteraction(interaction)).type;
35 | 
36 | 		if (Command.runInTypeIsSpecificsObject(context.types)) {
37 | 			return context.types.chatInputRun.includes(channelType) ? this.ok() : this.makeSharedError(context, commandType);
38 | 		}
39 | 
40 | 		return context.types.includes(channelType) ? this.ok() : this.makeSharedError(context, commandType);
41 | 	}
42 | 
43 | 	public override async contextMenuRun(
44 | 		interaction: ContextMenuCommandInteraction,
45 | 		_: ContextMenuCommand,
46 | 		context: RunInPreconditionContext
47 | 	): AllFlowsPrecondition.AsyncResult {
48 | 		const commandType = 'context menu';
49 | 		if (!context.types) return this.ok();
50 | 
51 | 		const channelType = (await this.fetchChannelFromInteraction(interaction)).type;
52 | 
53 | 		if (Command.runInTypeIsSpecificsObject(context.types)) {
54 | 			return context.types.contextMenuRun.includes(channelType) ? this.ok() : this.makeSharedError(context, commandType);
55 | 		}
56 | 
57 | 		return context.types.includes(channelType) ? this.ok() : this.makeSharedError(context, commandType);
58 | 	}
59 | 
60 | 	private makeSharedError(context: RunInPreconditionContext, commandType: string): AllFlowsPrecondition.Result {
61 | 		return this.error({
62 | 			identifier: Identifiers.PreconditionRunIn,
63 | 			message: `You cannot run this ${commandType} command in this type of channel.`,
64 | 			context: { types: context.types }
65 | 		});
66 | 	}
67 | }
68 | 
69 | void container.stores.loadPiece({
70 | 	name: 'RunIn',
71 | 	piece: CorePrecondition,
72 | 	store: 'preconditions'
73 | });
74 | 
--------------------------------------------------------------------------------
/src/preconditions/_load.ts:
--------------------------------------------------------------------------------
 1 | import './ClientPermissions';
 2 | import './Cooldown';
 3 | import './DMOnly';
 4 | import './Enabled';
 5 | import './GuildNewsOnly';
 6 | import './GuildNewsThreadOnly';
 7 | import './GuildOnly';
 8 | import './GuildPrivateThreadOnly';
 9 | import './GuildPublicThreadOnly';
10 | import './GuildTextOnly';
11 | import './GuildThreadOnly';
12 | import './GuildVoiceOnly';
13 | import './NSFW';
14 | import './RunIn';
15 | import './UserPermissions';
16 | 
--------------------------------------------------------------------------------
/src/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": "../tsconfig.base.json",
3 | 	"compilerOptions": {
4 | 		"rootDir": "./"
5 | 	},
6 | 	"include": ["."]
7 | }
8 | 
--------------------------------------------------------------------------------
/tests/Flags.test.ts:
--------------------------------------------------------------------------------
 1 | import { Lexer, Parser } from '@sapphire/lexure';
 2 | import { FlagUnorderedStrategy } from '../src/lib/utils/strategies/FlagUnorderedStrategy';
 3 | 
 4 | const parse = (testString: string) =>
 5 | 	new Parser(new FlagUnorderedStrategy({ flags: ['f', 'hello'], options: ['o', 'option'] })).run(
 6 | 		new Lexer({
 7 | 			quotes: [
 8 | 				['"', '"'],
 9 | 				['“', '”'],
10 | 				['「', '」'],
11 | 				['«', '»']
12 | 			]
13 | 		}).run(testString)
14 | 	);
15 | 
16 | describe('Flag parsing strategy', () => {
17 | 	test('GIVEN typeof FlagStrategy THEN returns function', () => {
18 | 		expect(typeof FlagUnorderedStrategy).toBe('function');
19 | 	});
20 | 	test('GIVEN flag without value THEN returns flag', () => {
21 | 		const { flags, options } = parse('-f');
22 | 		expect(flags.size).toBe(1);
23 | 		expect([...flags]).toStrictEqual(['f']);
24 | 		expect(options.size).toBe(0);
25 | 	});
26 | 
27 | 	test('GIVEN flag without value inside text THEN returns flag', () => {
28 | 		const { flags, options } = parse('commit "hello there" -f');
29 | 		expect(flags.size).toBe(1);
30 | 		expect([...flags]).toStrictEqual(['f']);
31 | 		expect(options.size).toBe(0);
32 | 	});
33 | 
34 | 	test('GIVEN flag with value THEN returns nothing', () => {
35 | 		const { flags, options } = parse('-f=hi');
36 | 		expect(flags.size).toBe(0);
37 | 		expect(options.size).toBe(0);
38 | 	});
39 | 
40 | 	test('GIVEN flag with value inside text THEN returns nothing', () => {
41 | 		const { flags, options } = parse('commit "hello there" -f=hi');
42 | 		expect(flags.size).toBe(0);
43 | 		expect(options.size).toBe(0);
44 | 	});
45 | 
46 | 	test('GIVEN option with value THEN returns option', () => {
47 | 		const { flags, options } = parse('--option=world');
48 | 		expect(flags.size).toBe(0);
49 | 		expect(options.size).toBe(1);
50 | 		expect(options.has('option')).toBe(true);
51 | 		expect(options.get('option')).toStrictEqual(['world']);
52 | 	});
53 | 
54 | 	test('GIVEN option with value inside text THEN returns option with single value', () => {
55 | 		const { flags, options } = parse('command --option=world');
56 | 		expect(flags.size).toBe(0);
57 | 		expect(options.size).toBe(1);
58 | 		expect(options.has('option')).toBe(true);
59 | 		expect(options.get('option')).toStrictEqual(['world']);
60 | 	});
61 | 
62 | 	test('GIVEN option with multiple occurences inside text THEN returns option with multiple values', () => {
63 | 		const { flags, options } = parse('command --option=world --option=sammy');
64 | 		expect(flags.size).toBe(0);
65 | 		expect(options.size).toBe(1);
66 | 		expect(options.has('option')).toBe(true);
67 | 		expect(options.get('option')).toStrictEqual(['world', 'sammy']);
68 | 	});
69 | 
70 | 	test('GIVEN flag inside quotes THEN returns nothing', () => {
71 | 		const { flags, options } = parse('commit "hello there -f"');
72 | 		expect(flags.size).toBe(0);
73 | 		expect(options.size).toBe(0);
74 | 	});
75 | 
76 | 	test('GIVEN option without value inside quote THEN returns nothing', () => {
77 | 		const { flags, options } = parse('mention "try --hello"');
78 | 		expect(flags.size).toBe(0);
79 | 		expect(options.size).toBe(0);
80 | 	});
81 | });
82 | 
--------------------------------------------------------------------------------
/tests/precondition-resolvers/clientPermissions.test.ts:
--------------------------------------------------------------------------------
 1 | import { PermissionFlagsBits } from 'discord.js';
 2 | import { parseConstructorPreConditionsRequiredClientPermissions } from '../../src/lib/precondition-resolvers/clientPermissions';
 3 | import { CommandPreConditions } from '../../src/lib/types/Enums';
 4 | import { PreconditionContainerArray } from '../../src/lib/utils/preconditions/PreconditionContainerArray';
 5 | import type { PreconditionContainerSingle } from '../../src/lib/utils/preconditions/PreconditionContainerSingle';
 6 | import type { PermissionPreconditionContext } from '../../src/preconditions/ClientPermissions';
 7 | 
 8 | describe('parseConstructorPreConditionsRequiredClientPermissions', () => {
 9 | 	test('GIVEN valid permissions THEN appends to preconditionContainerArray', () => {
10 | 		const preconditionContainerArray = new PreconditionContainerArray();
11 | 		parseConstructorPreConditionsRequiredClientPermissions(PermissionFlagsBits.Administrator, preconditionContainerArray);
12 | 		expect(preconditionContainerArray.entries.length).toBe(1);
13 | 		expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).name).toBe(CommandPreConditions.ClientPermissions);
14 | 		expect(
15 | 			((preconditionContainerArray.entries[0] as PreconditionContainerSingle).context as PermissionPreconditionContext).permissions?.has(
16 | 				PermissionFlagsBits.Administrator
17 | 			)
18 | 		).toBe(true);
19 | 	});
20 | 
21 | 	test('GIVEN no permissions THEN does not append to preconditionContainerArray', () => {
22 | 		const preconditionContainerArray = new PreconditionContainerArray();
23 | 		parseConstructorPreConditionsRequiredClientPermissions(undefined, preconditionContainerArray);
24 | 		expect(preconditionContainerArray.entries.length).toBe(0);
25 | 	});
26 | });
27 | 
--------------------------------------------------------------------------------
/tests/precondition-resolvers/nsfw.test.ts:
--------------------------------------------------------------------------------
 1 | import { parseConstructorPreConditionsNsfw } from '../../src/lib/precondition-resolvers/nsfw';
 2 | import { CommandPreConditions } from '../../src/lib/types/Enums';
 3 | import { PreconditionContainerArray } from '../../src/lib/utils/preconditions/PreconditionContainerArray';
 4 | 
 5 | describe('parseConstructorPreConditionsNsfw', () => {
 6 | 	test('GIVEN nsfw true THEN appends to preconditionContainerArray', () => {
 7 | 		const preconditionContainerArray = new PreconditionContainerArray();
 8 | 		parseConstructorPreConditionsNsfw(true, preconditionContainerArray);
 9 | 		expect(preconditionContainerArray.entries.length).toBe(1);
10 | 		expect((preconditionContainerArray.entries[0] as any).name).toBe(CommandPreConditions.NotSafeForWork);
11 | 	});
12 | 
13 | 	test('GIVEN nsfw false THEN does not append to preconditionContainerArray', () => {
14 | 		const preconditionContainerArray = new PreconditionContainerArray();
15 | 		parseConstructorPreConditionsNsfw(false, preconditionContainerArray);
16 | 		expect(preconditionContainerArray.entries.length).toBe(0);
17 | 	});
18 | 
19 | 	test('GIVEN nsfw undefined THEN does not append to preconditionContainerArray', () => {
20 | 		const preconditionContainerArray = new PreconditionContainerArray();
21 | 		parseConstructorPreConditionsNsfw(undefined, preconditionContainerArray);
22 | 		expect(preconditionContainerArray.entries.length).toBe(0);
23 | 	});
24 | });
25 | 
--------------------------------------------------------------------------------
/tests/precondition-resolvers/userPermissions.test.ts:
--------------------------------------------------------------------------------
 1 | import { PermissionFlagsBits } from 'discord.js';
 2 | import { parseConstructorPreConditionsRequiredUserPermissions } from '../../src/lib/precondition-resolvers/userPermissions';
 3 | import { CommandPreConditions } from '../../src/lib/types/Enums';
 4 | import { PreconditionContainerArray } from '../../src/lib/utils/preconditions/PreconditionContainerArray';
 5 | import type { PreconditionContainerSingle } from '../../src/lib/utils/preconditions/PreconditionContainerSingle';
 6 | import type { PermissionPreconditionContext } from '../../src/preconditions/ClientPermissions';
 7 | 
 8 | describe('parseConstructorPreConditionsRequiredUserPermissions', () => {
 9 | 	test('GIVEN valid permissions THEN appends to preconditionContainerArray', () => {
10 | 		const preconditionContainerArray = new PreconditionContainerArray();
11 | 		parseConstructorPreConditionsRequiredUserPermissions(PermissionFlagsBits.Administrator, preconditionContainerArray);
12 | 		expect(preconditionContainerArray.entries.length).toBe(1);
13 | 		expect((preconditionContainerArray.entries[0] as PreconditionContainerSingle).name).toBe(CommandPreConditions.UserPermissions);
14 | 		expect(
15 | 			((preconditionContainerArray.entries[0] as PreconditionContainerSingle).context as PermissionPreconditionContext).permissions?.has(
16 | 				PermissionFlagsBits.Administrator
17 | 			)
18 | 		).toBe(true);
19 | 	});
20 | 
21 | 	test('GIVEN no permissions THEN does not append to preconditionContainerArray', () => {
22 | 		const preconditionContainerArray = new PreconditionContainerArray();
23 | 		parseConstructorPreConditionsRequiredUserPermissions(undefined, preconditionContainerArray);
24 | 		expect(preconditionContainerArray.entries.length).toBe(0);
25 | 	});
26 | });
27 | 
--------------------------------------------------------------------------------
/tests/resolvers/boolean.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveBoolean } from '../../src/lib/resolvers/boolean';
 4 | 
 5 | describe('Boolean resolver tests', () => {
 6 | 	test('GIVEN a truthy value THEN returns true', () => {
 7 | 		expect(resolveBoolean('true')).toEqual(Result.ok(true));
 8 | 		expect(resolveBoolean('1')).toEqual(Result.ok(true));
 9 | 		expect(resolveBoolean('+')).toEqual(Result.ok(true));
10 | 		expect(resolveBoolean('yes')).toEqual(Result.ok(true));
11 | 	});
12 | 
13 | 	test('GIVEN a falsy value THEN returns false', () => {
14 | 		expect(resolveBoolean('false')).toEqual(Result.ok(false));
15 | 		expect(resolveBoolean('0')).toEqual(Result.ok(false));
16 | 		expect(resolveBoolean('-')).toEqual(Result.ok(false));
17 | 		expect(resolveBoolean('no')).toEqual(Result.ok(false));
18 | 	});
19 | 
20 | 	test('GIVEN a truthy value with custom ones THEN returns true', () => {
21 | 		expect(resolveBoolean('yay', { truths: ['yay'] })).toEqual(Result.ok(true));
22 | 		expect(resolveBoolean('yup', { truths: ['yay', 'yup', 'yop'] })).toEqual(Result.ok(true));
23 | 	});
24 | 
25 | 	test('GIVEN a falsy value with custom ones THEN returns false', () => {
26 | 		expect(resolveBoolean('nah', { falses: ['nah'] })).toEqual(Result.ok(false));
27 | 		expect(resolveBoolean('nope', { falses: ['nah', 'nope', 'noooo'] })).toEqual(Result.ok(false));
28 | 	});
29 | 
30 | 	test('GIVEN an invalid values THEN returns error', () => {
31 | 		expect(resolveBoolean('hello')).toEqual(Result.err(Identifiers.ArgumentBooleanError));
32 | 		expect(resolveBoolean('world', { truths: ['nah', 'nope', 'noooo'] })).toEqual(Result.err(Identifiers.ArgumentBooleanError));
33 | 	});
34 | });
35 | 
--------------------------------------------------------------------------------
/tests/resolvers/date.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveDate } from '../../src/lib/resolvers/date';
 4 | 
 5 | const DATE_2018_PLAIN_STRING = 'August 11, 2018 00:00:00';
 6 | const DATE_2018 = new Date(DATE_2018_PLAIN_STRING);
 7 | 
 8 | const DATE_2020_PLAIN_STRING = 'August 11, 2020 00:00:00';
 9 | const DATE_2020 = new Date(DATE_2020_PLAIN_STRING);
10 | 
11 | const DATE_2022_PLAIN_STRING = 'August 11, 2022 00:00:00';
12 | const DATE_2022 = new Date(DATE_2022_PLAIN_STRING);
13 | 
14 | const MINIMUM = { minimum: new Date('August 11, 2019 00:00:00').getTime() };
15 | const MAXIMUM = { maximum: new Date('August 11, 2021 00:00:00').getTime() };
16 | 
17 | describe('Date resolver tests', () => {
18 | 	test('GIVEN a valid date-time THEN returns the associated timestamp', () => {
19 | 		expect(resolveDate(DATE_2020_PLAIN_STRING)).toEqual(Result.ok(DATE_2020));
20 | 	});
21 | 	test('GIVEN a valid date-time with minimum THEN returns the associated timestamp', () => {
22 | 		expect(resolveDate(DATE_2022_PLAIN_STRING, MINIMUM)).toEqual(Result.ok(DATE_2022));
23 | 	});
24 | 	test('GIVEN a valid date-time with maximum THEN returns the associated timestamp', () => {
25 | 		expect(resolveDate(DATE_2018_PLAIN_STRING, MAXIMUM)).toEqual(Result.ok(DATE_2018));
26 | 	});
27 | 	test('GIVEN a date-time before minimum THEN returns error', () => {
28 | 		expect(resolveDate(DATE_2018_PLAIN_STRING, MINIMUM)).toEqual(Result.err(Identifiers.ArgumentDateTooEarly));
29 | 	});
30 | 	test('GIVEN a date-time beyond maximum THEN returns error', () => {
31 | 		expect(resolveDate(DATE_2022_PLAIN_STRING, MAXIMUM)).toEqual(Result.err(Identifiers.ArgumentDateTooFar));
32 | 	});
33 | 	test('GIVEN an invalid date THEN returns error', () => {
34 | 		expect(resolveDate('hello')).toEqual(Result.err(Identifiers.ArgumentDateError));
35 | 	});
36 | });
37 | 
--------------------------------------------------------------------------------
/tests/resolvers/emoji.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveEmoji } from '../../src/lib/resolvers/emoji';
 4 | 
 5 | describe('Emoji resolver tests', () => {
 6 | 	test('GIVEN an unicode emoji THEN returns emojiObject', () => {
 7 | 		const resolvedEmoji = resolveEmoji('😄');
 8 | 		expect(resolvedEmoji.isOk()).toBe(true);
 9 | 		expect(() => resolvedEmoji.unwrapErr()).toThrowError();
10 | 		expect(resolvedEmoji.unwrap()).toMatchObject({ id: null, name: '😄' });
11 | 	});
12 | 	test('GIVEN a string emoji THEN returns ArgumentEmojiError', () => {
13 | 		const resolvedEmoji = resolveEmoji(':smile:');
14 | 		expect(resolvedEmoji).toEqual(Result.err(Identifiers.ArgumentEmojiError));
15 | 	});
16 | 	test('GIVEN a string THEN returns ArgumentEmojiError', () => {
17 | 		const resolvedEmoji = resolveEmoji('foo');
18 | 		expect(resolvedEmoji).toEqual(Result.err(Identifiers.ArgumentEmojiError));
19 | 	});
20 | 	test('GIVEN a wrongly formatted string custom emoji THEN returns ArgumentEmojiError', () => {
21 | 		const resolvedEmoji = resolveEmoji('');
22 | 		expect(resolvedEmoji).toEqual(Result.err(Identifiers.ArgumentEmojiError));
23 | 	});
24 | 	test('GIVEN a string custom emoji THEN returns emojiObject', () => {
25 | 		const resolvedEmoji = resolveEmoji('<:custom:737141877803057244>');
26 | 		expect(resolvedEmoji.isOk()).toBe(true);
27 | 		expect(() => resolvedEmoji.unwrapErr()).toThrowError();
28 | 		expect(resolvedEmoji.unwrap()).toMatchObject({ id: '737141877803057244', name: 'custom' });
29 | 	});
30 | 	test('GIVEN a string custom animated emoji THEN returns emojiObject', () => {
31 | 		const resolvedEmoji = resolveEmoji('');
32 | 		expect(resolvedEmoji.isOk()).toBe(true);
33 | 		expect(() => resolvedEmoji.unwrapErr()).toThrowError();
34 | 		expect(resolvedEmoji.unwrap()).toMatchObject({ animated: true, id: '737141877803057244', name: 'custom' });
35 | 	});
36 | });
37 | 
--------------------------------------------------------------------------------
/tests/resolvers/enum.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveEnum } from '../../src/lib/resolvers/enum';
 4 | 
 5 | describe('Enum resolver tests', () => {
 6 | 	test('GIVEN good lowercase enum from one option THEN returns string', () => {
 7 | 		const resolvedEnum = resolveEnum('foo', { enum: ['foo'] });
 8 | 		expect(resolvedEnum).toEqual(Result.ok('foo'));
 9 | 	});
10 | 	test('GIVEN good mixedcase enum from one option THEN returns string', () => {
11 | 		const resolvedEnum = resolveEnum('FoO', { enum: ['FoO'] });
12 | 		expect(resolvedEnum).toEqual(Result.ok('FoO'));
13 | 	});
14 | 	test('GIVEN good enum from more options THEN returns string', () => {
15 | 		const resolvedEnum = resolveEnum('foo', { enum: ['foo', 'bar', 'baz'] });
16 | 		expect(resolvedEnum).toEqual(Result.ok('foo'));
17 | 	});
18 | 	test('GIVEN good case insensitive enum from more options THEN returns string', () => {
19 | 		const resolvedEnum = resolveEnum('FoO', { enum: ['FoO', 'foo', 'bar', 'baz'], caseInsensitive: false });
20 | 		expect(resolvedEnum).toEqual(Result.ok('FoO'));
21 | 	});
22 | 	test('GIVEN good enum from one option THEN returns ArgumentEnumError', () => {
23 | 		const resolvedEnum = resolveEnum('foo', { enum: ['foo'] });
24 | 		expect(resolvedEnum.isOk()).toBe(true);
25 | 	});
26 | 	test('GIVEN an empty enum array THEN returns ArgumentEnumEmptyError', () => {
27 | 		const resolvedEnum = resolveEnum('foo');
28 | 		expect(resolvedEnum).toEqual(Result.err(Identifiers.ArgumentEnumEmptyError));
29 | 	});
30 | 	test('GIVEN an enum not listed in the array THEN returns ArgumentEnumError', () => {
31 | 		const resolvedEnum = resolveEnum('foo', { enum: ['bar', 'baz'] });
32 | 		expect(resolvedEnum).toEqual(Result.err(Identifiers.ArgumentEnumError));
33 | 	});
34 | 	test('GIVEN an enum with wrong case THEN returns ArgumentEnumError', () => {
35 | 		const resolvedEnum = resolveEnum('FOO', { enum: ['bar', 'baz'], caseInsensitive: false });
36 | 		expect(resolvedEnum).toEqual(Result.err(Identifiers.ArgumentEnumError));
37 | 	});
38 | });
39 | 
--------------------------------------------------------------------------------
/tests/resolvers/float.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveFloat } from '../../src/lib/resolvers/float';
 4 | 
 5 | describe('Float resolver tests', () => {
 6 | 	test('GIVEN a valid float THEN returns its parsed value', () => {
 7 | 		expect(resolveFloat('1.23')).toEqual(Result.ok(1.23));
 8 | 	});
 9 | 	test('GIVEN a valid float with minimum THEN returns its parsed value', () => {
10 | 		expect(resolveFloat('2.34', { minimum: 2 })).toEqual(Result.ok(2.34));
11 | 	});
12 | 	test('GIVEN a valid float with maximum THEN returns its parsed value', () => {
13 | 		expect(resolveFloat('3.45', { maximum: 4 })).toEqual(Result.ok(3.45));
14 | 	});
15 | 	test('GIVEN a float before minimum THEN returns error', () => {
16 | 		expect(resolveFloat('1.23', { minimum: 2 })).toEqual(Result.err(Identifiers.ArgumentFloatTooSmall));
17 | 	});
18 | 	test('GIVEN a float beyond maximum THEN returns error', () => {
19 | 		expect(resolveFloat('4.56', { maximum: 4 })).toEqual(Result.err(Identifiers.ArgumentFloatTooLarge));
20 | 	});
21 | 	test('GIVEN an invalid float THEN returns error', () => {
22 | 		expect(resolveFloat('hello')).toEqual(Result.err(Identifiers.ArgumentFloatError));
23 | 	});
24 | });
25 | 
--------------------------------------------------------------------------------
/tests/resolvers/hyperlink.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { URL } from 'node:url';
 3 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 4 | import { resolveHyperlink } from '../../src/lib/resolvers/hyperlink';
 5 | 
 6 | const STRING_URL = 'https://github.com/sapphiredev';
 7 | const PARSED_URL = new URL(STRING_URL);
 8 | 
 9 | describe('Hyperlink resolver tests', () => {
10 | 	test('GIVEN a valid hyperlink THEN returns its parsed value', () => {
11 | 		expect(resolveHyperlink(STRING_URL)).toEqual(Result.ok(PARSED_URL));
12 | 	});
13 | 	test('GIVEN an invalid hyperlink THEN returns error', () => {
14 | 		expect(resolveHyperlink('hello')).toEqual(Result.err(Identifiers.ArgumentHyperlinkError));
15 | 	});
16 | });
17 | 
--------------------------------------------------------------------------------
/tests/resolvers/integer.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveInteger } from '../../src/lib/resolvers/integer';
 4 | 
 5 | describe('Integer resolver tests', () => {
 6 | 	test('GIVEN a valid integer THEN returns its parsed value', () => {
 7 | 		expect(resolveInteger('1')).toEqual(Result.ok(1));
 8 | 	});
 9 | 	test('GIVEN a valid integer with minimum THEN returns its parsed value', () => {
10 | 		expect(resolveInteger('2', { minimum: 2 })).toEqual(Result.ok(2));
11 | 	});
12 | 	test('GIVEN a valid integer with maximum THEN returns its parsed value', () => {
13 | 		expect(resolveInteger('3', { maximum: 4 })).toEqual(Result.ok(3));
14 | 	});
15 | 	test('GIVEN a integer before minimum THEN returns error', () => {
16 | 		expect(resolveInteger('1', { minimum: 2 })).toEqual(Result.err(Identifiers.ArgumentIntegerTooSmall));
17 | 	});
18 | 	test('GIVEN a integer beyond maximum THEN returns error', () => {
19 | 		expect(resolveInteger('5', { maximum: 4 })).toEqual(Result.err(Identifiers.ArgumentIntegerTooLarge));
20 | 	});
21 | 	test('GIVEN an invalid integer THEN returns error', () => {
22 | 		expect(resolveInteger('hello')).toEqual(Result.err(Identifiers.ArgumentIntegerError));
23 | 	});
24 | });
25 | 
--------------------------------------------------------------------------------
/tests/resolvers/number.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveNumber } from '../../src/lib/resolvers/number';
 4 | 
 5 | describe('Number resolver tests', () => {
 6 | 	test('GIVEN a valid number THEN returns its parsed value', () => {
 7 | 		expect(resolveNumber('1.23')).toEqual(Result.ok(1.23));
 8 | 
 9 | 		expect(resolveNumber('1')).toEqual(Result.ok(1));
10 | 	});
11 | 	test('GIVEN a valid number with minimum THEN returns its parsed value', () => {
12 | 		expect(resolveNumber('2.34', { minimum: 2 })).toEqual(Result.ok(2.34));
13 | 
14 | 		expect(resolveNumber('2', { minimum: 2 })).toEqual(Result.ok(2));
15 | 	});
16 | 	test('GIVEN a valid number with maximum THEN returns its parsed value', () => {
17 | 		expect(resolveNumber('3.45', { maximum: 4 })).toEqual(Result.ok(3.45));
18 | 
19 | 		expect(resolveNumber('3', { maximum: 4 })).toEqual(Result.ok(3));
20 | 	});
21 | 	test('GIVEN a number smaller than minimum THEN returns error', () => {
22 | 		expect(resolveNumber('1.23', { minimum: 2 })).toEqual(Result.err(Identifiers.ArgumentNumberTooSmall));
23 | 
24 | 		expect(resolveNumber('1', { minimum: 2 })).toEqual(Result.err(Identifiers.ArgumentNumberTooSmall));
25 | 	});
26 | 	test('GIVEN a number larger than maximum THEN returns error', () => {
27 | 		expect(resolveNumber('4.56', { maximum: 4 })).toEqual(Result.err(Identifiers.ArgumentNumberTooLarge));
28 | 
29 | 		expect(resolveNumber('5', { maximum: 4 })).toEqual(Result.err(Identifiers.ArgumentNumberTooLarge));
30 | 	});
31 | 	test('GIVEN an invalid number THEN returns error', () => {
32 | 		expect(resolveNumber('hello')).toEqual(Result.err(Identifiers.ArgumentNumberError));
33 | 	});
34 | });
35 | 
--------------------------------------------------------------------------------
/tests/resolvers/string.test.ts:
--------------------------------------------------------------------------------
 1 | import { Result } from '@sapphire/result';
 2 | import { Identifiers } from '../../src/lib/errors/Identifiers';
 3 | import { resolveString } from '../../src/lib/resolvers/string';
 4 | 
 5 | describe('String resolver tests', () => {
 6 | 	test('GIVEN a valid string THEN returns it', () => {
 7 | 		expect(resolveString('hello')).toEqual(Result.ok('hello'));
 8 | 
 9 | 		expect(resolveString('100')).toEqual(Result.ok('100'));
10 | 	});
11 | 	test('GIVEN a valid string with minimum THEN returns it', () => {
12 | 		expect(resolveString('hello', { minimum: 2 })).toEqual(Result.ok('hello'));
13 | 
14 | 		expect(resolveString('100', { minimum: 2 })).toEqual(Result.ok('100'));
15 | 	});
16 | 	test('GIVEN a valid string with maximum THEN returns its parsed value', () => {
17 | 		expect(resolveString('hello', { maximum: 10 })).toEqual(Result.ok('hello'));
18 | 
19 | 		expect(resolveString('100', { maximum: 100 })).toEqual(Result.ok('100'));
20 | 	});
21 | 	test('GIVEN a string shorter than minimum THEN returns error', () => {
22 | 		expect(resolveString('hello', { minimum: 10 })).toEqual(Result.err(Identifiers.ArgumentStringTooShort));
23 | 
24 | 		expect(resolveString('100', { minimum: 10 })).toEqual(Result.err(Identifiers.ArgumentStringTooShort));
25 | 	});
26 | 	test('GIVEN a string longer than maximum THEN returns error', () => {
27 | 		expect(resolveString('hello', { maximum: 2 })).toEqual(Result.err(Identifiers.ArgumentStringTooLong));
28 | 
29 | 		expect(resolveString('100', { maximum: 2 })).toEqual(Result.err(Identifiers.ArgumentStringTooLong));
30 | 	});
31 | });
32 | 
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": "../tsconfig.base.json",
3 | 	"compilerOptions": {
4 | 		"types": ["vitest/globals"]
5 | 	}
6 | }
7 | 
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": ["@sapphire/ts-config", "@sapphire/ts-config/bundler", "@sapphire/ts-config/extra-strict", "@sapphire/ts-config/verbatim"],
3 | 	"compilerOptions": {
4 | 		"noEmit": true,
5 | 		"incremental": false,
6 | 		"useDefineForClassFields": false
7 | 	}
8 | }
9 | 
--------------------------------------------------------------------------------
/tsconfig.eslint.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": "./tsconfig.base.json",
3 | 	"compilerOptions": {
4 | 		"types": ["vitest/globals"],
5 | 		"lib": ["DOM", "ESNext"]
6 | 	},
7 | 	"include": ["src", "tests", "scripts", "vitest.config.ts", "tsup.config.ts"]
8 | }
9 | 
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
 1 | import { esbuildPluginFilePathExtensions } from 'esbuild-plugin-file-path-extensions';
 2 | import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
 3 | import { defineConfig, type Options } from 'tsup';
 4 | 
 5 | const baseOptions: Options = {
 6 | 	clean: true,
 7 | 	entry: ['src/**/*.ts'],
 8 | 	dts: true,
 9 | 	minify: false,
10 | 	skipNodeModulesBundle: true,
11 | 	sourcemap: true,
12 | 	target: 'es2021',
13 | 	tsconfig: 'src/tsconfig.json',
14 | 	keepNames: true,
15 | 	esbuildPlugins: [esbuildPluginVersionInjector(), esbuildPluginFilePathExtensions()],
16 | 	treeshake: true
17 | };
18 | 
19 | export default [
20 | 	defineConfig({
21 | 		...baseOptions,
22 | 		outDir: 'dist/cjs',
23 | 		format: 'cjs',
24 | 		outExtension: () => ({ js: '.cjs' })
25 | 	}),
26 | 	defineConfig({
27 | 		...baseOptions,
28 | 		outDir: 'dist/esm',
29 | 		format: 'esm'
30 | 	})
31 | ];
32 | 
--------------------------------------------------------------------------------
/typedoc.json:
--------------------------------------------------------------------------------
1 | {
2 | 	"$schema": "https://typedoc.org/schema.json",
3 | 	"entryPoints": ["src/index.ts"],
4 | 	"json": "docs/api.json",
5 | 	"tsconfig": "src/tsconfig.json",
6 | 	"excludePrivate": false
7 | }
8 | 
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
 1 | import { defineConfig } from 'vitest/config';
 2 | 
 3 | export default defineConfig({
 4 | 	test: {
 5 | 		globals: true,
 6 | 		coverage: {
 7 | 			enabled: true,
 8 | 			reporter: ['text', 'lcov']
 9 | 		}
10 | 	},
11 | 	esbuild: {
12 | 		target: 'es2022'
13 | 	}
14 | });
15 | 
--------------------------------------------------------------------------------