From 086e4a95c34c09b1cb7e60ff5ee2d764c0ef6f9a Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 10 Feb 2021 17:59:21 +0530 Subject: [PATCH 1/5] lot of stuff --- deps.ts | 2 + src/gateway/handlers/interactionCreate.ts | 30 ++++- src/gateway/index.ts | 2 +- src/models/command.ts | 92 ++++++++++++- src/models/slashClient.ts | 26 +++- src/structures/slash.ts | 152 +++++++++++++++++----- src/types/channel.ts | 19 ++- src/types/slash.ts | 51 +++++--- 8 files changed, 302 insertions(+), 72 deletions(-) diff --git a/deps.ts b/deps.ts index 7a04e52..6656d75 100644 --- a/deps.ts +++ b/deps.ts @@ -7,3 +7,5 @@ export type { Redis, RedisConnectOptions } from 'https://deno.land/x/redis@v0.14.1/mod.ts' +export { walk } from 'https://deno.land/std@0.86.0/fs/walk.ts' +export { join } from 'https://deno.land/std@0.86.0/path/mod.ts' diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index fac882a..a27fa81 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -8,13 +8,30 @@ export const interactionCreate: GatewayEventHandler = async ( gateway: Gateway, d: InteractionPayload ) => { - const guild = await gateway.client.guilds.get(d.guild_id) + // NOTE(DjDeveloperr): Mason once mentioned that channel_id can be optional in Interaction. + // This case can be seen in future proofing Interactions, and one he mentioned was + // that bots will be able to add custom context menus. In that case, Interaction will not have it. + // Ref: https://github.com/discord/discord-api-docs/pull/2568/files#r569025697 + if (d.channel_id === undefined) return + + const guild = + d.guild_id === undefined + ? undefined + : await gateway.client.guilds.get(d.guild_id) if (guild === undefined) return - await guild.members.set(d.member.user.id, d.member) - const member = ((await guild.members.get( - d.member.user.id - )) as unknown) as Member + if (d.member !== undefined) + await guild.members.set(d.member.user.id, d.member) + const member = + d.member !== undefined + ? (((await guild.members.get(d.member.user.id)) as unknown) as Member) + : undefined + if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user) + const dmUser = + d.user !== undefined ? await gateway.client.users.get(d.user.id) : undefined + + const user = member !== undefined ? member.user : dmUser + if (user === undefined) return const channel = (await gateway.client.channels.get(d.channel_id)) ?? @@ -23,7 +40,8 @@ export const interactionCreate: GatewayEventHandler = async ( const interaction = new Interaction(gateway.client, d, { member, guild, - channel + channel, + user }) gateway.client.emit('interactionCreate', interaction) } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 955b61d..c6dbc0a 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -157,7 +157,7 @@ export class Gateway extends HarmonyEventEmitter { const handler = gatewayHandlers[t] - if (handler !== undefined) { + if (handler !== undefined && d !== null) { handler(this, d) } } diff --git a/src/models/command.ts b/src/models/command.ts index cca21c1..05e59ff 100644 --- a/src/models/command.ts +++ b/src/models/command.ts @@ -5,7 +5,7 @@ import { User } from '../structures/user.ts' import { Collection } from '../utils/collection.ts' import { CommandClient } from './commandClient.ts' import { Extension } from './extensions.ts' -import { parse } from '../../deps.ts' +import { join, parse, walk } from '../../deps.ts' export interface CommandContext { /** The Client object */ @@ -284,13 +284,103 @@ export class CommandBuilder extends Command { } } +export class CommandsLoader { + client: CommandClient + #importSeq: { [name: string]: number } = {} + + constructor(client: CommandClient) { + this.client = client + } + + /** + * Load a Command from file. + * + * @param filePath Path of Command file. + * @param exportName Export name. Default is the "default" export. + */ + async load( + filePath: string, + exportName: string = 'default', + onlyRead?: boolean + ): Promise { + const stat = await Deno.stat(filePath).catch(() => undefined) + if (stat === undefined || stat.isFile !== true) + throw new Error(`File not found on path ${filePath}`) + + let seq: number | undefined + + if (this.#importSeq[filePath] !== undefined) seq = this.#importSeq[filePath] + const mod = await import( + // eslint-disable-next-line @typescript-eslint/restrict-plus-operands + 'file:///' + + join(Deno.cwd(), filePath) + + (seq === undefined ? '' : `#${seq}`) + ) + if (this.#importSeq[filePath] === undefined) this.#importSeq[filePath] = 0 + else this.#importSeq[filePath]++ + + const Cmd = mod[exportName] + if (Cmd === undefined) + throw new Error(`Command not exported as ${exportName} from ${filePath}`) + + let cmd: Command + try { + if (Cmd instanceof Command) cmd = Cmd + else cmd = new Cmd() + if (!(cmd instanceof Command)) throw new Error('failed') + } catch (e) { + throw new Error(`Failed to load Command from ${filePath}`) + } + + if (onlyRead !== true) this.client.commands.add(cmd) + return cmd + } + + /** + * Load commands from a Directory. + * + * @param path Path of the directory. + * @param options Options to configure loading. + */ + async loadDirectory( + path: string, + options?: { + recursive?: boolean + exportName?: string + maxDepth?: number + exts?: string[] + onlyRead?: boolean + } + ): Promise { + const commands: Command[] = [] + + for await (const entry of walk(path, { + maxDepth: options?.maxDepth, + exts: options?.exts, + includeDirs: false + })) { + if (entry.isFile !== true) continue + const cmd = await this.load( + entry.path, + options?.exportName, + options?.onlyRead + ) + commands.push(cmd) + } + + return commands + } +} + export class CommandsManager { client: CommandClient list: Collection = new Collection() disabled: Set = new Set() + loader: CommandsLoader constructor(client: CommandClient) { this.client = client + this.loader = new CommandsLoader(client) } /** Number of loaded Commands */ diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index 3318e05..eac0486 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -155,6 +155,7 @@ function buildOptionsArray( ) } +/** Slash Command Builder */ export class SlashBuilder { data: SlashCommandPartial @@ -200,6 +201,7 @@ export class SlashBuilder { } } +/** Manages Slash Commands, allows fetching/modifying/deleting/creating Slash Commands. */ export class SlashCommandsManager { slash: SlashClient rest: RESTManager @@ -351,7 +353,7 @@ export class SlashCommandsManager { } } -export type SlashCommandHandlerCallback = (interaction: Interaction) => any +export type SlashCommandHandlerCallback = (interaction: Interaction) => unknown export interface SlashCommandHandler { name: string guild?: string @@ -360,6 +362,7 @@ export interface SlashCommandHandler { handler: SlashCommandHandlerCallback } +/** Options for SlashClient */ export interface SlashOptions { id?: string | (() => string) client?: Client @@ -369,6 +372,7 @@ export interface SlashOptions { publicKey?: string } +/** Slash Client represents an Interactions Client which can be used without Harmony Client. */ export class SlashClient { id: string | (() => string) client?: Client @@ -469,12 +473,20 @@ export class SlashClient { const groupMatched = e.group !== undefined && e.parent !== undefined ? i.options - .find((o) => o.name === e.group) + .find( + (o) => + o.name === e.group && + o.type === SlashCommandOptionType.SUB_COMMAND_GROUP + ) ?.options?.find((o) => o.name === e.name) !== undefined : true const subMatched = e.group === undefined && e.parent !== undefined - ? i.options.find((o) => o.name === e.name) !== undefined + ? i.options.find( + (o) => + o.name === e.name && + o.type === SlashCommandOptionType.SUB_COMMAND + ) !== undefined : true const nameMatched1 = e.name === i.name const parentMatched = hasGroupOrParent ? e.parent === i.name : true @@ -485,11 +497,15 @@ export class SlashClient { }) } - /** Process an incoming Slash Command (interaction) */ + /** Process an incoming Interaction */ private _process(interaction: Interaction): void { if (!this.enabled) return - if (interaction.type !== InteractionType.APPLICATION_COMMAND) return + if ( + interaction.type !== InteractionType.APPLICATION_COMMAND || + interaction.data === undefined + ) + return const cmd = this._getCommand(interaction) if (cmd?.group !== undefined) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 388aa56..0df43af 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -1,12 +1,18 @@ import { Client } from '../models/client.ts' -import { MessageOptions } from '../types/channel.ts' +import { + AllowedMentionsPayload, + EmbedPayload, + MessageOptions +} from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { - InteractionData, - InteractionOption, + InteractionApplicationCommandData, + InteractionApplicationCommandOption, InteractionPayload, + InteractionResponseFlags, InteractionResponsePayload, - InteractionResponseType + InteractionResponseType, + InteractionType } from '../types/slash.ts' import { SnowflakeBase } from './base.ts' import { Embed } from './embed.ts' @@ -15,7 +21,6 @@ import { Member } from './member.ts' import { Message } from './message.ts' import { GuildTextChannel, TextChannel } from './textChannel.ts' import { User } from './user.ts' -import { Webhook } from './webhook.ts' interface WebhookMessageOptions extends MessageOptions { embeds?: Embed[] @@ -25,39 +30,57 @@ interface WebhookMessageOptions extends MessageOptions { type AllWebhookMessageOptions = string | WebhookMessageOptions -export interface InteractionResponse { - type?: InteractionResponseType +/** Interaction Message related Options */ +export interface InteractionMessageOptions { content?: string - embeds?: Embed[] + embeds?: EmbedPayload[] tts?: boolean - flags?: number + flags?: number | InteractionResponseFlags[] + allowedMentions?: AllowedMentionsPayload + /** Whether to reply with Source or not. True by default */ + withSource?: boolean +} + +export interface InteractionResponse extends InteractionMessageOptions { + /** Type of Interaction Response */ + type?: InteractionResponseType + /** + * DEPRECATED: Use `ephemeral` instead. + * + * @deprecated + */ temp?: boolean - allowedMentions?: { - parse?: string - roles?: string[] - users?: string[] - everyone?: boolean - } + /** Whether the Message Response should be Ephemeral (only visible to User) or not */ + ephemeral?: boolean } export class Interaction extends SnowflakeBase { client: Client - type: number + /** Type of Interaction */ + type: InteractionType + /** Interaction Token */ token: string + /** Interaction ID */ id: string - data: InteractionData - channel: GuildTextChannel - guild: Guild - member: Member - _savedHook?: Webhook + /** Data sent with Interaction. Only applies to Application Command, type may change in future. */ + data?: InteractionApplicationCommandData + /** Channel in which Interaction was initiated */ + channel?: TextChannel | GuildTextChannel + guild?: Guild + /** Member object of who initiated the Interaction */ + member?: Member + /** User object of who invoked Interaction */ + user: User + responded: boolean = false constructor( client: Client, data: InteractionPayload, others: { - channel: GuildTextChannel - guild: Guild - member: Member + channel?: TextChannel | GuildTextChannel + guild?: Guild + member?: Member + user: User } ) { super(client) @@ -66,31 +89,43 @@ export class Interaction extends SnowflakeBase { this.token = data.token this.member = others.member this.id = data.id + this.user = others.user this.data = data.data this.guild = others.guild this.channel = others.channel } - get user(): User { - return this.member.user + /** Name of the Command Used (may change with future additions to Interactions!) */ + get name(): string | undefined { + return this.data?.name } - get name(): string { - return this.data.name - } - - get options(): InteractionOption[] { - return this.data.options ?? [] + get options(): InteractionApplicationCommandOption[] { + return this.data?.options ?? [] } + /** Get an option by name */ option(name: string): T { return this.options.find((e) => e.name === name)?.value } /** Respond to an Interaction */ async respond(data: InteractionResponse): Promise { + if (this.responded) throw new Error('Already responded to Interaction') + let flags = 0 + if (data.ephemeral === true || data.temp === true) + flags |= InteractionResponseFlags.EPHEMERAL + if (data.flags !== undefined) { + if (Array.isArray(data.flags)) + flags = data.flags.reduce((p, a) => p | a, flags) + else if (typeof data.flags === 'number') flags |= data.flags + } const payload: InteractionResponsePayload = { - type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + type: + data.type ?? + (data.withSource === false + ? InteractionResponseType.CHANNEL_MESSAGE + : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE), data: data.type === undefined || data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || @@ -99,8 +134,8 @@ export class Interaction extends SnowflakeBase { content: data.content ?? '', embeds: data.embeds, tts: data.tts ?? false, - flags: data.temp === true ? 64 : data.flags ?? undefined, - allowed_mentions: (data.allowedMentions ?? undefined) as any + flags, + allowed_mentions: data.allowedMentions ?? undefined } : undefined } @@ -109,6 +144,52 @@ export class Interaction extends SnowflakeBase { INTERACTION_CALLBACK(this.id, this.token), payload ) + this.responded = true + + return this + } + + /** Acknowledge the Interaction */ + async acknowledge(withSource?: boolean): Promise { + await this.respond({ + type: + withSource === true + ? InteractionResponseType.ACK_WITH_SOURCE + : InteractionResponseType.ACKNOWLEDGE + }) + return this + } + + /** Reply with a Message to the Interaction */ + async reply(content: string): Promise + async reply(options: InteractionMessageOptions): Promise + async reply( + content: string, + options: InteractionMessageOptions + ): Promise + async reply( + content: string | InteractionMessageOptions, + messageOptions?: InteractionMessageOptions + ): Promise { + let options: InteractionMessageOptions | undefined = + typeof content === 'object' ? content : messageOptions + if ( + typeof content === 'object' && + messageOptions !== undefined && + options !== undefined + ) + Object.assign(options, messageOptions) + if (options === undefined) options = {} + if (typeof content === 'string') Object.assign(options, { content }) + + await this.respond( + Object.assign(options, { + type: + options.withSource === false + ? InteractionResponseType.CHANNEL_MESSAGE + : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + }) + ) return this } @@ -232,6 +313,7 @@ export class Interaction extends SnowflakeBase { return this } + /** Delete a follow-up Message */ async deleteMessage(msg: Message | string): Promise { await this.client.rest.delete( WEBHOOK_MESSAGE( diff --git a/src/types/channel.ts b/src/types/channel.ts index e1d44e0..10c5ea2 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -156,16 +156,25 @@ export interface MessagePayload { stickers?: MessageStickerPayload[] } +export enum AllowedMentionType { + Roles = 'roles', + Users = 'users', + Everyone = 'everyone' +} + +export interface AllowedMentionsPayload { + parse?: AllowedMentionType[] + users?: string[] + roles?: string[] + replied_user?: boolean +} + export interface MessageOptions { tts?: boolean embed?: Embed file?: MessageAttachment files?: MessageAttachment[] - allowedMentions?: { - parse?: 'everyone' | 'users' | 'roles' - roles?: string[] - users?: string[] - } + allowedMentions?: AllowedMentionsPayload } export interface ChannelMention { diff --git a/src/types/slash.ts b/src/types/slash.ts index 2dac0cd..692611c 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,22 +1,25 @@ -import { EmbedPayload } from './channel.ts' +import { AllowedMentionsPayload, EmbedPayload } from './channel.ts' import { MemberPayload } from './guild.ts' +import { UserPayload } from './user.ts' -export interface InteractionOption { +export interface InteractionApplicationCommandOption { /** Option name */ name: string + /** Type of Option */ + type: SlashCommandOptionType /** Value of the option */ value?: any /** Sub options */ - options?: any[] + options?: InteractionApplicationCommandOption[] } -export interface InteractionData { +export interface InteractionApplicationCommandData { /** Name of the Slash Command */ name: string /** Unique ID of the Slash Command */ id: string /** Options (arguments) sent with Interaction */ - options: InteractionOption[] + options: InteractionApplicationCommandOption[] } export enum InteractionType { @@ -26,27 +29,31 @@ export enum InteractionType { APPLICATION_COMMAND = 2 } +export interface InteractionMemberPayload extends MemberPayload { + permissions: string +} + export interface InteractionPayload { /** Type of the Interaction */ type: InteractionType /** Token of the Interaction to respond */ token: string /** Member object of user who invoked */ - member: MemberPayload & { - /** Total permissions of the member in the channel, including overrides */ - permissions: string - } + member?: InteractionMemberPayload + /** User who initiated Interaction (only in DMs) */ + user?: UserPayload /** ID of the Interaction */ id: string /** * Data sent with the interaction - * **This can be undefined only when Interaction is not a Slash Command** + * + * This can be undefined only when Interaction is not a Slash Command. */ - data: InteractionData + data?: InteractionApplicationCommandData /** ID of the Guild in which Interaction was invoked */ - guild_id: string + guild_id?: string /** ID of the Channel in which Interaction was invoked */ - channel_id: string + channel_id?: string } export interface SlashCommandChoice { @@ -68,13 +75,18 @@ export enum SlashCommandOptionType { } export interface SlashCommandOption { + /** Name of the option. */ name: string - /** Description not required in Sub-Command or Sub-Command-Group */ + /** Description of the Option. Not required in Sub-Command-Group */ description?: string + /** Option type */ type: SlashCommandOptionType + /** Whether the option is required or not, false by default */ required?: boolean default?: boolean + /** Optional choices out of which User can choose value */ choices?: SlashCommandChoice[] + /** Nested options for Sub-Command or Sub-Command-Groups */ options?: SlashCommandOption[] } @@ -103,19 +115,20 @@ export enum InteractionResponseType { } export interface InteractionResponsePayload { + /** Type of the response */ type: InteractionResponseType + /** Data to be sent with response. Optional for types: Pong, Acknowledge, Ack with Source */ data?: InteractionResponseDataPayload } export interface InteractionResponseDataPayload { tts?: boolean + /** Text content of the Response (Message) */ content: string + /** Upto 10 Embed Objects to send with Response */ embeds?: EmbedPayload[] - allowed_mentions?: { - parse?: 'everyone' | 'users' | 'roles' - roles?: string[] - users?: string[] - } + /** Allowed Mentions object */ + allowed_mentions?: AllowedMentionsPayload flags?: number } From a800f394ac5e5dc848be0d28502b9191e8e75d17 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Fri, 12 Feb 2021 17:07:38 +0530 Subject: [PATCH 2/5] new things --- src/gateway/handlers/interactionCreate.ts | 73 +++++++++++- src/gateway/index.ts | 19 ++-- src/models/shard.ts | 2 - src/structures/base.ts | 4 +- src/structures/slash.ts | 56 +++++++++- src/test/slash.ts | 130 +++++++--------------- src/types/slash.ts | 27 ++++- src/utils/bitfield.ts | 34 +++--- src/utils/permissions.ts | 4 +- 9 files changed, 219 insertions(+), 130 deletions(-) diff --git a/src/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index a27fa81..4c8eb00 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -1,7 +1,16 @@ +import { Guild } from '../../structures/guild.ts' import { Member } from '../../structures/member.ts' -import { Interaction } from '../../structures/slash.ts' +import { Role } from '../../structures/role.ts' +import { + Interaction, + InteractionApplicationCommandResolved, + InteractionChannel +} from '../../structures/slash.ts' import { GuildTextChannel } from '../../structures/textChannel.ts' +import { User } from '../../structures/user.ts' import { InteractionPayload } from '../../types/slash.ts' +import { UserPayload } from '../../types/user.ts' +import { Permissions } from '../../utils/permissions.ts' import { Gateway, GatewayEventHandler } from '../index.ts' export const interactionCreate: GatewayEventHandler = async ( @@ -18,13 +27,12 @@ export const interactionCreate: GatewayEventHandler = async ( d.guild_id === undefined ? undefined : await gateway.client.guilds.get(d.guild_id) - if (guild === undefined) return if (d.member !== undefined) - await guild.members.set(d.member.user.id, d.member) + await guild?.members.set(d.member.user.id, d.member) const member = d.member !== undefined - ? (((await guild.members.get(d.member.user.id)) as unknown) as Member) + ? (((await guild?.members.get(d.member.user.id)) as unknown) as Member) : undefined if (d.user !== undefined) await gateway.client.users.set(d.user.id, d.user) const dmUser = @@ -37,11 +45,66 @@ export const interactionCreate: GatewayEventHandler = async ( (await gateway.client.channels.get(d.channel_id)) ?? (await gateway.client.channels.fetch(d.channel_id)) + const resolved: InteractionApplicationCommandResolved = { + users: {}, + channels: {}, + members: {}, + roles: {} + } + + if (d.data?.resolved !== undefined) { + for (const [id, data] of Object.entries(d.data.resolved.users ?? {})) { + await gateway.client.users.set(id, data) + resolved.users[id] = ((await gateway.client.users.get( + id + )) as unknown) as User + if (resolved.members[id] !== undefined) + resolved.users[id].member = resolved.members[id] + } + + for (const [id, data] of Object.entries(d.data.resolved.members ?? {})) { + const roles = await guild?.roles.array() + let permissions = new Permissions(Permissions.DEFAULT) + if (roles !== undefined) { + const mRoles = roles.filter( + (r) => (data?.roles?.includes(r.id) as boolean) || r.id === guild?.id + ) + permissions = new Permissions(mRoles.map((r) => r.permissions)) + } + data.user = (d.data.resolved.users?.[id] as unknown) as UserPayload + resolved.members[id] = new Member( + gateway.client, + data, + resolved.users[id], + guild as Guild, + permissions + ) + } + + for (const [id, data] of Object.entries(d.data.resolved.roles ?? {})) { + if (guild !== undefined) { + await guild.roles.set(id, data) + resolved.roles[id] = ((await guild.roles.get(id)) as unknown) as Role + } else { + resolved.roles[id] = new Role( + gateway.client, + data, + (guild as unknown) as Guild + ) + } + } + + for (const [id, data] of Object.entries(d.data.resolved.channels ?? {})) { + resolved.channels[id] = new InteractionChannel(gateway.client, data) + } + } + const interaction = new Interaction(gateway.client, d, { member, guild, channel, - user + user, + resolved }) gateway.client.emit('interactionCreate', interaction) } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index c6dbc0a..d890457 100644 --- a/src/gateway/index.ts +++ b/src/gateway/index.ts @@ -7,7 +7,6 @@ import { import { GatewayResponse } from '../types/gatewayResponse.ts' import { GatewayOpcodes, - GatewayIntents, GatewayCloseCodes, IdentityPayload, StatusUpdatePayload, @@ -57,8 +56,6 @@ export type GatewayTypedEvents = { */ export class Gateway extends HarmonyEventEmitter { websocket?: WebSocket - token?: string - intents?: GatewayIntents[] connected = false initialized = false heartbeatInterval = 0 @@ -269,8 +266,9 @@ export class Gateway extends HarmonyEventEmitter { } private async sendIdentify(forceNewSession?: boolean): Promise { - if (typeof this.token !== 'string') throw new Error('Token not specified') - if (typeof this.intents !== 'object') + if (typeof this.client.token !== 'string') + throw new Error('Token not specified') + if (typeof this.client.intents !== 'object') throw new Error('Intents not specified') if (this.client.fetchGatewayInfo === true) { @@ -300,7 +298,7 @@ export class Gateway extends HarmonyEventEmitter { } const payload: IdentityPayload = { - token: this.token, + token: this.client.token, properties: { $os: this.client.clientProperties.os ?? Deno.build.os, $browser: this.client.clientProperties.browser ?? 'harmony', @@ -311,7 +309,7 @@ export class Gateway extends HarmonyEventEmitter { this.shards === undefined ? [0, 1] : [this.shards[0] ?? 0, this.shards[1] ?? 1], - intents: this.intents.reduce( + intents: this.client.intents.reduce( (previous, current) => previous | current, 0 ), @@ -327,9 +325,8 @@ export class Gateway extends HarmonyEventEmitter { } private async sendResume(): Promise { - if (typeof this.token !== 'string') throw new Error('Token not specified') - if (typeof this.intents !== 'object') - throw new Error('Intents not specified') + if (typeof this.client.token !== 'string') + throw new Error('Token not specified') if (this.sessionID === undefined) { this.sessionID = await this.cache.get( @@ -348,7 +345,7 @@ export class Gateway extends HarmonyEventEmitter { const resumePayload = { op: GatewayOpcodes.RESUME, d: { - token: this.token, + token: this.client.token, session_id: this.sessionID, seq: this.sequenceID ?? null } diff --git a/src/models/shard.ts b/src/models/shard.ts index 80cd649..762ce40 100644 --- a/src/models/shard.ts +++ b/src/models/shard.ts @@ -79,8 +79,6 @@ export class ShardManager extends HarmonyEventEmitter { const shardCount = await this.getShardCount() const gw = new Gateway(this.client, [Number(id), shardCount]) - gw.token = this.client.token - gw.intents = this.client.intents this.list.set(id.toString(), gw) gw.initWebsocket() diff --git a/src/structures/base.ts b/src/structures/base.ts index e5806e5..0898cfe 100644 --- a/src/structures/base.ts +++ b/src/structures/base.ts @@ -2,10 +2,10 @@ import { Client } from '../models/client.ts' import { Snowflake } from '../utils/snowflake.ts' export class Base { - client: Client + client!: Client constructor(client: Client, _data?: any) { - this.client = client + Object.defineProperty(this, 'client', { value: client, enumerable: false }) } } diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 0df43af..99a1308 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -1,24 +1,31 @@ import { Client } from '../models/client.ts' import { AllowedMentionsPayload, + ChannelTypes, EmbedPayload, MessageOptions } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { + Dict, InteractionApplicationCommandData, InteractionApplicationCommandOption, + InteractionChannelPayload, InteractionPayload, InteractionResponseFlags, InteractionResponsePayload, InteractionResponseType, - InteractionType + InteractionType, + SlashCommandOptionType } from '../types/slash.ts' +import { Permissions } from '../utils/permissions.ts' import { SnowflakeBase } from './base.ts' +import { Channel } from './channel.ts' import { Embed } from './embed.ts' import { Guild } from './guild.ts' import { Member } from './member.ts' import { Message } from './message.ts' +import { Role } from './role.ts' import { GuildTextChannel, TextChannel } from './textChannel.ts' import { User } from './user.ts' @@ -54,8 +61,37 @@ export interface InteractionResponse extends InteractionMessageOptions { ephemeral?: boolean } +/** Represents a Channel Object for an Option in Slash Command */ +export class InteractionChannel extends SnowflakeBase { + name: string + type: ChannelTypes + permissions: Permissions + + constructor(client: Client, data: InteractionChannelPayload) { + super(client) + this.name = data.name + this.type = data.type + this.permissions = new Permissions(data.permissions) + } + + /** Resolve to actual Channel object if present in Cache */ + async resolve(): Promise { + return this.client.channels.get(this.id) + } +} + +export interface InteractionApplicationCommandResolved { + users: Dict + members: Dict + channels: Dict + roles: Dict +} + +export class InteractionUser extends User { + member?: Member +} + export class Interaction extends SnowflakeBase { - client: Client /** Type of Interaction */ type: InteractionType /** Interaction Token */ @@ -72,6 +108,7 @@ export class Interaction extends SnowflakeBase { /** User object of who invoked Interaction */ user: User responded: boolean = false + resolved: InteractionApplicationCommandResolved constructor( client: Client, @@ -81,10 +118,10 @@ export class Interaction extends SnowflakeBase { guild?: Guild member?: Member user: User + resolved: InteractionApplicationCommandResolved } ) { super(client) - this.client = client this.type = data.type this.token = data.token this.member = others.member @@ -93,6 +130,7 @@ export class Interaction extends SnowflakeBase { this.data = data.data this.guild = others.guild this.channel = others.channel + this.resolved = others.resolved } /** Name of the Command Used (may change with future additions to Interactions!) */ @@ -105,8 +143,16 @@ export class Interaction extends SnowflakeBase { } /** Get an option by name */ - option(name: string): T { - return this.options.find((e) => e.name === name)?.value + option(name: string): T { + const op = this.options.find((e) => e.name === name) + if (op === undefined || op.value === undefined) return undefined as any + if (op.type === SlashCommandOptionType.USER) + return this.resolved.users[op.value] as any + else if (op.type === SlashCommandOptionType.ROLE) + return this.resolved.roles[op.value] as any + else if (op.type === SlashCommandOptionType.CHANNEL) + return this.resolved.channels[op.value] as any + else return op.value } /** Respond to an Interaction */ diff --git a/src/test/slash.ts b/src/test/slash.ts index 155a745..215cad0 100644 --- a/src/test/slash.ts +++ b/src/test/slash.ts @@ -1,100 +1,56 @@ -import { Client, Intents, event, slash } from '../../mod.ts' -import { Embed } from '../structures/embed.ts' +import { + Client, + Intents, + event, + slash, + SlashCommandOptionType as Type +} from '../../mod.ts' import { Interaction } from '../structures/slash.ts' import { TOKEN } from './config.ts' export class MyClient extends Client { - @event() - ready(): void { + @event() ready(): void { console.log(`Logged in as ${this.user?.tag}!`) - this.slash.commands.bulkEdit([{ name: 'send', description: 'idk' }]) - } - - @event('debug') - debugEvt(txt: string): void { - console.log(txt) - } - - @slash() - send(d: Interaction): void { - d.respond({ - content: d.data.options?.find((e) => e.name === 'content')?.value - }) - } - - @slash() - async eval(d: Interaction): Promise { - if ( - d.user.id !== '422957901716652033' && - d.user.id !== '682849186227552266' - ) { - d.respond({ - content: 'This command can only be used by owner!' - }) - } else { - const code = d.data.options?.find((e) => e.name === 'code') - ?.value as string - try { - // eslint-disable-next-line no-eval - let evaled = eval(code) - if (evaled instanceof Promise) evaled = await evaled - if (typeof evaled === 'object') evaled = Deno.inspect(evaled) - let res = `${evaled}`.substring(0, 1990) - while (client.token !== undefined && res.includes(client.token)) { - res = res.replace(client.token, '[REMOVED]') + this.slash.commands.bulkEdit( + [ + { + name: 'test', + description: 'Test command.', + options: [ + { + name: 'user', + type: Type.USER, + description: 'User' + }, + { + name: 'role', + type: Type.ROLE, + description: 'Role' + }, + { + name: 'channel', + type: Type.CHANNEL, + description: 'Channel' + }, + { + name: 'string', + type: Type.STRING, + description: 'String' + } + ] } - d.respond({ - content: '```js\n' + `${res}` + '\n```' - }).catch(() => {}) - } catch (e) { - d.respond({ - content: '```js\n' + `${e.stack}` + '\n```' - }) - } - } + ], + '807935370556866560' + ) + this.slash.commands.bulkEdit([]) } - @slash() - async hug(d: Interaction): Promise { - const id = d.data.options?.find((e) => e.name === 'user')?.value as string - const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) - const url = await fetch('https://nekos.life/api/v2/img/hug') - .then((r) => r.json()) - .then((e) => e.url) - - d.respond({ - embeds: [ - new Embed() - .setTitle(`${d.user.username} hugged ${user?.username}!`) - .setImage({ url }) - .setColor(0x2f3136) - ] - }) + @slash() test(d: Interaction): void { + console.log(d.resolved) } - @slash() - async kiss(d: Interaction): Promise { - const id = d.data.options?.find((e) => e.name === 'user')?.value as string - const user = (await client.users.get(id)) ?? (await client.users.fetch(id)) - const url = await fetch('https://nekos.life/api/v2/img/kiss') - .then((r) => r.json()) - .then((e) => e.url) - - d.respond({ - embeds: [ - new Embed() - .setTitle(`${d.user.username} kissed ${user?.username}!`) - .setImage({ url }) - .setColor(0x2f3136) - ] - }) - } - - @slash('ping') - pingCmd(d: Interaction): void { - d.respond({ - content: `Pong!` - }) + @event() raw(evt: string, d: any): void { + if (evt === 'INTERACTION_CREATE') console.log(evt, d?.data?.resolved) } } diff --git a/src/types/slash.ts b/src/types/slash.ts index 692611c..69eea51 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,5 +1,10 @@ -import { AllowedMentionsPayload, EmbedPayload } from './channel.ts' +import { + AllowedMentionsPayload, + ChannelTypes, + EmbedPayload +} from './channel.ts' import { MemberPayload } from './guild.ts' +import { RolePayload } from './role.ts' import { UserPayload } from './user.ts' export interface InteractionApplicationCommandOption { @@ -13,6 +18,24 @@ export interface InteractionApplicationCommandOption { options?: InteractionApplicationCommandOption[] } +export interface InteractionChannelPayload { + id: string + name: string + permissions: string + type: ChannelTypes +} + +export interface Dict { + [name: string]: T +} + +export interface InteractionApplicationCommandResolvedPayload { + users?: Dict + members?: Dict + channels?: Dict + roles?: Dict +} + export interface InteractionApplicationCommandData { /** Name of the Slash Command */ name: string @@ -20,6 +43,8 @@ export interface InteractionApplicationCommandData { id: string /** Options (arguments) sent with Interaction */ options: InteractionApplicationCommandOption[] + /** Resolved data for options in Slash Command */ + resolved?: InteractionApplicationCommandResolvedPayload } export enum InteractionType { diff --git a/src/utils/bitfield.ts b/src/utils/bitfield.ts index eb7a67c..d2f5f03 100644 --- a/src/utils/bitfield.ts +++ b/src/utils/bitfield.ts @@ -9,31 +9,31 @@ export type BitFieldResolvable = /** Bit Field utility to work with Bits and Flags */ export class BitField { - flags: { [name: string]: number } = {} + #flags: { [name: string]: number } = {} bitfield: number constructor(flags: { [name: string]: number }, bits: any) { - this.flags = flags - this.bitfield = BitField.resolve(this.flags, bits) + this.#flags = flags + this.bitfield = BitField.resolve(this.#flags, bits) } any(bit: BitFieldResolvable): boolean { - return (this.bitfield & BitField.resolve(this.flags, bit)) !== 0 + return (this.bitfield & BitField.resolve(this.#flags, bit)) !== 0 } equals(bit: BitFieldResolvable): boolean { - return this.bitfield === BitField.resolve(this.flags, bit) + return this.bitfield === BitField.resolve(this.#flags, bit) } has(bit: BitFieldResolvable, ...args: any[]): boolean { if (Array.isArray(bit)) return (bit.every as any)((p: any) => this.has(p)) - bit = BitField.resolve(this.flags, bit) + bit = BitField.resolve(this.#flags, bit) return (this.bitfield & bit) === bit } missing(bits: any, ...hasParams: any[]): string[] { if (!Array.isArray(bits)) - bits = new BitField(this.flags, bits).toArray(false) + bits = new BitField(this.#flags, bits).toArray(false) return bits.filter((p: any) => !this.has(p, ...hasParams)) } @@ -44,10 +44,10 @@ export class BitField { add(...bits: BitFieldResolvable[]): BitField { let total = 0 for (const bit of bits) { - total |= BitField.resolve(this.flags, bit) + total |= BitField.resolve(this.#flags, bit) } if (Object.isFrozen(this)) - return new BitField(this.flags, this.bitfield | total) + return new BitField(this.#flags, this.bitfield | total) this.bitfield |= total return this } @@ -55,27 +55,31 @@ export class BitField { remove(...bits: BitFieldResolvable[]): BitField { let total = 0 for (const bit of bits) { - total |= BitField.resolve(this.flags, bit) + total |= BitField.resolve(this.#flags, bit) } if (Object.isFrozen(this)) - return new BitField(this.flags, this.bitfield & ~total) + return new BitField(this.#flags, this.bitfield & ~total) this.bitfield &= ~total return this } + flags(): { [name: string]: number } { + return this.#flags + } + serialize(...hasParams: any[]): { [key: string]: any } { const serialized: { [key: string]: any } = {} - for (const [flag, bit] of Object.entries(this.flags)) + for (const [flag, bit] of Object.entries(this.#flags)) serialized[flag] = this.has( - BitField.resolve(this.flags, bit), + BitField.resolve(this.#flags, bit), ...hasParams ) return serialized } toArray(...hasParams: any[]): string[] { - return Object.keys(this.flags).filter((bit) => - this.has(BitField.resolve(this.flags, bit), ...hasParams) + return Object.keys(this.#flags).filter((bit) => + this.has(BitField.resolve(this.#flags, bit), ...hasParams) ) } diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 03b4b82..99b258e 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -21,14 +21,14 @@ export class Permissions extends BitField { any(permission: PermissionResolvable, checkAdmin = true): boolean { return ( - (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || + (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.any(permission as any) ) } has(permission: PermissionResolvable, checkAdmin = true): boolean { return ( - (checkAdmin && super.has(this.flags.ADMINISTRATOR)) || + (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.has(permission as any) ) } From 98874dd7e75f29ef3b94b9e286b06bbd7c211154 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Mon, 15 Feb 2021 13:38:07 +0530 Subject: [PATCH 3/5] fix --- mod.ts | 1 + src/structures/slash.ts | 2 +- src/types/slash.ts | 5 +---- src/utils/dict.ts | 3 +++ 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 src/utils/dict.ts diff --git a/mod.ts b/mod.ts index bc096bb..bdfbde3 100644 --- a/mod.ts +++ b/mod.ts @@ -134,3 +134,4 @@ export { UserFlags } from './src/types/userFlags.ts' export type { VoiceStatePayload } from './src/types/voice.ts' export type { WebhookPayload } from './src/types/webhook.ts' export * from './src/models/collectors.ts' +export type { Dict } from './src/utils/dict.ts' diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 99a1308..1e4003c 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -7,7 +7,6 @@ import { } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { - Dict, InteractionApplicationCommandData, InteractionApplicationCommandOption, InteractionChannelPayload, @@ -18,6 +17,7 @@ import { InteractionType, SlashCommandOptionType } from '../types/slash.ts' +import { Dict } from '../utils/dict.ts' import { Permissions } from '../utils/permissions.ts' import { SnowflakeBase } from './base.ts' import { Channel } from './channel.ts' diff --git a/src/types/slash.ts b/src/types/slash.ts index 69eea51..f8a8ecd 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,3 +1,4 @@ +import { Dict } from '../utils/dict.ts' import { AllowedMentionsPayload, ChannelTypes, @@ -25,10 +26,6 @@ export interface InteractionChannelPayload { type: ChannelTypes } -export interface Dict { - [name: string]: T -} - export interface InteractionApplicationCommandResolvedPayload { users?: Dict members?: Dict diff --git a/src/utils/dict.ts b/src/utils/dict.ts new file mode 100644 index 0000000..6ffaaf3 --- /dev/null +++ b/src/utils/dict.ts @@ -0,0 +1,3 @@ +export interface Dict { + [name: string]: T +} From 7bdbb165f4898c355617370a7785b211c22c5fb0 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 17 Feb 2021 14:00:07 +0530 Subject: [PATCH 4/5] up --- src/models/commandClient.ts | 27 ++++++--------- src/test/index.ts | 8 +++-- src/types/permissionFlags.ts | 65 ++++++++++++++++++------------------ src/utils/bitfield.ts | 33 +++++++++--------- src/utils/mixedPromise.ts | 3 -- src/utils/permissions.ts | 15 ++++----- 6 files changed, 73 insertions(+), 78 deletions(-) delete mode 100644 src/utils/mixedPromise.ts diff --git a/src/models/commandClient.ts b/src/models/commandClient.ts index 94d01ba..67d121d 100644 --- a/src/models/commandClient.ts +++ b/src/models/commandClient.ts @@ -1,6 +1,5 @@ import { Message } from '../structures/message.ts' import { GuildTextChannel } from '../structures/textChannel.ts' -import { awaitSync } from '../utils/mixedPromise.ts' import { Client, ClientOptions } from './client.ts' import { CategoriesManager, @@ -129,35 +128,29 @@ export class CommandClient extends Client implements CommandClientOptions { async processMessage(msg: Message): Promise { if (!this.allowBots && msg.author.bot === true) return - const isUserBlacklisted = await awaitSync( - this.isUserBlacklisted(msg.author.id) - ) - if (isUserBlacklisted === true) return + const isUserBlacklisted = await this.isUserBlacklisted(msg.author.id) + if (isUserBlacklisted) return - const isChannelBlacklisted = await awaitSync( - this.isChannelBlacklisted(msg.channel.id) - ) - if (isChannelBlacklisted === true) return + const isChannelBlacklisted = await this.isChannelBlacklisted(msg.channel.id) + if (isChannelBlacklisted) return if (msg.guild !== undefined) { - const isGuildBlacklisted = await awaitSync( - this.isGuildBlacklisted(msg.guild.id) - ) - if (isGuildBlacklisted === true) return + const isGuildBlacklisted = await this.isGuildBlacklisted(msg.guild.id) + if (isGuildBlacklisted) return } let prefix: string | string[] = [] if (typeof this.prefix === 'string') prefix = [...prefix, this.prefix] else prefix = [...prefix, ...this.prefix] - const userPrefix = await awaitSync(this.getUserPrefix(msg.author.id)) + const userPrefix = await this.getUserPrefix(msg.author.id) if (userPrefix !== undefined) { if (typeof userPrefix === 'string') prefix = [...prefix, userPrefix] else prefix = [...prefix, ...userPrefix] } if (msg.guild !== undefined) { - const guildPrefix = await awaitSync(this.getGuildPrefix(msg.guild.id)) + const guildPrefix = await this.getGuildPrefix(msg.guild.id) if (guildPrefix !== undefined) { if (typeof guildPrefix === 'string') prefix = [...prefix, guildPrefix] else prefix = [...prefix, ...guildPrefix] @@ -361,10 +354,10 @@ export class CommandClient extends Client implements CommandClientOptions { try { this.emit('commandUsed', ctx) - const beforeExecute = await awaitSync(command.beforeExecute(ctx)) + const beforeExecute = await command.beforeExecute(ctx) if (beforeExecute === false) return - const result = await awaitSync(command.execute(ctx)) + const result = await command.execute(ctx) command.afterExecute(ctx, result) } catch (e) { this.emit('commandError', ctx, e) diff --git a/src/test/index.ts b/src/test/index.ts index 1d3d7d9..ba93b2a 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -201,7 +201,7 @@ client.on('messageCreate', async (msg: Message) => { ) .join('\n\n')}` ) - } else if (msg.content === '!getPermissions') { + } else if (msg.content === '!perms') { if (msg.channel.type !== ChannelTypes.GUILD_TEXT) { return msg.channel.send("This isn't a guild text channel!") } @@ -210,7 +210,11 @@ client.on('messageCreate', async (msg: Message) => { // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion msg.member as Member ) - msg.channel.send(`Your permissions:\n${permissions.toArray().join('\n')}`) + msg.channel.send( + Object.entries(permissions.serialize()) + .map((e) => `${e[0]}: ${e[1] === true ? '`✅`' : '`❌`'}`) + .join('\n') + ) } }) diff --git a/src/types/permissionFlags.ts b/src/types/permissionFlags.ts index 9cb3040..928a16f 100644 --- a/src/types/permissionFlags.ts +++ b/src/types/permissionFlags.ts @@ -1,35 +1,36 @@ // https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags -export const PermissionFlags: { [key: string]: number } = { - CREATE_INSTANT_INVITE: 1 << 0, - KICK_MEMBERS: 1 << 1, - BAN_MEMBERS: 1 << 2, - ADMINISTRATOR: 1 << 3, - MANAGE_CHANNELS: 1 << 4, - MANAGE_GUILD: 1 << 5, - ADD_REACTIONS: 1 << 6, - VIEW_AUDIT_LOG: 1 << 7, - PRIORITY_SPEAKER: 1 << 8, - STREAM: 1 << 9, - VIEW_CHANNEL: 1 << 10, - SEND_MESSAGES: 1 << 11, - SEND_TTS_MESSAGES: 1 << 12, - MANAGE_MESSAGES: 1 << 13, - EMBED_LINKS: 1 << 14, - ATTACH_FILES: 1 << 15, - READ_MESSAGE_HISTORY: 1 << 16, - MENTION_EVERYONE: 1 << 17, - USE_EXTERNAL_EMOJIS: 1 << 18, - VIEW_GUILD_INSIGHTS: 1 << 19, - CONNECT: 1 << 20, - SPEAK: 1 << 21, - MUTE_MEMBERS: 1 << 22, - DEAFEN_MEMBERS: 1 << 23, - MOVE_MEMBERS: 1 << 24, - USE_VAD: 1 << 25, - CHANGE_NICKNAME: 1 << 26, - MANAGE_NICKNAMES: 1 << 27, - MANAGE_ROLES: 1 << 28, - MANAGE_WEBHOOKS: 1 << 29, - MANAGE_EMOJIS: 1 << 30 +export const PermissionFlags: { [key: string]: bigint } = { + CREATE_INSTANT_INVITE: 1n << 0n, + KICK_MEMBERS: 1n << 1n, + BAN_MEMBERS: 1n << 2n, + ADMINISTRATOR: 1n << 3n, + MANAGE_CHANNELS: 1n << 4n, + MANAGE_GUILD: 1n << 5n, + ADD_REACTIONS: 1n << 6n, + VIEW_AUDIT_LOG: 1n << 7n, + PRIORITY_SPEAKER: 1n << 8n, + STREAM: 1n << 9n, + VIEW_CHANNEL: 1n << 10n, + SEND_MESSAGES: 1n << 11n, + SEND_TTS_MESSAGES: 1n << 12n, + MANAGE_MESSAGES: 1n << 13n, + EMBED_LINKS: 1n << 14n, + ATTACH_FILES: 1n << 15n, + READ_MESSAGE_HISTORY: 1n << 16n, + MENTION_EVERYONE: 1n << 17n, + USE_EXTERNAL_EMOJIS: 1n << 18n, + VIEW_GUILD_INSIGHTS: 1n << 19n, + CONNECT: 1n << 20n, + SPEAK: 1n << 21n, + MUTE_MEMBERS: 1n << 22n, + DEAFEN_MEMBERS: 1n << 23n, + MOVE_MEMBERS: 1n << 24n, + USE_VAD: 1n << 25n, + CHANGE_NICKNAME: 1n << 26n, + MANAGE_NICKNAMES: 1n << 27n, + MANAGE_ROLES: 1n << 28n, + MANAGE_WEBHOOKS: 1n << 29n, + MANAGE_EMOJIS: 1n << 30n, + USE_SLASH_COMMANDS: 1n << 31n } diff --git a/src/utils/bitfield.ts b/src/utils/bitfield.ts index d2f5f03..8631f1f 100644 --- a/src/utils/bitfield.ts +++ b/src/utils/bitfield.ts @@ -6,19 +6,21 @@ export type BitFieldResolvable = | string | string[] | BitField[] + | bigint + | Array /** Bit Field utility to work with Bits and Flags */ export class BitField { - #flags: { [name: string]: number } = {} - bitfield: number + #flags: { [name: string]: number | bigint } = {} + bitfield: bigint - constructor(flags: { [name: string]: number }, bits: any) { + constructor(flags: { [name: string]: number | bigint }, bits: any) { this.#flags = flags this.bitfield = BitField.resolve(this.#flags, bits) } any(bit: BitFieldResolvable): boolean { - return (this.bitfield & BitField.resolve(this.#flags, bit)) !== 0 + return (this.bitfield & BitField.resolve(this.#flags, bit)) !== 0n } equals(bit: BitFieldResolvable): boolean { @@ -42,7 +44,7 @@ export class BitField { } add(...bits: BitFieldResolvable[]): BitField { - let total = 0 + let total = 0n for (const bit of bits) { total |= BitField.resolve(this.#flags, bit) } @@ -53,7 +55,7 @@ export class BitField { } remove(...bits: BitFieldResolvable[]): BitField { - let total = 0 + let total = 0n for (const bit of bits) { total |= BitField.resolve(this.#flags, bit) } @@ -63,12 +65,12 @@ export class BitField { return this } - flags(): { [name: string]: number } { + flags(): { [name: string]: bigint | number } { return this.#flags } - serialize(...hasParams: any[]): { [key: string]: any } { - const serialized: { [key: string]: any } = {} + serialize(...hasParams: any[]): { [key: string]: boolean } { + const serialized: { [key: string]: boolean } = {} for (const [flag, bit] of Object.entries(this.#flags)) serialized[flag] = this.has( BitField.resolve(this.#flags, bit), @@ -83,11 +85,11 @@ export class BitField { ) } - toJSON(): number { - return this.bitfield + toJSON(): string { + return this.bitfield.toString() } - valueOf(): number { + valueOf(): bigint { return this.bitfield } @@ -95,9 +97,10 @@ export class BitField { yield* this.toArray() } - static resolve(flags: any, bit: BitFieldResolvable = 0): number { - if (typeof bit === 'string' && !isNaN(parseInt(bit))) return parseInt(bit) - if (typeof bit === 'number' && bit >= 0) return bit + static resolve(flags: any, bit: BitFieldResolvable = 0n): bigint { + if (typeof bit === 'bigint') return bit + if (typeof bit === 'string' && !isNaN(parseInt(bit))) return BigInt(bit) + if (typeof bit === 'number' && bit >= 0) return BigInt(bit) if (bit instanceof BitField) return this.resolve(flags, bit.bitfield) if (Array.isArray(bit)) return (bit.map as any)((p: any) => this.resolve(flags, p)).reduce( diff --git a/src/utils/mixedPromise.ts b/src/utils/mixedPromise.ts deleted file mode 100644 index 4f1b297..0000000 --- a/src/utils/mixedPromise.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const awaitSync = async (val: any | Promise): Promise => { - return val instanceof Promise ? await val : val -} diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 99b258e..8d7a488 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -2,18 +2,15 @@ import { PermissionFlags } from '../types/permissionFlags.ts' import { BitField, BitFieldResolvable } from './bitfield.ts' -export type PermissionResolvable = - | string - | string[] - | number - | number[] - | Permissions - | PermissionResolvable[] +export type PermissionResolvable = BitFieldResolvable /** Manages Discord's Bit-based Permissions */ export class Permissions extends BitField { - static DEFAULT = 104324673 - static ALL = Object.values(PermissionFlags).reduce((all, p) => all | p, 0) + static DEFAULT = 104324673n + static ALL = Object.values(PermissionFlags).reduce( + (all, p) => BigInt(all) | BigInt(p), + 0n + ) constructor(bits: BitFieldResolvable) { super(PermissionFlags, bits) From dffce5bd0bc0ed1bf88dedfbb4fb3149b9f537d4 Mon Sep 17 00:00:00 2001 From: DjDeveloperr Date: Wed, 17 Feb 2021 14:25:04 +0530 Subject: [PATCH 5/5] new --- src/structures/slash.ts | 66 ++++++++++++++++++++++------------------- src/types/slash.ts | 26 ++++++++++------ 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/src/structures/slash.ts b/src/structures/slash.ts index 1e4003c..9b8cc10 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -44,26 +44,20 @@ export interface InteractionMessageOptions { tts?: boolean flags?: number | InteractionResponseFlags[] allowedMentions?: AllowedMentionsPayload - /** Whether to reply with Source or not. True by default */ - withSource?: boolean } export interface InteractionResponse extends InteractionMessageOptions { /** Type of Interaction Response */ type?: InteractionResponseType - /** - * DEPRECATED: Use `ephemeral` instead. - * - * @deprecated - */ - temp?: boolean /** Whether the Message Response should be Ephemeral (only visible to User) or not */ ephemeral?: boolean } /** Represents a Channel Object for an Option in Slash Command */ export class InteractionChannel extends SnowflakeBase { + /** Name of the Channel */ name: string + /** Channel Type */ type: ChannelTypes permissions: Permissions @@ -98,17 +92,22 @@ export class Interaction extends SnowflakeBase { token: string /** Interaction ID */ id: string - /** Data sent with Interaction. Only applies to Application Command, type may change in future. */ + /** Data sent with Interaction. Only applies to Application Command */ data?: InteractionApplicationCommandData /** Channel in which Interaction was initiated */ channel?: TextChannel | GuildTextChannel + /** Guild in which Interaction was initiated */ guild?: Guild /** Member object of who initiated the Interaction */ member?: Member /** User object of who invoked Interaction */ user: User + /** Whether we have responded to Interaction or not */ responded: boolean = false + /** Resolved data for Snowflakes in Slash Command Arguments */ resolved: InteractionApplicationCommandResolved + /** Whether response was deferred or not */ + deferred: boolean = false constructor( client: Client, @@ -159,19 +158,14 @@ export class Interaction extends SnowflakeBase { async respond(data: InteractionResponse): Promise { if (this.responded) throw new Error('Already responded to Interaction') let flags = 0 - if (data.ephemeral === true || data.temp === true) - flags |= InteractionResponseFlags.EPHEMERAL + if (data.ephemeral === true) flags |= InteractionResponseFlags.EPHEMERAL if (data.flags !== undefined) { if (Array.isArray(data.flags)) flags = data.flags.reduce((p, a) => p | a, flags) else if (typeof data.flags === 'number') flags |= data.flags } const payload: InteractionResponsePayload = { - type: - data.type ?? - (data.withSource === false - ? InteractionResponseType.CHANNEL_MESSAGE - : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE), + type: data.type ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: data.type === undefined || data.type === InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE || @@ -195,14 +189,12 @@ export class Interaction extends SnowflakeBase { return this } - /** Acknowledge the Interaction */ - async acknowledge(withSource?: boolean): Promise { + /** Defer the Interaction i.e. let the user know bot is processing and will respond later. You only have 15 minutes to edit the response! */ + async defer(): Promise { await this.respond({ - type: - withSource === true - ? InteractionResponseType.ACK_WITH_SOURCE - : InteractionResponseType.ACKNOWLEDGE + type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE }) + this.deferred = true return this } @@ -228,14 +220,19 @@ export class Interaction extends SnowflakeBase { if (options === undefined) options = {} if (typeof content === 'string') Object.assign(options, { content }) - await this.respond( - Object.assign(options, { - type: - options.withSource === false - ? InteractionResponseType.CHANNEL_MESSAGE - : InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + if (this.deferred && this.responded) { + await this.editResponse({ + content: options.content, + embeds: options.embeds, + flags: options.flags, + allowedMentions: options.allowedMentions }) - ) + } else + await this.respond( + Object.assign(options, { + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE + }) + ) return this } @@ -243,7 +240,9 @@ export class Interaction extends SnowflakeBase { /** Edit the original Interaction response */ async editResponse(data: { content?: string - embeds?: Embed[] + embeds?: EmbedPayload[] + flags?: number | number[] + allowedMentions?: AllowedMentionsPayload }): Promise { const url = WEBHOOK_MESSAGE( this.client.user?.id as string, @@ -252,7 +251,12 @@ export class Interaction extends SnowflakeBase { ) await this.client.rest.patch(url, { content: data.content ?? '', - embeds: data.embeds ?? [] + embeds: data.embeds ?? [], + flags: + typeof data.flags === 'object' + ? data.flags.reduce((p, a) => p | a, 0) + : data.flags, + allowed_mentions: data.allowedMentions }) return this } diff --git a/src/types/slash.ts b/src/types/slash.ts index f8a8ecd..c700fbd 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -52,6 +52,7 @@ export enum InteractionType { } export interface InteractionMemberPayload extends MemberPayload { + /** Permissions of the Member who initiated Interaction (Guild-only) */ permissions: string } @@ -67,9 +68,7 @@ export interface InteractionPayload { /** ID of the Interaction */ id: string /** - * Data sent with the interaction - * - * This can be undefined only when Interaction is not a Slash Command. + * Data sent with the interaction. Undefined only when Interaction is not Slash Command.* */ data?: InteractionApplicationCommandData /** ID of the Guild in which Interaction was invoked */ @@ -86,7 +85,9 @@ export interface SlashCommandChoice { } export enum SlashCommandOptionType { + /** A sub command that is either a part of a root command or Sub Command Group */ SUB_COMMAND = 1, + /** A sub command group that is present in root command's options */ SUB_COMMAND_GROUP = 2, STRING = 3, INTEGER = 4, @@ -112,28 +113,35 @@ export interface SlashCommandOption { options?: SlashCommandOption[] } +/** Represents the Slash Command (Application Command) payload sent for creating/bulk editing. */ export interface SlashCommandPartial { + /** Name of the Slash Command */ name: string + /** Description of the Slash Command */ description: string + /** Options (arguments, sub commands or group) of the Slash Command */ options?: SlashCommandOption[] } +/** Represents a fully qualified Slash Command (Application Command) payload. */ export interface SlashCommandPayload extends SlashCommandPartial { + /** ID of the Slash Command */ id: string + /** Application ID */ application_id: string } export enum InteractionResponseType { /** Just ack a ping, Http-only. */ PONG = 1, - /** Do nothing, just acknowledge the Interaction */ + /** @deprecated **DEPRECATED:** Do nothing, just acknowledge the Interaction */ ACKNOWLEDGE = 2, - /** Send a channel message without " used / with " */ + /** @deprecated **DEPRECATED:** Send a channel message without " used / with " */ CHANNEL_MESSAGE = 3, - /** Send a channel message with " used / with " */ + /** Send a channel message as response. */ CHANNEL_MESSAGE_WITH_SOURCE = 4, - /** Send nothing further, but send " used / with " */ - ACK_WITH_SOURCE = 5 + /** Let the user know bot is processing ("thinking") and you can edit the response later */ + DEFERRED_CHANNEL_MESSAGE = 5 } export interface InteractionResponsePayload { @@ -155,6 +163,6 @@ export interface InteractionResponseDataPayload { } export enum InteractionResponseFlags { - /** A Message which is only visible to Interaction User, and is not saved on backend */ + /** A Message which is only visible to Interaction User. */ EPHEMERAL = 1 << 6 }