From e5bf5cd47cdfc61fd29c95899fae866d22f48144 Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Sat, 6 Mar 2021 13:59:45 -0500 Subject: [PATCH 01/17] feat(handlers/webhook): support ephemeral messages (#602) Co-authored-by: ayntee --- src/api/handlers/webhook.ts | 9 +++++++-- src/types/webhook.ts | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/api/handlers/webhook.ts b/src/api/handlers/webhook.ts index fdad0ec4b..74fda402b 100644 --- a/src/api/handlers/webhook.ts +++ b/src/api/handlers/webhook.ts @@ -6,10 +6,10 @@ import { EditSlashResponseOptions, EditWebhookMessageOptions, Errors, - ExecuteSlashCommandOptions, ExecuteWebhookOptions, MessageCreateOptions, SlashCommand, + SlashCommandResponseOptions, UpsertSlashCommandOptions, UpsertSlashCommandsOptions, WebhookCreateOptions, @@ -448,7 +448,7 @@ export function deleteSlashCommand(id: string, guildID?: string) { export async function executeSlashCommand( id: string, token: string, - options: ExecuteSlashCommandOptions, + options: SlashCommandResponseOptions, ) { // If its already been executed, we need to send a followup response if (cache.executedSlashCommands.has(token)) { @@ -464,6 +464,11 @@ export async function executeSlashCommand( 900000, ); + // If the user wants this as a private message mark it ephemeral + if (options.private) { + options.data.flags = 64; + } + // If no mentions are provided, force disable mentions if (!(options.data.allowed_mentions)) { options.data.allowed_mentions = { parse: [] }; diff --git a/src/types/webhook.ts b/src/types/webhook.ts index d5a680785..225034a86 100644 --- a/src/types/webhook.ts +++ b/src/types/webhook.ts @@ -191,7 +191,7 @@ export interface SlashCommandCallbackData { embeds?: Embed[]; /** allowed mentions for the message */ "allowed_mentions"?: AllowedMentions; - /** acceptable values are message flags */ + /** acceptable values are message flags, set to 64 to make your response ephemeral */ flags?: number; } @@ -224,6 +224,12 @@ export interface ExecuteSlashCommandOptions { data: SlashCommandCallbackData; } +export interface SlashCommandResponseOptions + extends ExecuteSlashCommandOptions { + /** Whether to make this response visible ONLY to the user who used this command. It will also be deleted after some time. */ + private?: boolean; +} + export interface EditSlashResponseOptions extends SlashCommandCallbackData { /** If this is not provided, it will default to editing the original response. */ messageID?: string; From d084c3f3c4d1399640e023f225e825399854074e Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Sat, 6 Mar 2021 14:44:31 -0500 Subject: [PATCH 02/17] fix(handlers/channel): handle max_age and max_uses limit in createInvite() (#597) * fix: handle invalid invite create limits * refactor: cleanup conditonals * chore: clarification Co-authored-by: ayntee --- src/api/handlers/channel.ts | 14 ++++++++++++++ src/types/channel.ts | 9 +++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/api/handlers/channel.ts b/src/api/handlers/channel.ts index e22b410ca..5e1b772d1 100644 --- a/src/api/handlers/channel.ts +++ b/src/api/handlers/channel.ts @@ -321,6 +321,20 @@ export async function createInvite( throw new Error(Errors.MISSING_CREATE_INSTANT_INVITE); } + if (options.max_age && (options.max_age > 604800 || options.max_age < 0)) { + console.log( + `The max age for invite created in ${channelID} was not between 0-604800. Using default values instead.`, + ); + options.max_age = undefined; + } + + if (options.max_uses && (options.max_uses > 100 || options.max_uses < 0)) { + console.log( + `The max uses for invite created in ${channelID} was not between 0-100. Using default values instead.`, + ); + options.max_uses = undefined; + } + const result = await RequestManager.post( endpoints.CHANNEL_INVITES(channelID), options, diff --git a/src/types/channel.ts b/src/types/channel.ts index ad3202a95..3743c2ebc 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -154,11 +154,12 @@ export interface GetMessagesAround extends GetMessages { around: string; } +// TODO: v11 change to camelcase export interface CreateInviteOptions { - /** Duration of invite in seconds before expiry, or 0 for never. Defaults to 86400 (24 hours) */ - "max_age": number; - /** Max number of uses or 0 for unlimited. Default 0 */ - "max_uses": number; + /** Duration of invite in seconds before expiry, or 0 for never. Between 0-604800 (7 days). Defaults to 86400 (24 hours). */ + "max_age"?: number; + /** Max number of uses or 0 for unlimited. Between 0-100. Default 0 */ + "max_uses"?: number; /** Whether this invite only grants temporary membership. */ temporary: boolean; /** If true, don't try to reuse a similar invite (useful for creating many unique one time use invites.) */ From c319ca66cdeaaa28c48bd3d4c8e4472d8acfef54 Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 08:55:18 +0100 Subject: [PATCH 03/17] feat(handlers/message): add options to getReactions (#607) --- src/api/handlers/message.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/api/handlers/message.ts b/src/api/handlers/message.ts index 5cd28d190..24c6579bb 100644 --- a/src/api/handlers/message.ts +++ b/src/api/handlers/message.ts @@ -1,6 +1,7 @@ import { botID } from "../../bot.ts"; import { RequestManager } from "../../rest/request_manager.ts"; import { + DiscordGetReactionsParams, Errors, MessageContent, MessageCreateOptions, @@ -264,9 +265,14 @@ export async function removeReactionEmoji( } /** Get a list of users that reacted with this emoji. */ -export async function getReactions(message: Message, reaction: string) { +export async function getReactions( + message: Message, + reaction: string, + options?: DiscordGetReactionsParams, +) { const result = (await RequestManager.get( endpoints.CHANNEL_MESSAGE_REACTION(message.channelID, message.id, reaction), + options, )) as UserPayload[]; return Promise.all(result.map(async (res) => { From 1c5c90834a6012641244eef15354bd817ef396a9 Mon Sep 17 00:00:00 2001 From: ayntee Date: Sun, 7 Mar 2021 11:59:22 +0400 Subject: [PATCH 04/17] Create CODEOWNERS --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..070268509 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +* @ayntee @Skillz4Killz + +*.ts @ayntee @Skillz4Killz @itohatweb From 9c7340f6240841f69db7b5132b4759a977ec48ce Mon Sep 17 00:00:00 2001 From: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Date: Sun, 7 Mar 2021 09:39:19 -0500 Subject: [PATCH 05/17] fix(handlers/webhook): return Message object instead of raw payload (#611) * fix: change any to message types * Update webhook.ts * Update src/api/handlers/webhook.ts Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com> Co-authored-by: ayntee Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com> --- src/api/handlers/webhook.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/api/handlers/webhook.ts b/src/api/handlers/webhook.ts index 74fda402b..8533ab256 100644 --- a/src/api/handlers/webhook.ts +++ b/src/api/handlers/webhook.ts @@ -252,9 +252,10 @@ export async function editWebhookMessage( const result = await RequestManager.patch( endpoints.WEBHOOK_MESSAGE(webhookID, webhookToken, messageID), { ...options, allowed_mentions: options.allowed_mentions }, - ); + ) as MessageCreateOptions; - return result; + const message = await structures.createMessage(result); + return message; } export async function deleteWebhookMessage( @@ -552,5 +553,11 @@ export async function editSlashResponse( options, ); - return result; + // If the original message was edited, this will not return a message + if (!options.messageID) return result; + + const message = await structures.createMessage( + result as MessageCreateOptions, + ); + return message; } From 30470d8551125f6826cc7ecb19066de64999d630 Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 15:45:53 +0100 Subject: [PATCH 06/17] update types --- src/types/webhook.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types/webhook.ts b/src/types/webhook.ts index 225034a86..573b0117a 100644 --- a/src/types/webhook.ts +++ b/src/types/webhook.ts @@ -237,20 +237,20 @@ export interface EditSlashResponseOptions extends SlashCommandCallbackData { export interface UpsertSlashCommandOptions { /** 3-32 character command name */ - name: string; + name?: string; /** 1-100 character description */ - description: string; + description?: string; /** The parameters for the command */ - options?: SlashCommandOption[]; + options?: SlashCommandOption[] | null; } export interface UpsertSlashCommandsOptions { /** The id of the command */ id: string; /** 3-32 character command name */ - name: string; + name?: string; /** 1-100 character description */ - description: string; + description?: string; /** The parameters for the command */ - options?: SlashCommandOption[]; + options?: SlashCommandOption[] | null; } From b095b995ec6a4833ece60ed4e7cbda0d088c5cdd Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 15:46:08 +0100 Subject: [PATCH 07/17] fix upsertSlashCommand & upsertSlashCommands --- src/api/handlers/webhook.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/api/handlers/webhook.ts b/src/api/handlers/webhook.ts index 74fda402b..4f3eef451 100644 --- a/src/api/handlers/webhook.ts +++ b/src/api/handlers/webhook.ts @@ -336,12 +336,17 @@ export async function upsertSlashCommand( guildID?: string, ) { // Use ... for content length due to unicode characters and js .length handling - if ([...options.name].length < 2 || [...options.name].length > 32) { + if ( + options.name && + ([...options.name].length < 2 || [...options.name].length > 32) + ) { throw new Error(Errors.INVALID_SLASH_NAME); } if ( - [...options.description].length < 1 || [...options.description].length > 100 + options.description && + ([...options.description].length < 1 || + [...options.description].length > 100) ) { throw new Error(Errors.INVALID_SLASH_DESCRIPTION); } @@ -371,12 +376,17 @@ export async function upsertSlashCommands( ) { const data = options.map((option) => { // Use ... for content length due to unicode characters and js .length handling - if ([...option.name].length < 2 || [...option.name].length > 32) { + if ( + option.name && + ([...option.name].length < 2 || [...option.name].length > 32) + ) { throw new Error(Errors.INVALID_SLASH_NAME); } if ( - [...option.description].length < 1 || [...option.description].length > 100 + option.description && + ([...option.description].length < 1 || + [...option.description].length > 100) ) { throw new Error(Errors.INVALID_SLASH_DESCRIPTION); } From 4af6f363f88eb2c2d20aa51aa7d636eeea183457 Mon Sep 17 00:00:00 2001 From: ayntee Date: Sun, 7 Mar 2021 18:48:12 +0400 Subject: [PATCH 08/17] feat(handlers/webhook): test slash command name against ^[\w-]{1,32}$ (#613) * feat(handlers/webhook): test slash command name against ^[\w-]{1,32}$ * Update src/api/handlers/webhook.ts Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com> * Update src/api/handlers/webhook.ts Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com> * Update src/api/handlers/webhook.ts Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com> * Update src/api/handlers/webhook.ts Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com> * move to constants file * idk * idk Co-authored-by: ITOH <72305210+itohatweb@users.noreply.github.com> --- src/api/handlers/webhook.ts | 14 +++++--------- src/util/constants.ts | 2 ++ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/api/handlers/webhook.ts b/src/api/handlers/webhook.ts index 8533ab256..bc181def7 100644 --- a/src/api/handlers/webhook.ts +++ b/src/api/handlers/webhook.ts @@ -17,7 +17,7 @@ import { WebhookPayload, } from "../../types/mod.ts"; import { cache } from "../../util/cache.ts"; -import { endpoints } from "../../util/constants.ts"; +import { endpoints, SLASH_COMMANDS_NAME_REGEX } from "../../util/constants.ts"; import { botHasChannelPermissions } from "../../util/permissions.ts"; import { urlToBase64 } from "../../util/utils.ts"; import { structures } from "../structures/mod.ts"; @@ -282,8 +282,7 @@ export async function deleteWebhookMessage( * Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use. */ export async 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) { + if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { throw new Error(Errors.INVALID_SLASH_NAME); } @@ -336,8 +335,7 @@ export async function upsertSlashCommand( options: UpsertSlashCommandOptions, guildID?: string, ) { - // Use ... for content length due to unicode characters and js .length handling - if ([...options.name].length < 2 || [...options.name].length > 32) { + if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { throw new Error(Errors.INVALID_SLASH_NAME); } @@ -371,8 +369,7 @@ export async function upsertSlashCommands( guildID?: string, ) { const data = options.map((option) => { - // Use ... for content length due to unicode characters and js .length handling - if ([...option.name].length < 2 || [...option.name].length > 32) { + if (!SLASH_COMMANDS_NAME_REGEX.test(option.name)) { throw new Error(Errors.INVALID_SLASH_NAME); } @@ -405,8 +402,7 @@ export async function editSlashCommand( options: EditSlashCommandOptions, guildID?: string, ) { - // Use ... for content length due to unicode characters and js .length handling - if ([...options.name].length < 2 || [...options.name].length > 32) { + if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { throw new Error(Errors.INVALID_SLASH_NAME); } diff --git a/src/util/constants.ts b/src/util/constants.ts index afad4433c..af1d8ef57 100644 --- a/src/util/constants.ts +++ b/src/util/constants.ts @@ -175,3 +175,5 @@ export const endpoints = { // oAuth2 OAUTH2_APPLICATION: `${baseEndpoints.BASE_URL}/oauth2/applications/@me`, }; + +export const SLASH_COMMANDS_NAME_REGEX = /^[\w-]{1,32}$/; From 2865f42f724b3cfa070095ef5693d00dddd11a67 Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 15:51:48 +0100 Subject: [PATCH 09/17] feat(types/embed): add proxy_url to EmbedVideo interface (#616) Co-authored-by: ayntee --- src/types/api/embed.ts | 2 ++ src/types/message.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/types/api/embed.ts b/src/types/api/embed.ts index 2188c3c11..0535d4516 100644 --- a/src/types/api/embed.ts +++ b/src/types/api/embed.ts @@ -56,6 +56,8 @@ export interface DiscordEmbedThumbnail { export interface DiscordEmbedVideo { /** source url of video */ url?: string; + /** a proxied url of the video */ + proxy_url?: string; /** height of video */ height?: number; /** width of video */ diff --git a/src/types/message.ts b/src/types/message.ts index 24f036a95..86f0e4d63 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -99,6 +99,8 @@ export interface EmbedThumbnail { export interface EmbedVideo { /** The source url of video */ url?: string; + /** a proxied url of the video */ + proxy_url?: string; /** The height of the video */ height?: number; /** The width of the video */ From 25858b314433d98241be50018c68ce880df0de32 Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 15:51:59 +0100 Subject: [PATCH 10/17] feat(util): add camelToSnakeCase() & snakeToCamelCase() (#615) * feat(util): add to camel/snake case functions * add tests * hmm * add lint ignore comment Co-authored-by: ayntee --- src/util/utils.ts | 54 ++++++++++++++++++++++++++++++++++ test/utils.test.ts | 73 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 test/utils.test.ts diff --git a/src/util/utils.ts b/src/util/utils.ts index a2febf9b3..1b2bb9b1f 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -60,3 +60,57 @@ export const formatImageURL = ( return `${url}.${format || (url.includes("/a_") ? "gif" : "jpg")}?size=${size}`; }; + +function camelToSnakeCase(text: string) { + return text.replace(/ID|[A-Z]/g, ($1) => { + if ($1 === "ID") return "_id"; + return `_${$1.toLowerCase()}`; + }); +} + +function snakeToCamelCase(text: string) { + return text.replace(/_id|([-_][a-z])/ig, ($1) => { + if ($1 === "_id") return "ID"; + return $1.toUpperCase().replace("_", ""); + }); +} + +function isObject(obj: unknown) { + return obj === Object(obj) && !Array.isArray(obj) && + typeof obj !== "function"; +} +// deno-lint-ignore no-explicit-any +export function camelKeysToSnakeCase(obj: Record) { + if (isObject(obj)) { + // deno-lint-ignore no-explicit-any + const convertedObject: Record = {}; + Object.keys(obj) + .forEach((key) => { + convertedObject[camelToSnakeCase(key)] = camelKeysToSnakeCase( + obj[key], + ); + }); + return convertedObject; + } else if (Array.isArray(obj)) { + obj = obj.map((element) => camelKeysToSnakeCase(element)); + } + return obj; +} + +// deno-lint-ignore no-explicit-any +export function snakeKeysToCamelCase(obj: Record) { + if (isObject(obj)) { + // deno-lint-ignore no-explicit-any + const convertedObject: Record = {}; + Object.keys(obj) + .forEach((key) => { + convertedObject[snakeToCamelCase(key)] = snakeKeysToCamelCase( + obj[key], + ); + }); + return convertedObject; + } else if (Array.isArray(obj)) { + obj = obj.map((element) => snakeKeysToCamelCase(element)); + } + return obj; +} diff --git a/test/utils.test.ts b/test/utils.test.ts new file mode 100644 index 000000000..d2df243c3 --- /dev/null +++ b/test/utils.test.ts @@ -0,0 +1,73 @@ +import { camelKeysToSnakeCase, snakeKeysToCamelCase } from "../mod.ts"; +import { assertEquals } from "./deps.ts"; + +const testSnakeObject = { + // deno-lint-ignore camelcase + hello_world: "hello_world", + // deno-lint-ignore camelcase + the_universe: { + blue_planet: { + water: "is_blue", + dirt: "isDirty", + }, + moon: { + earth_moon: { + is_round: true, + }, + other_moon: { + is_round: 0, + }, + }, + arrays: ["one_two", { moo_cow: { boo: true } }], + test_the_id: "123123123123", + }, +}; + +const testCamelObject = { + helloWorld: "hello_world", + theUniverse: { + bluePlanet: { + water: "is_blue", + dirt: "isDirty", + }, + moon: { + earthMoon: { + isRound: true, + }, + otherMoon: { + isRound: 0, + }, + }, + arrays: ["one_two", { mooCow: { boo: true } }], + testTheID: "123123123123", + }, +}; + +const someOther = { + helloWorld: 1, +}; + +const someElseOther = { + // deno-lint-ignore camelcase + hello_world: 1, +}; + +Deno.test({ + name: "[utils] snakeKeysToCamelCase: assert convertion", + fn() { + const result = snakeKeysToCamelCase(testSnakeObject); + assertEquals(result, testCamelObject); + const resultTwo = snakeKeysToCamelCase(someOther); + assertEquals(resultTwo, someOther); + }, +}); + +Deno.test({ + name: "[utils] camelKeysToSnakeCase: assert convertion", + fn() { + const result = camelKeysToSnakeCase(testCamelObject); + assertEquals(result, testSnakeObject); + const resultTwo = camelKeysToSnakeCase(someElseOther); + assertEquals(resultTwo, someElseOther); + }, +}); From 4e00c31545362ea6a06ebfdc4c6463eadd588c7b Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 16:00:50 +0100 Subject: [PATCH 11/17] add extends --- src/types/webhook.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/types/webhook.ts b/src/types/webhook.ts index 573b0117a..754d682c3 100644 --- a/src/types/webhook.ts +++ b/src/types/webhook.ts @@ -244,13 +244,7 @@ export interface UpsertSlashCommandOptions { options?: SlashCommandOption[] | null; } -export interface UpsertSlashCommandsOptions { +export interface UpsertSlashCommandsOptions extends UpsertSlashCommandOptions { /** The id of the command */ id: string; - /** 3-32 character command name */ - name?: string; - /** 1-100 character description */ - description?: string; - /** The parameters for the command */ - options?: SlashCommandOption[] | null; } From dce5f6f90e008940c42109e502606426c92df86c Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 16:02:10 +0100 Subject: [PATCH 12/17] update jsdoc --- src/types/webhook.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/webhook.ts b/src/types/webhook.ts index 754d682c3..c0ff25748 100644 --- a/src/types/webhook.ts +++ b/src/types/webhook.ts @@ -236,11 +236,11 @@ export interface EditSlashResponseOptions extends SlashCommandCallbackData { } export interface UpsertSlashCommandOptions { - /** 3-32 character command name */ + /** 1-32 character name matching ^[\w-]{1,32}$ */ name?: string; /** 1-100 character description */ description?: string; - /** The parameters for the command */ + /** the parameters for the command */ options?: SlashCommandOption[] | null; } From b0ebd5627d9697a7970ab356d147b22236fd28e3 Mon Sep 17 00:00:00 2001 From: ayntee Date: Sun, 7 Mar 2021 19:02:30 +0400 Subject: [PATCH 13/17] Update src/types/webhook.ts --- src/types/webhook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/webhook.ts b/src/types/webhook.ts index c0ff25748..bc52309f9 100644 --- a/src/types/webhook.ts +++ b/src/types/webhook.ts @@ -240,7 +240,7 @@ export interface UpsertSlashCommandOptions { name?: string; /** 1-100 character description */ description?: string; - /** the parameters for the command */ + /** The parameters for the command */ options?: SlashCommandOption[] | null; } From b511a0d39ff1f0126c969c7e14b95beb598c2e38 Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 16:21:58 +0100 Subject: [PATCH 14/17] fix(types/webhook): update interaction response types (#610) * add the changes from dc * Update src/types/webhook.ts * Update src/interactions/types/interactions.ts Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Co-authored-by: ayntee --- src/interactions/types/interactions.ts | 10 +++------- src/types/webhook.ts | 10 +++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/interactions/types/interactions.ts b/src/interactions/types/interactions.ts index 295015e34..e9d291a3c 100644 --- a/src/interactions/types/interactions.ts +++ b/src/interactions/types/interactions.ts @@ -66,12 +66,8 @@ export enum InteractionType { 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 */ + /** 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, + /** ACK an interaction and edit to a response later, the user sees a loading state */ + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, } diff --git a/src/types/webhook.ts b/src/types/webhook.ts index bc52309f9..925632135 100644 --- a/src/types/webhook.ts +++ b/src/types/webhook.ts @@ -198,14 +198,10 @@ export interface SlashCommandCallbackData { 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 */ + /** 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, + /** ACK an interaction and edit to a response later, the user sees a loading state */ + DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE, } // TODO: remove this interface for v11 From bbe3c636f11b081bcbde355acbe8f100d828db7e Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 16:32:15 +0100 Subject: [PATCH 15/17] feat(types/interaction): add "resolved" field to DiscordInteractionCommand (#603) * fix(types): DiscordMember not extending DiscordBaseMember * add(types): DiscordInteractionDataResolved * add(types): resolved field to DiscordInteractionData * fix(types): DiscordInteractionData options is optional * Apply suggestions from code review Co-authored-by: ayntee Co-authored-by: ayntee --- src/types/api/interaction.ts | 25 +++++++++++++++++++++++-- src/types/api/member.ts | 2 +- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/types/api/interaction.ts b/src/types/api/interaction.ts index d9cb62f7c..993409fc9 100644 --- a/src/types/api/interaction.ts +++ b/src/types/api/interaction.ts @@ -1,4 +1,9 @@ -import { DiscordMember } from "./mod.ts"; +import { + DiscordChannel, + DiscordMember, + DiscordRole, + DiscordUser, +} from "./mod.ts"; export interface DiscordInteractionCommand { /** id of the interaction */ @@ -31,8 +36,24 @@ export interface DiscordInteractionData { id: string; /** the name of the invoked command */ name: string; + /** converted users + roles + channels */ + resolved?: DiscordApplicationCommandInteractionDataResolved; /** the params + values from the user */ - options: DiscordInteractionDataOption[]; + options?: DiscordInteractionDataOption[]; +} + +export interface DiscordApplicationCommandInteractionDataResolved { + /** the IDs and User objects */ + users?: Record; + /** the IDs and partial Member objects */ + members?: Record>; + /** the IDs and Role objects */ + roles?: Record; + /** the IDs and partial Channel objects */ + channels?: Record< + string, + Pick + >; } export interface DiscordInteractionDataOption { diff --git a/src/types/api/member.ts b/src/types/api/member.ts index e17403cb0..3eaf32740 100644 --- a/src/types/api/member.ts +++ b/src/types/api/member.ts @@ -83,7 +83,7 @@ export interface DiscordBaseMember { } /** https://discord.com/developers/docs/resources/guild#guild-member-object-guild-member-structure */ -export interface DiscordMember { +export interface DiscordMember extends DiscordBaseMember { /** the user this guild member represents */ user?: DiscordUser; } From e21a86d497668d9952d9c5fb8f5fa42d8734f675 Mon Sep 17 00:00:00 2001 From: ITOH <72305210+itohatweb@users.noreply.github.com> Date: Sun, 7 Mar 2021 17:28:23 +0100 Subject: [PATCH 16/17] fix(handlers/webhook): handle slash commands limitations (#618) * add more errors * add the validation functions * spread --- src/api/handlers/webhook.ts | 119 +++++++++++++++++++++++++----------- src/types/errors.ts | 2 + 2 files changed, 85 insertions(+), 36 deletions(-) diff --git a/src/api/handlers/webhook.ts b/src/api/handlers/webhook.ts index 15f9039d1..1d2b56d8e 100644 --- a/src/api/handlers/webhook.ts +++ b/src/api/handlers/webhook.ts @@ -9,6 +9,9 @@ import { ExecuteWebhookOptions, MessageCreateOptions, SlashCommand, + SlashCommandOption, + SlashCommandOptionChoice, + SlashCommandOptionType, SlashCommandResponseOptions, UpsertSlashCommandOptions, UpsertSlashCommandsOptions, @@ -270,6 +273,82 @@ export async function deleteWebhookMessage( return result; } +function validateSlashOptionChoices( + choices: SlashCommandOptionChoice[], + optionType: SlashCommandOptionType, +) { + for (const choice of choices) { + if ([...choice.name].length < 1 || [...choice.name].length > 100) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + + if ( + (optionType === SlashCommandOptionType.STRING && + (typeof choice.value !== "string" || choice.value.length < 1 || + choice.value.length > 100)) || + (optionType === SlashCommandOptionType.INTEGER && + typeof choice.value !== "number") + ) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + } +} + +function validateSlashOptions(options: SlashCommandOption[]) { + for (const option of options) { + if ( + (option.choices?.length && option.choices.length > 25) || + option.type !== SlashCommandOptionType.STRING && + option.type !== SlashCommandOptionType.INTEGER + ) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + + if ( + ([...option.name].length < 1 || [...option.name].length > 32) || + ([...option.description].length < 1 || + [...option.description].length > 100) + ) { + throw new Error(Errors.INVALID_SLASH_OPTIONS_CHOICES); + } + + if (option.choices) { + validateSlashOptionChoices(option.choices, option.type); + } + } +} + +function validateSlashCommands( + commands: UpsertSlashCommandOptions[], + create = false, +) { + for (const command of commands) { + if ( + (command.name && !SLASH_COMMANDS_NAME_REGEX.test(command.name)) || + (create && !command.name) + ) { + throw new Error(Errors.INVALID_SLASH_NAME); + } + + if ( + (command.description && + ([...command.description].length < 1 || + [...command.description].length > 100)) || + (create && !command.description) + ) { + throw new Error(Errors.INVALID_SLASH_DESCRIPTION); + } + + if (command.options?.length) { + if (command.options.length > 25) { + throw new Error(Errors.INVALID_SLASH_OPTIONS); + } + + validateSlashOptions(command.options); + } + } +} + /** * 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: * @@ -282,15 +361,7 @@ export async function deleteWebhookMessage( * Guild commands update **instantly**. We recommend you use guild commands for quick testing, and global commands when they're ready for public use. */ export async function createSlashCommand(options: CreateSlashCommandOptions) { - if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - [...options.description].length < 1 || [...options.description].length > 100 - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } + validateSlashCommands([options], true); const result = await RequestManager.post( options.guildID @@ -335,17 +406,7 @@ export async function upsertSlashCommand( options: UpsertSlashCommandOptions, guildID?: string, ) { - if (options.name && !SLASH_COMMANDS_NAME_REGEX.test(options.name)) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - options.description && - ([...options.description].length < 1 || - [...options.description].length > 100) - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } + validateSlashCommands([options]); const result = await RequestManager.patch( guildID @@ -370,27 +431,13 @@ export async function upsertSlashCommands( options: UpsertSlashCommandsOptions[], guildID?: string, ) { - const data = options.map((option) => { - if (option.name && !SLASH_COMMANDS_NAME_REGEX.test(option.name)) { - throw new Error(Errors.INVALID_SLASH_NAME); - } - - if ( - option.description && - ([...option.description].length < 1 || - [...option.description].length > 100) - ) { - throw new Error(Errors.INVALID_SLASH_DESCRIPTION); - } - - return option; - }); + validateSlashCommands(options); const result = await RequestManager.put( guildID ? endpoints.COMMANDS_GUILD(applicationID, guildID) : endpoints.COMMANDS(applicationID), - data, + options, ); return result; diff --git a/src/types/errors.ts b/src/types/errors.ts index cc64e5b3c..f3895022a 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -20,6 +20,8 @@ export enum Errors { // Interaction Errors INVALID_SLASH_DESCRIPTION = "INVALID_SLASH_DESCRIPTION", INVALID_SLASH_NAME = "INVALID_SLASH_NAME", + INVALID_SLASH_OPTIONS = "INVALID_SLASH_OPTIONS", + INVALID_SLASH_OPTIONS_CHOICES = "INVALID_SLASH_OPTIONS_CHOICES", // Webhook Errors INVALID_WEBHOOK_NAME = "INVALID_WEBHOOK_NAME", INVALID_WEBHOOK_OPTIONS = "INVALID_WEBHOOK_OPTIONS", From 537d508cfa1da7f5f71863c5722a023869e08e20 Mon Sep 17 00:00:00 2001 From: ayntee Date: Sun, 7 Mar 2021 21:30:33 +0400 Subject: [PATCH 17/17] feat(types/guild): add computePruneCount to PruneOptions (#605) * fix(handlers/guild): rewrite and rename pruneMembers() * remove breaking change * Update src/api/handlers/guild.ts * Update src/types/guild.ts * Update src/types/guild.ts * Update src/types/guild.ts * Update src/api/handlers/guild.ts * Update src/api/handlers/guild.ts * Update src/api/handlers/guild.ts * Update src/types/guild.ts * Update src/types/guild.ts * Update src/types/guild.ts * Update src/api/handlers/guild.ts * Update src/api/handlers/guild.ts * Update src/api/handlers/guild.ts * fmt * fixes * Update src/types/guild.ts Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> * fmt Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> --- src/api/handlers/guild.ts | 31 +++++++++++++++++++++--------- src/types/guild.ts | 40 +++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/api/handlers/guild.ts b/src/api/handlers/guild.ts index b59c2258b..9aff87160 100644 --- a/src/api/handlers/guild.ts +++ b/src/api/handlers/guild.ts @@ -529,9 +529,11 @@ export async function swapRoles(guildID: string, rolePositons: PositionSwap) { } /** Check how many members would be removed from the server in a prune operation. Requires the KICK_MEMBERS permission */ -export async function getPruneCount(guildID: string, options: PruneOptions) { - if (options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); - if (options.days > 30) throw new Error(Errors.PRUNE_MAX_DAYS); +export async function getPruneCount(guildID: string, options?: PruneOptions) { + if (options?.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); + if (options?.days && options.days > 30) { + throw new Error(Errors.PRUNE_MAX_DAYS); + } const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); if (!hasPerm) { @@ -540,16 +542,23 @@ export async function getPruneCount(guildID: string, options: PruneOptions) { const result = await RequestManager.get( endpoints.GUILD_PRUNE(guildID), - { ...options, include_roles: options.roles.join(",") }, + { ...options, include_roles: options?.roles?.join(",") }, ) as PrunePayload; return result.pruned; } -/** Begin pruning all members in the given time period */ -export async function pruneMembers(guildID: string, options: PruneOptions) { - if (options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); - if (options.days > 30) throw new Error(Errors.PRUNE_MAX_DAYS); +/** + * Begin a prune operation. Requires the KICK_MEMBERS permission. Returns an object with one 'pruned' key indicating the number of members that were removed in the prune operation. For large guilds it's recommended to set the computePruneCount option to false, forcing 'pruned' to null. Fires multiple Guild Member Remove Gateway events. + * + * By default, prune will not remove users with roles. You can optionally include specific roles in your prune by providing the roles (resolved to include_roles internally) parameter. Any inactive user that has a subset of the provided role(s) will be included in the prune and users with additional roles will not. + */ +export async function pruneMembers( + guildID: string, + { roles, computePruneCount, ...options }: PruneOptions, +) { + if (options.days && options.days < 1) throw new Error(Errors.PRUNE_MIN_DAYS); + if (options.days && options.days > 30) throw new Error(Errors.PRUNE_MAX_DAYS); const hasPerm = await botHasPermission(guildID, ["KICK_MEMBERS"]); if (!hasPerm) { @@ -558,7 +567,11 @@ export async function pruneMembers(guildID: string, options: PruneOptions) { const result = await RequestManager.post( endpoints.GUILD_PRUNE(guildID), - { ...options, include_roles: options.roles.join(",") }, + { + ...options, + compute_prune_count: computePruneCount, + include_roles: roles, + }, ); return result; diff --git a/src/types/guild.ts b/src/types/guild.ts index 74f45390d..e31b0147f 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -547,10 +547,42 @@ export interface PrunePayload { } export interface PruneOptions { - /** number of days to count prune for (1 - 30). Defaults to 7 days. */ - days: number; - /** Include members with these role ids */ - roles: string[]; + /** Number of days to prune (1-30). Default: 7 */ + days?: + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | 7 + | 8 + | 9 + | 10 + | 11 + | 12 + | 13 + | 14 + | 15 + | 16 + | 17 + | 18 + | 19 + | 20 + | 21 + | 22 + | 23 + | 24 + | 25 + | 26 + | 27 + | 28 + | 29 + | 30; + /** Whether 'pruned' is returned, discouraged for large guilds. Default: true */ + computePruneCount?: boolean; + /** Role(s) to include */ + roles?: string[]; } export interface VoiceState {