Skip to content

Commit

Permalink
feat: add support for guild forums
Browse files Browse the repository at this point in the history
  • Loading branch information
suneettipirneni committed Jun 1, 2022
1 parent 68d5169 commit 38f345e
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 78 deletions.
2 changes: 2 additions & 0 deletions packages/discord.js/src/errors/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ const Messages = {
NOT_IMPLEMENTED: (what, name) => `Method ${what} not implemented on ${name}.`,

SWEEP_FILTER_RETURN: 'The return value of the sweepFilter function was not false or a Function',

GUILD_FORUM_MESSAGE_REQUIRED: 'You must provide a message to create a guild forum thread',
};

Messages.AuthenticationFailed = Messages.TOKEN_INVALID;
Expand Down
3 changes: 2 additions & 1 deletion packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,15 @@ exports.GuildMemberManager = require('./managers/GuildMemberManager');
exports.GuildMemberRoleManager = require('./managers/GuildMemberRoleManager');
exports.GuildScheduledEventManager = require('./managers/GuildScheduledEventManager');
exports.GuildStickerManager = require('./managers/GuildStickerManager');
exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager');
exports.MessageManager = require('./managers/MessageManager');
exports.PermissionOverwriteManager = require('./managers/PermissionOverwriteManager');
exports.PresenceManager = require('./managers/PresenceManager');
exports.ReactionManager = require('./managers/ReactionManager');
exports.ReactionUserManager = require('./managers/ReactionUserManager');
exports.RoleManager = require('./managers/RoleManager');
exports.StageInstanceManager = require('./managers/StageInstanceManager');
exports.ThreadManager = require('./managers/ThreadManager');
exports.ThreadManager = require('./managers/GuildTextThreadManager');
exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
exports.UserManager = require('./managers/UserManager');
exports.VoiceStateManager = require('./managers/VoiceStateManager');
Expand Down
4 changes: 2 additions & 2 deletions packages/discord.js/src/managers/GuildChannelManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { ChannelType, Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const ThreadManager = require('./ThreadManager');
const GuildTextThreadManager = require('./GuildTextThreadManager');
const { Error, TypeError } = require('../errors');
const GuildChannel = require('../structures/GuildChannel');
const PermissionOverwrites = require('../structures/PermissionOverwrites');
Expand Down Expand Up @@ -411,7 +411,7 @@ class GuildChannelManager extends CachedManager {
*/
async fetchActiveThreads(cache = true) {
const raw = await this.client.rest.get(Routes.guildActiveThreads(this.guild.id));
return ThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache });
return GuildTextThreadManager._mapThreads(raw, this.client, { guild: this.guild, cache });
}

/**
Expand Down
76 changes: 76 additions & 0 deletions packages/discord.js/src/managers/GuildForumThreadManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'use strict';

const { Routes } = require('discord-api-types/v10');
const ThreadManager = require('./ThreadManager');
const { TypeError } = require('../errors');
const MessagePayload = require('../structures/MessagePayload');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');

class GuildForumThreadManager extends ThreadManager {
/**
* Options for creating a thread. <warn>Only one of `startMessage` or `type` can be defined.</warn>
* @typedef {StartThreadOptions} GuildForumThreadCreateOptions
* @property {MessageOptions|MessagePayload} message The message associated with the thread post
*/

/**
* Creates a new thread in the channel.
* @param {GuildForumThreadCreateOptions} [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: ChannelType.GuildPrivateThread,
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
message,
reason,
rateLimitPerUser,
} = {}) {
if (!message) {
throw new TypeError('GUILD_FORUM_MESSAGE_REQUIRED');
}

const messagePayload =
message instanceof MessagePayload ? message.resolveBody() : MessagePayload.create(this, message);

const { body, files } = messagePayload.resolveFiles();

if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);

const data = await this.client.rest.post(Routes.threads(this.channel.id), {
body: {
name,
auto_archive_duration: autoArchiveDuration,
rate_limit_per_user: rateLimitPerUser,
...body,
},
files,
reason,
});

// TODO: Posts will most likely need to be serialized differently than regular threads.
return this.client.actions.ThreadCreate.handle(data).thread;
}
}

module.exports = GuildForumThreadManager;
91 changes: 91 additions & 0 deletions packages/discord.js/src/managers/GuildTextThreadManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
'use strict';

const { ChannelType, Routes } = require('discord-api-types/v10');
const ThreadManager = require('./ThreadManager');
const { TypeError } = require('../errors');
const { resolveAutoArchiveMaxLimit } = require('../util/Util');

/**
* Manages API methods for {@link ThreadChannel} objects and stores their cache.
* @extends {CachedManager}
*/
class GuildTextThreadManager extends ThreadManager {
/**
* 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 {@link ChannelType.GuildPublicThread} if created in a {@link TextChannel}
* <warn>When creating threads in a {@link NewsChannel} this is ignored and is always
* {@link ChannelType.GuildNewsThread}</warn>
* @property {boolean} [invitable] Whether non-moderators can add other non-moderators to the thread
* <info>Can only be set when type will be {@link ChannelType.GuildPrivateThread}</info>
*/

/**
* 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: ChannelType.GuildPrivateThread,
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
startMessage,
type,
invitable,
reason,
rateLimitPerUser,
} = {}) {
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === ChannelType.GuildNews ? ChannelType.GuildNewsThread : ChannelType.GuildPublicThread;
let startMessageId;
if (startMessage) {
startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
} else if (this.channel.type !== ChannelType.GuildNews) {
resolvedType = type ?? resolvedType;
}

if (autoArchiveDuration === 'MAX') autoArchiveDuration = resolveAutoArchiveMaxLimit(this.channel.guild);

const data = await this.client.rest.post(Routes.threads(this.channel.id, startMessageId), {
body: {
name,
auto_archive_duration: autoArchiveDuration,
type: resolvedType,
invitable: resolvedType === ChannelType.GuildPrivateThread ? invitable : undefined,
rate_limit_per_user: rateLimitPerUser,
},
reason,
});

return this.client.actions.ThreadCreate.handle(data).thread;
}
}

module.exports = GuildTextThreadManager;
93 changes: 28 additions & 65 deletions packages/discord.js/src/managers/ThreadManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { ChannelType, Routes } = require('discord-api-types/v10');
const { Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const { TypeError } = require('../errors');
const ThreadChannel = require('../structures/ThreadChannel');

/**
* Manages API methods for {@link ThreadChannel} objects and stores their cache.
* Manages API methods for thread based channels and stores their cache.
* @extends {CachedManager}
*/
class ThreadManager extends CachedManager {
Expand All @@ -17,11 +17,36 @@ class ThreadManager extends CachedManager {

/**
* The channel this Manager belongs to
* @type {NewsChannel|TextChannel}
* @type {NewsChannel|TextChannel|GuildForumChannel}
*/
this.channel = channel;
}

/**
* Data that can be resolved to a Thread Channel object. This can be:
* * A ThreadChannel object
* * A Snowflake
* @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable
*/

/**
* Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} object.
* @method resolve
* @memberof ThreadManager
* @instance
* @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
* @returns {?ThreadChannel}
*/

/**
* Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} id.
* @method resolveId
* @memberof ThreadManager
* @instance
* @param {ThreadChannelResolvable} thread The ThreadChannel resolvable to resolve
* @returns {?Snowflake}
*/

/**
* The cache of this Manager
* @type {Collection<Snowflake, ThreadChannel>}
Expand Down Expand Up @@ -73,68 +98,6 @@ class ThreadManager extends CachedManager {
* <info>Can only be set when type will be {@link ChannelType.GuildPrivateThread}</info>
*/

/**
* 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: ChannelType.GuildPrivateThread,
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
startMessage,
type,
invitable,
reason,
rateLimitPerUser,
} = {}) {
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError('INVALID_TYPE', 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === ChannelType.GuildNews ? ChannelType.GuildNewsThread : ChannelType.GuildPublicThread;
let startMessageId;
if (startMessage) {
startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError('INVALID_TYPE', 'startMessage', 'MessageResolvable');
} else if (this.channel.type !== ChannelType.GuildNews) {
resolvedType = type ?? resolvedType;
}

const data = await this.client.rest.post(Routes.threads(this.channel.id, startMessageId), {
body: {
name,
auto_archive_duration: autoArchiveDuration,
type: resolvedType,
invitable: resolvedType === ChannelType.GuildPrivateThread ? 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
Expand Down
4 changes: 2 additions & 2 deletions packages/discord.js/src/structures/BaseGuildTextChannel.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

const GuildChannel = require('./GuildChannel');
const TextBasedChannel = require('./interfaces/TextBasedChannel');
const ThreadManager = require('../managers/GuildTextThreadManager');
const MessageManager = require('../managers/MessageManager');
const ThreadManager = require('../managers/ThreadManager');

/**
* Represents a text-based guild channel on Discord.
Expand All @@ -22,7 +22,7 @@ class BaseGuildTextChannel extends GuildChannel {

/**
* A manager of the threads belonging to this channel
* @type {ThreadManager}
* @type {GuildTextThreadManager}
*/
this.threads = new ThreadManager(this);

Expand Down
22 changes: 22 additions & 0 deletions packages/discord.js/src/structures/GuildForumChannel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
'use strict';

const GuildChannel = require('./GuildChannel');
const GuildForumThreadManager = require('../managers/GuildForumThreadManager');

/**
* Represents a channel that only contains threads
* @extends {GuildChannel}
*/
class GuildForumChannel 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);
}
}

module.exports = GuildForumChannel;
2 changes: 1 addition & 1 deletion packages/discord.js/src/structures/Message.js
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,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>}
*/
Expand Down

0 comments on commit 38f345e

Please sign in to comment.