Skip to content

Commit

Permalink
feat: support interaction callback responses
Browse files Browse the repository at this point in the history
  • Loading branch information
Snazzah committed Sep 16, 2024
1 parent a08e8f3 commit 70de21a
Show file tree
Hide file tree
Showing 11 changed files with 210 additions and 63 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module.exports = {
],
'no-cond-assign': [2, 'except-parens'],
'no-unused-vars': 0,
'no-dupe-class-members': 0,
'@typescript-eslint/no-unused-vars': 1,
'no-empty': [
'error',
Expand Down
28 changes: 24 additions & 4 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { ApplicationCommand, BulkUpdateCommand, Endpoints, PartialApplicationCommand } from './constants';
import {
ApplicationCommand,
BulkUpdateCommand,
Endpoints,
InteractionCallbackResponse,
PartialApplicationCommand
} from './constants';
import { BaseSlashCreator } from './creator';
import type { FileContent } from './rest/requestHandler';
import type { MessageData } from './structures/message';
Expand Down Expand Up @@ -169,17 +175,31 @@ export class SlashCreatorAPI {
* @param interactionToken The interaction's token.
* @param body The body to send.
* @param files The files to send.
* @param withResponse Whether to recieve the response of the interaction callback
*/
interactionCallback<WithResponse extends boolean = false>(
interactionID: string,
interactionToken: string,
body: any,
files?: FileContent[],
withResponse?: WithResponse
): Promise<WithResponse extends true ? InteractionCallbackResponse : null>;
interactionCallback(
interactionID: string,
interactionToken: string,
body: any,
files?: FileContent[]
): Promise<unknown> {
files?: FileContent[],
withResponse = false
): Promise<InteractionCallbackResponse | null> {
return this._creator.requestHandler.request(
'POST',
Endpoints.INTERACTION_CALLBACK(interactionID, interactionToken),
{ auth: false, body, files }
{
auth: false,
body,
files,
query: { with_response: withResponse }
}
);
}
}
Expand Down
53 changes: 51 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { SlashCommand } from './command';
import type { CommandContext } from './structures/interfaces/commandContext';
import type { RespondFunction, TransformedRequest } from './server';
import type { ComponentContext } from './structures/interfaces/componentContext';
import type { MessageData } from './structures/message';
import type { Message, MessageData } from './structures/message';
import type { AutocompleteContext } from './structures/interfaces/autocompleteContext';
import type { ModalInteractionContext } from './structures/interfaces/modalInteractionContext';
import type { BaseSlashCreator } from './creator';
Expand Down Expand Up @@ -575,7 +575,7 @@ export interface AppEntitlement {
guild_id?: string;
application_id: string;
type: EntitlementType;
consumed: false;
consumed: boolean;
starts_at?: string;
ends_at?: string;
}
Expand Down Expand Up @@ -640,6 +640,25 @@ export interface AutocompleteData {
*/
export type CommandAutocompleteRequestData = DMCommandAutocompleteRequestData | GuildCommandAutocompleteRequestData;

/* @private **/
export interface InteractionCallbackResponse {
interaction: {
id: string;
type: InteractionType;
activity_instance_id?: string;
response_message_id?: string;
response_message_loading?: boolean;
response_message_ephemeral?: boolean;
};
resource?: {
type: InteractionResponseType;
activity_instance?: {
id: string;
};
message?: MessageData;
};
}

/** @private */
export interface ResolvedMemberData {
avatar?: string;
Expand Down Expand Up @@ -850,6 +869,36 @@ export interface CommandSubcommandOption {
options?: AnyCommandOption[];
}

/** The response to the initial interaction callback. */
export interface InitialInteractionResponse {
/** The interaction associated with this response */
interaction: {
/** ID of the interaction */
id: string;
/** The type of the interaction */
type: InteractionType;
/** The instance ID of the activity if one was launched/joined */
activityInstanceID?: string;
/** The ID of the message created by the interaction */
responseMessageID?: string;
/** Whether or not the message is in a loading state */
responseMessageLoading?: boolean;
/** Whether or not the response message is ephemeral */
responseMessageEphemeral?: boolean;
};
/** The resource created by this interaction response. */
resource?: {
/** The type of the interaction response */
type: InteractionResponseType;
/** The activity instance launched by this interaction */
activityInstance?: {
id: string;
};
/** The message created by this interaction */
message?: Message;
};
}

/** The types of components available. */
export enum ComponentType {
/** A row of components. */
Expand Down
7 changes: 3 additions & 4 deletions src/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,7 @@ export class BaseSlashCreator extends (EventEmitter as any as new () => TypedEve
if (!verified) {
this.emit('debug', 'A request failed to be verified');
this.emit('unverifiedRequest', treq);
return respond({
return void respond({
status: 401,
body: 'Invalid signature'
});
Expand Down Expand Up @@ -782,9 +782,8 @@ export class BaseSlashCreator extends (EventEmitter as any as new () => TypedEve
}

private _createGatewayRespond(interactionID: string, token: string): RespondFunction {
return async (response: Response) => {
await this.api.interactionCallback(interactionID, token, response.body, response.files);
};
return async (response: Response) =>
this.api.interactionCallback(interactionID, token, response.body, response.files, true);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */

import { AnyRequestData } from './constants';
import { AnyRequestData, InteractionCallbackResponse } from './constants';
import type { FileContent } from './rest/requestHandler';

/**
Expand Down Expand Up @@ -84,7 +84,7 @@ export interface Response {
* The response function for a {@link Server}.
* @private
*/
export type RespondFunction = (response: Response) => Promise<void>;
export type RespondFunction = (response: Response) => Promise<InteractionCallbackResponse | void>;

/**
* The handler for pushing requests to a {@link SlashCreator}.
Expand Down
15 changes: 11 additions & 4 deletions src/structures/interfaces/autocompleteContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { AnyCommandOption, CommandAutocompleteRequestData, InteractionResponseType } from '../../constants';
import {
AnyCommandOption,
CommandAutocompleteRequestData,
InitialInteractionResponse,
InteractionResponseType
} from '../../constants';
import { BaseSlashCreator } from '../../creator';
import { RespondFunction } from '../../server';
import { convertCallbackResponse } from '../../util';
import { BaseInteractionContext } from './baseInteraction';
import { CommandContext } from './commandContext';

Expand Down Expand Up @@ -44,19 +50,20 @@ export class AutocompleteContext<ServerContext extends any = unknown> extends Ba
/**
* Sends the results of an autocomplete interaction.
* @param choices The choices to display
* @returns boolean or a {@link InitialInteractionResponse} if the response passed
*/
async sendResults(choices: AutocompleteChoice[]): Promise<boolean> {
async sendResults(choices: AutocompleteChoice[]): Promise<boolean | InitialInteractionResponse> {
if (this.responded) return false;

this.responded = true;
await this._respond({
const response = await this._respond({
status: 200,
body: {
type: InteractionResponseType.APPLICATION_COMMAND_AUTOCOMPLETE_RESULT,
data: { choices }
}
});
return true;
return response ? convertCallbackResponse(response, this) : true;
}

/** @private */
Expand Down
26 changes: 16 additions & 10 deletions src/structures/interfaces/componentContext.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { ComponentType, InteractionResponseType, MessageComponentRequestData } from '../../constants';
import {
ComponentType,
InitialInteractionResponse,
InteractionResponseType,
MessageComponentRequestData
} from '../../constants';
import { EditMessageOptions } from './messageInteraction';
import { BaseSlashCreator } from '../../creator';
import { RespondFunction } from '../../server';
import { Message } from '../message';
import { formatAllowedMentions, FormattedAllowedMentions } from '../../util';
import { convertCallbackResponse, formatAllowedMentions, FormattedAllowedMentions } from '../../util';
import { ModalSendableContext } from './modalSendableContext';

/** Represents an interaction context from a message component. */
Expand Down Expand Up @@ -48,30 +53,31 @@ export class ComponentContext<ServerContext extends any = unknown> extends Modal

/**
* Acknowledges the interaction without replying.
* @returns Whether the acknowledgement passed passed
* @returns Whether the acknowledgement passed or the callback response if available
*/
async acknowledge(): Promise<boolean> {
async acknowledge(): Promise<boolean | InitialInteractionResponse> {
if (!this.initiallyResponded) {
this.initiallyResponded = true;
clearTimeout(this._timeout);
await this._respond({
const response = await this._respond({
status: 200,
body: {
type: InteractionResponseType.DEFERRED_UPDATE_MESSAGE
}
});
return true;
return response ? convertCallbackResponse(response, this) : true;
}

return false;
}

/**
* Edits the message that the component interaction came from.
* This will return a boolean if it's an initial response, otherwise a {@link Message} will be returned.
* This will return `true` or a {@link InitialInteractionResponse} if it's an initial response, otherwise a {@link Message} will be returned.
* @param content The content of the message
* @returns `true` or a {@link InitialInteractionResponse} if the initial response passed, otherwise a {@link Message} of the parent message.
*/
async editParent(content: string | EditMessageOptions): Promise<boolean | Message> {
async editParent(content: string | EditMessageOptions): Promise<boolean | InitialInteractionResponse | Message> {
if (this.expired) throw new Error('This interaction has expired');

const options = typeof content === 'string' ? { content } : content;
Expand All @@ -86,7 +92,7 @@ export class ComponentContext<ServerContext extends any = unknown> extends Modal
if (!this.initiallyResponded) {
this.initiallyResponded = true;
clearTimeout(this._timeout);
await this._respond({
const response = await this._respond({
status: 200,
body: {
type: InteractionResponseType.UPDATE_MESSAGE,
Expand All @@ -99,7 +105,7 @@ export class ComponentContext<ServerContext extends any = unknown> extends Modal
},
files: options.files
});
return true;
return response ? convertCallbackResponse(response, this) : true;
} else return this.edit(this.message.id, content);
}
}
Loading

0 comments on commit 70de21a

Please sign in to comment.