feat(helpers,types)!: slash command localization (#2129)

* feat(helpers,types): slash command localization
Discord has documented the slash command localization feature now.
This adds the full functionality for those to Discordeno.

Additionally a `Locales` has also been added to allow for better typing.

Reference: https://github.com/discord/discord-api-docs/pull/4653

* suggestion

* better locales type

* f

* fix serializing

* stupid direct pushes

* b
This commit is contained in:
ITOH
2022-03-27 17:02:27 +02:00
committed by GitHub
parent 23ebfac286
commit 34a358f8bf
8 changed files with 134 additions and 21 deletions
+10
View File
@@ -69,6 +69,7 @@ import { transformStageInstance } from "./transformers/stageInstance.ts";
import { StickerPack, transformSticker, transformStickerPack } from "./transformers/sticker.ts";
import { GetGatewayBot, transformGatewayBot } from "./transformers/gatewayBot.ts";
import {
DiscordApplicationCommandOptionChoice,
DiscordEmoji,
DiscordGatewayPayload,
DiscordInteractionDataOption,
@@ -127,6 +128,10 @@ import { VoiceRegions } from "./transformers/voiceRegion.ts";
import { GuildWidget } from "./transformers/widget.ts";
import { StageInstance } from "./transformers/stageInstance.ts";
import { Sticker } from "./transformers/sticker.ts";
import {
ApplicationCommandOptionChoice,
transformApplicationCommandOptionChoice,
} from "./transformers/applicationCommandOptionChoice.ts";
import { transformEmbedToDiscordEmbed } from "./transformers/reverse/embed.ts";
import { transformComponentToDiscordComponent } from "./transformers/reverse/component.ts";
@@ -421,6 +426,10 @@ export interface Transformers {
stageInstance: (bot: Bot, payload: DiscordStageInstance) => StageInstance;
sticker: (bot: Bot, payload: DiscordSticker) => Sticker;
stickerPack: (bot: Bot, payload: DiscordStickerPack) => StickerPack;
applicationCommandOptionChoice: (
bot: Bot,
payload: DiscordApplicationCommandOptionChoice,
) => ApplicationCommandOptionChoice;
template: (bot: Bot, payload: DiscordTemplate) => Template;
}
@@ -467,6 +476,7 @@ export function createTransformers(options: Partial<Transformers>) {
sticker: options.sticker || transformSticker,
stickerPack: options.stickerPack || transformStickerPack,
gatewayBot: options.gatewayBot || transformGatewayBot,
applicationCommandOptionChoice: options.applicationCommandOptionChoice || transformApplicationCommandOptionChoice,
template: options.template || transformTemplate,
};
}
@@ -1,5 +1,5 @@
import type { Bot } from "../../../bot.ts";
import { ApplicationCommandOption, ApplicationCommandTypes } from "../../../mod.ts";
import { ApplicationCommandOption, ApplicationCommandTypes, Localization } from "../../../mod.ts";
import { DiscordApplicationCommand, DiscordApplicationCommandOption } from "../../../types/discord.ts";
/**
@@ -24,9 +24,13 @@ export async function createApplicationCommand(
guildId
? bot.constants.endpoints.COMMANDS_GUILD(bot.applicationId, guildId)
: bot.constants.endpoints.COMMANDS(bot.applicationId),
isContextApplicationCommand(options) ? { name: options.name, type: options.type } : {
isContextApplicationCommand(options)
? { name: options.name, name_localizations: options.nameLocalizations, type: options.type }
: {
name: options.name,
name_localizations: options.nameLocalizations,
description: options.description,
description_localizations: options.descriptionLocalizations,
type: options.type,
options: options.options ? makeOptionsForCommand(options.options) : undefined,
},
@@ -39,9 +43,15 @@ export function makeOptionsForCommand(options: ApplicationCommandOption[]): Disc
return options.map((option) => ({
type: option.type,
name: option.name,
name_localizations: option.nameLocalizations,
description: option.description,
description_localizations: option.descriptionLocalizations,
required: option.required,
choices: option.choices,
choices: option.choices?.map((choice) => ({
name: choice.name,
name_localizations: choice.nameLocalizations,
value: choice.value,
})),
options: option.options ? makeOptionsForCommand(option.options) : undefined,
channel_types: option.channelTypes,
autocomplete: option.autocomplete,
@@ -50,12 +60,16 @@ export function makeOptionsForCommand(options: ApplicationCommandOption[]): Disc
}));
}
/** https://discord.com/developers/docs/interactions/slash-commands#create-global-application-command-json-params */
/** https://discord.com/developers/docs/interactions/application-commands#endpoints-json-params */
export interface CreateApplicationCommand {
/** 1-31 character name matching lowercase `^[\w-]{1,32}$` */
name: string;
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
nameLocalizations?: Localization;
/** 1-100 character description */
description: string;
/** Localization object for the `description` field. Values follow the same restrictions as `description` */
descriptionLocalizations?: Localization;
/** The type of the command */
type?: ApplicationCommandTypes;
/** The parameters for the command */
@@ -64,10 +78,12 @@ export interface CreateApplicationCommand {
defaultPermission?: boolean;
}
/** https://discord.com/developers/docs/interactions/slash-commands#create-global-application-command-json-params */
/** https://discord.com/developers/docs/interactions/application-commands#endpoints-json-params */
export interface CreateContextApplicationCommand {
/** 1-31 character name matching lowercase `^[\w-]{1,32}$` */
name: string;
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
nameLocalizations?: Localization;
/** The type of the command */
type: ApplicationCommandTypes.Message | ApplicationCommandTypes.User;
}
@@ -2,14 +2,30 @@ import type { Bot } from "../../../bot.ts";
import { DiscordApplicationCommand } from "../../../types/discord.ts";
/** Fetches the global command for the given Id. If a guildId is provided, the guild command will be fetched. */
export async function getApplicationCommand(bot: Bot, commandId: bigint, guildId?: bigint) {
export async function getApplicationCommand(bot: Bot, commandId: bigint, options?: GetApplicationCommand) {
let url = `${
options?.guildId
? bot.constants.endpoints.COMMANDS_GUILD_ID(bot.applicationId, options.guildId, commandId)
: bot.constants.endpoints.COMMANDS_ID(bot.applicationId, commandId)
}?`;
if (options?.withLocalizations !== undefined) {
url += `with_localizations=${options.withLocalizations}`;
}
const result = await bot.rest.runMethod<DiscordApplicationCommand>(
bot.rest,
"get",
guildId
? bot.constants.endpoints.COMMANDS_GUILD_ID(bot.applicationId, guildId, commandId)
: bot.constants.endpoints.COMMANDS_ID(bot.applicationId, commandId),
url,
);
return bot.transformers.applicationCommand(bot, result);
}
/** https://discord.com/developers/docs/interactions/application-commands#endpoints-query-string-params */
export interface GetApplicationCommand {
/** Guild ID of the guild in which the command is available if it is a guild-specific command */
guildId?: bigint;
/** Whether to include full localization object (`name_localizations` and `description_localizations`) in the returned objects, instead of the `name_localized` and `description_localized` fields. Default false */
withLocalizations?: boolean;
}
+2
View File
@@ -8,7 +8,9 @@ export function transformApplicationCommand(bot: Bot, payload: DiscordApplicatio
applicationId: bot.transformers.snowflake(payload.application_id),
guildId: payload.guild_id ? bot.transformers.snowflake(payload.guild_id) : undefined,
name: payload.name,
nameLocalizations: payload.name_localizations,
description: payload.description,
descriptionLocalizations: payload.description_localizations,
defaultPermission: payload.default_permission ?? false,
type: payload.type,
version: payload.version,
+10 -3
View File
@@ -1,7 +1,8 @@
import { Bot } from "../bot.ts";
import { DiscordApplicationCommandOption, DiscordApplicationCommandOptionChoice } from "../types/discord.ts";
import { ApplicationCommandOptionTypes, ChannelTypes } from "../types/shared.ts";
import { ApplicationCommandOptionTypes, ChannelTypes, Localization } from "../types/shared.ts";
import { Optionalize } from "../types/shared.ts";
import { ApplicationCommandOptionChoice } from "./applicationCommandOptionChoice.ts";
export function transformApplicationCommandOption(
bot: Bot,
@@ -10,9 +11,11 @@ export function transformApplicationCommandOption(
return {
type: payload.type,
name: payload.name,
nameLocalizations: payload.name_localizations,
description: payload.description,
descriptionLocalizations: payload.description_localizations,
required: payload.required ?? false,
choices: payload.choices,
choices: payload.choices?.map((choice) => bot.transformers.applicationCommandOptionChoice(bot, choice)),
autocomplete: payload.autocomplete,
channelTypes: payload.channel_types,
minValue: payload.min_value,
@@ -29,12 +32,16 @@ export interface ApplicationCommandOption {
type: ApplicationCommandOptionTypes;
/** 1-32 character name matching lowercase `^[\w-]{1,32}$` */
name: string;
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
nameLocalizations?: Localization;
/** 1-100 character description */
description: string;
/** Localization object for the `description` field. Values follow the same restrictions as `description` */
descriptionLocalizations?: Localization;
/** If the parameter is required or optional--default `false` */
required: boolean;
/** Choices for `string` and `int` types for the user to pick from */
choices?: DiscordApplicationCommandOptionChoice[];
choices?: ApplicationCommandOptionChoice[];
/** If the option is a subcommand or subcommand group type, this nested options will be the parameters */
options?: ApplicationCommandOption[];
/** if autocomplete interactions are enabled for this `String`, `Integer`, or `Number` type option */
@@ -0,0 +1,15 @@
import { Bot } from "../bot.ts";
import { DiscordApplicationCommandOptionChoice } from "../types/discord.ts";
import { Camelize, Localization, Optionalize } from "../types/shared.ts";
export function transformApplicationCommandOptionChoice(bot: Bot, payload: DiscordApplicationCommandOptionChoice) {
const applicationCommandChoice = {
name: payload.name,
nameLocalizations: payload.name_localizations,
value: payload.value,
};
return applicationCommandChoice as Optionalize<typeof applicationCommandChoice>;
}
export interface ApplicationCommandOptionChoice extends ReturnType<typeof transformApplicationCommandOptionChoice> {}
+16 -4
View File
@@ -16,6 +16,8 @@ import {
GuildNsfwLevel,
IntegrationExpireBehaviors,
InteractionTypes,
Locales,
Localization,
MessageActivityTypes,
MessageComponentTypes,
MessageTypes,
@@ -1633,7 +1635,7 @@ export interface DiscordInviteStageInstance {
topic: string;
}
/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommand */
/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-structure */
export interface DiscordApplicationCommand {
/** Unique id of the command */
id: string;
@@ -1643,8 +1645,12 @@ export interface DiscordApplicationCommand {
guild_id?: string;
/** 1-32 character name matching */
name: string;
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
name_localizations?: Localization;
/** 1-100 character description */
description?: string;
description: string;
/** Localization object for the `description` field. Values follow the same restrictions as `description` */
description_localizations?: Localization;
/** The parameters for the command */
options?: DiscordApplicationCommandOption[];
/** Whether the command is enbaled by default when the app is added to a guild */
@@ -1655,14 +1661,18 @@ export interface DiscordApplicationCommand {
version: string;
}
/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoption */
/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-structure */
export interface DiscordApplicationCommandOption {
/** Value of Application Command Option Type */
type: ApplicationCommandOptionTypes;
/** 1-32 character name matching lowercase `^[\w-]{1,32}$` */
name: string;
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
name_localizations?: Localization;
/** 1-100 character description */
description: string;
/** Localization object for the `description` field. Values follow the same restrictions as `description` */
description_localizations?: Localization;
/** If the parameter is required or optional--default `false` */
required?: boolean;
/** Choices for `string` and `int` types for the user to pick from */
@@ -1679,10 +1689,12 @@ export interface DiscordApplicationCommandOption {
max_value?: number;
}
/** https://discord.com/developers/docs/interactions/slash-commands#applicationcommandoptionchoice */
/** https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-option-choice-structure */
export interface DiscordApplicationCommandOptionChoice {
/** 1-100 character choice name */
name: string;
/** Localization object for the `name` field. Values follow the same restrictions as `name` */
name_localizations?: Localization;
/** Value of the choice, up to 100 characters if string */
value: string | number;
}
+35
View File
@@ -1240,6 +1240,41 @@ export enum Errors {
YOU_CAN_NOT_DM_THE_BOT_ITSELF = "YOU_CAN_NOT_DM_THE_BOT_ITSELF",
}
export enum Locales {
Danish = "da",
German = "de",
EnglishUk = "en-GB",
EnglishUs = "en-US",
Spanish = "es-ES",
French = "fr",
Croatian = "hr",
Italian = "it",
Lithuanian = "lt",
Hungarian = "hu",
Dutch = "nl",
Norwegian = "no",
Polish = "pl",
PortugueseBrazilian = "pt-BR",
RomanianRomania = "ro",
Finnish = "fi",
Swedish = "sv-SE",
Vietnamese = "vi",
Turkish = "tr",
Czech = "cs",
Greek = "el",
Bulgarian = "bg",
Russian = "ru",
Ukrainian = "uk",
Hindi = "hi",
Thai = "th",
ChineseChina = "zh-CN",
Japanese = "ja",
ChineseTaiwan = "zh-TW",
Korean = "ko",
}
export type Localization = Partial<Record<Locales, string>>;
// UTILS
export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];