feat: Guild Forum channel
https://github.com/discordjs/discord.js/pull/8651
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
@@ -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);
|
||||
|
@@ -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,
|
||||
});
|
||||
|
92
src/managers/GuildForumThreadManager.js
Normal file
92
src/managers/GuildForumThreadManager.js
Normal file
@@ -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<ThreadChannel>}
|
||||
* @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;
|
98
src/managers/GuildTextThreadManager.js
Normal file
98
src/managers/GuildTextThreadManager.js
Normal file
@@ -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. <warn>Only one of `startMessage` or `type` can be defined.</warn>
|
||||
* @typedef {StartThreadOptions} GuildTextThreadCreateOptions
|
||||
* @property {MessageResolvable} [startMessage] The message to start a thread from. <warn>If this is defined then type
|
||||
* of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored</warn>
|
||||
* @property {ThreadChannelTypes|number} [type] The type of thread to create. Defaults to `GUILD_PUBLIC_THREAD` if
|
||||
* created in a {@link TextChannel} <warn>When creating threads in a {@link NewsChannel} this is ignored and is always
|
||||
* `GUILD_NEWS_THREAD`</warn>
|
||||
* @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread
|
||||
* <info>Can only be set when type will be `GUILD_PRIVATE_THREAD`</info>
|
||||
* @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<ThreadChannel>}
|
||||
* @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;
|
@@ -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. <warn>Only one of `startMessage` or `type` can be defined.</warn>
|
||||
* @typedef {StartThreadOptions} ThreadCreateOptions
|
||||
* @property {MessageResolvable} [startMessage] The message to start a thread from. <warn>If this is defined then type
|
||||
* of thread gets automatically defined and cannot be changed. The provided `type` field will be ignored</warn>
|
||||
* @property {ThreadChannelTypes|number} [type] The type of thread to create. Defaults to `GUILD_PUBLIC_THREAD` if
|
||||
* created in a {@link TextChannel} <warn>When creating threads in a {@link NewsChannel} this is ignored and is always
|
||||
* `GUILD_NEWS_THREAD`</warn>
|
||||
* @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread
|
||||
* <info>Can only be set when type will be `GUILD_PRIVATE_THREAD`</info>
|
||||
* @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<ThreadChannel>}
|
||||
* @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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
*/
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
249
src/structures/ForumChannel.js
Normal file
249
src/structures/ForumChannel.js
Normal 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;
|
@@ -564,7 +564,7 @@ class Guild extends AnonymousGuild {
|
||||
|
||||
/**
|
||||
* Widget channel for this guild
|
||||
* @type {?TextChannel}
|
||||
* @type {?(TextChannel|NewsChannel|VoiceChannel|StageChannel|ForumChannel)}
|
||||
* @readonly
|
||||
*/
|
||||
get widgetChannel() {
|
||||
|
@@ -14,6 +14,7 @@ const Permissions = require('../util/Permissions');
|
||||
* - {@link NewsChannel}
|
||||
* - {@link StoreChannel}
|
||||
* - {@link StageChannel}
|
||||
* - {@link ForumChannel}
|
||||
* @extends {Channel}
|
||||
* @abstract
|
||||
*/
|
||||
|
@@ -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>}
|
||||
*/
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@@ -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);
|
||||
|
45
src/util/ChannelFlags.js
Normal file
45
src/util/ChannelFlags.js
Normal file
@@ -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;
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user