diff --git a/.github/workflows/local_tests.yml b/.github/workflows/local_tests.yml index 3b5f4387c..6dafe8c14 100644 --- a/.github/workflows/local_tests.yml +++ b/.github/workflows/local_tests.yml @@ -9,6 +9,7 @@ on: - "rest/**" - "tests/**" - "transformers/**" + - "template/**" - "types/**" - "util/**" - "gateway/**" @@ -20,6 +21,7 @@ on: - "rest/**" - "tests/**" - "transformers/**" + - "template/**" - "types/**" - "util/**" - "gateway/**" @@ -37,5 +39,7 @@ jobs: deno-version: ${{ matrix.deno }} - name: Cache dependencies run: deno cache mod.ts + - name: Cache Templates + run: deno cache template/beginner/mod.ts template/bigbot/src/bot/mod.ts template/bigbot/src/gateway/mod.ts template/bigbot/src/rest/mod.ts template/minimal/mod.ts - name: Run Local tests that don't need Discord's API run: deno test -A tests/local.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6f57eaa23..e487d06e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,9 +9,10 @@ on: - "rest/**" - "tests/**" - "transformers/**" + - "template/**" - "types/**" - "util/**" - - "ws/**" + - "gateway/**" jobs: test: @@ -27,6 +28,8 @@ jobs: deno-version: ${{ matrix.deno }} - name: Cache dependencies run: deno cache mod.ts + - name: Cache Templates + run: deno cache template/beginner/mod.ts template/bigbot/src/bot/mod.ts template/bigbot/src/gateway/mod.ts template/bigbot/src/rest/mod.ts template/minimal/mod.ts - name: Run test script for maintainers if: ${{ github.actor == 'Skillz4Killz' || github.actor == 'itohatweb' }} run: deno test --unstable --coverage=coverage -A tests/mod.ts diff --git a/bot.ts b/bot.ts index d2d4ff274..95e27c40c 100644 --- a/bot.ts +++ b/bot.ts @@ -8,11 +8,13 @@ import { Message, Role, ScheduledEvent, + Template, transformChannel, transformGuild, transformMember, transformMessage, transformRole, + transformTemplate, transformUser, transformVoiceState, User, @@ -67,6 +69,7 @@ import { DiscordInteractionDataOption, DiscordReady, DiscordStickerPack, + DiscordTemplate, } from "./types/discord.ts"; import { Errors, GatewayDispatchEventNames, GatewayIntents } from "./types/shared.ts"; @@ -411,6 +414,7 @@ export interface Transformers { stageInstance: (bot: Bot, payload: DiscordStageInstance) => StageInstance; sticker: (bot: Bot, payload: DiscordSticker) => Sticker; stickerPack: (bot: Bot, payload: DiscordStickerPack) => StickerPack; + template: (bot: Bot, payload: DiscordTemplate) => Template; } export function createTransformers(options: Partial) { @@ -451,6 +455,7 @@ export function createTransformers(options: Partial) { sticker: options.sticker || transformSticker, stickerPack: options.stickerPack || transformStickerPack, gatewayBot: options.gatewayBot || transformGatewayBot, + template: options.template || transformTemplate, }; } diff --git a/helpers/roles/modifyRolePositions.ts b/helpers/roles/modifyRolePositions.ts index 5b43997a5..a2face3e7 100644 --- a/helpers/roles/modifyRolePositions.ts +++ b/helpers/roles/modifyRolePositions.ts @@ -23,4 +23,3 @@ export interface ModifyRolePositions { /** The sorting position for the role. */ position?: number | null; } - diff --git a/helpers/templates/createGuildTemplate.ts b/helpers/templates/createGuildTemplate.ts index 21d04b091..dbc8770a0 100644 --- a/helpers/templates/createGuildTemplate.ts +++ b/helpers/templates/createGuildTemplate.ts @@ -1,10 +1,8 @@ import type { Bot } from "../../bot.ts"; -import { Guild } from "../../transformers/guild.ts"; -import { User } from "../../transformers/member.ts"; import { DiscordTemplate } from "../../types/discord.ts"; /** Creates a template for the guild. Requires the `MANAGE_GUILD` permission. */ -export async function createGuildTemplate(bot: Bot, guildId: bigint, data: Template) { +export async function createGuildTemplate(bot: Bot, guildId: bigint, data: CreateTemplate) { if (data.name.length < 1 || data.name.length > 100) { throw new Error("The name can only be in between 1-100 characters."); } @@ -13,42 +11,17 @@ export async function createGuildTemplate(bot: Bot, guildId: bigint, data: Templ throw new Error("The description can only be in between 0-120 characters."); } - return await bot.rest.runMethod(bot.rest, "post", bot.constants.endpoints.GUILD_TEMPLATES(guildId), { - code: data.code, - name: data.name, - description: data.description, - usage_count: data.usageCount, - creator_id: data.creatorId, - creator: data.creator, - created_at: data.createdAt, - updated_at: data.updatedAt, - source_guild_id: data.sourceGuildId, - serialized_source_guild: data.serializedSourceGuild, - is_dirty: data.isDirty, - }); + return await bot.rest.runMethod( + bot.rest, + "post", + bot.constants.endpoints.GUILD_TEMPLATES(guildId), + data, + ); } -export interface Template { - /** The template code (unique Id) */ - code: string; - /** Template name */ +export interface CreateTemplate { + /** Name which the template should have */ name: string; - /** The description for the template */ - description: string; - /** Number of times this template has been used */ - usageCount: number; - /** The Id of the user who created the template */ - creatorId: string; - /** The user who created the template */ - creator: User; - /** When this template was created */ - createdAt: string; - /** When this template was last synced to the source guild */ - updatedAt: string; - /** The Id of the guild this template is based on */ - sourceGuildId: string; - /** The guild snapshot this template contains */ - serializedSourceGuild: Partial; - /** Whether the template has unsynced changes */ - isDirty: boolean; + /** Description of the template */ + description?: string; } diff --git a/helpers/templates/getGuildTemplates.ts b/helpers/templates/getGuildTemplates.ts index cde995bdd..da086f687 100644 --- a/helpers/templates/getGuildTemplates.ts +++ b/helpers/templates/getGuildTemplates.ts @@ -10,5 +10,5 @@ export async function getGuildTemplates(bot: Bot, guildId: bigint) { bot.constants.endpoints.GUILD_TEMPLATES(guildId), ); - return new Collection(templates.map((template) => [template.code, template])); + return new Collection(templates.map((template) => [template.code, bot.transformers.template(bot, template)])); } diff --git a/helpers/templates/getTemplate.ts b/helpers/templates/getTemplate.ts index 785fefc9c..1389fbb67 100644 --- a/helpers/templates/getTemplate.ts +++ b/helpers/templates/getTemplate.ts @@ -3,9 +3,11 @@ import { DiscordTemplate } from "../../types/discord.ts"; /** Returns the guild template if it exists */ export async function getTemplate(bot: Bot, templateCode: string) { - return await bot.rest.runMethod( + const result = await bot.rest.runMethod( bot.rest, "get", bot.constants.endpoints.GUILD_TEMPLATE(templateCode), ); + + return bot.transformers.template(bot, result); } diff --git a/plugins/permissions/src/interactions/commands.ts b/plugins/permissions/src/interactions/commands.ts index 5c369e26c..6a92757c0 100644 --- a/plugins/permissions/src/interactions/commands.ts +++ b/plugins/permissions/src/interactions/commands.ts @@ -83,6 +83,7 @@ export function createApplicationCommand(bot: BotWithCache) { if (!options.name) { throw new Error("A name is required to create a options."); } + if (isChatInput) { if (!SLASH_COMMANDS_NAME_REGEX.test(options.name)) { throw new Error( @@ -92,6 +93,25 @@ export function createApplicationCommand(bot: BotWithCache) { // Only slash need to be lowercase options.name = options.name.toLowerCase(); + + // Slash commands require description + if (!options.description) { + throw new Error( + "Slash commands require some form of a description be provided.", + ); + } else if (!bot.utils.validateLength(options.description, { min: 1, max: 100 })) { + throw new Error( + "Application command descriptions must be between 1 and 100 characters.", + ); + } + + if (options.options?.length) { + if (options.options.length > 25) { + throw new Error("Only 25 options are allowed to be provided."); + } + + options.options = validateApplicationCommandOptions(bot, options.options); + } } else { if (!CONTEXT_MENU_COMMANDS_NAME_REGEX.test(options.name)) { throw new Error( @@ -100,41 +120,6 @@ export function createApplicationCommand(bot: BotWithCache) { } } - // Slash commands require description - if ( - !options.description && - (isChatInput) - ) { - throw new Error( - "Slash commands require some form of a description be provided.", - ); - } - - if ( - options.description && - ((options.type === ApplicationCommandTypes.User) || - (options.type === ApplicationCommandTypes.Message)) - ) { - throw new Error("Context menu commands do not allow a description."); - } - - if ( - options.description && - !bot.utils.validateLength(options.description, { min: 1, max: 100 }) - ) { - throw new Error( - "Application command descriptions must be between 1 and 100 characters.", - ); - } - - if (options.options?.length) { - if (options.options.length > 25) { - throw new Error("Only 25 options are allowed to be provided."); - } - - options.options = validateApplicationCommandOptions(bot, options.options); - } - return await createApplicationCommandOld(options, guildId); }; } diff --git a/plugins/permissions/src/interactions/mod.ts b/plugins/permissions/src/interactions/mod.ts index afc56bde6..55a7738ef 100644 --- a/plugins/permissions/src/interactions/mod.ts +++ b/plugins/permissions/src/interactions/mod.ts @@ -6,6 +6,14 @@ export function sendInteractionResponse(bot: BotWithCache) { const sendInteractionResponseOld = bot.helpers.sendInteractionResponse; bot.helpers.sendInteractionResponse = function (id, token, options) { + if (options.data?.title !== undefined) { + if (!bot.utils.validateLength(options.data.title, { min: 1, max: 45 })) { + throw new Error( + "Invalid modal title. Must be between 1-45 characters long.", + ); + } + } + options.data?.choices?.every((choice) => { if (!bot.utils.validateLength(choice.name, { min: 1, max: 100 })) { throw new Error( diff --git a/plugins/permissions/src/roles/edit.ts b/plugins/permissions/src/roles/edit.ts index e277fbfbf..ff14a4d4a 100644 --- a/plugins/permissions/src/roles/edit.ts +++ b/plugins/permissions/src/roles/edit.ts @@ -29,4 +29,3 @@ export default function editRole(bot: BotWithCache) { return await editRoleOld(guildId, id, options); }; } - diff --git a/site/package-lock.json b/site/package-lock.json index 4396c2b6b..ec498d504 100644 --- a/site/package-lock.json +++ b/site/package-lock.json @@ -8182,9 +8182,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/mkdirp": { "version": "0.5.5", @@ -8288,9 +8288,9 @@ } }, "node_modules/node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==", "engines": { "node": ">= 6.13.0" } @@ -18866,9 +18866,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mkdirp": { "version": "0.5.5", @@ -18943,9 +18943,9 @@ } }, "node-forge": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.2.1.tgz", - "integrity": "sha512-Fcvtbb+zBcZXbTTVwqGA5W+MKBj56UjVRevvchv5XrcyXbmNdesfZL37nlcWOfpgHhgmxApw3tQbTr4CqNmX4w==" + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz", + "integrity": "sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA==" }, "node-releases": { "version": "2.0.1", diff --git a/transformers/mod.ts b/transformers/mod.ts index 38c514967..fd52050d9 100644 --- a/transformers/mod.ts +++ b/transformers/mod.ts @@ -30,3 +30,4 @@ export * from "./voiceState.ts"; export * from "./webhook.ts"; export * from "./welcomeScreen.ts"; export * from "./widget.ts"; +export * from "./template.ts"; diff --git a/transformers/template.ts b/transformers/template.ts new file mode 100644 index 000000000..af76569b1 --- /dev/null +++ b/transformers/template.ts @@ -0,0 +1,23 @@ +import { Bot } from "../bot.ts"; +import { DiscordTemplate } from "../types/discord.ts"; +import { Optionalize } from "../types/shared.ts"; + +export function transformTemplate(bot: Bot, payload: DiscordTemplate) { + const template = { + code: payload.code, + name: payload.name, + description: payload.description, + usageCount: payload.usage_count, + creatorId: bot.transformers.snowflake(payload.creator_id), + creator: bot.transformers.user(bot, payload.creator), + createdAt: Date.parse(payload.created_at), + updatedAt: Date.parse(payload.updated_at), + sourceGuildId: bot.transformers.snowflake(payload.source_guild_id), + serializedSourceGuild: payload.serialized_source_guild, + isDirty: payload.is_dirty ?? undefined, + }; + + return template as Optionalize; +} + +export interface Template extends ReturnType {} diff --git a/types/discord.ts b/types/discord.ts index 89a899ff6..b8a65445f 100644 --- a/types/discord.ts +++ b/types/discord.ts @@ -21,6 +21,7 @@ import { MessageTypes, MfaLevels, OverwriteTypes, + PickPartial, PremiumTiers, PremiumTypes, ScheduledEventEntityType, @@ -2000,7 +2001,36 @@ export interface DiscordTemplate { /** The Id of the guild this template is based on */ source_guild_id: string; /** The guild snapshot this template contains */ - serialized_source_guild: Partial; + serialized_source_guild: + & Omit< + PickPartial< + DiscordGuild, + | "name" + | "description" + | "verification_level" + | "default_message_notifications" + | "explicit_content_filter" + | "preferred_locale" + | "afk_timeout" + | "channels" + | "afk_channel_id" + | "system_channel_id" + | "system_channel_flags" + >, + "roles" + > + & { + roles: ( + & Omit< + PickPartial< + DiscordRole, + "name" | "color" | "hoist" | "mentionable" | "permissions" | "icon" | "unicode_emoji" + >, + "id" + > + & { id: number } + )[]; + }; /** Whether the template has unsynced changes */ is_dirty: boolean | null; } diff --git a/types/shared.ts b/types/shared.ts index 522d5bea5..e3c331d56 100644 --- a/types/shared.ts +++ b/types/shared.ts @@ -1335,3 +1335,9 @@ export type Optionalize = } > : T; + +export type PickPartial = + & { + [P in keyof T]?: T[P] | undefined; + } + & { [P in K]: T[P] };