diff --git a/package.json b/package.json index cfd7d72..bb4312c 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "axios": "^0.27.2", "bignumber.js": "^9.1.0", "chalk": "^4.1.2", - "discord-api-types": "^0.37.10", + "discord-api-types": "^0.37.11", "form-data": "^4.0.0", "json-bigint": "^1.0.0", "node-fetch": "^2.6.1", diff --git a/src/client/actions/WebhooksUpdate.js b/src/client/actions/WebhooksUpdate.js index 6c9aa35..b23fe23 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 84df648..3e5972d 100644 --- a/src/errors/Messages.js +++ b/src/errors/Messages.js @@ -207,6 +207,8 @@ const Messages = { NITRO_REQUIRED: 'This feature is only available for Nitro users.', NITRO_BOOST_REQUIRED: feature => `This feature (${feature}) is only available for Nitro Boost users.`, ONLY_ME: 'This feature is only available for self.', + + 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 f45df22..3c30bbc 100644 --- a/src/managers/GuildChannelManager.js +++ b/src/managers/GuildChannelManager.js @@ -9,10 +9,11 @@ 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 } = 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 +74,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 +140,10 @@ class GuildChannelManager extends CachedManager { position, rateLimitPerUser, rtcRegion, + videoQualityMode, + availableTags, + defaultReactionEmoji, + defaultSortOrder, reason, } = {}, ) { @@ -145,6 +151,10 @@ 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]; + if (intType === ChannelTypes.GUILD_STORE && !storeChannelDeprecationEmitted) { storeChannelDeprecationEmitted = true; process.emitWarning( @@ -167,6 +177,10 @@ 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, }, reason, }); @@ -224,6 +238,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 +301,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 00000000..b75332e --- /dev/null +++ b/src/managers/GuildForumThreadManager.js @@ -0,0 +1,92 @@ +'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 {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 {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, + } = {}) { + let path = this.client.api.channels(this.channel.id); + + 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 path.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 00000000..c11820d --- /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 cb6b837..5341e51 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 f1ae2ee..033d8cf 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 ce5b5f6..bb5c302 100644 --- a/src/structures/CategoryChannel.js +++ b/src/structures/CategoryChannel.js @@ -40,6 +40,11 @@ class CategoryChannel extends GuildChannel { * @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 {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 {SortOrderType} [defaultSortOrder] The default sort order mode used to order posts (forum only). * @property {string} [reason] Reason for creating the new channel */ diff --git a/src/structures/Channel.js b/src/structures/Channel.js index 2460dd6..77dae59 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'); // Const { ApplicationCommand } = require('discord.js-selfbot-v13'); - Not being used in this file, not necessary. @@ -48,6 +50,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(); + } } /** @@ -184,6 +197,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,9 @@ 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 00000000..330740f --- /dev/null +++ b/src/structures/ForumChannel.js @@ -0,0 +1,249 @@ +'use strict'; + +const GuildChannel = require('./GuildChannel'); +const TextBasedChannel = require('./interfaces/TextBasedChannel'); +const GuildForumThreadManager = require('../managers/GuildForumThreadManager'); +const { SortOrderTypes } = 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) + : 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; + } + + 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; + } + } + + /** + * 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 }); + } + + /** + * 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 f918712..ceba3dc 100644 --- a/src/structures/Guild.js +++ b/src/structures/Guild.js @@ -564,7 +564,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 37dace2..2a79609 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 ce03c66..125bdb7 100644 --- a/src/structures/Message.js +++ b/src/structures/Message.js @@ -61,6 +61,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} @@ -860,7 +871,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 32364ea..b598624 100644 --- a/src/structures/MessagePayload.js +++ b/src/structures/MessagePayload.js @@ -144,9 +144,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; @@ -259,6 +261,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 42d666b..3319715 100644 --- a/src/structures/PartialGroupDMChannel.js +++ b/src/structures/PartialGroupDMChannel.js @@ -17,6 +17,10 @@ const DataResolver = require('../util/DataResolver'); 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 10afa20..60b4553 100644 --- a/src/structures/ThreadChannel.js +++ b/src/structures/ThreadChannel.js @@ -158,9 +158,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 +179,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 +245,7 @@ class ThreadChannel extends Channel { /** * The parent channel of this thread - * @type {?(NewsChannel|TextChannel)} + * @type {?(NewsChannel|TextChannel|ForumChannel)} * @readonly */ get parent() { @@ -280,14 +300,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; } /** @@ -326,6 +348,7 @@ 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, }, reason, }); @@ -333,6 +356,16 @@ class ThreadChannel extends Channel { return this.client.actions.ChannelUpdate.handle(newData).updated; } + /** + * 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 }); + } + /** * Sets whether the thread is archived. * @param {boolean} [archived=true] Whether the thread is archived diff --git a/src/structures/Webhook.js b/src/structures/Webhook.js index 6b2ddb2..5745211 100644 --- a/src/structures/Webhook.js +++ b/src/structures/Webhook.js @@ -132,6 +132,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 0741ab7..2b1fcba 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 00000000..302baa9 --- /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 3716d0f..5dafbb1 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -1640,6 +1640,15 @@ exports.GuildScheduledEventEntityTypes = createEnum([null, 'STAGE_INSTANCE', 'VO */ exports.VideoQualityModes = createEnum([null, 'AUTO', 'FULL']); +/** + * Sort {@link ForumChannel} posts by ? + * * 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']); + exports._cleanupSymbol = Symbol('djsCleanup'); function keyMirror(arr) { diff --git a/src/util/Util.js b/src/util/Util.js index 69048ef..ca3c38d 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -606,6 +606,71 @@ class Util extends null { let defaultValue; return () => (defaultValue ??= cb()); } + + /** + * 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, + }; + } } module.exports = Util; diff --git a/typings/index.d.ts b/typings/index.d.ts index b23a565..ca6ebc1 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -53,6 +53,7 @@ import { RESTPostAPIApplicationCommandsJSONBody, Snowflake, LocalizationMap, + SortOrderType, } from 'discord-api-types/v9'; import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; @@ -674,7 +675,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>; @@ -757,7 +758,7 @@ export type MappedChannelCategoryTypes = EnumValueMapped< GUILD_TEXT: TextChannel; GUILD_STORE: StoreChannel; GUILD_STAGE_VOICE: StageChannel; - GUILD_FORUM: undefined; + GUILD_FORUM: ForumChannel; } >; @@ -801,6 +802,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; @@ -1193,6 +1195,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; public readonly voiceAdapterCreator: InternalDiscordGatewayAdapterCreator; public call(options?: object): Promise; @@ -1272,7 +1275,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; @@ -1416,6 +1419,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; @@ -1900,6 +1904,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]>; @@ -2334,7 +2339,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; } @@ -2362,6 +2367,7 @@ export class PartialGroupDMChannel extends TextBasedChannelMixin(Channel, [ public lastPinTimestamp: number | null; public owner: User | null; public ownerId: Snowflake | null; + public flags: null; public iconURL(options?: StaticImageURLOptions): string | null; public addMember(user: User): Promise; public removeMember(user: User): Promise; @@ -2661,7 +2667,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); @@ -2835,7 +2843,7 @@ export class TeamMember extends Base { export class TextChannel extends BaseGuildTextChannel { public rateLimitPerUser: number; - public threads: ThreadManager; + public threads: GuildTextThreadManager; public type: 'GUILD_TEXT'; } @@ -2886,10 +2894,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; @@ -2915,6 +2926,7 @@ 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; } export class ThreadMember extends Base { @@ -3296,7 +3308,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; } @@ -3979,10 +3991,9 @@ 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; @@ -4413,7 +4424,7 @@ export interface ClientEvents extends BaseClientEvents { userUpdate: [oldUser: User | PartialUser, newUser: User]; userSettingsUpdate: [setting: RawUserSettingsData]; 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 | { nonce: Snowflake; id: Snowflake }]; @@ -4969,6 +4980,10 @@ export interface CategoryCreateChannelOptions { rateLimitPerUser?: number; position?: number; rtcRegion?: string; + videoQualityMode?: VideoQualityMode; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultSortOrder?: SortOrderType; reason?: string; } @@ -4993,6 +5008,11 @@ export interface ChannelData { defaultAutoArchiveDuration?: ThreadAutoArchiveDuration | 'MAX'; rtcRegion?: string | null; videoQualityMode?: VideoQualityMode | null; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultThreadRateLimitPerUser?: number; + defaultSortOrder?: SortOrderType | null; + flags?: ChannelFlagsResolvable; } export interface ChannelLogsQueryOptions { @@ -6015,11 +6035,32 @@ export type MessageComponentTypeResolvable = MessageComponentType | MessageCompo export interface MessageEditOptions { attachments?: MessageAttachment[]; content?: string | null; - embeds?: (MessageEmbed | MessageEmbedOptions | APIEmbed)[] | null; - files?: (FileOptions | BufferResolvable | Stream | MessageAttachment)[]; - flags?: BitFieldResolvable; - allowedMentions?: MessageMentionOptions; - components?: (MessageActionRow | (Required & MessageActionRowOptions))[]; + id?: Snowflake | number; + parentId?: Snowflake | number; + type?: ExcludeEnum< + typeof ChannelTypes, + | 'DM' + | 'GROUP_DM' + | 'GUILD_NEWS' + | 'GUILD_STORE' + | 'UNKNOWN' + | 'GUILD_NEWS_THREAD' + | 'GUILD_PUBLIC_THREAD' + | 'GUILD_PRIVATE_THREAD' + | 'GUILD_STAGE_VOICE' + >; + name: string; + topic?: string; + nsfw?: boolean; + bitrate?: number; + userLimit?: number; + rtcRegion?: string | null; + videoQualityMode?: VideoQualityMode; + permissionOverwrites?: PartialOverwriteData[]; + rateLimitPerUser?: number; + availableTags?: GuildForumTagData[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultThreadRateLimitPerUser?: number; } export interface MessageEmbedAuthor { @@ -6572,9 +6613,10 @@ export type AnyChannel = | StoreChannel | TextChannel | ThreadChannel - | VoiceChannel; + | VoiceChannel + | ForumChannel; -export type TextBasedChannel = Extract; +export type TextBasedChannel = Exclude, ForumChannel>; export type TextBasedChannelTypes = TextBasedChannel['type']; @@ -6603,7 +6645,7 @@ export type GuildBasedChannel = Extract; export type NonThreadGuildBasedChannel = Exclude; -export type GuildTextBasedChannel = Extract; +export type GuildTextBasedChannel = Exclude, ForumChannel>; export type TextChannelResolvable = Snowflake | TextChannel; @@ -6615,7 +6657,7 @@ 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; @@ -6629,6 +6671,7 @@ export interface ThreadEditData { rateLimitPerUser?: number; locked?: boolean; invitable?: boolean; + threadName?: string; } export type ThreadMemberFlagsString = ''; @@ -6737,7 +6780,7 @@ export interface WidgetChannel { export interface WelcomeChannelData { description: string; - channel: TextChannel | NewsChannel | StoreChannel | Snowflake; + channel: TextChannel | NewsChannel | StoreChannel | ForumChannel | Snowflake; emoji?: EmojiIdentifierResolvable; } @@ -6840,4 +6883,82 @@ export type InternalDiscordGatewayAdapterCreator = ( methods: InternalDiscordGatewayAdapterLibraryMethods, ) => InternalDiscordGatewayAdapterImplementerMethods; +// GuildForum +export type ChannelFlagsString = + | 'PINNED' + | 'REQUIRE_TAG'; +export class ChannelFlags extends BitField { + public static FLAGS: Record; + public static resolve(bit?: BitFieldResolvable): number; +} + +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 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; +} + +export class GuildTextThreadManager extends ThreadManager { + public create(options: GuildTextThreadCreateOptions): Promise; +} + +export class GuildForumThreadManager extends ThreadManager { + public create(options: GuildForumThreadCreateOptions): Promise; +} + +export type GuildForumThreadMessageCreateOptions = MessageOptions & Pick; + +export type ChannelFlagsResolvable = BitFieldResolvable; + +export interface GuildForumThreadCreateOptions extends StartThreadOptions { + message: GuildForumThreadMessageCreateOptions | MessagePayload; + appliedTags?: Snowflake[]; +} + //#endregion