import { ApplicationCommandOptionType, ChannelType, InteractionContextType, type APIApplicationCommandInteractionDataOption, type GatewayMessageCreateDispatchData, } from 'discord-api-types/v10'; import { Command, CommandContext, IgnoreCommand, OptionResolver, SubCommand, User, type Client, type CommandOption, type ContextOptionsResolved, type SeyfertChannelOption, type SeyfertIntegerOption, type SeyfertNumberOption, type SeyfertStringOption, type WorkerClient, } from '..'; import type { MakeRequired } from '../common'; import { Message } from '../structures'; function getCommandFromContent( commandRaw: string[], self: Client | WorkerClient, ): { command?: Command | SubCommand; parent?: Command; fullCommandName: string; } { const rawParentName = commandRaw[0]; const rawGroupName = commandRaw.length === 3 ? commandRaw[1] : undefined; const rawSubcommandName = rawGroupName ? commandRaw[2] : commandRaw[1]; const parent = self.commands!.values.find( x => (!('ignore' in x) || x.ignore !== IgnoreCommand.Message) && (x.name === rawParentName || ('aliases' in x ? x.aliases?.includes(rawParentName) : false)), ); const fullCommandName = `${rawParentName}${ rawGroupName ? ` ${rawGroupName} ${rawSubcommandName}` : `${rawSubcommandName ? ` ${rawSubcommandName}` : ''}` }`; if (!(parent instanceof Command)) return { fullCommandName }; if (rawGroupName && !parent.groups?.[rawGroupName] && !parent.groupsAliases?.[rawGroupName]) return getCommandFromContent([rawParentName, rawGroupName], self); if ( rawSubcommandName && !parent.options?.some( x => x instanceof SubCommand && (x.name === rawSubcommandName || x.aliases?.includes(rawSubcommandName)), ) ) return getCommandFromContent([rawParentName], self); const groupName = rawGroupName ? parent.groupsAliases?.[rawGroupName] || rawGroupName : undefined; const command = groupName || rawSubcommandName ? (parent.options?.find(opt => { if (opt instanceof SubCommand) { if (groupName) { if (opt.group !== groupName) return false; } if (opt.group && !groupName) return false; return rawSubcommandName === opt.name || opt.aliases?.includes(rawSubcommandName); } return false; }) as SubCommand) : parent; return { command, fullCommandName, parent, }; } export async function onMessageCreate( self: Client | WorkerClient, rawMessage: GatewayMessageCreateDispatchData, shardId: number, ) { if (!self.options?.commands?.prefix) return; const message = new Message(self, rawMessage); const prefixes = (await self.options.commands.prefix(message)).sort((a, b) => b.length - a.length); const prefix = prefixes.find(x => message.content.startsWith(x)); if (!prefix || !message.content.startsWith(prefix)) return; const content = message.content.slice(prefix.length).trimStart(); const { fullCommandName, command, parent } = getCommandFromContent( content .split(' ') .filter(x => x) .slice(0, 3), self, ); if (!command) return; if (!command.run) return self.logger.warn(`${fullCommandName} command does not have 'run' callback`); if (!command.contexts?.includes(InteractionContextType.BotDM) && !message.guildId) return; if (command.guildId && !command.guildId?.includes(message.guildId!)) return; const resolved: MakeRequired = { channels: {}, roles: {}, users: {}, members: {}, attachments: {}, }; let newContent = content; for (const i of fullCommandName.split(' ')) { newContent = newContent.slice(newContent.indexOf(i) + i.length); } const args = (self.options?.commands?.argsParser ?? defaultArgsParser)(newContent.slice(1), command, message); const { options, errors } = await parseOptions(self, command, rawMessage, args, resolved); const optionsResolver = new OptionResolver(self, options, parent as Command, message.guildId, resolved); const context = new CommandContext(self, message, optionsResolver, shardId, command); //@ts-expect-error const extendContext = self.options?.context?.(message) ?? {}; Object.assign(context, extendContext); try { if (command.defaultMemberPermissions && message.guildId) { const memberPermissions = await self.members.permissions(message.guildId, message.author.id); const permissions = memberPermissions.missings(...memberPermissions.values([command.defaultMemberPermissions])); if ( !memberPermissions.has('Administrator') && permissions.length && (await message.guild())!.ownerId !== message.author.id ) { return command.onPermissionsFail?.(context, memberPermissions.keys(permissions)); } } if (command.botPermissions && message.guildId) { const meMember = await self.cache.members?.get(self.botId, message.guildId); if (!meMember) return; //enable member cache and "Guilds" intent, lol const appPermissions = await meMember.fetchPermissions(); const permissions = appPermissions.missings(...appPermissions.values([command.botPermissions])); if (!appPermissions.has('Administrator') && permissions.length) { return command.onBotPermissionsFail?.(context, appPermissions.keys(permissions)); } } if (errors.length) { return command.onOptionsError?.( context, Object.fromEntries( errors.map(x => { return [ x.name, { failed: true, value: x.error, }, ]; }), ), ); } const [erroredOptions, result] = await command.__runOptions(context, optionsResolver); if (erroredOptions) { return command.onOptionsError?.(context, result); } const resultRunGlobalMiddlewares = await command.__runGlobalMiddlewares(context); if (resultRunGlobalMiddlewares.pass) { return; } if ('error' in resultRunGlobalMiddlewares) { return command.onMiddlewaresError?.(context, resultRunGlobalMiddlewares.error ?? 'Unknown error'); } const resultRunMiddlewares = await command.__runMiddlewares(context); if (resultRunMiddlewares.pass) { return; } if ('error' in resultRunMiddlewares) { return command.onMiddlewaresError?.(context, resultRunMiddlewares.error ?? 'Unknown error'); } try { await command.run?.(context); await command.onAfterRun?.(context, undefined); } catch (error) { await command.onRunError?.(context, error); await command.onAfterRun?.(context, error); } } catch (error) { try { await command.onInternalError?.(self, context.command, error); } catch { // supress error } } } async function parseOptions( self: Client | WorkerClient, command: Command | SubCommand, message: GatewayMessageCreateDispatchData, args: Partial>, resolved: MakeRequired, ) { const options: APIApplicationCommandInteractionDataOption[] = []; const errors: { name: string; error: string }[] = []; for (const i of (command.options ?? []) as (CommandOption & { type: ApplicationCommandOptionType })[]) { try { let value: string | boolean | number | undefined; let indexAttachment = -1; switch (i.type) { case ApplicationCommandOptionType.Attachment: if (message.attachments[++indexAttachment]) { value = message.attachments[indexAttachment].id; resolved.attachments[value] = message.attachments[indexAttachment]; } break; case ApplicationCommandOptionType.Boolean: if (args[i.name]) { value = ['yes', 'y', 'true', 'treu'].includes(args[i.name]!.toLowerCase()); } break; case ApplicationCommandOptionType.Channel: { const rawId = message.content.match(/(?<=<#)[0-9]{17,19}(?=>)/g)?.find(x => args[i.name]?.includes(x)) || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; if (rawId) { const channel = (await self.cache.channels?.get(rawId)) ?? (i.required ? await self.channels.fetch(rawId) : undefined); if (channel) { if ('channel_types' in i) { if (!(i as SeyfertChannelOption).channel_types!.includes(channel.type)) { errors.push({ name: i.name, error: `The entered channel type is not one of ${(i as SeyfertChannelOption) .channel_types!.map(t => ChannelType[t]) .join(', ')}`, }); break; } } value = rawId; resolved.channels[rawId] = channel; } } } break; case ApplicationCommandOptionType.Mentionable: { const matches = message.content.match(/<@[0-9]{17,19}(?=>)|<@&[0-9]{17,19}(?=>)/g) ?? []; for (const match of matches) { if (match.includes('&')) { const rawId = match.slice(3); if (rawId) { const role = (await self.cache.roles?.get(rawId)) ?? (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); if (role) { value = rawId; resolved.roles[rawId] = role; break; } } } else { const rawId = match.slice(2); const raw = message.mentions.find(x => rawId === x.id); if (raw) { const { member, ...user } = raw; value = raw.id; resolved.users[raw.id] = user; if (member) resolved.members[raw.id] = member; break; } } } } break; case ApplicationCommandOptionType.Role: { const rawId = message.mention_roles.find(x => args[i.name]?.includes(x)) || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; if (rawId) { const role = (await self.cache.roles?.get(rawId)) ?? (i.required ? (await self.roles.list(message.guild_id!)).find(x => x.id === rawId) : undefined); if (role) { value = rawId; resolved.roles[rawId] = role; } } } break; case ApplicationCommandOptionType.User: { const rawId = message.mentions.find(x => args[i.name]?.includes(x.id))?.id || args[i.name]?.match(/[0-9]{17,19}/g)?.[0]; if (rawId) { const raw = message.mentions.find(x => args[i.name]?.includes(x.id)) ?? (await self.cache.users?.get(rawId)) ?? (i.required ? await self.users.fetch(rawId) : undefined); if (raw) { value = raw.id; if (raw instanceof User) { resolved.users[raw.id] = raw; if (message.guild_id) { const member = message.mentions.find(x => args[i.name]?.includes(x.id))?.member ?? (await self.cache.members?.get(rawId, message.guild_id)) ?? (i.required ? await self.members.fetch(rawId, message.guild_id) : undefined); if (member) resolved.members[raw.id] = member; } } else { const { member, ...user } = raw; resolved.users[user.id] = user; if (member) resolved.members[user.id] = member; } } } } break; case ApplicationCommandOptionType.String: { value = args[i.name]; const option = i as SeyfertStringOption; if (!value) break; if (option.min_length) { if (value.length < option.min_length) { value = undefined; errors.push({ name: i.name, error: `The entered string has less than ${option.min_length} characters. The minimum required is ${option.min_length} characters.`, }); break; } } if (option.max_length) { if (value.length > option.max_length) { value = undefined; errors.push({ name: i.name, error: `The entered string has more than ${option.max_length} characters. The maximum required is ${option.max_length} characters.`, }); break; } } if (option.choices?.length) { const choice = option.choices.find(x => x.name === value); if (!choice) { value = undefined; errors.push({ name: i.name, error: `The entered choice is invalid. Please choose one of the following options: ${option.choices .map(x => x.name) .join(', ')}.`, }); break; } value = choice.value; } } break; case ApplicationCommandOptionType.Number: case ApplicationCommandOptionType.Integer: { const option = i as SeyfertNumberOption | SeyfertIntegerOption; if (!option.choices?.length) { value = Number(args[i.name]); if (args[i.name] === undefined) { value = undefined; break; } if (Number.isNaN(value)) { value = undefined; errors.push({ name: i.name, error: 'The entered choice is an invalid number.', }); break; } if (option.min_value) { if (value < option.min_value) { value = undefined; errors.push({ name: i.name, error: `The entered number is less than ${option.min_value}. The minimum allowed is ${option.min_value}`, }); break; } } if (option.max_value) { if (value > option.max_value) { value = undefined; errors.push({ name: i.name, error: `The entered number is greater than ${option.max_value}. The maximum allowed is ${option.max_value}`, }); break; } } break; } const choice = option.choices.find(x => x.name === args[i.name]); if (!choice) { value = undefined; errors.push({ name: i.name, error: `The entered choice is invalid. Please choose one of the following options: ${option.choices .map(x => x.name) .join(', ')}.`, }); break; } value = choice.value; } break; default: break; } if (value !== undefined) { options.push({ name: i.name, type: i.type, value, } as APIApplicationCommandInteractionDataOption); } else if (i.required) if (!errors.some(x => x.name === i.name)) errors.push({ error: 'Option is required but returned undefined', name: i.name, }); } catch (e) { errors.push({ error: e && typeof e === 'object' && 'message' in e ? (e.message as string) : `${e}`, name: i.name, }); } } return { errors, options }; } function defaultArgsParser(content: string) { const args: Record = {}; for (const i of content.match(/-(.*?)(?=\s-|$)/gs) ?? []) { args[i.slice(1).split(' ')[0]] = i.split(' ').slice(1).join(' '); } return args; } //-(.*?)(?=\s-|$)/gs //-(?[^-]*)/gm