feat: Guild Forum channel

https://github.com/discordjs/discord.js/pull/8651
This commit is contained in:
March 7th
2022-10-03 19:25:39 +07:00
parent 5e715fad5e
commit 2fb09d7da6
23 changed files with 821 additions and 120 deletions

View File

@@ -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

View File

@@ -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
*/

View File

@@ -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.
* <info>This is only `null` in a {@link PartialGroupDMChannel}. In all other cases, it is not `null`.</info>
* @type {?Readonly<ChannelFlags>}
*/
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);
}

View File

@@ -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<ForumChannel>}
*/
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<ForumChannel>}
*/
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<ForumChannel>}
*/
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<ForumChannel>}
*/
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<Invite>}
* @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<Collection<string, Invite>>}
*/
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<ForumChannel>}
*/
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<ForumChannel>}
* @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;

View File

@@ -564,7 +564,7 @@ class Guild extends AnonymousGuild {
/**
* Widget channel for this guild
* @type {?TextChannel}
* @type {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel)}
* @readonly
*/
get widgetChannel() {

View File

@@ -14,6 +14,7 @@ const Permissions = require('../util/Permissions');
* - {@link NewsChannel}
* - {@link StoreChannel}
* - {@link StageChannel}
* - {@link ForumChannel}
* @extends {Channel}
* @abstract
*/

View File

@@ -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<ThreadChannel>}
*/

View File

@@ -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;
}

View File

@@ -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}

View File

@@ -158,9 +158,8 @@ class ThreadChannel extends Channel {
if ('message_count' in data) {
/**
* The approximate count of messages in this thread
* <info>This stops counting at 50. If you need an approximate value higher than that, use
* `ThreadChannel#messages.cache.size`</info>
* <info>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`</info>
* @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.
* <info>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.</info>
* <info>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.</info>
* @param {BaseFetchOptions} [options] Additional options for this fetch
* @returns {Promise<Message|null>}
*/
// 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<GuildForumThreadChannel>}
*/
setAppliedTags(appliedTags, reason) {
return this.edit({ appliedTags, reason });
}
/**
* Sets whether the thread is archived.
* @param {boolean} [archived=true] Whether the thread is archived

View File

@@ -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
* <info>For interaction webhooks, this property is ignored</info>
* @property {string} [threadName] Name of the thread to create (only available if webhook is in a forum channel)
*/
/**

View File

@@ -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);