diff --git a/mod.ts b/mod.ts index fdd25de..227ef63 100644 --- a/mod.ts +++ b/mod.ts @@ -136,3 +136,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/gateway/handlers/interactionCreate.ts b/src/gateway/handlers/interactionCreate.ts index fac882a..4c8eb00 100644 --- a/src/gateway/handlers/interactionCreate.ts +++ b/src/gateway/handlers/interactionCreate.ts @@ -1,29 +1,110 @@ +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 ( gateway: Gateway, d: InteractionPayload ) => { - const guild = await gateway.client.guilds.get(d.guild_id) - if (guild === undefined) return + // 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 - await guild.members.set(d.member.user.id, d.member) - const member = ((await guild.members.get( - d.member.user.id - )) as unknown) as Member + const guild = + d.guild_id === undefined + ? undefined + : await gateway.client.guilds.get(d.guild_id) + + 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)) ?? (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 + channel, + user, + resolved }) gateway.client.emit('interactionCreate', interaction) } diff --git a/src/gateway/index.ts b/src/gateway/index.ts index 955b61d..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 @@ -157,7 +154,7 @@ export class Gateway extends HarmonyEventEmitter { const handler = gatewayHandlers[t] - if (handler !== undefined) { + if (handler !== undefined && d !== null) { handler(this, d) } } @@ -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/models/slashClient.ts b/src/models/slashClient.ts index 7e9b581..5f8955d 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,9 +372,7 @@ export interface SlashOptions { publicKey?: string } -const encoder = new TextEncoder() -const decoder = new TextDecoder('utf-8') - +/** Slash Client represents an Interactions Client which can be used without Harmony Client. */ export class SlashClient { id: string | (() => string) client?: Client @@ -472,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 @@ -488,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/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 a564a30..9b8cc10 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -1,21 +1,33 @@ import { Client } from '../models/client.ts' -import { MessageOptions } from '../types/channel.ts' +import { + AllowedMentionsPayload, + ChannelTypes, + EmbedPayload, + MessageOptions +} from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { - InteractionData, - InteractionOption, + InteractionApplicationCommandData, + InteractionApplicationCommandOption, + InteractionChannelPayload, InteractionPayload, + InteractionResponseFlags, InteractionResponsePayload, - InteractionResponseType + InteractionResponseType, + 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' 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' -import { Webhook } from './webhook.ts' interface WebhookMessageOptions extends MessageOptions { embeds?: Embed[] @@ -25,72 +37,133 @@ 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 - temp?: boolean - allowedMentions?: { - parse?: string - roles?: string[] - users?: string[] - everyone?: boolean + flags?: number | InteractionResponseFlags[] + allowedMentions?: AllowedMentionsPayload +} + +export interface InteractionResponse extends InteractionMessageOptions { + /** Type of Interaction Response */ + type?: InteractionResponseType + /** 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 + + 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 { - /** This will be `SlashClient` in case of `SlashClient#verifyServerRequest` */ - 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 - _respond?: (data: InteractionResponsePayload) => unknown + /** 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, data: InteractionPayload, others: { - channel: GuildTextChannel - guild: Guild - member: Member + channel?: TextChannel | GuildTextChannel + 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 this.id = data.id + this.user = others.user this.data = data.data this.guild = others.guild this.channel = others.channel + this.resolved = others.resolved } - 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(): InteractionApplicationCommandOption[] { + return this.data?.options ?? [] } - get options(): InteractionOption[] { - return this.data.options ?? [] - } - - option(name: string): T { - return this.options.find((e) => e.name === name)?.value + /** Get an option by name */ + 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 */ async respond(data: InteractionResponse): Promise { + if (this.responded) throw new Error('Already responded to Interaction') + let flags = 0 + 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 ?? InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, data: @@ -101,8 +174,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 } @@ -111,6 +184,55 @@ export class Interaction extends SnowflakeBase { INTERACTION_CALLBACK(this.id, this.token), payload ) + this.responded = true + + return this + } + + /** 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: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE + }) + this.deferred = true + 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 }) + + 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 } @@ -118,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, @@ -127,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 } @@ -234,6 +363,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/test/index.ts b/src/test/index.ts index 3923b5e..4678fc8 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,30 +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')}`) - } else if (msg.content === '!addAllRoles') { - const roles = await msg.guild?.roles.array() - if (roles !== undefined) { - roles.forEach(async (role) => { - await msg.member?.roles.add(role) - console.log(role) - }) - } - } else if (msg.content === '!createAndAddRole') { - if (msg.guild !== undefined) { - const role = await msg.guild.roles.create({ - name: 'asdf', - permissions: 0 - }) - await msg.member?.roles.add(role) - } - } else if (msg.content === '!roles') { - let buf = 'Roles:' - if (msg.member === undefined) return - for await (const role of msg.member.roles) { - buf += `\n${role.name}` - } - msg.reply(buf) + msg.channel.send( + Object.entries(permissions.serialize()) + .map((e) => `${e[0]}: ${e[1] === true ? '`✅`' : '`❌`'}`) + .join('\n') + ) } }) 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/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..c700fbd 100644 --- a/src/types/slash.ts +++ b/src/types/slash.ts @@ -1,22 +1,47 @@ -import { EmbedPayload } from './channel.ts' +import { Dict } from '../utils/dict.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 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 InteractionChannelPayload { + id: string + name: string + permissions: string + type: ChannelTypes +} + +export interface InteractionApplicationCommandResolvedPayload { + users?: Dict + members?: Dict + channels?: Dict + roles?: Dict +} + +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[] + /** Resolved data for options in Slash Command */ + resolved?: InteractionApplicationCommandResolvedPayload } export enum InteractionType { @@ -26,27 +51,30 @@ export enum InteractionType { APPLICATION_COMMAND = 2 } +export interface InteractionMemberPayload extends MemberPayload { + /** Permissions of the Member who initiated Interaction (Guild-only) */ + 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** + * Data sent with the interaction. Undefined only when Interaction is not 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 { @@ -57,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, @@ -68,58 +98,71 @@ 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[] } +/** 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 { + /** 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 } 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 } 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 +}