feat: slash commands and interactions (#248)

* start slash commands

* lil bit of this

* a little bit of that

* chore: add slash commands' types  (#249)

* Add slash Types

* style: format source files

* interaction create event

* make it a valid controller

* respond to a slash command

* interactions

* shtuff

* gotta get them all

* you gotta hit the bullseye

* you gotta survive

* most important thing i forgot

* please keep this

* more shuttttfgvuasdafwesdvjzdk

* more endpoints

* TYPO

* making that party jam

* this is my jam

* refactor: move slash commands types to webhook.ts (#250)

* Move Types

* Move to webhook.ts types file

* Update webhook.ts

* fix: update ExecuteSlashCommandOptions (#252)

* Move Types

* idk

* Remove Unnecessary Comment

* details

* http side of slash

* Format

* idk

* cleanup

* fmt

* fix imports

* tet

* remove log

* Update interactions.ts

* Update interactions.ts

* Update interactions.ts

* Use tweetnacl_deno for verification

* chore: move tweetnacl import to deps.ts

* Update deps.ts

* deps: use tweetnacl from GitHub

* fix: use serverOptions.slashHexKey

* fix: res -> req

* fix: use TextEncoder

* deps: add std@0.81.0/encoding/hex.ts

* chore: use encode() from std/encoding/hex.ts

* I am using the GitHub online editor pls help

* Update deps.ts

* Update interactions.ts

* Update interactions.ts

* Update interactions.ts

* Update interactions.ts

* fix: respond with 400 if timestamp and signature not present

* style: format files

* refactor!: merge createServer() into startServer()

* style: format files

Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com>
Co-authored-by: ayntee <ayyantee@gmail.com>
This commit is contained in:
Skillz4Killz
2020-12-22 12:39:01 -05:00
committed by GitHub
parent e49a23687e
commit 19f228c329
22 changed files with 882 additions and 1 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,6 @@
# Allows quick testing of changes and keeps stuff like tokens private
debug.ts
.DS_Store
.lock

View File

@@ -44,6 +44,10 @@ import {
handleInternalGuildRoleDelete,
handleInternalGuildRoleUpdate,
} from "./roles.ts";
import {
handleInternalInteractionsCommandCreate,
handleInternalInteractionsCreate,
} from "./interactions.ts";
export let controllers = {
READY: handleInternalReady,
@@ -63,6 +67,8 @@ export let controllers = {
GUILD_ROLE_CREATE: handleInternalGuildRoleCreate,
GUILD_ROLE_DELETE: handleInternalGuildRoleDelete,
GUILD_ROLE_UPDATE: handleInternalGuildRoleUpdate,
INTERACTION_CREATE: handleInternalInteractionsCreate,
APPLICATION_COMMAND_CREATE: handleInternalInteractionsCommandCreate,
MESSAGE_CREATE: handleInternalMessageCreate,
MESSAGE_DELETE: handleInternalMessageDelete,
MESSAGE_DELETE_BULK: handleInternalMessageDeleteBulk,

View File

@@ -1,16 +1,23 @@
import { RequestManager } from "../../rest/mod.ts";
import { structures } from "../structures/structures.ts";
import {
CreateSlashCommandOptions,
EditSlashCommandOptions,
EditSlashResponseOptions,
EditWebhookMessageOptions,
Errors,
ExecuteSlashCommandOptions,
ExecuteWebhookOptions,
MessageCreateOptions,
UpsertSlashCommandOptions,
WebhookCreateOptions,
WebhookPayload,
} from "../../types/types.ts";
import { endpoints } from "../../util/constants.ts";
import { botHasChannelPermissions } from "../../util/permissions.ts";
import { urlToBase64 } from "../../util/utils.ts";
import { botID } from "../../bot.ts";
import { cache } from "../../util/cache.ts";
/** Create a new webhook. Requires the MANAGE_WEBHOOKS permission. Returns a webhook object on success. Webhook names follow our naming restrictions that can be found in our Usernames and Nicknames documentation, with the following additional stipulations:
*
@@ -171,3 +178,139 @@ export function deleteWebhookMessage(
endpoints.WEBHOOK_DELETE(webhookID, webhookToken, messageID),
);
}
/**
* There are two kinds of Slash Commands: global commands and guild commands. Global commands are available for every guild that adds your app; guild commands are specific to the guild you specify when making them. Command names are unique per application within each scope (global and guild). That means:
*
* - Your app **cannot** have two global commands with the same name
* - Your app **cannot** have two guild commands within the same name **on the same guild**
* - Your app **can** have a global and guild command with the same name
* - Multiple apps **can** have commands with the same names
*
* Global commands are cached for **1 hour**. That means that new global commands will fan out slowly across all guilds, and will be guaranteed to be updated in an hour.
* Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use.
*/
export function createSlashCommand(options: CreateSlashCommandOptions) {
// Use ... for content length due to unicode characters and js .length handling
if ([...options.name].length < 2 || [...options.name].length > 32) {
throw new Error(Errors.INVALID_SLASH_NAME);
}
if (
[...options.description].length < 1 || [...options.description].length > 100
) {
throw new Error(Errors.INVALID_SLASH_DESCRIPTION);
}
return RequestManager.post(
options.guildID
? endpoints.COMMANDS_GUILD(botID, options.guildID)
: endpoints.COMMANDS(botID),
{
...options,
},
);
}
/** Fetch all of the global commands for your application. */
export function getSlashCommands(guildID?: string) {
// TODO: Should this be a returned as a collection?
return RequestManager.get(
guildID
? endpoints.COMMANDS_GUILD(botID, guildID)
: endpoints.COMMANDS(botID),
);
}
/**
* Edit an existing slash command. If this command did not exist, it will create it.
*/
export function upsertSlashCommand(options: UpsertSlashCommandOptions) {
return RequestManager.post(
options.guildID
? endpoints.COMMANDS_GUILD_ID(botID, options.id, options.guildID)
: endpoints.COMMANDS_ID(botID, options.id),
{
...options,
},
);
}
/** Edit an existing slash command. */
export function editSlashCommand(options: EditSlashCommandOptions) {
return RequestManager.patch(
options.guildID
? endpoints.COMMANDS_GUILD_ID(botID, options.id, options.guildID)
: endpoints.COMMANDS_ID(botID, options.id),
{
...options,
},
);
}
/** Deletes a slash command. */
export function deleteSlashCommand(id: string, guildID?: string) {
if (!guildID) return RequestManager.delete(endpoints.COMMANDS_ID(botID, id));
return RequestManager.delete(endpoints.COMMANDS_GUILD_ID(botID, id, guildID));
}
/**
* Send a response to a users slash command. The command data will have the id and token necessary to respond.
* Interaction `tokens` are valid for **15 minutes** and can be used to send followup messages.
*
* NOTE: By default we will suppress mentions. To enable mentions, just pass any mentions object.
*/
export function executeSlashCommand(
id: string,
token: string,
options: ExecuteSlashCommandOptions,
) {
// If its already been executed, we need to send a followup response
if (cache.executedSlashCommands.has(token)) {
return RequestManager.post(endpoints.WEBHOOK(botID, token), {
...options,
});
}
// Expire in 15 minutes
cache.executedSlashCommands.set(token, id);
setTimeout(
() => cache.executedSlashCommands.delete(token),
Date.now() + 900000,
);
// IF NO MENTIONS ARE PROVIDED, FORCE DISABLE MENTIONS
if (!(options.data.allowed_mentions)) {
options.data.allowed_mentions = { parse: [] };
}
return RequestManager.post(endpoints.INTERACTION_ID_TOKEN(id, token), {
...options,
});
}
/** To delete your response to a slash command. If a message id is not provided, it will default to deleting the original response. */
export function deleteSlashResponse(
token: string,
messageID?: string,
) {
if (!messageID) {
return RequestManager.delete(
endpoints.INTERACTION_ORIGINAL_ID_TOKEN(botID, token),
);
}
return RequestManager.delete(
endpoints.INTERACTION_ID_TOKEN_MESSAGEID(botID, token, messageID),
);
}
/** To edit your response to a slash command. If a messageID is not provided it will default to editing the original response. */
export function editSlashResponse(
token: string,
options: EditSlashResponseOptions,
) {
return RequestManager.patch(
endpoints.INTERACTION_ORIGINAL_ID_TOKEN(botID, token),
options,
);
}

View File

@@ -0,0 +1,26 @@
import { DiscordPayload } from "../types/types.ts";
import { eventHandlers } from "../module/client.ts";
import { structures } from "../structures/mod.ts";
import { InteractionCommandPayload } from "../types/types.ts";
export async function handleInternalInteractionsCreate(data: DiscordPayload) {
if (data.t !== "INTERACTION_CREATE") return;
const payload = data.d as InteractionCommandPayload;
eventHandlers.interactionCreate?.(
{
...payload,
member: await structures.createMember(payload.member, payload.guild_id),
},
);
}
export async function handleInternalInteractionsCommandCreate(
data: DiscordPayload,
) {
if (data.t !== "APPLICATION_COMMAND_CREATE") return;
console.log(data);
eventHandlers.interactionCreate?.(data);
}

2
src/interactions/deps.ts Normal file
View File

@@ -0,0 +1,2 @@
export { serve } from "https://deno.land/std@0.81.0/http/server.ts";
export { verify } from "https://esm.sh/@evan/wasm@0.0.18/target/ed25519/deno.js";

View File

@@ -0,0 +1,133 @@
import { serve, verify } from "./deps.ts";
import {
Interaction,
InteractionResponse,
InteractionResponseType,
InteractionType,
} from "./types/mod.ts";
/** This variable is a holder for the public key and other configuration */
const serverOptions = {
publicKey: "",
port: 80,
};
/** Theses are the controllers that you can plug into and customize to your needs. */
export const controllers = {
handlePayload,
handleApplicationCommand,
};
export interface StartServerConfig {
/** The public key from your discord bot dashboard at discord.dev */
publicKey: string;
/** The port number you are wanting to listen to, if you are following the guide, you probably want 80 */
port: number;
/** The function you would like to provide to handle your commands. */
handleApplicationCommand?(
payload: Interaction,
): Promise<{ status?: number; body: InteractionResponse }>;
}
/** Starts the slash command server */
export async function startServer(
{ port, publicKey, handleApplicationCommand }: StartServerConfig,
) {
serverOptions.publicKey = publicKey;
serverOptions.port = port;
if (handleApplicationCommand) {
controllers.handleApplicationCommand = handleApplicationCommand;
}
const server = serve({ port: serverOptions.port });
for await (const req of server) {
const buffer = await Deno.readAll(req.body);
const signature = req.headers.get("X-Signature-Ed25519");
const timestamp = req.headers.get("X-Signature-Timestamp");
if (!signature || !timestamp) {
req.respond({ status: 400, body: "Bad request" });
continue;
}
const isVerified = verifySecurity(buffer, signature!, timestamp!);
if (!isVerified) {
req.respond({ status: 401, body: "Invalid request signature" });
continue;
}
try {
const data = JSON.parse(new TextDecoder().decode(buffer));
const response = await controllers.handlePayload(data);
req.respond(
{ status: response.status || 200, body: JSON.stringify(response.body) },
);
} catch (error) {
console.error(error);
}
}
}
async function handlePayload(payload: Interaction) {
switch (payload.type) {
case InteractionType.PING:
return { status: 200, body: { type: InteractionResponseType.PONG } };
default: // APPLICATION_COMMAND
return controllers.handleApplicationCommand(payload);
}
}
/** The function that handles your commands. This command can be overriden by you and you can receive the payload and handle accordingly and respond back. The status if not provided will default to 200. */
async function handleApplicationCommand(
payload: Interaction,
): Promise<{ status?: number; body: InteractionResponse }> {
// Handle the command
if (payload.data?.name === "ping") {
return {
status: 200,
body: {
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: { content: "Pong from Discordeno!" },
},
};
}
return {
status: 200,
body: {
type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
data: {
content:
"Whoopsies! Seems the handling for this command is missing. Please contact my developers!",
},
},
};
}
/** Internal function to verify security. Discord will send bad and good data and this function is important to verify it. If it is not verified properly, Discord will kill your bot. */
function verifySecurity(buffer: Uint8Array, signature: string, time: string) {
const sig = new Uint8Array(64);
const timestamp = new TextEncoder().encode(time);
let offset = 0;
const message = new Uint8Array(buffer.length + timestamp.length);
while (offset < 2 * 64) {
sig[offset / 2] = parseInt(signature!.substring(offset, offset += 2), 16);
}
const slash_key = new Uint8Array(32);
let keyoffset = 0;
while (keyoffset < 2 * 32) {
slash_key[keyoffset / 2] = parseInt(
serverOptions.publicKey.substring(keyoffset, keyoffset += 2),
16,
);
}
message.set(timestamp);
message.set(buffer, timestamp.length);
return verify(slash_key, sig, message);
}

2
src/interactions/mod.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from "./interactions.ts";
export * from "./types/mod.ts";

View File

@@ -0,0 +1,95 @@
export interface Embed {
/** The title of the embed */
title?: string;
/** The type of embed (always rich for webhook embeds) */
type?: string;
/** The description of embeds */
description?: string;
/** The url of embed */
url?: string;
/** The timestap of the embed content */
timestamp?: string;
/** The color code of the embed */
color?: number;
/** The footer information */
footer?: EmbedFooter;
/** The image information */
image?: EmbedImage;
/** The thumbnail information */
thumbnail?: EmbedThumbnail;
/** The video information */
video?: EmbedVideo;
/** Provider information */
provider?: EmbedProvider;
/** Author information */
author?: EmbedAuthor;
/** Fields information */
fields?: EmbedField[];
}
export interface EmbedFooter {
/** The text of the footer */
text: string;
/** The url of the footer icon. Only supports http(s) and attachments */
icon_url?: string;
/** A proxied url of footer icon */
proxy_icon_url?: string;
}
export interface EmbedImage {
/** The source url of image (only supports http(s) and attachments) */
url?: string;
/** A proxied url of the image */
proxy_url?: string;
/** The height of image */
height?: number;
/** The width of the image */
width?: number;
}
export interface EmbedThumbnail {
/** The source url of image (only supports http(s) and attachments) */
url?: string;
/** A proxied url of the thumbnail */
proxy_url?: string;
/** The height of the thumbnail */
height?: number;
/** The width of the thumbnail */
width?: number;
}
export interface EmbedVideo {
/** The source url of video */
url?: string;
/** The height of the video */
height?: number;
/** The width of the video */
width?: number;
}
export interface EmbedProvider {
/** The name of the provider */
name?: string;
/** The url of the provider */
url?: string;
}
export interface EmbedAuthor {
/** The name of the author */
name?: string;
/** The url of the author */
url?: string;
/** The url of the author icon (supports http(s) and attachments) */
icon_url?: string;
/** A proxied url of author icon */
proxy_icon_url?: string;
}
export interface EmbedField {
/** The name of the field */
name: string;
/** The value of the field */
value: string;
/** Whether or not this field should display inline */
inline?: boolean;
}

View File

@@ -0,0 +1,76 @@
import { Embed } from "./embed.ts";
import { AllowedMentions } from "./misc.ts";
import { MemberCreatePayload } from "./member.ts";
export interface Interaction {
/** The id of the interaction */
id: string;
/** The type of interaction */
type: InteractionType;
/** The command data payload */
data?: SlashCommandInteractionData;
/** The id of the guild it was sent from */
guild_id: string;
/** The id of the channel it was sent from */
channel_id: string;
/** The Payload of the member it was sent from */
member: MemberCreatePayload;
/** The token for this interaction */
token: string;
}
export interface SlashCommandInteractionData {
/** The id of the command */
id: string;
/** The name of the command */
name: string;
/** the params and values from the user */
options: SlashCommandInteractionDataOption[];
}
export interface SlashCommandInteractionDataOption {
/** The name of the parammeter */
name: string;
/** The value of the pair */
value?: any;
/** Present if this option is a group or subcommand */
options?: SlashCommandInteractionDataOption[];
}
export interface InteractionResponse {
/** The type of response */
type: InteractionResponseType;
/** The optional response message */
data?: SlashCommandCallbackData;
}
export interface SlashCommandCallbackData {
/** is the response TTS */
tts?: boolean;
/** message content */
content: string;
/** supports up to 10 embeds */
embeds?: Embed[];
/** allowed mentions for the message */
allowed_mentions?: AllowedMentions;
/** acceptable values are message flags */
flags?: number;
}
export enum InteractionType {
PING = 1,
APPLICATION_COMMAND = 2,
}
export enum InteractionResponseType {
/** ACK a `Ping` */
PONG = 1,
/** ACK a command without sending a message, eating the user's input */
ACKNOWLEDGE = 2,
/** respond with a message, eating the user's input */
CHANNEL_MESSAGE = 3,
/** respond with a message, showing the user's input */
CHANNEL_MESSAGE_WITH_SOURCE = 4,
/** ACK a command without sending a message, showing the user's input */
ACK_WITH_SOURCE = 5,
}

View File

@@ -0,0 +1,43 @@
export interface UserPayload {
/** The user's id */
id: string;
/** the user's username, not unique across the platform */
username: string;
/** The user's 4 digit discord tag */
discriminator: string;
/** The user's avatar hash */
avatar: string | null;
/** Whether the user is a bot */
bot?: boolean;
/** Whether the user is an official discord system user (part of the urgent message system.) */
system?: boolean;
/** Whether the user has two factor enabled on their account */
mfa_enabled?: boolean;
/** the user's chosen language option */
locale?: string;
/** Whether the email on this account has been verified */
verified?: boolean;
/** The user's email */
email?: string;
/** The flags on a user's account. */
flags?: number;
/** The type of Nitro subscription on a user's account. */
premium_type?: number;
}
export interface MemberCreatePayload {
/** The user this guild member represents */
user: UserPayload;
/** The user's guild nickname if one is set. */
nick?: string;
/** Array of role ids that the member has */
roles: string[];
/** When the user joined the guild. */
joined_at: string;
/** When the user used their nitro boost on the server. */
premium_since?: string;
/** Whether the user is deafened in voice channels */
deaf: boolean;
/** Whether the user is muted in voice channels */
mute: boolean;
}

View File

@@ -0,0 +1,8 @@
export interface AllowedMentions {
/** An array of allowed mention types to parse from the content. */
parse: ("roles" | "users" | "everyone")[];
/** Array of role_ids to mention (Max size of 100) */
roles?: string[];
/** Array of user_ids to mention (Max size of 100) */
users?: string[];
}

View File

@@ -0,0 +1,6 @@
export * from "./embed.ts";
export * from "./interactions.ts";
export * from "./misc.ts";
export * from "./slash.ts";
export * from "./member.ts";
export * from "./webhook.ts";

View File

@@ -0,0 +1,86 @@
import {
InteractionResponseType,
SlashCommandCallbackData,
} from "./interactions.ts";
export interface CreateSlashCommandOptions {
/** The name of the slash command. */
name: string;
/** The description of the slash command. */
description: String;
/** If a guildID is provided, this will be a GUILD command. If none is provided it will be a GLOBAL command. */
guildID?: string;
/** The options for this command */
options?: SlashCommandOption[];
}
export interface SlashCommand {
/** unique id of the command */
id: string;
/** unique id of the parent application */
application_id: string;
/** 3-32 character name */
name: string;
/** 1-100 character description */
description: string;
/** the parameters for the command */
options?: SlashCommandOption[];
}
export interface SlashCommandOption {
/** The type of option */
type: SlashCommandOptionType;
/** 1-32 character name */
name: string;
/** 1-100 character description*/
description: string;
/** the first `required` option for the user to complete--only one option can be `default` */
default?: boolean;
/** if the parameter is required or optional--default `false`*/
required?: boolean;
/**
* If you specify `choices` for an option, they are the **only** valid values for a user to pick.
* choices for `string` and `int` types for the user to pick from
*/
choices?: SlashCommandOptionChoice[];
/** if the option is a subcommand or subcommand group type, this nested options will be the parameters */
options?: SlashCommandOption[];
}
export interface SlashCommandOptionChoice {
/** The name of the choice */
name: string;
/** The value of the choice */
value: string | number;
}
export enum SlashCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
}
export interface EditSlashCommandOptions {
id: string;
guildID?: string;
}
export interface ExecuteSlashCommandOptions {
type: InteractionResponseType;
data: SlashCommandCallbackData;
}
export interface EditSlashResponseOptions extends SlashCommandCallbackData {
/** If this is not provided, it will default to editing the original response. */
messageID?: string;
}
export interface UpsertSlashCommandOptions {
id: string;
guildID?: string;
}

View File

@@ -0,0 +1,27 @@
import { Embed } from "./embed.ts";
export interface ExecuteWebhookOptions {
/** waits for server confirmation of message send before response, and returns the created message body (defaults to false; when false a message that is not saved does not return an error) */
wait?: boolean;
/** the message contents (up to 2000 characters) */
content?: string;
/** override the default username of the webhook */
username?: string;
/** override the default avatar of the webhook*/
avatar_url?: string;
/** true if this is a TTS message */
tts?: boolean;
/** file contents the contents of the file being sent one of content, file, embeds */
file?: { blob: unknown; name: string };
/** array of up to 10 embed objects embedded rich content. */
embeds?: Embed[];
/** allowed mentions for the message */
mentions?: {
/** An array of allowed mention types to parse from the content. */
parse: ("roles" | "users" | "everyone")[];
/** Array of role_ids to mention (Max size of 100) */
roles?: string[];
/** Array of user_ids to mention (Max size of 100) */
users?: string[];
};
}

View File

@@ -12,7 +12,7 @@ export interface DiscordPayload {
s?: number;
/** The event name for this payload. ONLY for OPCode 0 */
t?:
| "READY"
| "APPLICATION_COMMAND_CREATE"
| "CHANNEL_CREATE"
| "CHANNEL_DELETE"
| "CHANNEL_UPDATE"
@@ -29,6 +29,7 @@ export interface DiscordPayload {
| "GUILD_ROLE_CREATE"
| "GUILD_ROLE_DELETE"
| "GUILD_ROLE_UPDATE"
| "INTERACTION_CREATE"
| "MESSAGE_CREATE"
| "MESSAGE_DELETE"
| "MESSAGE_DELETE_BULK"
@@ -38,6 +39,7 @@ export interface DiscordPayload {
| "MESSAGE_REACTION_REMOVE_ALL"
| "MESSAGE_REACTION_REMOVE_EMOJI"
| "PRESENCE_UPDATE"
| "READY"
| "TYPING_START"
| "USER_UPDATE"
| "VOICE_STATE_UPDATE"

View File

@@ -30,6 +30,8 @@ export enum Errors {
BOTS_HIGHEST_ROLE_TOO_LOW = "BOTS_HIGHEST_ROLE_TOO_LOW",
CHANNEL_NOT_IN_GUILD = "CHANNEL_NOT_IN_GUILD",
INVALID_WEBHOOK_NAME = "INVALID_WEBHOOK_NAME",
INVALID_SLASH_NAME = "INVALID_SLASH_NAME",
INVALID_SLASH_DESCRIPTION = "INVALID_SLASH_DESCRIPTION",
INVALID_WEBHOOK_OPTIONS = "INVALID_WEBHOOK_OPTIONS",
CHANNEL_NOT_FOUND = "CHANNEL_NOT_FOUND",
CHANNEL_NOT_TEXT_BASED = "CHANNEL_NOT_TEXT_BASED",

43
src/types/interactions.ts Normal file
View File

@@ -0,0 +1,43 @@
import { MemberCreatePayload } from "./member.ts";
export interface InteractionCommandPayload {
/** id of the interaction */
id: string;
/** the type of interaction */
type: InteractionType;
/** the command data payload */
data?: InteractionData;
/** the guild it was sent from */
guild_id: string;
/** the channel it was sent from */
channel_id: string;
/** guild member data for the invoking user */
member: MemberCreatePayload;
/** a contintuation token for responding to the interaction */
token: string;
}
export enum InteractionType {
/** This type is for ACK on webhook only setup. Discord may send these which require. In a sense its a heartbeat. */
PING = 1,
/** Slash commands */
APPLICATION_COMMAND,
}
export interface InteractionData {
/** the ID of the invoked command */
id: string;
/** the name of the invoked command */
name: string;
/** the params + values from the user */
options: InteractionDataOption[];
}
export interface InteractionDataOption {
/** the name of the parameter */
name: string;
/** the value of the pair. present if there was no more options */
value?: string | number;
/** present if this option is a group or subcommand */
options?: InteractionDataOption[];
}

View File

@@ -104,6 +104,8 @@ export interface EventHandlers {
cachedMember?: Member,
) => unknown;
heartbeat?: () => unknown;
// TODO: FIX THIS
interactionCreate?: (data: unknown) => unknown;
messageCreate?: (message: Message) => unknown;
messageDelete?: (partial: PartialMessage, message?: Message) => unknown;
messageUpdate?: (message: Message, cachedMessage: OldMessage) => unknown;

View File

@@ -13,3 +13,4 @@ export * from "./permission.ts";
export * from "./presence.ts";
export * from "./role.ts";
export * from "./webhook.ts";
export * from "./interactions.ts";

View File

@@ -1,5 +1,6 @@
import { AllowedMentions } from "./channel.ts";
import { UserPayload } from "./guild.ts";
import { InteractionType } from "./interactions.ts";
import { Embed } from "./message.ts";
export interface WebhookPayload {
@@ -66,3 +67,153 @@ export interface EditWebhookMessageOptions {
embeds?: Embed[];
allowed_mentions?: AllowedMentions;
}
export interface CreateSlashCommandOptions {
/** The name of the slash command. */
name: string;
/** The description of the slash command. */
description: String;
/** If a guildID is provided, this will be a GUILD command. If none is provided it will be a GLOBAL command. */
guildID?: string;
/** The options for this command */
options?: SlashCommandOption[];
}
export interface SlashCommand {
/** unique id of the command */
id: string;
/** unique id of the parent application */
application_id: string;
/** 3-32 character name */
name: string;
/** 1-100 character description */
description: string;
/** the parameters for the command */
options?: SlashCommandOption[];
}
export interface SlashCommandOption {
/** The type of option */
type: SlashCommandOptionType;
/** 1-32 character name */
name: string;
/** 1-100 character description*/
description: string;
/** the first `required` option for the user to complete--only one option can be `default` */
default?: boolean;
/** if the parameter is required or optional--default `false`*/
required?: boolean;
/**
* If you specify `choices` for an option, they are the **only** valid values for a user to pick.
* choices for `string` and `int` types for the user to pick from
*/
choices?: SlashCommandOptionChoice[];
/** if the option is a subcommand or subcommand group type, this nested options will be the parameters */
options?: SlashCommandOption[];
}
export interface SlashCommandOptionChoice {
/** The name of the choice */
name: string;
/** The value of the choice */
value: string | number;
}
export enum SlashCommandOptionType {
SUB_COMMAND = 1,
SUB_COMMAND_GROUP = 2,
STRING = 3,
INTEGER = 4,
BOOLEAN = 5,
USER = 6,
CHANNEL = 7,
ROLE = 8,
}
export interface Interaction {
/** The id of the interaction */
id: string;
/** The type of interaction */
type: InteractionType;
/** The command data payload */
data?: SlashCommandInteractionData;
/** The id of the guild it was sent from */
guild_id: string;
/** The id of the channel it was sent from */
channel_id: string;
/** The Payload of the member it was sent from */
member: UserPayload;
/** The token for this interaction */
token: string;
}
export interface SlashCommandInteractionData {
/** The id of the command */
id: string;
/** The name of the command */
name: string;
/** the params and values from the user */
options: SlashCommandInteractionDataOption[];
}
export interface SlashCommandInteractionDataOption {
/** The name of the parammeter */
name: string;
/** The value of the pair */
value?: any;
/** Present if this option is a group or subcommand */
options?: SlashCommandInteractionDataOption[];
}
export interface InteractionResponse {
/** The type of response */
type: InteractionResponseType;
/** The optional response message */
data?: SlashCommandCallbackData;
}
export interface SlashCommandCallbackData {
/** is the response TTS */
tts?: boolean;
/** message content */
content: string;
/** supports up to 10 embeds */
embeds?: Embed[];
/** allowed mentions for the message */
allowed_mentions?: AllowedMentions;
/** acceptable values are message flags */
flags?: number;
}
export enum InteractionResponseType {
/** ACK a `Ping` */
PONG = 1,
/** ACK a command without sending a message, eating the user's input */
ACKNOWLEDGE = 2,
/** respond with a message, eating the user's input */
CHANNEL_MESSAGE = 3,
/** respond with a message, showing the user's input */
CHANNEL_MESSAGE_WITH_SOURCE = 4,
/** ACK a command without sending a message, showing the user's input */
ACK_WITH_SOURCE = 5,
}
export interface EditSlashCommandOptions {
id: string;
guildID?: string;
}
export interface ExecuteSlashCommandOptions {
type: InteractionResponseType;
data: SlashCommandCallbackData;
}
export interface EditSlashResponseOptions extends SlashCommandCallbackData {
/** If this is not provided, it will default to editing the original response. */
messageID?: string;
}
export interface UpsertSlashCommandOptions {
id: string;
guildID?: string;
}

View File

@@ -16,6 +16,7 @@ export interface CacheData {
unavailableGuilds: Collection<string, number>;
presences: Collection<string, PresenceUpdatePayload>;
fetchAllMembersProcessingRequests: Collection<string, Function>;
executedSlashCommands: Collection<string, string>;
}
export const cache: CacheData = {
@@ -27,4 +28,5 @@ export const cache: CacheData = {
unavailableGuilds: new Collection(),
presences: new Collection(),
fetchAllMembersProcessingRequests: new Collection(),
executedSlashCommands: new Collection(),
};

View File

@@ -101,6 +101,28 @@ export const endpoints = {
WEBHOOK_DELETE: (id: string, token: string, messageID: string) =>
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}/messages/${messageID}`,
// Application Endpoints
COMMANDS: (botID: string) =>
`${baseEndpoints.BASE_URL}/applications/${botID}/commands`,
COMMANDS_GUILD: (botID: string, id: string) =>
`${baseEndpoints.BASE_URL}/applications/${botID}/guilds/${id}/commands`,
COMMANDS_ID: (botID: string, id: string) =>
`${baseEndpoints.BASE_URL}/applications/${botID}/commands/${id}`,
COMMANDS_GUILD_ID: (botID: string, id: string, guildID: string) =>
`${baseEndpoints.BASE_URL}/applications/${botID}/guilds/${guildID}/commands/${id}`,
// Interaction Endpoints
INTERACTION_ID_TOKEN: (id: string, token: string) =>
`${baseEndpoints.BASE_URL}/interactions/${id}/${token}/callback`,
INTERACTION_ORIGINAL_ID_TOKEN: (id: string, token: string) =>
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}/messages/@original`,
INTERACTION_ID_TOKEN_MESSAGEID: (
id: string,
token: string,
messageID: string,
) =>
`${baseEndpoints.BASE_URL}/webhooks/${id}/${token}/messages/${messageID}`,
// User endpoints
USER: (id: string) => `${baseEndpoints.BASE_URL}/users/${id}`,
USER_BOT: `${baseEndpoints.BASE_URL}/users/@me`,