From 8a8d519c9c4c082370fc6935b56dafb525b873df Mon Sep 17 00:00:00 2001 From: Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> Date: Sun, 18 Sep 2022 10:23:44 -0400 Subject: [PATCH] feat: add support for guild forums (#7791) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for guild forums * feat(webhook): add support for creating forum channel posts * fix: duplicated docs * feat: add support for message counts * feat: add support for latest upstream changes * fix: serialize forum channels * types: fix channel unions * types: fix tests * types: fix tests (again) * types: fix tests (again (again)) * chore: make requested changes * chore: fix bugs and make requested changes * types: use correct type for guild forum start messages * chore: remove console.log * chore: make requested changes * chore: make requested changes * chore: fix docs * Update packages/discord.js/src/managers/GuildForumThreadManager.js Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> * chore: update types * chore: make requested changes * chore: Apply suggestions Co-authored-by: Jaworek Co-authored-by: Jonathan Rubenstein * fix: import `ErrorCodes` * fix: remove defunct code * refactor: be consistent with channel class names * feat(GuildChannel): add flags * fix: rename file * refactor: channel flags are everywhere! * fix: import flags correctly * chore(ThreadChannel): update message count string * docs(Channels): correct `@param` type * docs(Channels): ignore transformGuildDefaultReaction * refactor: emoji object in tags * chore: renaming consistency * fix: document default reaction emojis in patching * fix(GuildChannelManager): document `defaultThreadRateLimitPerUser` * chore: semicolon * docs(ErrorCodes): document `GuildForumMessageRequired` * refactor: transform default reactions * docs(APITypes): Add `ChannelFlags` * fix: convert tags properly * fix: pass an array of snowflakes * refactor: handle flags better * fix(ThreadChannel): receive tags * fix(PartialGroupDMChannel): nullify `flags` Apparently did not do this earlier. * chore: misc sorting * refactor: nullify emoji on tags if not present * refactor(ForumChannel): modify returns * types: protect the thread manager! Co-authored-by: SpaceEEC * chore: update `ChannelType` usage * Update index.d.ts * docs: Update default reaction emoji property names Co-authored-by: Almeida * fix: only `name` is required when editing tags - discord/discord-api-docs#5458 * types: add tests for `channel.flags` * fix: allow unsetting the default reaction emoji * refactor: remove v13 remnants * docs: add missing closing tag * feat: add `rateLimitPerUser` * feat: add missing properties for create guild channel - discord/discord-api-docs#5474 * refactor(GuildForumThreadManager): refactor message payload * fix: handle magical `null` case Co-authored-by: A. Román Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com> Co-authored-by: Jaworek Co-authored-by: Jonathan Rubenstein Co-authored-by: SpaceEEC Co-authored-by: Almeida Co-authored-by: A. Román --- packages/discord.js/src/errors/ErrorCodes.js | 4 + packages/discord.js/src/errors/Messages.js | 2 + packages/discord.js/src/index.js | 4 + .../managers/CategoryChannelChildManager.js | 3 + .../src/managers/GuildChannelManager.js | 15 +- .../src/managers/GuildForumThreadManager.js | 76 ++++++++++ .../src/managers/GuildTextThreadManager.js | 85 +++++++++++ .../discord.js/src/managers/ThreadManager.js | 82 ++-------- .../discord.js/src/structures/BaseChannel.js | 12 ++ .../src/structures/BaseGuildTextChannel.js | 6 +- .../discord.js/src/structures/ForumChannel.js | 140 ++++++++++++++++++ .../discord.js/src/structures/GuildChannel.js | 1 + packages/discord.js/src/structures/Message.js | 13 +- .../src/structures/MessagePayload.js | 3 + .../src/structures/PartialGroupDMChannel.js | 3 + .../src/structures/ThreadChannel.js | 36 ++++- packages/discord.js/src/structures/Webhook.js | 1 + packages/discord.js/src/util/APITypes.js | 15 ++ .../src/util/ChannelFlagsBitField.js | 32 ++++ packages/discord.js/src/util/Channels.js | 73 +++++++++ packages/discord.js/typings/index.d.ts | 98 ++++++++++-- packages/discord.js/typings/index.test-d.ts | 36 ++++- 22 files changed, 645 insertions(+), 95 deletions(-) create mode 100644 packages/discord.js/src/managers/GuildForumThreadManager.js create mode 100644 packages/discord.js/src/managers/GuildTextThreadManager.js create mode 100644 packages/discord.js/src/structures/ForumChannel.js create mode 100644 packages/discord.js/src/util/ChannelFlagsBitField.js diff --git a/packages/discord.js/src/errors/ErrorCodes.js b/packages/discord.js/src/errors/ErrorCodes.js index df748196984c..f0566a0433a8 100644 --- a/packages/discord.js/src/errors/ErrorCodes.js +++ b/packages/discord.js/src/errors/ErrorCodes.js @@ -143,6 +143,8 @@ * @property {'NotImplemented'} NotImplemented + * @property {'GuildForumMessageRequired'} GuildForumMessageRequired + * @property {'SweepFilterReturn'} SweepFilterReturn */ @@ -288,6 +290,8 @@ const keys = [ 'NotImplemented', 'SweepFilterReturn', + + 'GuildForumMessageRequired', ]; // JSDoc for IntelliSense purposes diff --git a/packages/discord.js/src/errors/Messages.js b/packages/discord.js/src/errors/Messages.js index 417e7a809637..24437b5023c9 100644 --- a/packages/discord.js/src/errors/Messages.js +++ b/packages/discord.js/src/errors/Messages.js @@ -157,6 +157,8 @@ const Messages = { [DjsErrorCodes.NotImplemented]: (what, name) => `Method ${what} not implemented on ${name}.`, [DjsErrorCodes.SweepFilterReturn]: 'The return value of the sweepFilter function was not false or a Function', + + [DjsErrorCodes.GuildForumMessageRequired]: 'You must provide a message to create a guild forum thread', }; module.exports = Messages; diff --git a/packages/discord.js/src/index.js b/packages/discord.js/src/index.js index 9ec254f017f3..a015ba4954c0 100644 --- a/packages/discord.js/src/index.js +++ b/packages/discord.js/src/index.js @@ -22,6 +22,7 @@ exports.ActivityFlagsBitField = require('./util/ActivityFlagsBitField'); exports.ApplicationFlagsBitField = require('./util/ApplicationFlagsBitField'); exports.BaseManager = require('./managers/BaseManager'); exports.BitField = require('./util/BitField'); +exports.ChannelFlagsBitField = require('./util/ChannelFlagsBitField'); exports.Collection = require('@discordjs/collection').Collection; exports.Constants = require('./util/Constants'); exports.Colors = require('./util/Colors'); @@ -58,12 +59,14 @@ exports.GuildBanManager = require('./managers/GuildBanManager'); exports.GuildChannelManager = require('./managers/GuildChannelManager'); exports.GuildEmojiManager = require('./managers/GuildEmojiManager'); exports.GuildEmojiRoleManager = require('./managers/GuildEmojiRoleManager'); +exports.GuildForumThreadManager = require('./managers/GuildForumThreadManager'); exports.GuildInviteManager = require('./managers/GuildInviteManager'); exports.GuildManager = require('./managers/GuildManager'); exports.GuildMemberManager = require('./managers/GuildMemberManager'); exports.GuildMemberRoleManager = require('./managers/GuildMemberRoleManager'); exports.GuildScheduledEventManager = require('./managers/GuildScheduledEventManager'); exports.GuildStickerManager = require('./managers/GuildStickerManager'); +exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager'); exports.MessageManager = require('./managers/MessageManager'); exports.PermissionOverwriteManager = require('./managers/PermissionOverwriteManager'); exports.PresenceManager = require('./managers/PresenceManager'); @@ -109,6 +112,7 @@ exports.DMChannel = require('./structures/DMChannel'); exports.Embed = require('./structures/Embed'); exports.EmbedBuilder = require('./structures/EmbedBuilder'); exports.Emoji = require('./structures/Emoji').Emoji; +exports.ForumChannel = require('./structures/ForumChannel'); exports.Guild = require('./structures/Guild').Guild; exports.GuildAuditLogs = require('./structures/GuildAuditLogs'); exports.GuildAuditLogsEntry = require('./structures/GuildAuditLogsEntry'); diff --git a/packages/discord.js/src/managers/CategoryChannelChildManager.js b/packages/discord.js/src/managers/CategoryChannelChildManager.js index bd594fd40a4b..49c593b59684 100644 --- a/packages/discord.js/src/managers/CategoryChannelChildManager.js +++ b/packages/discord.js/src/managers/CategoryChannelChildManager.js @@ -50,6 +50,9 @@ class CategoryChannelChildManager extends DataManager { * @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 {VideoQualityMode} [videoQualityMode] The camera video quality mode of the voice channel + * @property {GuildForumTagData[]} [availableTags] The tags that can be used in this channel (forum only). + * @property {DefaultReactionEmoji} [defaultReactionEmoji] + * The emoji to show in the add reaction button on a thread in a guild forum channel. * @property {string} [reason] Reason for creating the new channel */ diff --git a/packages/discord.js/src/managers/GuildChannelManager.js b/packages/discord.js/src/managers/GuildChannelManager.js index 7e76d5bc89b5..7549dfc99296 100644 --- a/packages/discord.js/src/managers/GuildChannelManager.js +++ b/packages/discord.js/src/managers/GuildChannelManager.js @@ -4,12 +4,13 @@ const process = require('node:process'); const { Collection } = require('@discordjs/collection'); const { ChannelType, Routes } = require('discord-api-types/v10'); const CachedManager = require('./CachedManager'); -const ThreadManager = require('./ThreadManager'); +const GuildTextThreadManager = require('./GuildTextThreadManager'); const { Error, TypeError, ErrorCodes } = require('../errors'); const GuildChannel = require('../structures/GuildChannel'); const PermissionOverwrites = require('../structures/PermissionOverwrites'); const ThreadChannel = require('../structures/ThreadChannel'); const Webhook = require('../structures/Webhook'); +const { transformGuildForumTag, transformGuildDefaultReaction } = require('../util/Channels'); const { ThreadChannelTypes } = require('../util/Constants'); const DataResolver = require('../util/DataResolver'); const { setPosition } = require('../util/Util'); @@ -137,6 +138,8 @@ class GuildChannelManager extends CachedManager { rateLimitPerUser, rtcRegion, videoQualityMode, + availableTags, + defaultReactionEmoji, reason, }) { parent &&= this.client.channels.resolveId(parent); @@ -156,6 +159,8 @@ class GuildChannelManager extends CachedManager { rate_limit_per_user: rateLimitPerUser, rtc_region: rtcRegion, video_quality_mode: videoQualityMode, + available_tags: availableTags?.map(availableTag => transformGuildForumTag(availableTag)), + default_reaction_emoji: defaultReactionEmoji && transformGuildDefaultReaction(defaultReactionEmoji), }, reason, }); @@ -218,6 +223,9 @@ 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} [videoQualityMode] The camera video quality mode of 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 {string} [reason] Reason for editing this channel */ @@ -274,6 +282,9 @@ class GuildChannelManager extends CachedManager { rate_limit_per_user: data.rateLimitPerUser, default_auto_archive_duration: data.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, }, reason: data.reason, }); @@ -418,7 +429,7 @@ class GuildChannelManager extends CachedManager { */ async fetchActiveThreads(cache = true) { const raw = await this.client.rest.get(Routes.guildActiveThreads(this.guild.id)); - return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache }); + return GuildTextThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache }); } /** diff --git a/packages/discord.js/src/managers/GuildForumThreadManager.js b/packages/discord.js/src/managers/GuildForumThreadManager.js new file mode 100644 index 000000000000..d124e8b82ae3 --- /dev/null +++ b/packages/discord.js/src/managers/GuildForumThreadManager.js @@ -0,0 +1,76 @@ +'use strict'; + +const { Routes } = require('discord-api-types/v10'); +const ThreadManager = require('./ThreadManager'); +const { TypeError, ErrorCodes } = require('../errors'); +const MessagePayload = require('../structures/MessagePayload'); + +/** + * Manages API methods for threads in forum channels and stores their cache. + * @extends {ThreadManager} + */ +class GuildForumThreadManager extends ThreadManager { + /** + * @typedef {BaseMessageOptions} GuildForumThreadCreateOptions + * @property {stickers} [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 {GuildForumThreadCreateOptions|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: ThreadAutoArchiveDuration.OneHour, + * 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(ErrorCodes.GuildForumMessageRequired); + } + + const { body, files } = await (message instanceof MessagePayload ? message : MessagePayload.create(this, message)) + .resolveBody() + .resolveFiles(); + + const data = await this.client.rest.post(Routes.threads(this.channel.id), { + body: { + 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/packages/discord.js/src/managers/GuildTextThreadManager.js b/packages/discord.js/src/managers/GuildTextThreadManager.js new file mode 100644 index 000000000000..9f1ac9b44e15 --- /dev/null +++ b/packages/discord.js/src/managers/GuildTextThreadManager.js @@ -0,0 +1,85 @@ +'use strict'; + +const { ChannelType, Routes } = require('discord-api-types/v10'); +const ThreadManager = require('./ThreadManager'); +const { ErrorCodes, TypeError } = require('../errors'); + +/** + * Manages API methods for {@link ThreadChannel} objects and stores their cache. + * @extends {CachedManager} + */ +class GuildTextThreadManager extends ThreadManager { + /** + * 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 the `type` of thread gets inferred automatically and cannot be changed. + * @property {ThreadChannelTypes} [type] The type of thread to create. + * Defaults to {@link ChannelType.PublicThread} if created in a {@link TextChannel} + * When creating threads in a {@link NewsChannel}, this is ignored and is always + * {@link ChannelType.AnnouncementThread} + * @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread + * Can only be set when type will be {@link ChannelType.PrivateThread} + */ + + /** + * 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: ThreadAutoArchiveDuration.OneHour, + * 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: ThreadAutoArchiveDuration.OneHour, + * type: ChannelType.PrivateThread, + * 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 resolvedType = + this.channel.type === ChannelType.GuildAnnouncement ? ChannelType.AnnouncementThread : ChannelType.PublicThread; + let startMessageId; + if (startMessage) { + startMessageId = this.channel.messages.resolveId(startMessage); + if (!startMessageId) throw new TypeError(ErrorCodes.InvalidType, 'startMessage', 'MessageResolvable'); + } else if (this.channel.type !== ChannelType.GuildAnnouncement) { + resolvedType = type ?? resolvedType; + } + + const data = await this.client.rest.post(Routes.threads(this.channel.id, startMessageId), { + body: { + name, + auto_archive_duration: autoArchiveDuration, + type: resolvedType, + invitable: resolvedType === ChannelType.PrivateThread ? invitable : undefined, + rate_limit_per_user: rateLimitPerUser, + }, + reason, + }); + + return this.client.actions.ThreadCreate.handle(data).thread; + } +} + +module.exports = GuildTextThreadManager; diff --git a/packages/discord.js/src/managers/ThreadManager.js b/packages/discord.js/src/managers/ThreadManager.js index f2662d862676..fc32c668c7c1 100644 --- a/packages/discord.js/src/managers/ThreadManager.js +++ b/packages/discord.js/src/managers/ThreadManager.js @@ -2,13 +2,13 @@ const { Collection } = require('@discordjs/collection'); const { makeURLSearchParams } = require('@discordjs/rest'); -const { ChannelType, Routes } = require('discord-api-types/v10'); +const { Routes } = require('discord-api-types/v10'); const CachedManager = require('./CachedManager'); const { TypeError, ErrorCodes } = require('../errors'); const ThreadChannel = require('../structures/ThreadChannel'); /** - * Manages API methods for {@link ThreadChannel} objects and stores their cache. + * Manages API methods for thread-based channels and stores their cache. * @extends {CachedManager} */ class ThreadManager extends CachedManager { @@ -17,11 +17,18 @@ class ThreadManager extends CachedManager { /** * The channel this Manager belongs to - * @type {NewsChannel|TextChannel} + * @type {NewsChannel|TextChannel|ForumChannel} */ this.channel = channel; } + /** + * Data that can be resolved to a Thread Channel object. This can be: + * * A ThreadChannel object + * * A Snowflake + * @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable + */ + /** * The cache of this Manager * @type {Collection} @@ -35,13 +42,6 @@ class ThreadManager extends CachedManager { return thread; } - /** - * Data that can be resolved to a Thread Channel object. This can be: - * * A ThreadChannel object - * * A Snowflake - * @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable - */ - /** * Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} object. * @method resolve @@ -74,68 +74,6 @@ class ThreadManager extends CachedManager { * Can only be set when type will be {@link ChannelType.PrivateThread} */ - /** - * 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: ThreadAutoArchiveDuration.OneHour, - * 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: ThreadAutoArchiveDuration.OneHour, - * type: ChannelType.PrivateThread, - * 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, - } = {}) { - if (type && typeof type !== 'string' && typeof type !== 'number') { - throw new TypeError(ErrorCodes.InvalidType, 'type', 'ThreadChannelType or Number'); - } - let resolvedType = - this.channel.type === ChannelType.GuildAnnouncement ? ChannelType.AnnouncementThread : ChannelType.PublicThread; - let startMessageId; - if (startMessage) { - startMessageId = this.channel.messages.resolveId(startMessage); - if (!startMessageId) throw new TypeError(ErrorCodes.InvalidType, 'startMessage', 'MessageResolvable'); - } else if (this.channel.type !== ChannelType.GuildAnnouncement) { - resolvedType = type ?? resolvedType; - } - - const data = await this.client.rest.post(Routes.threads(this.channel.id, startMessageId), { - body: { - name, - auto_archive_duration: autoArchiveDuration, - type: resolvedType, - invitable: resolvedType === ChannelType.PrivateThread ? 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/packages/discord.js/src/structures/BaseChannel.js b/packages/discord.js/src/structures/BaseChannel.js index 49d611436e19..d3096d969633 100644 --- a/packages/discord.js/src/structures/BaseChannel.js +++ b/packages/discord.js/src/structures/BaseChannel.js @@ -4,6 +4,7 @@ const { channelLink } = require('@discordjs/builders'); const { DiscordSnowflake } = require('@sapphire/snowflake'); const { ChannelType, Routes } = require('discord-api-types/v10'); const Base = require('./Base'); +const ChannelFlagsBitField = require('../util/ChannelFlagsBitField'); const { ThreadChannelTypes } = require('../util/Constants'); /** @@ -25,6 +26,17 @@ class BaseChannel extends Base { } _patch(data) { + 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 ChannelFlagsBitField(data.flags).freeze(); + } else { + this.flags ??= new ChannelFlagsBitField().freeze(); + } + /** * The channel's id * @type {Snowflake} diff --git a/packages/discord.js/src/structures/BaseGuildTextChannel.js b/packages/discord.js/src/structures/BaseGuildTextChannel.js index 12cad909249a..71cb3c797aac 100644 --- a/packages/discord.js/src/structures/BaseGuildTextChannel.js +++ b/packages/discord.js/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/packages/discord.js/src/structures/ForumChannel.js b/packages/discord.js/src/structures/ForumChannel.js new file mode 100644 index 000000000000..a19ab19a43b8 --- /dev/null +++ b/packages/discord.js/src/structures/ForumChannel.js @@ -0,0 +1,140 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const GuildForumThreadManager = require('../managers/GuildForumThreadManager'); +const { transformAPIGuildForumTag, transformAPIGuildDefaultReaction } = require('../util/Channels'); + +/** + * @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} + */ +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) + : null; + } 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; + } + } + + /** + * 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 rate limit per user (slowmode) for this channel + * @param {?number} rateLimitPerUser The rate limit to set on this channel + * @param {string} [reason] Reason for changing the rate limit + * @returns {Promise} + */ + setRateLimitPerUser(rateLimitPerUser, reason) { + return this.edit({ rateLimitPerUser, reason }); + } +} + +module.exports = ForumChannel; diff --git a/packages/discord.js/src/structures/GuildChannel.js b/packages/discord.js/src/structures/GuildChannel.js index c8e42c66e272..8f05faa8262b 100644 --- a/packages/discord.js/src/structures/GuildChannel.js +++ b/packages/discord.js/src/structures/GuildChannel.js @@ -14,6 +14,7 @@ const PermissionsBitField = require('../util/PermissionsBitField'); * - {@link CategoryChannel} * - {@link NewsChannel} * - {@link StageChannel} + * - {@link ForumChannel} * @extends {BaseChannel} * @abstract */ diff --git a/packages/discord.js/src/structures/Message.js b/packages/discord.js/src/structures/Message.js index efb48ea6a2a0..c5133feb481e 100644 --- a/packages/discord.js/src/structures/Message.js +++ b/packages/discord.js/src/structures/Message.js @@ -187,6 +187,17 @@ class Message extends Base { this.stickers = new Collection(this.stickers); } + 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; + } + // Discord sends null if the message has not been edited if (data.edited_timestamp) { /** @@ -800,7 +811,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/packages/discord.js/src/structures/MessagePayload.js b/packages/discord.js/src/structures/MessagePayload.js index 3d7b693ae2ea..71a895347e1e 100644 --- a/packages/discord.js/src/structures/MessagePayload.js +++ b/packages/discord.js/src/structures/MessagePayload.js @@ -137,9 +137,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; @@ -207,6 +209,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/packages/discord.js/src/structures/PartialGroupDMChannel.js b/packages/discord.js/src/structures/PartialGroupDMChannel.js index 92d7d1ea3f88..6f0c81a5ad23 100644 --- a/packages/discord.js/src/structures/PartialGroupDMChannel.js +++ b/packages/discord.js/src/structures/PartialGroupDMChannel.js @@ -11,6 +11,9 @@ class PartialGroupDMChannel extends BaseChannel { 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/packages/discord.js/src/structures/ThreadChannel.js b/packages/discord.js/src/structures/ThreadChannel.js index b63db743efc0..fc365c17dc43 100644 --- a/packages/discord.js/src/structures/ThreadChannel.js +++ b/packages/discord.js/src/structures/ThreadChannel.js @@ -159,8 +159,8 @@ class ThreadChannel extends BaseChannel { 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,8 +180,29 @@ class ThreadChannel extends BaseChannel { 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 (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); + + if ('applied_tags' in data) { + /** + * The tags applied to this thread + * @type {Snowflake[]} + */ + this.appliedTags = data.applied_tags; + } else { + this.appliedTags ??= []; + } } /** @@ -323,6 +344,7 @@ class ThreadChannel extends BaseChannel { rate_limit_per_user: data.rateLimitPerUser, locked: data.locked, invitable: this.type === ChannelType.PrivateThread ? data.invitable : undefined, + applied_tags: data.appliedTags, }, reason: data.reason, }); @@ -419,6 +441,16 @@ class ThreadChannel extends BaseChannel { return this.edit({ rateLimitPerUser, reason }); } + /** + * Set the applied tags for this channel (only applicable to forum threads) + * @param {GuildForumTag[]} 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/packages/discord.js/src/structures/Webhook.js b/packages/discord.js/src/structures/Webhook.js index 729e1d613072..ba03fae09d95 100644 --- a/packages/discord.js/src/structures/Webhook.js +++ b/packages/discord.js/src/structures/Webhook.js @@ -130,6 +130,7 @@ class Webhook { * @property {string} [avatarURL] Avatar URL override for the message * @property {Snowflake} [threadId] The id of the thread in the channel to send 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/packages/discord.js/src/util/APITypes.js b/packages/discord.js/src/util/APITypes.js index 1edae74bc81f..dd70c87295ad 100644 --- a/packages/discord.js/src/util/APITypes.js +++ b/packages/discord.js/src/util/APITypes.js @@ -63,6 +63,16 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuild} */ +/** + * @external APIGuildForumDefaultReactionEmoji + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuildForumDefaultReactionEmoji} + */ + +/** + * @external APIGuildForumTag + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/interface/APIGuildForumTag} + */ + /** * @external APIInteraction * @see {@link https://discord-api-types.dev/api/discord-api-types-v10#APIInteraction} @@ -168,6 +178,11 @@ * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ButtonStyle} */ +/** + * @external ChannelFlags + * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ChannelFlags} + */ + /** * @external ChannelType * @see {@link https://discord-api-types.dev/api/discord-api-types-v10/enum/ChannelType} diff --git a/packages/discord.js/src/util/ChannelFlagsBitField.js b/packages/discord.js/src/util/ChannelFlagsBitField.js new file mode 100644 index 000000000000..c6b6d07d3f7e --- /dev/null +++ b/packages/discord.js/src/util/ChannelFlagsBitField.js @@ -0,0 +1,32 @@ +'use strict'; + +const { ChannelFlags } = require('discord-api-types/v10'); +const BitField = require('./BitField'); + +/** + * Data structure that makes it easy to interact with a {@link BaseChannel#flags} bitfield. + * @extends {BitField} + */ +class ChannelFlagsBitField extends BitField { + /** + * Numeric guild channel flags. + * @type {ChannelFlags} + * @memberof ChannelFlagsBitField + */ + static Flags = ChannelFlags; +} + +/** + * @name ChannelFlagsBitField + * @kind constructor + * @memberof ChannelFlagsBitField + * @param {BitFieldResolvable} [bits=0] Bit(s) to read from + */ + +/** + * Bitfield of the packed bits + * @type {number} + * @name ChannelFlagsBitField#bitfield + */ + +module.exports = ChannelFlagsBitField; diff --git a/packages/discord.js/src/util/Channels.js b/packages/discord.js/src/util/Channels.js index a1dc1de4222d..d08e00ccc4f8 100644 --- a/packages/discord.js/src/util/Channels.js +++ b/packages/discord.js/src/util/Channels.js @@ -12,6 +12,7 @@ const getThreadChannel = lazy(() => require('../structures/ThreadChannel')); const getVoiceChannel = lazy(() => require('../structures/VoiceChannel')); const getDirectoryChannel = lazy(() => require('../structures/DirectoryChannel')); const getPartialGroupDMChannel = lazy(() => require('../structures/PartialGroupDMChannel')); +const getForumChannel = lazy(() => require('../structures/ForumChannel')); /** * Creates a discord.js channel from data received from the API. @@ -65,6 +66,9 @@ function createChannel(client, data, guild, { allowUnknownGuild, fromInteraction case ChannelType.GuildDirectory: channel = new (getDirectoryChannel())(guild, data, client); break; + case ChannelType.GuildForum: + channel = new (getForumChannel())(guild, data, client); + break; } if (channel && !allowUnknownGuild) guild.channels?.cache.set(channel.id, channel); } @@ -72,6 +76,75 @@ function createChannel(client, data, guild, { allowUnknownGuild, fromInteraction return channel; } +/** + * Transforms an API guild forum tag to camel-cased guild forum tag. + * @param {APIGuildForumTag} tag The tag to transform + * @returns {GuildForumTag} + * @ignore + */ +function 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 + */ +function 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 + */ +function 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 + */ +function transformGuildDefaultReaction(defaultReaction) { + return { + emoji_id: defaultReaction.id, + emoji_name: defaultReaction.name, + }; +} + module.exports = { createChannel, + transformAPIGuildForumTag, + transformGuildForumTag, + transformAPIGuildDefaultReaction, + transformGuildDefaultReaction, }; diff --git a/packages/discord.js/typings/index.d.ts b/packages/discord.js/typings/index.d.ts index 4ceeb1d27844..5cd71eca2aad 100644 --- a/packages/discord.js/typings/index.d.ts +++ b/packages/discord.js/typings/index.d.ts @@ -124,6 +124,7 @@ import { APIEmbedProvider, AuditLogOptionsType, TextChannelType, + ChannelFlags, } from 'discord-api-types/v10'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -507,7 +508,7 @@ export class BaseGuildTextChannel extends TextBasedChannelMixin(GuildChannel, tr 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>; @@ -726,11 +727,19 @@ export class CategoryChannel extends GuildChannel { export type CategoryChannelResolvable = Snowflake | CategoryChannel; +export type ChannelFlagsString = keyof typeof ChannelFlags; + +export class ChannelFlagsBitField extends BitField { + public static Flags: typeof ChannelFlags; + public static resolve(bit?: BitFieldResolvable): number; +} + export abstract class BaseChannel extends Base { public constructor(client: Client, data?: RawChannelData, immediatePatch?: boolean); public get createdAt(): Date | null; public get createdTimestamp(): number | null; public id: Snowflake; + public flags: Readonly | null; public get partial(): false; public type: ChannelType; public get url(): string; @@ -1042,6 +1051,7 @@ export class DMChannel extends TextBasedChannelMixin(BaseChannel, false, [ 'setNSFW', ]) { private constructor(client: Client, data?: RawDMChannelData); + public flags: Readonly; public recipientId: Snowflake; public get recipient(): User | null; public type: ChannelType.DM; @@ -1224,6 +1234,7 @@ export abstract class GuildChannel extends BaseChannel { public get createdAt(): Date; public get createdTimestamp(): number; public get deletable(): boolean; + public flags: Readonly; public guild: Guild; public guildId: Snowflake; public get manageable(): boolean; @@ -1698,6 +1709,7 @@ export class Message extends Base { public pinned: boolean; public reactions: ReactionManager; public stickers: Collection; + public position: number | null; public system: boolean; public get thread(): AnyThreadChannel | null; public tts: boolean; @@ -2018,7 +2030,7 @@ export class ModalSubmitInteraction extend } export class NewsChannel extends BaseGuildTextChannel { - public threads: ThreadManager; + public threads: GuildTextThreadManager; public type: ChannelType.GuildAnnouncement; public addFollower(channel: TextChannelResolvable, reason?: string): Promise; } @@ -2032,6 +2044,7 @@ export class OAuth2Guild extends BaseGuild { export class PartialGroupDMChannel extends BaseChannel { private constructor(client: Client, data: RawPartialGroupDMChannelData); public type: ChannelType.GroupDM; + public flags: null; public name: string | null; public icon: string | null; public recipients: PartialRecipient[]; @@ -2039,6 +2052,39 @@ export class PartialGroupDMChannel extends BaseChannel { public toString(): ChannelMention; } +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 GuildChannel { + public type: ChannelType.GuildForum; + public threads: GuildForumThreadManager; + public availableTags: GuildForumTag[]; + public defaultReactionEmoji: DefaultReactionEmoji | null; + public defaultThreadRateLimitPerUser: number | null; + public rateLimitPerUser: number | null; + + public setAvailableTags(tags: GuildForumTagData[], reason?: string): Promise; + public setDefaultReactionEmoji(emojiId: DefaultReactionEmoji | null, reason?: string): Promise; + public setDefaultThreadRateLimitPerUser(rateLimit: number, reason?: string): Promise; + public setRateLimitPerUser(rateLimitPerUser: number | null, reason?: string): Promise; +} + export class PermissionOverwrites extends Base { private constructor(client: Client, data: RawPermissionOverwriteData, channel: NonThreadGuildBasedChannel); public allow: Readonly; @@ -2325,6 +2371,7 @@ export class StageChannel extends BaseGuildVoiceChannel { } export class DirectoryChannel extends BaseChannel { + public flags: Readonly; public guild: InviteGuild; public guildId: Snowflake; public name: string; @@ -2489,7 +2536,7 @@ export class TeamMember extends Base { export class TextChannel extends BaseGuildTextChannel { public rateLimitPerUser: number; - public threads: ThreadManager; + public threads: GuildTextThreadManager; public type: ChannelType.GuildText; } @@ -2519,6 +2566,7 @@ export class ThreadChannel extends TextBasedChannelMixin(BaseChannel, true, [ public get createdTimestamp(): number | null; public autoArchiveDuration: ThreadAutoArchiveDuration | null; public get editable(): boolean; + public flags: Readonly; public guild: Guild; public guildId: Snowflake; public get guildMembers(): Collection; @@ -2531,6 +2579,8 @@ export class ThreadChannel extends TextBasedChannelMixin(BaseChannel, true, [ public get sendable(): boolean; public memberCount: number | null; public messageCount: number | null; + public appliedTags: Snowflake[]; + public totalMessageSent: number | null; public members: ThreadMemberManager; public name: string; public ownerId: Snowflake | null; @@ -2558,6 +2608,7 @@ export class ThreadChannel extends TextBasedChannelMixin(BaseChannel, true, [ public setInvitable(invitable?: boolean, reason?: string): Promise; public setLocked(locked?: boolean, reason?: string): Promise; public setName(name: string, reason?: string): Promise; + public setAppliedTags(appliedTags: GuildForumTag[], reason?: string): Promise; public toString(): ChannelMention; } @@ -3176,6 +3227,8 @@ export enum DiscordjsErrorCodes { NotImplemented = 'NotImplemented', SweepFilterReturn = 'SweepFilterReturn', + + GuildForumMessageRequired = 'GuildForumMessageRequired', } export interface DiscordjsErrorFields { @@ -3620,16 +3673,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: AnyThreadChannel; @@ -4158,6 +4218,8 @@ export interface CategoryCreateChannelOptions { position?: number; rtcRegion?: string; videoQualityMode?: VideoQualityMode; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji; reason?: string; } @@ -4835,6 +4897,9 @@ export interface GuildChannelEditOptions { defaultAutoArchiveDuration?: ThreadAutoArchiveDuration; rtcRegion?: string | null; videoQualityMode?: VideoQualityMode | null; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji | null; + defaultThreadRateLimitPerUser?: number; reason?: string; } @@ -5204,6 +5269,9 @@ export interface MessageCreateOptions extends BaseMessageOptions { flags?: BitFieldResolvable, MessageFlags.SuppressEmbeds>; } +export type GuildForumThreadMessageCreateOptions = BaseMessageOptions & + Pick; + export interface MessageEditOptions extends Omit { content?: string | null; attachments?: JSONEncodable[]; @@ -5538,9 +5606,13 @@ export type Channel = | StageChannel | TextChannel | AnyThreadChannel - | VoiceChannel; + | VoiceChannel + | ForumChannel; -export type TextBasedChannel = Exclude, PartialGroupDMChannel>; +export type TextBasedChannel = Exclude< + Extract, + PartialGroupDMChannel | ForumChannel +>; export type TextBasedChannelTypes = TextBasedChannel['type']; @@ -5552,7 +5624,7 @@ export type CategoryChildChannel = Exclude; -export type GuildTextBasedChannel = Extract; +export type GuildTextBasedChannel = Exclude, ForumChannel>; export type TextChannelResolvable = Snowflake | TextChannel; @@ -5562,12 +5634,17 @@ export type ThreadChannelResolvable = AnyThreadChannel | Snowflake; export type ThreadChannelType = ChannelType.AnnouncementThread | ChannelType.PublicThread | ChannelType.PrivateThread; -export interface ThreadCreateOptions extends StartThreadOptions { +export interface GuildTextThreadCreateOptions extends StartThreadOptions { startMessage?: MessageResolvable; type?: AllowedThreadType; invitable?: AllowedThreadType extends ChannelType.PrivateThread ? boolean : never; } +export interface GuildForumThreadCreateOptions extends StartThreadOptions { + message: GuildForumThreadMessageCreateOptions | MessagePayload; + appliedTags?: Snowflake[]; +} + export interface ThreadEditData { name?: string; archived?: boolean; @@ -5630,6 +5707,7 @@ export interface WebhookCreateMessageOptions extends Omit>(guildChannelManager.create({ name: 'name' })); expectType>(guildChannelManager.create({ name: 'name' })); expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildVoice })); @@ -1370,8 +1370,10 @@ declare const guildChannelManager: GuildChannelManager; expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildAnnouncement })); expectType>(guildChannelManager.create({ name: 'name', type: ChannelType.GuildStageVoice })); - expectType>>(guildChannelManager.fetch()); - expectType>>(guildChannelManager.fetch(undefined, {})); + expectType>>(guildChannelManager.fetch()); + expectType>>( + guildChannelManager.fetch(undefined, {}), + ); expectType>(guildChannelManager.fetch('0')); const channel = guildChannelManager.cache.first()!; @@ -1846,7 +1848,9 @@ expectType< >(TextBasedChannelTypes); expectType(VoiceBasedChannel); expectType(GuildBasedChannel); -expectType(NonThreadGuildBasedChannel); +expectType( + NonThreadGuildBasedChannel, +); expectType(GuildTextBasedChannel); const button = new ButtonBuilder({ @@ -1976,3 +1980,25 @@ expectType>(webhookClient.fetchMessage(snowflake)); expectType>(interactionWebhook.send('content')); expectType>(interactionWebhook.editMessage(snowflake, 'content')); expectType>(interactionWebhook.fetchMessage(snowflake)); + +declare const categoryChannel: CategoryChannel; +declare const forumChannel: ForumChannel; + +await forumChannel.edit({ + availableTags: [...forumChannel.availableTags, { name: 'tag' }], +}); + +await forumChannel.setAvailableTags([{ ...forumChannel.availableTags, name: 'tag' }]); +await forumChannel.setAvailableTags([{ name: 'tag' }]); + +expectType>(textChannel.flags); +expectType>(voiceChannel.flags); +expectType>(stageChannel.flags); +expectType>(forumChannel.flags); +expectType>(dmChannel.flags); +expectType>(categoryChannel.flags); +expectType>(newsChannel.flags); +expectType>(categoryChannel.flags); +expectType>(threadChannel.flags); + +expectType(partialGroupDMChannel.flags);