From 7671a836f4b080a0c0d42bbbacc6ddd1df7c0ba8 Mon Sep 17 00:00:00 2001 From: Almeida Date: Tue, 17 Oct 2023 20:30:10 +0100 Subject: [PATCH] feat: onboarding mode and edit method (#9647) * feat: onboarding mode and edit method * feat(guild): add `editOnboarding` * fix: use discord-api-types * types: make arrays readonly Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * fix: bring up to date * docs: id is a snowflake * fix: requested changes * refactor: make most options optional * refactor: provide GuildEmoji or Emoji instance * revert: changes to Util * fix: rebase leftovers * fix: allow passing option id * fix: requested changes --------- Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> --- packages/core/src/api/guild.ts | 22 +++++ packages/discord.js/src/structures/Guild.js | 82 ++++++++++++++++++- .../src/structures/GuildOnboarding.js | 6 ++ .../structures/GuildOnboardingPromptOption.js | 26 +++--- packages/discord.js/src/util/APITypes.js | 10 +++ packages/discord.js/src/util/Util.js | 2 +- packages/discord.js/typings/index.d.ts | 38 +++++++-- packages/discord.js/typings/index.test-d.ts | 19 +++++ 8 files changed, 185 insertions(+), 20 deletions(-) diff --git a/packages/core/src/api/guild.ts b/packages/core/src/api/guild.ts index d2a31643a7f5..c1b6a9cac57c 100644 --- a/packages/core/src/api/guild.ts +++ b/packages/core/src/api/guild.ts @@ -92,6 +92,8 @@ import { type RESTPostAPIGuildsMFAResult, type RESTPostAPIGuildsResult, type RESTPutAPIGuildBanJSONBody, + type RESTPutAPIGuildOnboardingJSONBody, + type RESTPutAPIGuildOnboardingResult, type RESTPutAPIGuildTemplateSyncResult, type Snowflake, } from 'discord-api-types/v10'; @@ -1241,4 +1243,24 @@ export class GuildsAPI { public async getOnboarding(guildId: Snowflake, { signal }: Pick = {}) { return this.rest.get(Routes.guildOnboarding(guildId), { signal }) as Promise; } + + /** + * Edits a guild onboarding + * + * @see {@link https://discord.com/developers/docs/resources/guild#modify-guild-onboarding} + * @param guildId - The id of the guild + * @param body - The data for editing the guild onboarding + * @param options - The options for editing the guild onboarding + */ + public async editOnboarding( + guildId: Snowflake, + body: RESTPutAPIGuildOnboardingJSONBody, + { reason, signal }: Pick = {}, + ) { + return this.rest.put(Routes.guildOnboarding(guildId), { + reason, + body, + signal, + }) as Promise; + } } diff --git a/packages/discord.js/src/structures/Guild.js b/packages/discord.js/src/structures/Guild.js index ad5ed235ef39..da3843855b4d 100644 --- a/packages/discord.js/src/structures/Guild.js +++ b/packages/discord.js/src/structures/Guild.js @@ -2,6 +2,7 @@ const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); +const { DiscordSnowflake } = require('@sapphire/snowflake'); const { ChannelType, GuildPremiumTier, Routes, GuildFeature } = require('discord-api-types/v10'); const AnonymousGuild = require('./AnonymousGuild'); const GuildAuditLogs = require('./GuildAuditLogs'); @@ -28,7 +29,7 @@ const VoiceStateManager = require('../managers/VoiceStateManager'); const DataResolver = require('../util/DataResolver'); const Status = require('../util/Status'); const SystemChannelFlagsBitField = require('../util/SystemChannelFlagsBitField'); -const { discordSort, getSortableGroupTypes } = require('../util/Util'); +const { discordSort, getSortableGroupTypes, resolvePartialEmoji } = require('../util/Util'); /** * Represents a guild (or a server) on Discord. @@ -881,6 +882,85 @@ class Guild extends AnonymousGuild { return this.client.actions.GuildUpdate.handle(data).updated; } + /** + * Options used to edit the guild onboarding. + * @typedef {Object} GuildOnboardingEditOptions + * @property {GuildOnboardingPromptData[]|Collection} [prompts] + * The prompts shown during onboarding and in customize community + * @property {ChannelResolvable[]|Collection} [defaultChannels] + * The channels that new members get opted into automatically + * @property {boolean} [enabled] Whether the onboarding is enabled + * @property {GuildOnboardingMode} [mode] The mode to edit the guild onboarding with + * @property {string} [reason] The reason for editing the guild onboarding + */ + + /** + * Data for editing a guild onboarding prompt. + * @typedef {Object} GuildOnboardingPromptData + * @property {Snowflake} [id] The id of the prompt + * @property {string} title The title for the prompt + * @property {boolean} [singleSelect] Whether users are limited to selecting one option for the prompt + * @property {boolean} [required] Whether the prompt is required before a user completes the onboarding flow + * @property {boolean} [inOnboarding] Whether the prompt is present in the onboarding flow + * @property {GuildOnboardingPromptType} [type] The type of the prompt + * @property {GuildOnboardingPromptOptionData[]|Collection} options + * The options available within the prompt + */ + + /** + * Data for editing a guild onboarding prompt option. + * @typedef {Object} GuildOnboardingPromptOptionData + * @property {?Snowflake} [id] The id of the option + * @property {ChannelResolvable[]|Collection} [channels] + * The channels a member is added to when the option is selected + * @property {RoleResolvable[]|Collection} [roles] + * The roles assigned to a member when the option is selected + * @property {string} title The title of the option + * @property {?string} [description] The description of the option + * @property {?(EmojiIdentifierResolvable|Emoji)} [emoji] The emoji of the option + */ + + /** + * Edits the guild onboarding data for this guild. + * @param {GuildOnboardingEditOptions} options The options to provide + * @returns {Promise} + */ + async editOnboarding(options) { + const newData = await this.client.rest.put(Routes.guildOnboarding(this.id), { + body: { + prompts: options.prompts?.map(prompt => ({ + // Currently, the prompt ids are required even for new ones (which won't be used) + id: prompt.id ?? DiscordSnowflake.generate().toString(), + title: prompt.title, + single_select: prompt.singleSelect, + required: prompt.required, + in_onboarding: prompt.inOnboarding, + type: prompt.type, + options: prompt.options.map(option => { + const emoji = resolvePartialEmoji(option.emoji); + + return { + id: option.id, + channel_ids: option.channels?.map(channel => this.channels.resolveId(channel)), + role_ids: option.roles?.map(role => this.roles.resolveId(role)), + title: option.title, + description: option.description, + emoji_animated: emoji?.animated, + emoji_id: emoji?.id, + emoji_name: emoji?.name, + }; + }), + })), + default_channel_ids: options.defaultChannels?.map(channel => this.channels.resolveId(channel)), + enabled: options.enabled, + mode: options.mode, + }, + reason: options.reason, + }); + + return new GuildOnboarding(this.client, newData); + } + /** * Welcome channel data * @typedef {Object} WelcomeChannelData diff --git a/packages/discord.js/src/structures/GuildOnboarding.js b/packages/discord.js/src/structures/GuildOnboarding.js index 119f905ee96e..2e91f48bee64 100644 --- a/packages/discord.js/src/structures/GuildOnboarding.js +++ b/packages/discord.js/src/structures/GuildOnboarding.js @@ -43,6 +43,12 @@ class GuildOnboarding extends Base { * @type {boolean} */ this.enabled = data.enabled; + + /** + * The mode of this onboarding + * @type {GuildOnboardingMode} + */ + this.mode = data.mode; } /** diff --git a/packages/discord.js/src/structures/GuildOnboardingPromptOption.js b/packages/discord.js/src/structures/GuildOnboardingPromptOption.js index 3002144cf725..2982ff456b66 100644 --- a/packages/discord.js/src/structures/GuildOnboardingPromptOption.js +++ b/packages/discord.js/src/structures/GuildOnboardingPromptOption.js @@ -2,7 +2,7 @@ const { Collection } = require('@discordjs/collection'); const Base = require('./Base'); -const { resolvePartialEmoji } = require('../util/Util'); +const { Emoji } = require('./Emoji.js'); /** * Represents the data of an option from a prompt of a guilds onboarding. @@ -45,18 +45,11 @@ class GuildOnboardingPromptOption extends Base { ); /** - * The data for an emoji of a guilds onboarding prompt option - * @typedef {Object} GuildOnboardingPromptOptionEmoji - * @property {?Snowflake} id The id of the emoji - * @property {string} name The name of the emoji - * @property {boolean} animated Whether the emoji is animated + * The raw emoji of the option + * @type {APIPartialEmoji} + * @private */ - - /** - * The emoji of the option - * @type {?GuildOnboardingPromptOptionEmoji} - */ - this.emoji = resolvePartialEmoji(data.emoji); + this._emoji = data.emoji; /** * The title of the option @@ -79,6 +72,15 @@ class GuildOnboardingPromptOption extends Base { get guild() { return this.client.guilds.cache.get(this.guildId); } + + /** + * The emoji of this onboarding prompt option + * @type {?(GuildEmoji|Emoji)} + */ + get emoji() { + if (!this._emoji.id && !this._emoji.name) return null; + return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji); + } } exports.GuildOnboardingPromptOption = GuildOnboardingPromptOption; diff --git a/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 700bd4b4c026..a1b4fd827312 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -160,6 +160,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIModalSubmission} */ +/** + * @external APIPartialEmoji + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIPartialEmoji} + */ + /** * @external APIRole * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIRole} @@ -335,6 +340,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildNSFWLevel} */ +/** + * @external GuildOnboardingMode + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildOnboardingMode} + */ + /** * @external GuildOnboardingPromptType * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/GuildOnboardingPromptType} diff --git a/packages/discord.js/src/util/Util.js b/packages/discord.js/src/util/Util.js index c4a7e320f666..7a8d57ff36ac 100644 --- a/packages/discord.js/src/util/Util.js +++ b/packages/discord.js/src/util/Util.js @@ -111,7 +111,7 @@ function parseEmoji(text) { /** * Resolves a partial emoji object from an {@link EmojiIdentifierResolvable}, without checking a Client. * @param {Emoji|EmojiIdentifierResolvable} emoji Emoji identifier to resolve - * @returns {?(PartialEmoji|PartialEmojiOnlyId)} Suppling a snowflake yields `PartialEmojiOnlyId`. + * @returns {?(PartialEmoji|PartialEmojiOnlyId)} Supplying a snowflake yields `PartialEmojiOnlyId`. * @private */ function resolvePartialEmoji(emoji) { diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index c24caa2e4d79..4dd638a05612 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -35,7 +35,7 @@ import { ApplicationCommandOptionAllowedChannelTypes, } from '@discordjs/builders'; import { Awaitable, JSONEncodable } from '@discordjs/util'; -import { Collection } from '@discordjs/collection'; +import { Collection, ReadonlyCollection } from '@discordjs/collection'; import { BaseImageURLOptions, ImageURLOptions, RawFile, REST, RESTOptions } from '@discordjs/rest'; import { WebSocketManager as WSWebSocketManager, @@ -167,6 +167,7 @@ import { RoleFlags, TeamMemberRole, GuildWidgetStyle, + GuildOnboardingMode, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -1378,6 +1379,7 @@ export class Guild extends AnonymousGuild { public delete(): Promise; public discoverySplashURL(options?: ImageURLOptions): string | null; public edit(options: GuildEditOptions): Promise; + public editOnboarding(options: GuildOnboardingEditOptions): Promise; public editWelcomeScreen(options: WelcomeScreenEditOptions): Promise; public equals(guild: Guild): boolean; public fetchAuditLogs( @@ -1603,6 +1605,7 @@ export class GuildOnboarding extends Base { public prompts: Collection; public defaultChannels: Collection; public enabled: boolean; + public mode: GuildOnboardingMode; } export class GuildOnboardingPrompt extends Base { @@ -1620,12 +1623,14 @@ export class GuildOnboardingPrompt extends Base { export class GuildOnboardingPromptOption extends Base { private constructor(client: Client, data: APIGuildOnboardingPromptOption, guildId: Snowflake); + private _emoji: APIPartialEmoji; + public id: Snowflake; + public get emoji(): Emoji | GuildEmoji | null; public get guild(): Guild; public guildId: Snowflake; public channels: Collection; public roles: Collection; - public emoji: GuildOnboardingPromptOptionEmoji | null; public title: string; public description: string | null; } @@ -5784,10 +5789,31 @@ export type GuildTemplateResolvable = string; export type GuildVoiceChannelResolvable = VoiceBasedChannel | Snowflake; -export interface GuildOnboardingPromptOptionEmoji { - id: Snowflake | null; - name: string; - animated: boolean; +export interface GuildOnboardingEditOptions { + prompts?: readonly GuildOnboardingPromptData[] | ReadonlyCollection; + defaultChannels?: readonly ChannelResolvable[] | ReadonlyCollection; + enabled?: boolean; + mode?: GuildOnboardingMode; + reason?: string; +} + +export interface GuildOnboardingPromptData { + id?: Snowflake; + title: string; + singleSelect?: boolean; + required?: boolean; + inOnboarding?: boolean; + type?: GuildOnboardingPromptType; + options: readonly GuildOnboardingPromptOptionData[] | ReadonlyCollection; +} + +export interface GuildOnboardingPromptOptionData { + id?: Snowflake | null; + channels?: readonly ChannelResolvable[] | ReadonlyCollection; + roles?: readonly RoleResolvable[] | ReadonlyCollection; + title: string; + description?: string | null; + emoji?: EmojiIdentifierResolvable | Emoji | null; } export type HexColorString = `#${string}`; diff --git a/packages/discord.js/typings/index.test-d.ts b/packages/discord.js/typings/index.test-d.ts index e0364a69a0cf..09716af8d9ad 100644 --- a/packages/discord.js/typings/index.test-d.ts +++ b/packages/discord.js/typings/index.test-d.ts @@ -2327,9 +2327,28 @@ client.on('guildAuditLogEntryCreate', (auditLogEntry, guild) => { expectType>(guildMember.flags); +declare const emojiResolvable: GuildEmoji | Emoji | string; + { const onboarding = await guild.fetchOnboarding(); expectType(onboarding); + + expectType(await guild.editOnboarding(onboarding)); + + await guild.editOnboarding({ + defaultChannels: onboarding.defaultChannels, + enabled: onboarding.enabled, + mode: onboarding.mode, + prompts: onboarding.prompts, + }); + + const prompt = onboarding.prompts.first()!; + const option = prompt.options.first()!; + + await guild.editOnboarding({ prompts: [prompt] }); + await guild.editOnboarding({ prompts: [{ ...prompt, options: [option] }] }); + + await guild.editOnboarding({ prompts: [{ ...prompt, options: [{ ...option, emoji: emojiResolvable }] }] }); } declare const partialDMChannel: PartialDMChannel;