diff --git a/src/models/slashClient.ts b/src/models/slashClient.ts index fcb2577..33b3aca 100644 --- a/src/models/slashClient.ts +++ b/src/models/slashClient.ts @@ -1,7 +1,11 @@ import { Guild } from '../structures/guild.ts' -import { Interaction } from '../structures/slash.ts' +import { + Interaction, + InteractionApplicationCommandResolved +} from '../structures/slash.ts' import { InteractionPayload, + InteractionResponsePayload, InteractionType, SlashCommandChoice, SlashCommandOption, @@ -14,6 +18,7 @@ import { Client } from './client.ts' import { RESTManager } from './rest.ts' import { SlashModule } from './slashModule.ts' import { verify as edverify } from 'https://deno.land/x/ed25519@1.0.1/mod.ts' +import { User } from '../structures/user.ts' export class SlashCommand { slash: SlashCommandsManager @@ -375,6 +380,7 @@ export interface SlashOptions { 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 @@ -539,16 +545,17 @@ export class SlashClient { return edverify(signature, fullBody, this.publicKey).catch(() => false) } - /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction */ + /** Verify [Deno Std HTTP Server Request](https://deno.land/std/http/server.ts) and return Interaction. **Data present in Interaction returned by this method is very different from actual typings as there is no real `Client` behind the scenes to cache things.** */ async verifyServerRequest(req: { headers: Headers method: string body: Deno.Reader respond: (options: { status?: number + headers?: Headers body?: string | Uint8Array }) => Promise - }): Promise { + }): Promise { if (req.method.toLowerCase() !== 'post') return false const signature = req.headers.get('x-signature-ed25519') @@ -561,7 +568,31 @@ export class SlashClient { try { const payload: InteractionPayload = JSON.parse(decoder.decode(rawbody)) - const res = new Interaction(this as any, payload, {}) + + // TODO: Maybe fix all this hackery going on here? + const res = new Interaction(this as any, payload, { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + user: new User(this as any, (payload.member?.user ?? payload.user)!), + member: payload.member as any, + guild: payload.guild_id as any, + channel: payload.channel_id as any, + resolved: ((payload.data + ?.resolved as unknown) as InteractionApplicationCommandResolved) ?? { + users: {}, + members: {}, + roles: {}, + channels: {} + } + }) + res._httpRespond = async (d: InteractionResponsePayload) => + await req.respond({ + status: 200, + headers: new Headers({ + 'content-type': 'application/json' + }), + body: JSON.stringify(d) + }) + return res } catch (e) { return false diff --git a/src/structures/slash.ts b/src/structures/slash.ts index b7c0058..fbd95d8 100644 --- a/src/structures/slash.ts +++ b/src/structures/slash.ts @@ -7,12 +7,14 @@ import { } from '../types/channel.ts' import { INTERACTION_CALLBACK, WEBHOOK_MESSAGE } from '../types/endpoint.ts' import { + InteractionApplicationCommandData, InteractionApplicationCommandOption, InteractionChannelPayload, InteractionPayload, InteractionResponseFlags, InteractionResponsePayload, InteractionResponseType, + InteractionType, SlashCommandOptionType } from '../types/slash.ts' import { Dict } from '../utils/dict.ts' @@ -26,7 +28,6 @@ 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[] @@ -86,18 +87,30 @@ export class InteractionUser extends User { } 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 + /** 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 - _savedHook?: Webhook - _respond?: (data: InteractionResponsePayload) => unknown + /** 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 + _httpRespond?: (d: InteractionResponsePayload) => unknown + _httpResponded?: boolean constructor( client: Client, @@ -137,7 +150,8 @@ export class Interaction extends SnowflakeBase { if (op === undefined || op.value === undefined) return undefined as any if (op.type === SlashCommandOptionType.USER) { const u: InteractionUser = this.resolved.users[op.value] as any - if (this.resolved.members[op.value] !== undefined) u.member = this.resolved.members[op.value] + if (this.resolved.members[op.value] !== undefined) + u.member = this.resolved.members[op.value] return u as any } else if (op.type === SlashCommandOptionType.ROLE) return this.resolved.roles[op.value] as any @@ -172,19 +186,24 @@ export class Interaction extends SnowflakeBase { : undefined } - await this.client.rest.post( - INTERACTION_CALLBACK(this.id, this.token), - payload - ) + if (this._httpRespond !== undefined && this._httpResponded !== true) { + this._httpResponded = true + await this._httpRespond(payload) + } else + await this.client.rest.post( + 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 { + async defer(ephemeral = false): Promise { await this.respond({ - type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE + type: InteractionResponseType.DEFERRED_CHANNEL_MESSAGE, + flags: ephemeral ? 1 << 6 : 0 }) this.deferred = true return this diff --git a/src/test/slash-http.ts b/src/test/slash-http.ts index 02f9836..893feaf 100644 --- a/src/test/slash-http.ts +++ b/src/test/slash-http.ts @@ -18,27 +18,10 @@ await slash.commands.bulkEdit([ const options = { port: 8000 } console.log('Listen on port: ' + options.port.toString()) listenAndServe(options, async (req) => { - const verify = await slash.verifyServerRequest(req) - if (verify === false) - return req.respond({ status: 401, body: 'not authorized' }) + const d = await slash.verifyServerRequest(req) + if (d === false) return req.respond({ status: 401, body: 'not authorized' }) - const respond = async (d: any): Promise => - req.respond({ - status: 200, - body: JSON.stringify(d), - headers: new Headers({ - 'content-type': 'application/json' - }) - }) - - const body = JSON.parse( - new TextDecoder('utf-8').decode(await Deno.readAll(req.body)) - ) - if (body.type === 1) return await respond({ type: 1 }) - await respond({ - type: 4, - data: { - content: 'Pong!' - } - }) + console.log(d) + if (d.type === 1) return d.respond({ type: 1 }) + d.reply('Pong!') })