From 546ac439115f4017f01e0c02dee2ff80b789c28b Mon Sep 17 00:00:00 2001 From: Elysia <71698422+aiko-chan-ai@users.noreply.github.com> Date: Mon, 2 Jan 2023 22:21:15 +0700 Subject: [PATCH] feat: backport guild forum support to v13 (#8651) Co-authored-by: Jaworek Co-authored-by: Sugden <28943913+NotSugden@users.noreply.github.com> --- src/client/actions/WebhooksUpdate.js | 2 +- src/errors/Messages.js | 2 + src/managers/GuildChannelManager.js | 44 +++- src/managers/GuildForumThreadManager.js | 90 ++++++++ src/managers/GuildTextThreadManager.js | 98 +++++++++ src/managers/ThreadManager.js | 80 ------- src/structures/BaseGuildTextChannel.js | 6 +- src/structures/CategoryChannel.js | 13 +- src/structures/Channel.js | 18 ++ src/structures/ForumChannel.js | 264 ++++++++++++++++++++++++ src/structures/Guild.js | 2 +- src/structures/GuildChannel.js | 1 + src/structures/Message.js | 20 +- src/structures/MessagePayload.js | 3 + src/structures/PartialGroupDMChannel.js | 3 + src/structures/ThreadChannel.js | 68 +++++- src/structures/Webhook.js | 1 + src/structures/WelcomeChannel.js | 2 +- src/util/ChannelFlags.js | 45 ++++ src/util/Constants.js | 21 ++ src/util/Util.js | 81 +++++++- typings/enums.d.ts | 12 ++ typings/index.d.ts | 147 +++++++++++-- typings/index.test-d.ts | 7 +- 24 files changed, 903 insertions(+), 127 deletions(-) create mode 100644 src/managers/GuildForumThreadManager.js create mode 100644 src/managers/GuildTextThreadManager.js create mode 100644 src/structures/ForumChannel.js create mode 100644 src/util/ChannelFlags.js diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 6c9aa3583..b23fe23b2 100644 --- a/src/client/actions/WebhooksUpdate.js +++ b/src/client/actions/WebhooksUpdate.js @@ -10,7 +10,7 @@ class WebhooksUpdate extends Action { /** * Emitted whenever a channel has its webhooks changed. * @event Client#webhookUpdate - * @param {TextChannel|NewsChannel|VoiceChannel} channel The channel that had a webhook update + * @param {TextChannel|NewsChannel|VoiceChannel|ForumChannel} channel The channel that had a webhook update */ if (channel) client.emit(Events.WEBHOOKS_UPDATE, channel); } diff --git a/src/errors/Messages.js b/src/errors/Messages.js index 7227db7bb..cd9def472 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -166,6 +166,8 @@ const Messages = { NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`, SWEEP_FILTER_RETURN: 'The return value of the sweepFilter function was not false or a Function', + + GUILD_FORUM_MESSAGE_REQUIRED: 'You must provide a message to create a guild forum thread', }; for (const [name, message] of Object.entries(Messages)) register(name, message); diff --git a/src/managers/GuildChannelManager.js b/src/managers/GuildChannelManager.js index 2967402a5..0b4ad0def 100644 --- a/src/managers/GuildChannelManager.js +++ b/src/managers/GuildChannelManager.js @@ -9,10 +9,17 @@ const GuildChannel = require('../structures/GuildChannel'); const PermissionOverwrites = require('../structures/PermissionOverwrites'); const ThreadChannel = require('../structures/ThreadChannel'); const Webhook = require('../structures/Webhook'); -const { ThreadChannelTypes, ChannelTypes, VideoQualityModes } = require('../util/Constants'); +const ChannelFlags = require('../util/ChannelFlags'); +const { + ThreadChannelTypes, + ChannelTypes, + VideoQualityModes, + SortOrderTypes, + ForumLayoutTypes, +} = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const Util = require('../util/Util'); -const { resolveAutoArchiveMaxLimit } = require('../util/Util'); +const { resolveAutoArchiveMaxLimit, transformGuildForumTag, transformGuildDefaultReaction } = require('../util/Util'); let cacheWarningEmitted = false; let storeChannelDeprecationEmitted = false; @@ -73,8 +80,9 @@ class GuildChannelManager extends CachedManager { * Data that can be resolved to give a Guild Channel object. This can be: * * A GuildChannel object * * A ThreadChannel object + * * A ForumChannel object * * A Snowflake - * @typedef {GuildChannel|ThreadChannel|Snowflake} GuildChannelResolvable + * @typedef {GuildChannel|ThreadChannel|ForumChannel|Snowflake} GuildChannelResolvable */ /** @@ -138,6 +146,11 @@ class GuildChannelManager extends CachedManager { position, rateLimitPerUser, rtcRegion, + videoQualityMode, + availableTags, + defaultReactionEmoji, + defaultSortOrder, + defaultForumLayout, reason, } = {}, ) { @@ -145,6 +158,13 @@ class GuildChannelManager extends CachedManager { permissionOverwrites &&= permissionOverwrites.map(o => PermissionOverwrites.resolve(o, this.guild)); const intType = typeof type === 'number' ? type : ChannelTypes[type] ?? ChannelTypes.GUILD_TEXT; + const videoMode = typeof videoQualityMode === 'number' ? videoQualityMode : VideoQualityModes[videoQualityMode]; + + const sortMode = typeof defaultSortOrder === 'number' ? defaultSortOrder : SortOrderTypes[defaultSortOrder]; + + const layoutMode = + typeof defaultForumLayout === 'number' ? defaultForumLayout : ForumLayoutTypes[defaultForumLayout]; + if (intType === ChannelTypes.GUILD_STORE && !storeChannelDeprecationEmitted) { storeChannelDeprecationEmitted = true; process.emitWarning( @@ -167,6 +187,11 @@ class GuildChannelManager extends CachedManager { permission_overwrites: permissionOverwrites, rate_limit_per_user: rateLimitPerUser, rtc_region: rtcRegion, + video_quality_mode: videoMode, + available_tags: availableTags?.map(availableTag => transformGuildForumTag(availableTag)), + default_reaction_emoji: defaultReactionEmoji && transformGuildDefaultReaction(defaultReactionEmoji), + default_sort_order: sortMode, + default_forum_layout: layoutMode, }, reason, }); @@ -175,7 +200,7 @@ class GuildChannelManager extends CachedManager { /** * Creates a webhook for the channel. - * @param {TextChannel|NewsChannel|VoiceChannel|Snowflake} channel The channel to create the webhook for + * @param {GuildChannelResolvable} channel The channel to create the webhook for * @param {string} name The name of the webhook * @param {ChannelWebhookCreateOptions} [options] Options for creating the webhook * @returns {Promise} Returns the created Webhook @@ -224,6 +249,11 @@ class GuildChannelManager extends CachedManager { * The default auto archive duration for all new threads in this channel * @property {?string} [rtcRegion] The RTC region of the channel * @property {?VideoQualityMode|number} [videoQualityMode] The camera video quality mode of the channel + * @property {ChannelFlagsResolvable} [flags] The flags to set on the channel + * @property {GuildForumTagData[]} [availableTags] The tags to set as available in a forum channel + * @property {?DefaultReactionEmoji} [defaultReactionEmoji] The emoji to set as the default reaction emoji + * @property {number} [defaultThreadRateLimitPerUser] The rate limit per user (slowmode) to set on forum posts + * @property {?SortOrderType} [defaultSortOrder] The default sort order mode to set on the channel */ /** @@ -282,6 +312,12 @@ class GuildChannelManager extends CachedManager { rate_limit_per_user: data.rateLimitPerUser, default_auto_archive_duration: defaultAutoArchiveDuration, permission_overwrites, + available_tags: data.availableTags?.map(availableTag => transformGuildForumTag(availableTag)), + default_reaction_emoji: data.defaultReactionEmoji && transformGuildDefaultReaction(data.defaultReactionEmoji), + default_thread_rate_limit_per_user: data.defaultThreadRateLimitPerUser, + flags: 'flags' in data ? ChannelFlags.resolve(data.flags) : undefined, + default_sort_order: + typeof data.defaultSortOrder === 'string' ? SortOrderTypes[data.defaultSortOrder] : data.defaultSortOrder, }, reason, }); diff --git a/src/managers/GuildForumThreadManager.js b/src/managers/GuildForumThreadManager.js new file mode 100644 index 000000000..8ffa21ed0 --- /dev/null +++ b/src/managers/GuildForumThreadManager.js @@ -0,0 +1,90 @@ +'use strict'; + +const ThreadManager = require('./ThreadManager'); +const { TypeError } = require('../errors'); +const MessagePayload = require('../structures/MessagePayload'); +const { resolveAutoArchiveMaxLimit } = require('../util/Util'); + +/** + * Manages API methods for threads in forum channels and stores their cache. + * @extends {ThreadManager} + */ +class GuildForumThreadManager extends ThreadManager { + /** + * The channel this Manager belongs to + * @name GuildForumThreadManager#channel + * @type {ForumChannel} + */ + + /** + * @typedef {BaseMessageOptions} GuildForumThreadMessageCreateOptions + * @property {StickerResolvable} [stickers] The stickers to send with the message + * @property {BitFieldResolvable} [flags] The flags to send with the message + */ + + /** + * Options for creating a thread. + * @typedef {StartThreadOptions} GuildForumThreadCreateOptions + * @property {GuildForumThreadMessageCreateOptions|MessagePayload} message The message associated with the thread post + * @property {Snowflake[]} [appliedTags] The tags to apply to the thread + */ + + /** + * Creates a new thread in the channel. + * @param {GuildForumThreadCreateOptions} [options] Options to create a new thread + * @returns {Promise} + * @example + * // Create a new forum post + * forum.threads + * .create({ + * name: 'Food Talk', + * autoArchiveDuration: 60, + * message: { + * content: 'Discuss your favorite food!', + * }, + * reason: 'Needed a separate thread for food', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + */ + async create({ + name, + autoArchiveDuration = this.channel.defaultAutoArchiveDuration, + message, + reason, + rateLimitPerUser, + appliedTags, + } = {}) { + if (!message) { + throw new TypeError('GUILD_FORUM_MESSAGE_REQUIRED'); + } + + let messagePayload; + + if (message instanceof MessagePayload) { + messagePayload = message.resolveData(); + } else { + messagePayload = MessagePayload.create(this, message).resolveData(); + } + + const { data: body, files } = await messagePayload.resolveFiles(); + + if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild); + + const data = await this.client.api.channels(this.channel.id).threads.post({ + data: { + name, + auto_archive_duration: autoArchiveDuration, + rate_limit_per_user: rateLimitPerUser, + applied_tags: appliedTags, + message: body, + }, + files, + reason, + }); + + return this.client.actions.ThreadCreate.handle(data).thread; + } +} + +module.exports = GuildForumThreadManager; diff --git a/src/managers/GuildTextThreadManager.js b/src/managers/GuildTextThreadManager.js new file mode 100644 index 000000000..c11820d08 --- /dev/null +++ b/src/managers/GuildTextThreadManager.js @@ -0,0 +1,98 @@ +'use strict'; + +const ThreadManager = require('./ThreadManager'); +const { TypeError } = require('../errors'); +const { ChannelTypes } = require('../util/Constants'); +const { resolveAutoArchiveMaxLimit } = require('../util/Util'); + +/** + * Manages API methods for {@link ThreadChannel} objects and stores their cache. + * @extends {ThreadManager} + */ +class GuildTextThreadManager extends ThreadManager { + /** + * The channel this Manager belongs to + * @name GuildTextThreadManager#channel + * @type {TextChannel|NewsChannel} + */ + + /** + * Options for creating a thread. Only one of `startMessage` or `type` can be defined. + * @typedef {StartThreadOptions} GuildTextThreadCreateOptions + * @property {MessageResolvable} [startMessage] The message to start a thread from. If this is defined then type + * of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored + * @property {ThreadChannelTypes|number} [type] The type of thread to create. Defaults to `GUILD_PUBLIC_THREAD` if + * created in a {@link TextChannel} When creating threads in a {@link NewsChannel} this is ignored and is always + * `GUILD_NEWS_THREAD` + * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread + * Can only be set when type will be `GUILD_PRIVATE_THREAD` + * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds + */ + + /** + * Creates a new thread in the channel. + * @param {GuildTextThreadCreateOptions} [options] Options to create a new thread + * @returns {Promise} + * @example + * // Create a new public thread + * channel.threads + * .create({ + * name: 'food-talk', + * autoArchiveDuration: 60, + * reason: 'Needed a separate thread for food', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + * @example + * // Create a new private thread + * channel.threads + * .create({ + * name: 'mod-talk', + * autoArchiveDuration: 60, + * type: 'GUILD_PRIVATE_THREAD', + * reason: 'Needed a separate thread for moderation', + * }) + * .then(threadChannel => console.log(threadChannel)) + * .catch(console.error); + */ + async create({ + name, + autoArchiveDuration = this.channel.defaultAutoArchiveDuration, + startMessage, + type, + invitable, + reason, + rateLimitPerUser, + } = {}) { + let path = this.client.api.channels(this.channel.id); + if (type && typeof type !== 'string' && typeof type !== 'number') { + throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number'); + } + let resolvedType = + this.channel.type === 'GUILD_NEWS' ? ChannelTypes.GUILD_NEWS_THREAD : ChannelTypes.GUILD_PUBLIC_THREAD; + if (startMessage) { + const startMessageId = this.channel.messages.resolveId(startMessage); + if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable'); + path = path.messages(startMessageId); + } else if (this.channel.type !== 'GUILD_NEWS') { + resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType; + } + + if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild); + + const data = await path.threads.post({ + data: { + name, + auto_archive_duration: autoArchiveDuration, + type: resolvedType, + invitable: resolvedType === ChannelTypes.GUILD_PRIVATE_THREAD ? invitable : undefined, + rate_limit_per_user: rateLimitPerUser, + }, + reason, + }); + + return this.client.actions.ThreadCreate.handle(data).thread; + } +} + +module.exports = GuildTextThreadManager; diff --git a/src/managers/ThreadManager.js b/src/managers/ThreadManager.js index 0568c6c39..fc37a7b1a 100644 --- a/src/managers/ThreadManager.js +++ b/src/managers/ThreadManager.js @@ -4,8 +4,6 @@ const { Collection } = require('@discordjs/collection'); const CachedManager = require('./CachedManager'); const { TypeError } = require('../errors'); const ThreadChannel = require('../structures/ThreadChannel'); -const { ChannelTypes } = require('../util/Constants'); -const { resolveAutoArchiveMaxLimit } = require('../util/Util'); /** * Manages API methods for {@link ThreadChannel} objects and stores their cache. @@ -60,84 +58,6 @@ class ThreadManager extends CachedManager { * @returns {?Snowflake} */ - /** - * Options for creating a thread. Only one of `startMessage` or `type` can be defined. - * @typedef {StartThreadOptions} ThreadCreateOptions - * @property {MessageResolvable} [startMessage] The message to start a thread from. If this is defined then type - * of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored - * @property {ThreadChannelTypes|number} [type] The type of thread to create. Defaults to `GUILD_PUBLIC_THREAD` if - * created in a {@link TextChannel} When creating threads in a {@link NewsChannel} this is ignored and is always - * `GUILD_NEWS_THREAD` - * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread - * Can only be set when type will be `GUILD_PRIVATE_THREAD` - * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds - */ - - /** - * Creates a new thread in the channel. - * @param {ThreadCreateOptions} [options] Options to create a new thread - * @returns {Promise} - * @example - * // Create a new public thread - * channel.threads - * .create({ - * name: 'food-talk', - * autoArchiveDuration: 60, - * reason: 'Needed a separate thread for food', - * }) - * .then(threadChannel => console.log(threadChannel)) - * .catch(console.error); - * @example - * // Create a new private thread - * channel.threads - * .create({ - * name: 'mod-talk', - * autoArchiveDuration: 60, - * type: 'GUILD_PRIVATE_THREAD', - * reason: 'Needed a separate thread for moderation', - * }) - * .then(threadChannel => console.log(threadChannel)) - * .catch(console.error); - */ - async create({ - name, - autoArchiveDuration = this.channel.defaultAutoArchiveDuration, - startMessage, - type, - invitable, - reason, - rateLimitPerUser, - } = {}) { - let path = this.client.api.channels(this.channel.id); - if (type && typeof type !== 'string' && typeof type !== 'number') { - throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number'); - } - let resolvedType = - this.channel.type === 'GUILD_NEWS' ? ChannelTypes.GUILD_NEWS_THREAD : ChannelTypes.GUILD_PUBLIC_THREAD; - if (startMessage) { - const startMessageId = this.channel.messages.resolveId(startMessage); - if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable'); - path = path.messages(startMessageId); - } else if (this.channel.type !== 'GUILD_NEWS') { - resolvedType = typeof type === 'string' ? ChannelTypes[type] : type ?? resolvedType; - } - - if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild); - - const data = await path.threads.post({ - data: { - name, - auto_archive_duration: autoArchiveDuration, - type: resolvedType, - invitable: resolvedType === ChannelTypes.GUILD_PRIVATE_THREAD ? invitable : undefined, - rate_limit_per_user: rateLimitPerUser, - }, - reason, - }); - - return this.client.actions.ThreadCreate.handle(data).thread; - } - /** * The options for fetching multiple threads, the properties are mutually exclusive * @typedef {Object} FetchThreadsOptions diff --git a/src/structures/BaseGuildTextChannel.js b/src/structures/BaseGuildTextChannel.js index 5e71f54f9..ee2e8f8ab 100644 --- a/src/structures/BaseGuildTextChannel.js +++ b/src/structures/BaseGuildTextChannel.js @@ -2,8 +2,8 @@ const GuildChannel = require('./GuildChannel'); const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const GuildTextThreadManager = require('../managers/GuildTextThreadManager'); const MessageManager = require('../managers/MessageManager'); -const ThreadManager = require('../managers/ThreadManager'); /** * Represents a text-based guild channel on Discord. @@ -22,9 +22,9 @@ class BaseGuildTextChannel extends GuildChannel { /** * A manager of the threads belonging to this channel - * @type {ThreadManager} + * @type {GuildTextThreadManager} */ - this.threads = new ThreadManager(this); + this.threads = new GuildTextThreadManager(this); /** * If the guild considers this channel NSFW diff --git a/src/structures/CategoryChannel.js b/src/structures/CategoryChannel.js index ce5b5f61c..831b787bc 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -30,16 +30,25 @@ class CategoryChannel extends GuildChannel { /** * Options for creating a channel using {@link CategoryChannel#createChannel}. * @typedef {Object} CategoryCreateChannelOptions + * @property {string} [name] The name of the new channel * @property {ChannelType|number} [type='GUILD_TEXT'] The type of the new channel. + * @property {number} [position] Position of the new channel * @property {string} [topic] The topic for the new channel * @property {boolean} [nsfw] Whether the new channel is NSFW * @property {number} [bitrate] Bitrate of the new channel in bits (only voice) * @property {number} [userLimit] Maximum amount of users allowed in the new channel (only voice) * @property {OverwriteResolvable[]|Collection} [permissionOverwrites] * Permission overwrites of the new channel - * @property {number} [position] Position of the new channel * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the new channel in seconds - * @property {string} [rtcRegion] The specific region of the new channel. + * @property {ThreadAutoArchiveDuration} [defaultAutoArchiveDuration] + * The default auto archive duration for all new threads in this channel + * @property {?string} [rtcRegion] The specific region of the new channel + * @property {?VideoQualityMode|number} [videoQualityMode] The camera video quality mode of the new channel + * @property {ChannelFlagsResolvable} [flags] The flags to set on the new channel + * @property {GuildForumTagData[]} [availableTags] The tags to set as available in a forum channel + * @property {?DefaultReactionEmoji} [defaultReactionEmoji] The emoji to set as the default reaction emoji + * @property {number} [defaultThreadRateLimitPerUser] The rate limit per user (slowmode) to set on forum posts + * @property {?SortOrderType} [defaultSortOrder] The default sort order mode to set on the new channel * @property {string} [reason] Reason for creating the new channel */ diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 1832237ae..50dfcefb5 100644 --- a/src/structures/Channel.js +++ b/src/structures/Channel.js @@ -11,6 +11,8 @@ let TextChannel; let ThreadChannel; let VoiceChannel; let DirectoryChannel; +let ForumChannel; +const ChannelFlags = require('../util/ChannelFlags'); const { ChannelTypes, ThreadChannelTypes, VoiceBasedChannelTypes } = require('../util/Constants'); const SnowflakeUtil = require('../util/SnowflakeUtil'); @@ -47,6 +49,17 @@ class Channel extends Base { * @type {Snowflake} */ this.id = data.id; + + if ('flags' in data) { + /** + * The flags that are applied to the channel. + * This is only `null` in a {@link PartialGroupDMChannel}. In all other cases, it is not `null`. + * @type {?Readonly} + */ + this.flags = new ChannelFlags(data.flags).freeze(); + } else { + this.flags ??= new ChannelFlags().freeze(); + } } /** @@ -183,6 +196,7 @@ class Channel extends Base { ThreadChannel ??= require('./ThreadChannel'); VoiceChannel ??= require('./VoiceChannel'); DirectoryChannel ??= require('./DirectoryChannel'); + ForumChannel ??= require('./ForumChannel'); let channel; if (!data.guild_id && !guild) { @@ -232,6 +246,10 @@ class Channel extends Base { case ChannelTypes.GUILD_DIRECTORY: channel = new DirectoryChannel(client, data); break; + + case ChannelTypes.GUILD_FORUM: + channel = new ForumChannel(guild, data, client); + break; } if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel); } diff --git a/src/structures/ForumChannel.js b/src/structures/ForumChannel.js new file mode 100644 index 000000000..10802caf3 --- /dev/null +++ b/src/structures/ForumChannel.js @@ -0,0 +1,264 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const GuildForumThreadManager = require('../managers/GuildForumThreadManager'); +const { SortOrderTypes, ForumLayoutTypes } = require('../util/Constants'); +const { transformAPIGuildForumTag, transformAPIGuildDefaultReaction } = require('../util/Util'); + +/** + * @typedef {Object} GuildForumTagEmoji + * @property {?Snowflake} id The id of a guild's custom emoji + * @property {?string} name The unicode character of the emoji + */ + +/** + * @typedef {Object} GuildForumTag + * @property {Snowflake} id The id of the tag + * @property {string} name The name of the tag + * @property {boolean} moderated Whether this tag can only be added to or removed from threads + * by a member with the `ManageThreads` permission + * @property {?GuildForumTagEmoji} emoji The emoji of this tag + */ + +/** + * @typedef {Object} GuildForumTagData + * @property {Snowflake} [id] The id of the tag + * @property {string} name The name of the tag + * @property {boolean} [moderated] Whether this tag can only be added to or removed from threads + * by a member with the `ManageThreads` permission + * @property {?GuildForumTagEmoji} [emoji] The emoji of this tag + */ + +/** + * @typedef {Object} DefaultReactionEmoji + * @property {?Snowflake} id The id of a guild's custom emoji + * @property {?string} name The unicode character of the emoji + */ + +/** + * Represents a channel that only contains threads + * @extends {GuildChannel} + * @implements {TextBasedChannel} + */ +class ForumChannel extends GuildChannel { + constructor(guild, data, client) { + super(guild, data, client, false); + + /** + * A manager of the threads belonging to this channel + * @type {GuildForumThreadManager} + */ + this.threads = new GuildForumThreadManager(this); + + this._patch(data); + } + + _patch(data) { + super._patch(data); + if ('available_tags' in data) { + /** + * The set of tags that can be used in this channel. + * @type {GuildForumTag[]} + */ + this.availableTags = data.available_tags.map(tag => transformAPIGuildForumTag(tag)); + } else { + this.availableTags ??= []; + } + + if ('default_reaction_emoji' in data) { + /** + * The emoji to show in the add reaction button on a thread in a guild forum channel + * @type {?DefaultReactionEmoji} + */ + this.defaultReactionEmoji = + data.default_reaction_emoji && transformAPIGuildDefaultReaction(data.default_reaction_emoji); + } else { + this.defaultReactionEmoji ??= null; + } + + if ('default_thread_rate_limit_per_user' in data) { + /** + * The initial rate limit per user (slowmode) to set on newly created threads in a channel. + * @type {?number} + */ + this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user; + } else { + this.defaultThreadRateLimitPerUser ??= null; + } + + if ('rate_limit_per_user' in data) { + /** + * The rate limit per user (slowmode) for this channel. + * @type {?number} + */ + this.rateLimitPerUser = data.rate_limit_per_user; + } else { + this.rateLimitPerUser ??= null; + } + + if ('default_auto_archive_duration' in data) { + /** + * The default auto archive duration for newly created threads in this channel. + * @type {?ThreadAutoArchiveDuration} + */ + this.defaultAutoArchiveDuration = data.default_auto_archive_duration; + } else { + this.defaultAutoArchiveDuration ??= null; + } + + if ('nsfw' in data) { + /** + * If this channel is considered NSFW. + * @type {boolean} + */ + this.nsfw = data.nsfw; + } else { + this.nsfw ??= false; + } + + if ('topic' in data) { + /** + * The topic of this channel. + * @type {?string} + */ + this.topic = data.topic; + } + + if ('default_sort_order' in data) { + /** + * The default sort order mode used to order posts + * @type {?SortOrderType} + */ + this.defaultSortOrder = SortOrderTypes[data.default_sort_order]; + } else { + this.defaultSortOrder ??= null; + } + + /** + * The default layout type used to display posts + * @type {ForumLayoutType} + */ + this.defaultForumLayout = ForumLayoutTypes[data.default_forum_layout]; + } + + /** + * Sets the available tags for this forum channel + * @param {GuildForumTagData[]} availableTags The tags to set as available in this channel + * @param {string} [reason] Reason for changing the available tags + * @returns {Promise} + */ + setAvailableTags(availableTags, reason) { + return this.edit({ availableTags }, reason); + } + + /** + * Sets the default reaction emoji for this channel + * @param {?DefaultReactionEmoji} defaultReactionEmoji The emoji to set as the default reaction emoji + * @param {string} [reason] Reason for changing the default reaction emoji + * @returns {Promise} + */ + setDefaultReactionEmoji(defaultReactionEmoji, reason) { + return this.edit({ defaultReactionEmoji }, reason); + } + + /** + * Sets the default rate limit per user (slowmode) for new threads in this channel + * @param {number} defaultThreadRateLimitPerUser The rate limit to set on newly created threads in this channel + * @param {string} [reason] Reason for changing the default rate limit + * @returns {Promise} + */ + setDefaultThreadRateLimitPerUser(defaultThreadRateLimitPerUser, reason) { + return this.edit({ defaultThreadRateLimitPerUser }, reason); + } + + /** + * Sets the default sort order mode used to order posts + * @param {?SortOrderType} defaultSortOrder The default sort order mode to set on this channel + * @param {string} [reason] Reason for changing the default sort order + * @returns {Promise} + */ + setDefaultSortOrder(defaultSortOrder, reason) { + return this.edit({ defaultSortOrder }, reason); + } + + /** + * Sets the default forum layout type used to display posts + * @param {ForumLayoutType} defaultForumLayout The default forum layout type to set on this channel + * @param {string} [reason] Reason for changing the default forum layout + * @returns {Promise} + */ + setDefaultForumLayout(defaultForumLayout, reason) { + return this.edit({ defaultForumLayout }, reason); + } + + /** + * Creates an invite to this guild channel. + * @param {CreateInviteOptions} [options={}] The options for creating the invite + * @returns {Promise} + * @example + * // Create an invite to a channel + * channel.createInvite() + * .then(invite => console.log(`Created an invite with a code of ${invite.code}`)) + * .catch(console.error); + */ + createInvite(options) { + return this.guild.invites.create(this.id, options); + } + + /** + * Fetches a collection of invites to this guild channel. + * Resolves with a collection mapping invites by their codes. + * @param {boolean} [cache=true] Whether or not to cache the fetched invites + * @returns {Promise>} + */ + fetchInvites(cache = true) { + return this.guild.invites.fetch({ channelId: this.id, cache }); + } + + /** + * Sets the default auto archive duration for all newly created threads in this channel. + * @param {ThreadAutoArchiveDuration} defaultAutoArchiveDuration The new default auto archive duration + * @param {string} [reason] Reason for changing the channel's default auto archive duration + * @returns {Promise} + */ + setDefaultAutoArchiveDuration(defaultAutoArchiveDuration, reason) { + return this.edit({ defaultAutoArchiveDuration }, reason); + } + + /** + * Sets a new topic for the guild channel. + * @param {?string} topic The new topic for the guild channel + * @param {string} [reason] Reason for changing the guild channel's topic + * @returns {Promise} + * @example + * // Set a new channel topic + * channel.setTopic('needs more rate limiting') + * .then(newChannel => console.log(`Channel's new topic is ${newChannel.topic}`)) + * .catch(console.error); + */ + setTopic(topic, reason) { + return this.edit({ topic }, reason); + } + + // These are here only for documentation purposes - they are implemented by TextBasedChannel + /* eslint-disable no-empty-function */ + createWebhook() {} + fetchWebhooks() {} + setNSFW() {} + setRateLimitPerUser() {} +} + +TextBasedChannel.applyToClass(ForumChannel, true, [ + 'send', + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'sendTyping', + 'createMessageCollector', + 'awaitMessages', + 'createMessageComponentCollector', + 'awaitMessageComponent', +]); + +module.exports = ForumChannel; diff --git a/src/structures/Guild.js b/src/structures/Guild.js index ff06ea5ed..bfb0c4830 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -562,7 +562,7 @@ class Guild extends AnonymousGuild { /** * Widget channel for this guild - * @type {?TextChannel} + * @type {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel)} * @readonly */ get widgetChannel() { diff --git a/src/structures/GuildChannel.js b/src/structures/GuildChannel.js index 37dace27e..2a796094e 100644 --- a/src/structures/GuildChannel.js +++ b/src/structures/GuildChannel.js @@ -14,6 +14,7 @@ const Permissions = require('../util/Permissions'); * - {@link NewsChannel} * - {@link StoreChannel} * - {@link StageChannel} + * - {@link ForumChannel} * @extends {Channel} * @abstract */ diff --git a/src/structures/Message.js b/src/structures/Message.js index fd18ec6b1..2fb213bd2 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -58,6 +58,17 @@ class Message extends Base { */ this.id = data.id; + if ('position' in data) { + /** + * A generally increasing integer (there may be gaps or duplicates) that represents + * the approximate position of the message in a thread. + * @type {?number} + */ + this.position = data.position; + } else { + this.position ??= null; + } + /** * The timestamp the message was sent at * @type {number} @@ -855,9 +866,10 @@ class Message extends Base { * archived. This can be: * * `60` (1 hour) * * `1440` (1 day) - * * `4320` (3 days) This is only available when the guild has the `THREE_DAY_THREAD_ARCHIVE` feature. - * * `10080` (7 days) This is only available when the guild has the `SEVEN_DAY_THREAD_ARCHIVE` feature. - * * `'MAX'` Based on the guild's features + * * `4320` (3 days) + * * `10080` (7 days) + * * `'MAX'` (7 days) + * This option is deprecated and will be removed in the next major version. * @typedef {number|string} ThreadAutoArchiveDuration */ @@ -873,7 +885,7 @@ class Message extends Base { /** * Create a new public thread from this message - * @see ThreadManager#create + * @see GuildTextThreadManager#create * @param {StartThreadOptions} [options] Options for starting a thread on this message * @returns {Promise} */ diff --git a/src/structures/MessagePayload.js b/src/structures/MessagePayload.js index 2b062b031..a16d4f0ad 100644 --- a/src/structures/MessagePayload.js +++ b/src/structures/MessagePayload.js @@ -142,9 +142,11 @@ class MessagePayload { let username; let avatarURL; + let threadName; if (isWebhook) { username = this.options.username ?? this.target.name; if (this.options.avatarURL) avatarURL = this.options.avatarURL; + if (this.options.threadName) threadName = this.options.threadName; } let flags; @@ -208,6 +210,7 @@ class MessagePayload { message_reference, attachments: this.options.attachments, sticker_ids: this.options.stickers?.map(sticker => sticker.id ?? sticker), + thread_name: threadName, }; return this; } diff --git a/src/structures/PartialGroupDMChannel.js b/src/structures/PartialGroupDMChannel.js index 715d0b67d..d9a59db72 100644 --- a/src/structures/PartialGroupDMChannel.js +++ b/src/structures/PartialGroupDMChannel.js @@ -11,6 +11,9 @@ class PartialGroupDMChannel extends Channel { constructor(client, data) { super(client, data); + // No flags are present when fetching partial group DM channels. + this.flags = null; + /** * The name of this Group DM Channel * @type {?string} diff --git a/src/structures/ThreadChannel.js b/src/structures/ThreadChannel.js index 10afa20e6..8bb5b8f29 100644 --- a/src/structures/ThreadChannel.js +++ b/src/structures/ThreadChannel.js @@ -5,6 +5,7 @@ const TextBasedChannel = require('./interfaces/TextBasedChannel'); const { RangeError } = require('../errors'); const MessageManager = require('../managers/MessageManager'); const ThreadMemberManager = require('../managers/ThreadMemberManager'); +const ChannelFlags = require('../util/ChannelFlags'); const Permissions = require('../util/Permissions'); const { resolveAutoArchiveMaxLimit } = require('../util/Util'); @@ -158,9 +159,8 @@ class ThreadChannel extends Channel { if ('message_count' in data) { /** - * The approximate count of messages in this thread - * This stops counting at 50. If you need an approximate value higher than that, use - * `ThreadChannel#messages.cache.size` + * Threads created before July 1, 2022 may have an inaccurate count. + * If you need an approximate value higher than that, use `ThreadChannel#messages.cache.size` * @type {?number} */ this.messageCount = data.message_count; @@ -180,6 +180,27 @@ class ThreadChannel extends Channel { this.memberCount ??= null; } + if ('total_message_sent' in data) { + /** + * The number of messages ever sent in a thread, similar to {@link ThreadChannel#messageCount} except it + * will not decrement whenever a message is deleted + * @type {?number} + */ + this.totalMessageSent = data.total_message_sent; + } else { + this.totalMessageSent ??= null; + } + + if ('applied_tags' in data) { + /** + * The tags applied to this thread + * @type {Snowflake[]} + */ + this.appliedTags = data.applied_tags; + } else { + this.appliedTags ??= []; + } + if (data.member && this.client.user) this.members._add({ user_id: this.client.user.id, ...data.member }); if (data.messages) for (const message of data.messages) this.messages._add(message); } @@ -225,7 +246,7 @@ class ThreadChannel extends Channel { /** * The parent channel of this thread - * @type {?(NewsChannel|TextChannel)} + * @type {?(NewsChannel|TextChannel|ForumChannel)} * @readonly */ get parent() { @@ -280,14 +301,16 @@ class ThreadChannel extends Channel { /** * Fetches the message that started this thread, if any. - * This only works when the thread started from a message in the parent channel, otherwise the promise will - * reject. If you just need the id of that message, use {@link ThreadChannel#id} instead. + * The `Promise` will reject if the original message in a forum post is deleted + * or when the original message in the parent channel is deleted. + * If you just need the id of that message, use {@link ThreadChannel#id} instead. * @param {BaseFetchOptions} [options] Additional options for this fetch * @returns {Promise} */ // eslint-disable-next-line require-await async fetchStarterMessage(options) { - return this.parent?.messages.fetch(this.id, options) ?? null; + const channel = this.parent?.type === 'GUILD_FORUM' ? this : this.parent; + return channel?.messages.fetch({ message: this.id, ...options }) ?? null; } /** @@ -300,6 +323,7 @@ class ThreadChannel extends Channel { * @property {number} [rateLimitPerUser] The rate limit per user (slowmode) for the thread in seconds * @property {boolean} [locked] Whether the thread is locked * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to a thread + * @property {ChannelFlagsResolvable} [flags] The flags to set on the channel * Can only be edited on `GUILD_PRIVATE_THREAD` */ @@ -326,6 +350,8 @@ class ThreadChannel extends Channel { rate_limit_per_user: data.rateLimitPerUser, locked: data.locked, invitable: this.type === 'GUILD_PRIVATE_THREAD' ? data.invitable : undefined, + applied_tags: data.appliedTags, + flags: 'flags' in data ? ChannelFlags.resolve(data.flags) : undefined, }, reason, }); @@ -419,6 +445,34 @@ class ThreadChannel extends Channel { return this.edit({ rateLimitPerUser }, reason); } + /** + * Pins this thread from the forum channel. + * @param {string} [reason] Reason for pinning + * @returns {Promise} + */ + pin(reason) { + return this.edit({ flags: this.flags.add(ChannelFlags.FLAGS.PINNED), reason }); + } + + /** + * Unpins this thread from the forum channel. + * @param {string} [reason] Reason for unpinning + * @returns {Promise} + */ + unpin(reason) { + return this.edit({ flags: this.flags.remove(ChannelFlags.FLAGS.PINNED), reason }); + } + + /** + * Set the applied tags for this channel (only applicable to forum threads) + * @param {Snowflake[]} appliedTags The tags to set for this channel + * @param {string} [reason] Reason for changing the thread's applied tags + * @returns {Promise} + */ + setAppliedTags(appliedTags, reason) { + return this.edit({ appliedTags, reason }); + } + /** * Whether the client user is a member of the thread. * @type {boolean} diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 33540b992..d1b3144d8 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -131,6 +131,7 @@ class Webhook { * Action rows containing interactive components for the message (buttons, select menus) * @property {Snowflake} [threadId] The id of the thread this message belongs to * For interaction webhooks, this property is ignored + * @property {string} [threadName] Name of the thread to create (only available if webhook is in a forum channel) */ /** diff --git a/src/structures/WelcomeChannel.js b/src/structures/WelcomeChannel.js index 0741ab7cb..2b1fcba01 100644 --- a/src/structures/WelcomeChannel.js +++ b/src/structures/WelcomeChannel.js @@ -42,7 +42,7 @@ class WelcomeChannel extends Base { /** * The channel of this welcome channel - * @type {?(TextChannel|NewsChannel|StoreChannel)} + * @type {?(TextChannel|NewsChannel|StoreChannel|ForumChannel)} */ get channel() { return this.client.channels.resolve(this.channelId); diff --git a/src/util/ChannelFlags.js b/src/util/ChannelFlags.js new file mode 100644 index 000000000..302baa98b --- /dev/null +++ b/src/util/ChannelFlags.js @@ -0,0 +1,45 @@ +'use strict'; + +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link BaseChannel#flags} bitfield. + * @extends {BitField} + */ +class ChannelFlags extends BitField {} + +/** + * Numeric guild channel flags. All available properties: + * * `PINNED` + * * `REQUIRE_TAG` + * @type {Object} + * @see {@link https://discord.com/developers/docs/resources/channel#channel-object-channel-flags} + */ +ChannelFlags.FLAGS = { + PINNED: 1 << 1, + REQUIRE_TAG: 1 << 4, +}; + +/** + * @name ChannelFlags + * @kind constructor + * @memberof ChannelFlags + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Bitfield of the packed bits + * @type {number} + * @name ChannelFlags#bitfield + */ + +/** + * Data that can be resolved to give a channel flag bitfield. This can be: + * * A string (see {@link ChannelFlags.FLAGS}) + * * A channel flag + * * An instance of ChannelFlags + * * An Array of ChannelFlags + * @typedef {string|number|ChannelFlags|ChannelFlagsResolvable[]} ChannelFlagsResolvable + */ + +module.exports = ChannelFlags; diff --git a/src/util/Constants.js b/src/util/Constants.js index 7db258bab..887f4514f 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -648,6 +648,7 @@ exports.ActivityTypes = createEnum(['PLAYING', 'STREAMING', 'LISTENING', 'WATCHI * * `GUILD_PRIVATE_THREAD` - a guild text channel's private thread channel * * `GUILD_STAGE_VOICE` - a guild stage voice channel * * `GUILD_DIRECTORY` - the channel in a hub containing guilds + * * `GUILD_FORUM` - a channel that can only contain threads * * `UNKNOWN` - a generic channel of unknown type, could be Channel or GuildChannel * @typedef {string} ChannelType * @see {@link https://discord.com/developers/docs/resources/channel#channel-object-channel-types} @@ -667,6 +668,7 @@ exports.ChannelTypes = createEnum([ 'GUILD_PRIVATE_THREAD', 'GUILD_STAGE_VOICE', 'GUILD_DIRECTORY', + 'GUILD_FORUM', ]); /** @@ -1383,6 +1385,25 @@ exports.GuildScheduledEventEntityTypes = createEnum([null, 'STAGE_INSTANCE', 'VO */ exports.VideoQualityModes = createEnum([null, 'AUTO', 'FULL']); +/** + * Sort {@link ForumChannel} posts by creation time or activity + * * LATEST_ACTIVITY + * * CREATION_DATE + * @typedef {string} SortOrderType + * @see {@link https://discord.com/developers/docs/resources/channel/#channel-object-sort-order-types} + */ +exports.SortOrderTypes = createEnum([null, 'LATEST_ACTIVITY', 'CREATION_DATE']); + +/** + * The default forum layout to set on the {@link ForumChannel} + * * NOT_SET + * * LIST_VIEW + * * GALLERY_VIEW + * @typedef {string} ForumLayoutType + * @see {@link https://discord.com/developers/docs/resources/channel/#channel-object-forum-layout-types} + */ +exports.ForumLayoutTypes = createEnum(['NOT_SET', 'LIST_VIEW', 'GALLERY_VIEW']); + exports._cleanupSymbol = Symbol('djsCleanup'); function keyMirror(arr) { diff --git a/src/util/Util.js b/src/util/Util.js index eb1a1dfe1..bf9e9d44f 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -12,6 +12,7 @@ const isObject = d => typeof d === 'object' && d !== null; let deprecationEmittedForSplitMessage = false; let deprecationEmittedForRemoveMentions = false; +let deprecationEmittedForResolveAutoArchiveMaxLimit = false; /** * Contains various general-purpose utility methods. @@ -661,12 +662,84 @@ class Util extends null { /** * Resolves the maximum time a guild's thread channels should automatcally archive in case of no recent activity. * @param {Guild} guild The guild to resolve this limit from. + * @deprecated This will be removed in the next major version. * @returns {number} */ - static resolveAutoArchiveMaxLimit({ features }) { - if (features.includes('SEVEN_DAY_THREAD_ARCHIVE')) return 10080; - if (features.includes('THREE_DAY_THREAD_ARCHIVE')) return 4320; - return 1440; + static resolveAutoArchiveMaxLimit() { + if (!deprecationEmittedForResolveAutoArchiveMaxLimit) { + process.emitWarning( + // eslint-disable-next-line max-len + "The Util.resolveAutoArchiveMaxLimit method and the 'MAX' option are deprecated and will be removed in the next major version.", + 'DeprecationWarning', + ); + deprecationEmittedForResolveAutoArchiveMaxLimit = true; + } + return 10080; + } + + /** + * Transforms an API guild forum tag to camel-cased guild forum tag. + * @param {APIGuildForumTag} tag The tag to transform + * @returns {GuildForumTag} + * @ignore + */ + static transformAPIGuildForumTag(tag) { + return { + id: tag.id, + name: tag.name, + moderated: tag.moderated, + emoji: + tag.emoji_id ?? tag.emoji_name + ? { + id: tag.emoji_id, + name: tag.emoji_name, + } + : null, + }; + } + + /** + * Transforms a camel-cased guild forum tag to an API guild forum tag. + * @param {GuildForumTag} tag The tag to transform + * @returns {APIGuildForumTag} + * @ignore + */ + static transformGuildForumTag(tag) { + return { + id: tag.id, + name: tag.name, + moderated: tag.moderated, + emoji_id: tag.emoji?.id ?? null, + emoji_name: tag.emoji?.name ?? null, + }; + } + + /** + * Transforms an API guild forum default reaction object to a + * camel-cased guild forum default reaction object. + * @param {APIGuildForumDefaultReactionEmoji} defaultReaction The default reaction to transform + * @returns {DefaultReactionEmoji} + * @ignore + */ + static transformAPIGuildDefaultReaction(defaultReaction) { + return { + id: defaultReaction.emoji_id, + name: defaultReaction.emoji_name, + }; + } + + /** + * Transforms a camel-cased guild forum default reaction object to an + * API guild forum default reaction object. + * @param {DefaultReactionEmoji} defaultReaction The default reaction to transform + * @returns {APIGuildForumDefaultReactionEmoji} + * @ignore + */ + static transformGuildDefaultReaction(defaultReaction) { + return { + emoji_id: defaultReaction.id, + emoji_name: defaultReaction.name, + }; } } diff --git a/typings/enums.d.ts b/typings/enums.d.ts index 67e0fa84f..f79b38256 100644 --- a/typings/enums.d.ts +++ b/typings/enums.d.ts @@ -49,6 +49,18 @@ export const enum ChannelTypes { GUILD_PRIVATE_THREAD = 12, GUILD_STAGE_VOICE = 13, GUILD_DIRECTORY = 14, + GUILD_FORUM = 15, +} + +export const enum SortOrderType { + LATEST_ACTIVITY = 1, + CREATION_DATE = 2, +} + +export const enum ForumLayoutType { + NOT_SET = 0, + LIST_VIEW = 1, + GALLERY_VIEW = 2, } export const enum MessageTypes { diff --git a/typings/index.d.ts b/typings/index.d.ts index ad646ad39..d5714ef9c 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -90,6 +90,8 @@ import { GuildScheduledEventStatuses, GuildScheduledEventPrivacyLevels, VideoQualityModes, + SortOrderType, + ForumLayoutType, } from './enums'; import { RawActivityData, @@ -442,7 +444,7 @@ export class BaseGuildTextChannel extends TextBasedChannelMixin(GuildChannel) { public defaultAutoArchiveDuration?: ThreadAutoArchiveDuration; public rateLimitPerUser: number | null; public nsfw: boolean; - public threads: ThreadManager; + public threads: GuildTextThreadManager; public topic: string | null; public createInvite(options?: CreateInviteOptions): Promise; public fetchInvites(cache?: boolean): Promise>; @@ -525,6 +527,7 @@ export type MappedChannelCategoryTypes = EnumValueMapped< GUILD_TEXT: TextChannel; GUILD_STORE: StoreChannel; GUILD_STAGE_VOICE: StageChannel; + GUILD_FORUM: ForumChannel; } >; @@ -567,6 +570,7 @@ export abstract class Channel extends Base { public id: Snowflake; public readonly partial: false; public type: keyof typeof ChannelTypes; + public flags: Readonly | null; public delete(): Promise; public fetch(force?: boolean): Promise; public isText(): this is TextBasedChannel; @@ -701,6 +705,14 @@ export interface CollectorEventTypes { end: [collected: Collection, reason: string]; } +export type ChannelFlagsString = + | 'PINNED' + | 'REQUIRE_TAG'; +export class ChannelFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; +} + export abstract class Collector extends EventEmitter { protected constructor(client: Client, options?: CollectorOptions<[V, ...F]>); private _timeout: NodeJS.Timeout | null; @@ -907,6 +919,7 @@ export class DMChannel extends TextBasedChannelMixin(Channel, [ private constructor(client: Client, data?: RawDMChannelData); public recipient: User; public type: 'DM'; + public flags: Readonly; public fetch(force?: boolean): Promise; } @@ -978,7 +991,7 @@ export class Guild extends AnonymousGuild { public vanityURLUses: number | null; public readonly voiceAdapterCreator: InternalDiscordGatewayAdapterCreator; public readonly voiceStates: VoiceStateManager; - public readonly widgetChannel: TextChannel | null; + public readonly widgetChannel: TextChannel | NewsChannel | VoiceBasedChannel | ForumChannel | null; public widgetChannelId: Snowflake | null; public widgetEnabled: boolean | null; public readonly maximumBitrate: number; @@ -1111,6 +1124,7 @@ export abstract class GuildChannel extends Channel { public readonly position: number; public rawPosition: number; public type: Exclude; + public flags: Readonly; public readonly viewable: boolean; public clone(options?: GuildChannelCloneOptions): Promise; public delete(reason?: string): Promise; @@ -1593,6 +1607,7 @@ export class Message extends Base { public webhookId: Snowflake | null; public flags: Readonly; public reference: MessageReference | null; + public position: number | null; public awaitMessageComponent( options?: AwaitMessageCollectorOptionsParams, ): Promise[T]>; @@ -1972,7 +1987,7 @@ export class ModalSubmitInteraction extend } export class NewsChannel extends BaseGuildTextChannel { - public threads: ThreadManager; + public threads: GuildTextThreadManager; public type: 'GUILD_NEWS'; public addFollower(channel: TextChannelResolvable, reason?: string): Promise; } @@ -1987,6 +2002,7 @@ export class PartialGroupDMChannel extends Channel { private constructor(client: Client, data: RawPartialGroupDMChannelData); public name: string | null; public icon: string | null; + public flags: null; public recipients: PartialRecipient[]; public iconURL(options?: StaticImageURLOptions): string | null; } @@ -2273,7 +2289,9 @@ export class StageChannel extends BaseGuildVoiceChannel { public setTopic(topic: string): Promise; } -export class DirectoryChannel extends Channel {} +export class DirectoryChannel extends Channel { + public flags: Readonly; +} export class StageInstance extends Base { private constructor(client: Client, data: RawStageInstanceData, channel: StageChannel); @@ -2447,10 +2465,65 @@ export class TeamMember extends Base { export class TextChannel extends BaseGuildTextChannel { public rateLimitPerUser: number; - public threads: ThreadManager; + public threads: GuildTextThreadManager; public type: 'GUILD_TEXT'; } +export interface GuildForumTagEmoji { + id: Snowflake | null; + name: string | null; +} + +export interface GuildForumTag { + id: Snowflake; + name: string; + moderated: boolean; + emoji: GuildForumTagEmoji | null; +} + +export type GuildForumTagData = Partial & { name: string }; + +export interface DefaultReactionEmoji { + id: Snowflake | null; + name: string | null; +} + +export class ForumChannel extends TextBasedChannelMixin(GuildChannel, [ + 'send', + 'lastMessage', + 'lastPinAt', + 'bulkDelete', + 'sendTyping', + 'createMessageCollector', + 'awaitMessages', + 'createMessageComponentCollector', + 'awaitMessageComponent', +]) { + public type: 'GUILD_FORUM'; + public threads: GuildForumThreadManager; + public availableTags: GuildForumTag[]; + public defaultReactionEmoji: DefaultReactionEmoji | null; + public defaultThreadRateLimitPerUser: number | null; + public rateLimitPerUser: number | null; + public defaultAutoArchiveDuration: ThreadAutoArchiveDuration | null; + public nsfw: boolean; + public topic: string | null; + public defaultSortOrder: SortOrderType | null; + public defaultForumLayout: ForumLayoutType; + public setAvailableTags(tags: GuildForumTagData[], reason?: string): Promise; + public setDefaultReactionEmoji(emojiId: DefaultReactionEmoji | null, reason?: string): Promise; + public setDefaultThreadRateLimitPerUser(rateLimit: number, reason?: string): Promise; + public createInvite(options?: CreateInviteOptions): Promise; + public fetchInvites(cache?: boolean): Promise>; + public setDefaultAutoArchiveDuration( + defaultAutoArchiveDuration: ThreadAutoArchiveDuration, + reason?: string, + ): Promise; + public setTopic(topic: string | null, reason?: string): Promise; + public setDefaultSortOrder(defaultSortOrder: SortOrderType | null, reason?: string): Promise; + public setDefaultForumLayout(defaultForumLayout: ForumLayoutType, reason?: string): Promise; +} + export class TextInputComponent extends BaseMessageComponent { public constructor(data?: TextInputComponent | TextInputComponentOptions); public customId: string | null; @@ -2498,10 +2571,13 @@ export class ThreadChannel extends TextBasedChannelMixin(Channel, ['fetchWebhook public members: ThreadMemberManager; public name: string; public ownerId: Snowflake | null; - public readonly parent: TextChannel | NewsChannel | null; + public readonly parent: TextChannel | NewsChannel | ForumChannel | null; public parentId: Snowflake | null; public rateLimitPerUser: number | null; public type: ThreadChannelTypes; + public flags: Readonly; + public appliedTags: Snowflake[]; + public totalMessageSent: number | null; public readonly unarchivable: boolean; public isPrivate(): this is this & { readonly createdTimestamp: number; @@ -2527,6 +2603,9 @@ export class ThreadChannel extends TextBasedChannelMixin(Channel, ['fetchWebhook public setInvitable(invitable?: boolean, reason?: string): Promise; public setLocked(locked?: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; + public setAppliedTags(appliedTags: Snowflake[], reason?: string): Promise; + public pin(reason?: string): Promise; + public unpin(reason?: string): Promise; } export class ThreadMember extends Base { @@ -2652,6 +2731,7 @@ export class Util extends null { ): Promise<{ id: Snowflake; position: number }[]>; /** @deprecated This will be removed in the next major version. */ public static splitMessage(text: string, options?: SplitOptions): string[]; + /** @deprecated This will be removed in the next major version. */ public static resolveAutoArchiveMaxLimit(guild: Guild): Exclude; } @@ -2893,7 +2973,7 @@ export class WelcomeChannel extends Base { public channelId: Snowflake; public guild: Guild | InviteGuild; public description: string; - public readonly channel: TextChannel | NewsChannel | StoreChannel | null; + public readonly channel: TextChannel | NewsChannel | StoreChannel | ForumChannel | null; public readonly emoji: GuildEmoji | Emoji; } @@ -3479,16 +3559,23 @@ export class StageInstanceManager extends CachedManager; } -export class ThreadManager extends CachedManager { - private constructor(channel: TextChannel | NewsChannel, iterable?: Iterable); +export class ThreadManager extends CachedManager { + protected constructor(channel: TextChannel | NewsChannel, iterable?: Iterable); public channel: TextChannel | NewsChannel; - public create(options: ThreadCreateOptions): Promise; public fetch(options: ThreadChannelResolvable, cacheOptions?: BaseFetchOptions): Promise; public fetch(options?: FetchThreadsOptions, cacheOptions?: { cache?: boolean }): Promise; public fetchArchived(options?: FetchArchivedThreadOptions, cache?: boolean): Promise; public fetchActive(cache?: boolean): Promise; } +export class GuildTextThreadManager extends ThreadManager { + public create(options: GuildTextThreadCreateOptions): Promise; +} + +export class GuildForumThreadManager extends ThreadManager { + public create(options: GuildForumThreadCreateOptions): Promise; +} + export class ThreadMemberManager extends CachedManager { private constructor(thread: ThreadChannel, iterable?: Iterable); public thread: ThreadChannel; @@ -4111,7 +4198,6 @@ export type CacheWithLimitsOptions = { ? LimitedCollectionOptions | number : never; }; - export interface CategoryCreateChannelOptions { permissionOverwrites?: OverwriteResolvable[] | Collection; topic?: string; @@ -4131,6 +4217,11 @@ export interface CategoryCreateChannelOptions { rateLimitPerUser?: number; position?: number; rtcRegion?: string; + videoQualityMode?: VideoQualityMode; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultSortOrder?: SortOrderType; + defaultForumLayout?: ForumLayoutType; reason?: string; } @@ -4155,6 +4246,12 @@ export interface ChannelData { defaultAutoArchiveDuration?: ThreadAutoArchiveDuration | 'MAX'; rtcRegion?: string | null; videoQualityMode?: VideoQualityMode | null; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultThreadRateLimitPerUser?: number; + defaultSortOrder?: SortOrderType | null; + defaultForumLayout?: ForumLayoutType; + flags?: ChannelFlagsResolvable; } export interface ChannelLogsQueryOptions { @@ -4258,7 +4355,7 @@ export interface ClientEvents extends BaseClientEvents { typingStart: [typing: Typing]; userUpdate: [oldUser: User | PartialUser, newUser: User]; voiceStateUpdate: [oldState: VoiceState, newState: VoiceState]; - webhookUpdate: [channel: TextChannel | NewsChannel | VoiceChannel]; + webhookUpdate: [channel: TextChannel | NewsChannel | VoiceChannel | ForumChannel]; /** @deprecated Use interactionCreate instead */ interaction: [interaction: Interaction]; interactionCreate: [interaction: Interaction]; @@ -5378,6 +5475,9 @@ export type MessageComponentType = keyof typeof MessageComponentTypes; export type MessageComponentTypeResolvable = MessageComponentType | MessageComponentTypes; +export type GuildForumThreadMessageCreateOptions = MessageOptions & + Pick; + export interface MessageEditOptions { attachments?: MessageAttachment[]; content?: string | null; @@ -5690,6 +5790,9 @@ export interface PartialChannelData { videoQualityMode?: VideoQualityMode; permissionOverwrites?: PartialOverwriteData[]; rateLimitPerUser?: number; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultThreadRateLimitPerUser?: number; } export type Partialize< @@ -5863,6 +5966,8 @@ export type SystemChannelFlagsString = export type SystemChannelFlagsResolvable = BitFieldResolvable; +export type ChannelFlagsResolvable = BitFieldResolvable; + export type SystemMessageType = Exclude< MessageType, 'DEFAULT' | 'REPLY' | 'APPLICATION_COMMAND' | 'CONTEXT_MENU_COMMAND' @@ -5935,9 +6040,10 @@ export type AnyChannel = | StoreChannel | TextChannel | ThreadChannel - | VoiceChannel; + | VoiceChannel + | ForumChannel; -export type TextBasedChannel = Extract; +export type TextBasedChannel = Exclude, ForumChannel>; export type TextBasedChannelTypes = TextBasedChannel['type']; @@ -5966,7 +6072,7 @@ export type GuildBasedChannel = Extract; export type NonThreadGuildBasedChannel = Exclude; -export type GuildTextBasedChannel = Extract; +export type GuildTextBasedChannel = Exclude, ForumChannel>; export type TextChannelResolvable = Snowflake | TextChannel; @@ -5978,13 +6084,18 @@ export type ThreadChannelResolvable = ThreadChannel | Snowflake; export type ThreadChannelTypes = 'GUILD_NEWS_THREAD' | 'GUILD_PUBLIC_THREAD' | 'GUILD_PRIVATE_THREAD'; -export interface ThreadCreateOptions extends StartThreadOptions { +export interface GuildTextThreadCreateOptions extends StartThreadOptions { startMessage?: MessageResolvable; type?: AllowedThreadType; invitable?: AllowedThreadType extends 'GUILD_PRIVATE_THREAD' | 12 ? boolean : never; rateLimitPerUser?: number; } +export interface GuildForumThreadCreateOptions extends StartThreadOptions { + message: GuildForumThreadMessageCreateOptions | MessagePayload; + appliedTags?: Snowflake[]; +} + export interface ThreadEditData { name?: string; archived?: boolean; @@ -5992,6 +6103,8 @@ export interface ThreadEditData { rateLimitPerUser?: number; locked?: boolean; invitable?: boolean; + threadName?: string; + flags?: ChannelFlagsResolvable; } export type ThreadMemberFlagsString = ''; @@ -6104,7 +6217,7 @@ export interface WidgetChannel { export interface WelcomeChannelData { description: string; - channel: TextChannel | NewsChannel | StoreChannel | Snowflake; + channel: TextChannel | NewsChannel | StoreChannel | ForumChannel | Snowflake; emoji?: EmojiIdentifierResolvable; } diff --git a/typings/index.test-d.ts b/typings/index.test-d.ts index 1108c3bb3..2132ada45 100644 --- a/typings/index.test-d.ts +++ b/typings/index.test-d.ts @@ -97,6 +97,7 @@ import { InteractionResponseFields, GuildBan, GuildBanManager, + ForumChannel, } from '.'; import type { ApplicationCommandOptionTypes } from './enums'; import { expectAssignable, expectDeprecated, expectNotAssignable, expectNotType, expectType } from 'tsd'; @@ -897,7 +898,7 @@ declare const categoryChannel: CategoryChannel; declare const guildChannelManager: GuildChannelManager; { - type AnyChannel = TextChannel | VoiceChannel | CategoryChannel | NewsChannel | StoreChannel | StageChannel; + type AnyChannel = TextChannel | VoiceChannel | CategoryChannel | NewsChannel | StoreChannel | StageChannel | ForumChannel; expectType>(guildChannelManager.create('name')); expectType>(guildChannelManager.create('name', {})); @@ -1333,10 +1334,10 @@ expectType< | 'GUILD_VOICE' >(TextBasedChannelTypes); expectType(VoiceBasedChannel); -expectType( +expectType( GuildBasedChannel, ); -expectType( +expectType( NonThreadGuildBasedChannel, ); expectType(GuildTextBasedChannel);