diff --git a/src/managers/ChannelManager.js b/src/managers/ChannelManager.js
index 128893ba48cb..1aeb292d162c 100644
--- a/src/managers/ChannelManager.js
+++ b/src/managers/ChannelManager.js
@@ -23,7 +23,7 @@ class ChannelManager extends BaseManager {
const existing = this.cache.get(data.id);
if (existing) {
if (existing._patch && cache) existing._patch(data);
- if (guild) guild.channels.add(existing);
+ if (guild) guild.channels?.add(existing);
return existing;
}
diff --git a/src/structures/AnonymousGuild.js b/src/structures/AnonymousGuild.js
new file mode 100644
index 000000000000..4b4bb5bf430b
--- /dev/null
+++ b/src/structures/AnonymousGuild.js
@@ -0,0 +1,78 @@
+'use strict';
+
+const BaseGuild = require('./BaseGuild');
+const { VerificationLevels, NSFWLevels } = require('../util/Constants');
+
+/**
+ * Bundles common attributes and methods between {@link Guild} and {@link InviteGuild}
+ * @abstract
+ */
+class AnonymousGuild extends BaseGuild {
+ constructor(client, data) {
+ super(client, data);
+ this._patch(data);
+ }
+
+ _patch(data) {
+ this.features = data.features;
+ /**
+ * The hash of the guild invite splash image
+ * @type {?string}
+ */
+ this.splash = data.splash;
+
+ /**
+ * The hash of the guild banner
+ * @type {?string}
+ */
+ this.banner = data.banner;
+
+ /**
+ * The description of the guild, if any
+ * @type {?string}
+ */
+ this.description = data.description;
+
+ /**
+ * The verification level of the guild
+ * @type {VerificationLevel}
+ */
+ this.verificationLevel = VerificationLevels[data.verification_level];
+
+ /**
+ * The vanity invite code of the guild, if any
+ * @type {?string}
+ */
+ this.vanityURLCode = data.vanity_url_code;
+
+ if ('nsfw_level' in data) {
+ /**
+ * The NSFW level of this guild
+ * @type {NSFWLevel}
+ */
+ this.nsfwLevel = NSFWLevels[data.nsfw_level];
+ }
+ }
+
+ /**
+ * The URL to this guild's banner.
+ * @param {ImageURLOptions} [options={}] Options for the Image URL
+ * @returns {?string}
+ */
+ bannerURL({ format, size } = {}) {
+ if (!this.banner) return null;
+ return this.client.rest.cdn.Banner(this.id, this.banner, format, size);
+ }
+
+ /**
+ * The URL to this guild's invite splash image.
+ * @param {ImageURLOptions} [options={}] Options for the Image URL
+ * @returns {?string}
+ */
+ splashURL({ format, size } = {}) {
+ if (!this.splash) return null;
+ return this.client.rest.cdn.Splash(this.id, this.splash, format, size);
+ }
+}
+
+module.exports = AnonymousGuild;
diff --git a/src/structures/BaseGuild.js b/src/structures/BaseGuild.js
index 561375f3c0d0..dfed553f087c 100644
--- a/src/structures/BaseGuild.js
+++ b/src/structures/BaseGuild.js
@@ -4,8 +4,9 @@ const Base = require('./Base');
const SnowflakeUtil = require('../util/SnowflakeUtil');
/**
- * The base class for {@link Guild} and {@link OAuth2Guild}.
+ * The base class for {@link Guild}, {@link OAuth2Guild} and {@link InviteGuild}.
* @extends {Base}
+ * @abstract
*/
class BaseGuild extends Base {
constructor(client, data) {
diff --git a/src/structures/Channel.js b/src/structures/Channel.js
index f0c28eb3f53e..c86c9c8fffce 100644
--- a/src/structures/Channel.js
+++ b/src/structures/Channel.js
@@ -153,7 +153,7 @@ class Channel extends Base {
break;
}
}
- if (channel) guild.channels.cache.set(channel.id, channel);
+ if (channel) guild.channels?.cache.set(channel.id, channel);
}
}
return channel;
diff --git a/src/structures/Emoji.js b/src/structures/Emoji.js
index e9f1306eec96..e89faf8b033e 100644
--- a/src/structures/Emoji.js
+++ b/src/structures/Emoji.js
@@ -20,9 +20,9 @@ class Emoji extends Base {
super(client);
/**
* Whether this emoji is animated
- * @type {boolean}
+ * @type {?boolean}
*/
- this.animated = emoji.animated;
+ this.animated = emoji.animated ?? null;
/**
* The name of this emoji
diff --git a/src/structures/Guild.js b/src/structures/Guild.js
index 945976bad0b5..36f09e7a478f 100644
--- a/src/structures/Guild.js
+++ b/src/structures/Guild.js
@@ -1,12 +1,13 @@
'use strict';
-const BaseGuild = require('./BaseGuild');
+const AnonymousGuild = require('./AnonymousGuild');
const GuildAuditLogs = require('./GuildAuditLogs');
const GuildPreview = require('./GuildPreview');
const GuildTemplate = require('./GuildTemplate');
const Integration = require('./Integration');
const Invite = require('./Invite');
const Webhook = require('./Webhook');
+const WelcomeScreen = require('./WelcomeScreen');
const { Error, TypeError } = require('../errors');
const GuildApplicationCommandManager = require('../managers/GuildApplicationCommandManager');
const GuildBanManager = require('../managers/GuildBanManager');
@@ -24,7 +25,6 @@ const {
PartialTypes,
VerificationLevels,
ExplicitContentFilterLevels,
- NSFWLevels,
Status,
MFALevels,
PremiumTiers,
@@ -37,9 +37,9 @@ const Util = require('../util/Util');
* Represents a guild (or a server) on Discord.
* It's recommended to see if a guild is available before performing operations or reading data from it. You can
* check this with `guild.available`.
- * @extends {BaseGuild}
+ * @extends {AnonymousGuild}
*/
-class Guild extends BaseGuild {
+class Guild extends AnonymousGuild {
constructor(client, data) {
super(client, data);
@@ -131,18 +131,12 @@ class Guild extends BaseGuild {
* @private
*/
_patch(data) {
+ super._patch(data);
this.id = data.id;
this.name = data.name;
this.icon = data.icon;
- this.features = data.features;
this.available = !data.unavailable;
- /**
- * The hash of the guild invite splash image
- * @type {?string}
- */
- this.splash = data.splash;
-
/**
* The hash of the guild discovery splash image
* @type {?string}
@@ -155,14 +149,6 @@ class Guild extends BaseGuild {
*/
this.memberCount = data.member_count || this.memberCount;
- if ('nsfw_level' in data) {
- /**
- * The NSFW level of this guild
- * @type {NSFWLevel}
- */
- this.nsfwLevel = NSFWLevels[data.nsfw_level];
- }
-
/**
* Whether the guild is "large" (has more than large_threshold members, 50 by default)
* @type {boolean}
@@ -247,12 +233,6 @@ class Guild extends BaseGuild {
this.widgetChannelID = data.widget_channel_id;
}
- /**
- * The verification level of the guild
- * @type {VerificationLevel}
- */
- this.verificationLevel = VerificationLevels[data.verification_level];
-
/**
* The explicit content filter level of the guild
* @type {ExplicitContentFilterLevel}
@@ -326,12 +306,6 @@ class Guild extends BaseGuild {
this.approximatePresenceCount = null;
}
- /**
- * The vanity invite code of the guild, if any
- * @type {?string}
- */
- this.vanityURLCode = data.vanity_url_code;
-
/**
* The use count of the vanity URL code of the guild, if any
* You will need to fetch this parameter using {@link Guild#fetchVanityData} if you want to receive it
@@ -339,18 +313,6 @@ class Guild extends BaseGuild {
*/
this.vanityURLUses = null;
- /**
- * The description of the guild, if any
- * @type {?string}
- */
- this.description = data.description;
-
- /**
- * The hash of the guild banner
- * @type {?string}
- */
- this.banner = data.banner;
-
/**
* The ID of the rules channel for the guild
* @type {?Snowflake}
@@ -576,6 +538,15 @@ class Guild extends BaseGuild {
);
}
+ /**
+ * Fetches the welcome screen for this guild.
+ * @returns {Promise}
+ */
+ async fetchWelcomeScreen() {
+ const data = await this.client.api.guilds(this.id, 'welcome-screen').get();
+ return new WelcomeScreen(this, data);
+ }
+
/**
* The data for creating an integration.
* @typedef {Object} IntegrationData
@@ -900,6 +871,60 @@ class Guild extends BaseGuild {
.then(newData => this.client.actions.GuildUpdate.handle(newData).updated);
}
+ /**
+ * Welcome channel data
+ * @typedef {Object} WelcomeChannelData
+ * @property {string} description The description to show for this welcome channel
+ * @property {GuildTextChannelResolvable} channel The channel to link for this welcome channel
+ * @property {EmojiIdentifierResolvable} [emoji] The emoji to display for this welcome channel
+ */
+
+ /**
+ * Welcome screen edit data
+ * @typedef {Object} WelcomeScreenEditData
+ * @property {boolean} [enabled] Whether the welcome screen is enabled
+ * @property {string} [description] The description for the welcome screen
+ * @property {WelcomeChannelData[]} [welcomeChannels] The welcome channel data for the welcome screen
+ */
+
+ /**
+ * Updates the guild's welcome screen
+ * @param {WelcomeScreenEditData} data Data to edit the welcome screen with
+ * @returns {Promise}
+ * @example
+ * guild.editWelcomeScreen({
+ * description: 'Hello World',
+ * enabled: true,
+ * welcomeChannels: [
+ * {
+ * description: 'foobar',
+ * channel: '222197033908436994',
+ * }
+ * ],
+ * })
+ */
+ async editWelcomeScreen(data) {
+ const { enabled, description, welcomeChannels } = data;
+ const welcome_channels = welcomeChannels?.map(welcomeChannelData => {
+ const emoji = this.emojis.resolve(welcomeChannelData.emoji);
+ return {
+ emoji_id: emoji?.id ?? null,
+ emoji_name: emoji?.name ?? welcomeChannelData.emoji,
+ channel_id: this.channels.resolveID(welcomeChannelData.channel),
+ description: welcomeChannelData.description,
+ };
+ });
+
+ const patchData = await this.client.api.guilds(this.id, 'welcome-screen').patch({
+ data: {
+ welcome_channels,
+ description,
+ enabled,
+ },
+ });
+ return new WelcomeScreen(this, patchData);
+ }
+
/**
* Edits the level of the explicit content filter.
* @param {ExplicitContentFilterLevel|number} explicitContentFilter The new level of the explicit content filter
diff --git a/src/structures/Invite.js b/src/structures/Invite.js
index c123e527a80e..e72082aaa7ab 100644
--- a/src/structures/Invite.js
+++ b/src/structures/Invite.js
@@ -19,11 +19,16 @@ class Invite extends Base {
}
_patch(data) {
+ const InviteGuild = require('./InviteGuild');
+ const Guild = require('./Guild');
/**
- * The guild the invite is for
- * @type {?Guild}
+ * The guild the invite is for including welcome screen data if present
+ * @type {?(Guild|InviteGuild)}
*/
- this.guild = data.guild ? this.client.guilds.add(data.guild, false) : null;
+ this.guild = null;
+ if (data.guild) {
+ this.guild = data.guild instanceof Guild ? data.guild : new InviteGuild(this.client, data.guild);
+ }
/**
* The code for this invite
diff --git a/src/structures/InviteGuild.js b/src/structures/InviteGuild.js
new file mode 100644
index 000000000000..ab1aed492144
--- /dev/null
+++ b/src/structures/InviteGuild.js
@@ -0,0 +1,23 @@
+'use strict';
+
+const AnonymousGuild = require('./AnonymousGuild');
+const WelcomeScreen = require('./WelcomeScreen');
+
+/**
+ * Represents a guild received from an invite, includes welcome screen data if available.
+ * @extends {AnonymousGuild}
+ */
+class InviteGuild extends AnonymousGuild {
+ constructor(client, data) {
+ super(client, data);
+
+ /**
+ * The welcome screen for this invite guild
+ * @type {?WelcomeScreen}
+ */
+ this.welcomeScreen =
+ typeof data.welcome_screen !== 'undefined' ? new WelcomeScreen(this, data.welcome_screen) : null;
+ }
+}
+
+module.exports = InviteGuild;
diff --git a/src/structures/WelcomeChannel.js b/src/structures/WelcomeChannel.js
new file mode 100644
index 000000000000..21b6b51179d5
--- /dev/null
+++ b/src/structures/WelcomeChannel.js
@@ -0,0 +1,60 @@
+'use strict';
+
+const Base = require('./Base');
+const Emoji = require('./Emoji');
+
+/**
+ * Represents a channel link in a guild's welcome screen.
+ * @extends {Base}
+ */
+class WelcomeChannel extends Base {
+ constructor(guild, data) {
+ super(guild.client);
+
+ /**
+ * The guild for this welcome channel
+ * @type {Guild|WelcomeGuild}
+ */
+ this.guild = guild;
+
+ /**
+ * The description of this welcome channel
+ * @type {string}
+ */
+ this.description = data.description;
+
+ /**
+ * The raw emoji data
+ * @type {Object}
+ * @private
+ */
+ this._emoji = {
+ name: data.emoji_name,
+ id: data.emoji_id,
+ };
+
+ /**
+ * The id of this welcome channel
+ * @type {Snowflake}
+ */
+ this.channelID = data.channel_id;
+ }
+
+ /**
+ * The channel of this welcome channel
+ * @type {?TextChannel|NewsChannel}
+ */
+ get channel() {
+ return this.client.channels.resolve(this.channelID);
+ }
+
+ /**
+ * The emoji of this welcome channel
+ * @type {GuildEmoji|Emoji}
+ */
+ get emoji() {
+ return this.client.emojis.resolve(this._emoji.id) ?? new Emoji(this.client, this._emoji);
+ }
+}
+
+module.exports = WelcomeChannel;
diff --git a/src/structures/WelcomeScreen.js b/src/structures/WelcomeScreen.js
new file mode 100644
index 000000000000..2b8d176b298c
--- /dev/null
+++ b/src/structures/WelcomeScreen.js
@@ -0,0 +1,48 @@
+'use strict';
+
+const Base = require('./Base');
+const WelcomeChannel = require('./WelcomeChannel');
+const Collection = require('../util/Collection');
+
+/**
+ * Represents a welcome screen.
+ * @extends {Base}
+ */
+class WelcomeScreen extends Base {
+ constructor(guild, data) {
+ super(guild.client);
+
+ /**
+ * The guild for this welcome screen
+ * @type {Guild}
+ */
+ this.guild = guild;
+
+ /**
+ * The description of this welcome screen
+ * @type {?string}
+ */
+ this.description = data.description ?? null;
+
+ /**
+ * Collection of welcome channels belonging to this welcome screen
+ * @type {Collection}
+ */
+ this.welcomeChannels = new Collection();
+
+ for (const channel of data.welcome_channels) {
+ const welcomeChannel = new WelcomeChannel(this.guild, channel);
+ this.welcomeChannels.set(welcomeChannel.channelID, welcomeChannel);
+ }
+ }
+
+ /**
+ * Whether the welcome screen is enabled on the guild or not
+ * @type {boolean}
+ */
+ get enabled() {
+ return this.guild.features.includes('WELCOME_SCREEN_ENABLED');
+ }
+}
+
+module.exports = WelcomeScreen;
diff --git a/typings/index.d.ts b/typings/index.d.ts
index 694d6f8f433b..a7bfef6c343c 100644
--- a/typings/index.d.ts
+++ b/typings/index.d.ts
@@ -187,6 +187,17 @@ declare module 'discord.js' {
public static resolve(bit?: BitFieldResolvable): number;
}
+ export abstract class AnonymousGuild extends BaseGuild {
+ public banner: string | null;
+ public description: string | null;
+ public nsfwLevel: NSFWLevel;
+ public splash: string | null;
+ public vanityURLCode: string | null;
+ public verificationLevel: VerificationLevel;
+ public bannerURL(options?: StaticImageURLOptions): string | null;
+ public splashURL(options?: StaticImageURLOptions): string | null;
+ }
+
export class APIMessage {
constructor(target: MessageTarget, options: MessageOptions | WebhookMessageOptions);
public data: unknown | null;
@@ -282,7 +293,7 @@ declare module 'discord.js' {
public toJSON(...props: { [key: string]: boolean | string }[]): unknown;
}
- export class BaseGuild extends Base {
+ export abstract class BaseGuild extends Base {
public readonly createdAt: Date;
public readonly createdTimestamp: number;
public features: GuildFeatures[];
@@ -759,7 +770,7 @@ declare module 'discord.js' {
export class Emoji extends Base {
constructor(client: Client, emoji: unknown);
- public animated: boolean;
+ public animated: boolean | null;
public readonly createdAt: Date | null;
public readonly createdTimestamp: number | null;
public deleted: boolean;
@@ -771,7 +782,7 @@ declare module 'discord.js' {
public toString(): string;
}
- export class Guild extends BaseGuild {
+ export class Guild extends AnonymousGuild {
constructor(client: Client, data: unknown);
private _sortedRoles(): Collection;
private _sortedChannels(channel: Channel): Collection;
@@ -783,13 +794,11 @@ declare module 'discord.js' {
public approximateMemberCount: number | null;
public approximatePresenceCount: number | null;
public available: boolean;
- public banner: string | null;
public bans: GuildBanManager;
public channels: GuildChannelManager;
public commands: GuildApplicationCommandManager;
public defaultMessageNotifications: DefaultMessageNotificationLevel | number;
public deleted: boolean;
- public description: string | null;
public discoverySplash: string | null;
public emojis: GuildEmojiManager;
public explicitContentFilter: ExplicitContentFilterLevel;
@@ -802,7 +811,6 @@ declare module 'discord.js' {
public memberCount: number;
public members: GuildMemberManager;
public mfaLevel: MFALevel;
- public nsfwLevel: NSFWLevel;
public ownerID: Snowflake;
public preferredLocale: string;
public premiumSubscriptionCount: number | null;
@@ -815,26 +823,23 @@ declare module 'discord.js' {
public rulesChannelID: Snowflake | null;
public readonly shard: WebSocketShard;
public shardID: number;
- public splash: string | null;
public stageInstances: StageInstanceManager;
public readonly systemChannel: TextChannel | null;
public systemChannelFlags: Readonly;
public systemChannelID: Snowflake | null;
- public vanityURLCode: string | null;
public vanityURLUses: number | null;
- public verificationLevel: VerificationLevel;
public readonly voiceAdapterCreator: DiscordGatewayAdapterCreator;
public readonly voiceStates: VoiceStateManager;
public readonly widgetChannel: TextChannel | null;
public widgetChannelID: Snowflake | null;
public widgetEnabled: boolean | null;
public addMember(user: UserResolvable, options: AddGuildMemberOptions): Promise;
- public bannerURL(options?: StaticImageURLOptions): string | null;
public createIntegration(data: IntegrationData, reason?: string): Promise;
public createTemplate(name: string, description?: string): Promise;
public delete(): Promise;
public discoverySplashURL(options?: StaticImageURLOptions): string | null;
public edit(data: GuildEditData, reason?: string): Promise;
+ public editWelcomeScreen(data: WelcomeScreenEditData): Promise;
public equals(guild: Guild): boolean;
public fetchAuditLogs(options?: GuildAuditLogsFetchOptions): Promise;
public fetchIntegrations(): Promise>;
@@ -845,6 +850,7 @@ declare module 'discord.js' {
public fetchVanityData(): Promise;
public fetchVoiceRegions(): Promise>;
public fetchWebhooks(): Promise>;
+ public fetchWelcomeScreen(): Promise;
public fetchWidget(): Promise;
public leave(): Promise;
public setAFKChannel(afkChannel: ChannelResolvable | null, reason?: string): Promise;
@@ -872,7 +878,6 @@ declare module 'discord.js' {
public setSystemChannelFlags(systemChannelFlags: SystemChannelFlagsResolvable, reason?: string): Promise;
public setVerificationLevel(verificationLevel: VerificationLevel | number, reason?: string): Promise;
public setWidget(widget: GuildWidgetData, reason?: string): Promise;
- public splashURL(options?: StaticImageURLOptions): string | null;
public toJSON(): unknown;
}
@@ -1176,7 +1181,7 @@ declare module 'discord.js' {
public createdTimestamp: number | null;
public readonly expiresAt: Date | null;
public readonly expiresTimestamp: number | null;
- public guild: Guild | null;
+ public guild: InviteGuild | Guild | null;
public inviter: User | null;
public maxAge: number | null;
public maxUses: number | null;
@@ -1207,6 +1212,11 @@ declare module 'discord.js' {
public readonly guild: Guild | null;
}
+ export class InviteGuild extends AnonymousGuild {
+ constructor(client: Client, data: unknown);
+ public welcomeScreen: WelcomeScreen | null;
+ }
+
export class Message extends Base {
constructor(client: Client, data: unknown, channel: TextChannel | DMChannel | NewsChannel);
private patch(data: unknown): Message;
@@ -2117,6 +2127,22 @@ declare module 'discord.js' {
public activity?: WidgetActivity;
}
+ export class WelcomeChannel extends Base {
+ private _emoji: unknown;
+ public channelID: Snowflake;
+ public guild: Guild | InviteGuild;
+ public description: string;
+ public readonly channel: TextChannel | NewsChannel | null;
+ public readonly emoji: GuildEmoji | Emoji;
+ }
+
+ export class WelcomeScreen extends Base {
+ public readonly enabled: boolean;
+ public guild: Guild | InviteGuild;
+ public description: string | null;
+ public welcomeChannels: Collection;
+ }
+
//#endregion
//#region Collections
@@ -2714,6 +2740,7 @@ declare module 'discord.js' {
position?: number;
}
+ type GuildTextChannelResolvable = TextChannel | NewsChannel | Snowflake;
type ChannelResolvable = Channel | Snowflake;
interface ChannelWebhookCreateOptions {
@@ -3967,6 +3994,18 @@ declare module 'discord.js' {
position: number;
}
+ interface WelcomeChannelData {
+ description: string;
+ channel: GuildChannelResolvable;
+ emoji?: EmojiIdentifierResolvable;
+ }
+
+ interface WelcomeScreenEditData {
+ enabled?: boolean;
+ description?: string;
+ welcomeChannels?: WelcomeChannelData[];
+ }
+
type WSEventType =
| 'READY'
| 'RESUMED'