Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: onboarding mode and edit method #9647

Merged
merged 14 commits into from
Oct 17, 2023
22 changes: 22 additions & 0 deletions packages/core/src/api/guild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ import {
type RESTPostAPIGuildsMFAResult,
type RESTPostAPIGuildsResult,
type RESTPutAPIGuildBanJSONBody,
type RESTPutAPIGuildOnboardingJSONBody,
type RESTPutAPIGuildOnboardingResult,
type RESTPutAPIGuildTemplateSyncResult,
type Snowflake,
} from 'discord-api-types/v10';
Expand Down Expand Up @@ -1241,4 +1243,24 @@ export class GuildsAPI {
public async getOnboarding(guildId: Snowflake, { signal }: Pick<RequestData, 'signal'> = {}) {
return this.rest.get(Routes.guildOnboarding(guildId), { signal }) as Promise<RESTGetAPIGuildOnboardingResult>;
}

/**
* 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<RequestData, 'reason' | 'signal'> = {},
) {
return this.rest.put(Routes.guildOnboarding(guildId), {
reason,
body,
signal,
}) as Promise<RESTPutAPIGuildOnboardingResult>;
}
}
82 changes: 81 additions & 1 deletion packages/discord.js/src/structures/Guild.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.
Expand Down Expand Up @@ -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<Snowflake, GuildOnboardingPrompt>} [prompts]
* The prompts shown during onboarding and in customize community
* @property {ChannelResolvable[]|Collection<Snowflake, GuildChannel>} [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<Snowflake, GuildOnboardingPrompt>} 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<Snowflake, GuildChannel>} [channels]
* The channels a member is added to when the option is selected
* @property {RoleResolvable[]|Collection<Snowflake, Role>} [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<GuildOnboarding>}
*/
async editOnboarding(options) {
const newData = await this.client.rest.put(Routes.guildOnboarding(this.id), {
body: {
prompts: options.prompts?.map(prompt => ({
almeidx marked this conversation as resolved.
Show resolved Hide resolved
// 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
Expand Down
6 changes: 6 additions & 0 deletions packages/discord.js/src/structures/GuildOnboarding.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
26 changes: 14 additions & 12 deletions packages/discord.js/src/structures/GuildOnboardingPromptOption.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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;
10 changes: 10 additions & 0 deletions packages/discord.js/src/util/APITypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion packages/discord.js/src/util/Util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
38 changes: 32 additions & 6 deletions packages/discord.js/typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -1378,6 +1379,7 @@ export class Guild extends AnonymousGuild {
public delete(): Promise<Guild>;
public discoverySplashURL(options?: ImageURLOptions): string | null;
public edit(options: GuildEditOptions): Promise<Guild>;
public editOnboarding(options: GuildOnboardingEditOptions): Promise<GuildOnboarding>;
public editWelcomeScreen(options: WelcomeScreenEditOptions): Promise<WelcomeScreen>;
public equals(guild: Guild): boolean;
public fetchAuditLogs<T extends GuildAuditLogsResolvable = null>(
Expand Down Expand Up @@ -1603,6 +1605,7 @@ export class GuildOnboarding extends Base {
public prompts: Collection<Snowflake, GuildOnboardingPrompt>;
public defaultChannels: Collection<Snowflake, GuildChannel>;
public enabled: boolean;
public mode: GuildOnboardingMode;
}

export class GuildOnboardingPrompt extends Base {
Expand All @@ -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<Snowflake, GuildChannel>;
public roles: Collection<Snowflake, Role>;
public emoji: GuildOnboardingPromptOptionEmoji | null;
public title: string;
public description: string | null;
}
Expand Down Expand Up @@ -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<Snowflake, GuildOnboardingPrompt>;
defaultChannels?: readonly ChannelResolvable[] | ReadonlyCollection<Snowflake, GuildChannel>;
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<Snowflake, GuildOnboardingPromptOption>;
}

export interface GuildOnboardingPromptOptionData {
id?: Snowflake | null;
channels?: readonly ChannelResolvable[] | ReadonlyCollection<Snowflake, GuildChannel>;
roles?: readonly RoleResolvable[] | ReadonlyCollection<Snowflake, Role>;
title: string;
description?: string | null;
emoji?: EmojiIdentifierResolvable | Emoji | null;
}

export type HexColorString = `#${string}`;
Expand Down
19 changes: 19 additions & 0 deletions packages/discord.js/typings/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2327,9 +2327,28 @@ client.on('guildAuditLogEntryCreate', (auditLogEntry, guild) => {

expectType<Readonly<GuildMemberFlagsBitField>>(guildMember.flags);

declare const emojiResolvable: GuildEmoji | Emoji | string;

{
const onboarding = await guild.fetchOnboarding();
expectType<GuildOnboarding>(onboarding);

expectType<GuildOnboarding>(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;
Expand Down