Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for guild forums #7791

Merged
merged 60 commits into from Sep 18, 2022
Merged
Show file tree
Hide file tree
Changes from 48 commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
f5def06
feat: add support for guild forums
suneettipirneni Apr 13, 2022
d4c05c3
feat(webhook): add support for creating forum channel posts
suneettipirneni May 31, 2022
74fd857
fix: duplicated docs
suneettipirneni Jun 1, 2022
d69a223
feat: add support for message counts
suneettipirneni Jul 20, 2022
ee20f2e
feat: add support for latest upstream changes
suneettipirneni Aug 30, 2022
a39129e
fix: serialize forum channels
suneettipirneni Sep 2, 2022
0938831
types: fix channel unions
suneettipirneni Sep 2, 2022
8968684
types: fix tests
suneettipirneni Sep 2, 2022
8f9c78a
types: fix tests (again)
suneettipirneni Sep 2, 2022
73a1877
types: fix tests (again (again))
suneettipirneni Sep 2, 2022
3704d20
chore: make requested changes
suneettipirneni Sep 13, 2022
2353b63
chore: fix bugs and make requested changes
suneettipirneni Sep 13, 2022
36ff6ef
types: use correct type for guild forum start messages
suneettipirneni Sep 13, 2022
ee6c7c7
chore: remove console.log
suneettipirneni Sep 13, 2022
0f9261c
chore: make requested changes
suneettipirneni Sep 13, 2022
aedb3d3
chore: make requested changes
suneettipirneni Sep 13, 2022
24e2b4c
chore: fix docs
suneettipirneni Sep 13, 2022
5385ccf
Update packages/discord.js/src/managers/GuildForumThreadManager.js
suneettipirneni Sep 14, 2022
0df75a7
chore: update types
suneettipirneni Sep 14, 2022
634ba28
chore: make requested changes
suneettipirneni Sep 14, 2022
03fd070
chore: Apply suggestions
Jiralite Sep 15, 2022
46d3629
fix: import `ErrorCodes`
Jiralite Sep 15, 2022
05f0094
fix: remove defunct code
Jiralite Sep 15, 2022
8020ee8
refactor: be consistent with channel class names
Jiralite Sep 15, 2022
62a5d96
feat(GuildChannel): add flags
Jiralite Sep 15, 2022
bbd0581
fix: rename file
Jiralite Sep 15, 2022
16033b4
refactor: channel flags are everywhere!
Jiralite Sep 15, 2022
f34566f
fix: import flags correctly
Jiralite Sep 15, 2022
c8494d0
chore(ThreadChannel): update message count string
Jiralite Sep 15, 2022
abe4d1e
docs(Channels): correct `@param` type
Jiralite Sep 15, 2022
a475de3
docs(Channels): ignore transformGuildDefaultReaction
Jiralite Sep 15, 2022
a3b5deb
refactor: emoji object in tags
Jiralite Sep 15, 2022
aeeba37
chore: renaming consistency
Jiralite Sep 15, 2022
ebbb6eb
fix: document default reaction emojis in patching
Jiralite Sep 15, 2022
b8294b9
fix(GuildChannelManager): document `defaultThreadRateLimitPerUser`
Jiralite Sep 15, 2022
84c9eab
chore: semicolon
Jiralite Sep 15, 2022
564ddba
docs(ErrorCodes): document `GuildForumMessageRequired`
Jiralite Sep 16, 2022
b9815d1
refactor: transform default reactions
Jiralite Sep 16, 2022
4c88604
docs(APITypes): Add `ChannelFlags`
Jiralite Sep 16, 2022
a0d17b7
fix: convert tags properly
Jiralite Sep 16, 2022
6c2d2fc
fix: pass an array of snowflakes
Jiralite Sep 16, 2022
6a24955
refactor: handle flags better
Jiralite Sep 16, 2022
8278805
fix(ThreadChannel): receive tags
Jiralite Sep 16, 2022
2bd75f9
fix(PartialGroupDMChannel): nullify `flags`
Jiralite Sep 16, 2022
6ef3db7
chore: misc sorting
Jiralite Sep 16, 2022
b69417a
refactor: nullify emoji on tags if not present
Jiralite Sep 16, 2022
510a0ae
refactor(ForumChannel): modify returns
Jiralite Sep 16, 2022
d1cc0b5
types: protect the thread manager!
Jiralite Sep 17, 2022
ee73916
chore: update `ChannelType` usage
Jiralite Sep 17, 2022
376e344
Update index.d.ts
Jiralite Sep 17, 2022
606e299
docs: Update default reaction emoji property names
Jiralite Sep 17, 2022
761786b
fix: only `name` is required when editing tags
almeidx Sep 17, 2022
e5ca18a
types: add tests for `channel.flags`
almeidx Sep 17, 2022
b8f99de
fix: allow unsetting the default reaction emoji
almeidx Sep 17, 2022
ed9667a
refactor: remove v13 remnants
almeidx Sep 17, 2022
097db15
docs: add missing closing tag
almeidx Sep 17, 2022
9369a14
feat: add `rateLimitPerUser`
almeidx Sep 17, 2022
231d7f0
feat: add missing properties for create guild channel
almeidx Sep 17, 2022
25af6b6
refactor(GuildForumThreadManager): refactor message payload
Jiralite Sep 17, 2022
45bd884
fix: handle magical `null` case
Jiralite Sep 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/discord.js/src/errors/ErrorCodes.js
Expand Up @@ -143,6 +143,8 @@

* @property {'NotImplemented'} NotImplemented

* @property {'GuildForumMessageRequired'} GuildForumMessageRequired

* @property {'SweepFilterReturn'} SweepFilterReturn
*/

Expand Down Expand Up @@ -288,6 +290,8 @@ const keys = [
'NotImplemented',

'SweepFilterReturn',

'GuildForumMessageRequired',
vladfrangu marked this conversation as resolved.
Show resolved Hide resolved
];

// JSDoc for IntelliSense purposes
Expand Down
2 changes: 2 additions & 0 deletions packages/discord.js/src/errors/Messages.js
Expand Up @@ -157,6 +157,8 @@ const Messages = {
[DjsErrorCodes.NotImplemented]: (what, name) => `Method ${what} not implemented on ${name}.`,

[DjsErrorCodes.SweepFilterReturn]: 'The return value of the sweepFilter function was not false or a Function',

[DjsErrorCodes.GuildForumMessageRequired]: 'You must provide a message to create a guild forum thread',
};

module.exports = Messages;
4 changes: 4 additions & 0 deletions packages/discord.js/src/index.js
Expand Up @@ -22,6 +22,7 @@ exports.ActivityFlagsBitField = require('./util/ActivityFlagsBitField');
exports.ApplicationFlagsBitField = require('./util/ApplicationFlagsBitField');
exports.BaseManager = require('./managers/BaseManager');
exports.BitField = require('./util/BitField');
exports.ChannelFlagsBitField = require('./util/ChannelFlagsBitField');
exports.Collection = require('@discordjs/collection').Collection;
exports.Constants = require('./util/Constants');
exports.Colors = require('./util/Colors');
Expand Down Expand Up @@ -58,12 +59,14 @@ exports.GuildBanManager = require('./managers/GuildBanManager');
exports.GuildChannelManager = require('./managers/GuildChannelManager');
exports.GuildEmojiManager = require('./managers/GuildEmojiManager');
exports.GuildEmojiRoleManager = require('./managers/GuildEmojiRoleManager');
exports.GuildForumThreadManager = require('./managers/GuildForumThreadManager');
exports.GuildInviteManager = require('./managers/GuildInviteManager');
exports.GuildManager = require('./managers/GuildManager');
exports.GuildMemberManager = require('./managers/GuildMemberManager');
exports.GuildMemberRoleManager = require('./managers/GuildMemberRoleManager');
exports.GuildScheduledEventManager = require('./managers/GuildScheduledEventManager');
exports.GuildStickerManager = require('./managers/GuildStickerManager');
exports.GuildTextThreadManager = require('./managers/GuildTextThreadManager');
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved
exports.MessageManager = require('./managers/MessageManager');
exports.PermissionOverwriteManager = require('./managers/PermissionOverwriteManager');
exports.PresenceManager = require('./managers/PresenceManager');
Expand Down Expand Up @@ -109,6 +112,7 @@ exports.DMChannel = require('./structures/DMChannel');
exports.Embed = require('./structures/Embed');
exports.EmbedBuilder = require('./structures/EmbedBuilder');
exports.Emoji = require('./structures/Emoji').Emoji;
exports.ForumChannel = require('./structures/ForumChannel');
exports.Guild = require('./structures/Guild').Guild;
exports.GuildAuditLogs = require('./structures/GuildAuditLogs');
exports.GuildAuditLogsEntry = require('./structures/GuildAuditLogsEntry');
Expand Down
13 changes: 11 additions & 2 deletions packages/discord.js/src/managers/GuildChannelManager.js
Expand Up @@ -4,12 +4,13 @@ const process = require('node:process');
const { Collection } = require('@discordjs/collection');
const { ChannelType, Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const ThreadManager = require('./ThreadManager');
const GuildTextThreadManager = require('./GuildTextThreadManager');
const { Error, TypeError, ErrorCodes } = require('../errors');
const GuildChannel = require('../structures/GuildChannel');
const PermissionOverwrites = require('../structures/PermissionOverwrites');
const ThreadChannel = require('../structures/ThreadChannel');
const Webhook = require('../structures/Webhook');
const { transformGuildForumTag, transformGuildDefaultReaction } = require('../util/Channels');
const { ThreadChannelTypes } = require('../util/Constants');
const DataResolver = require('../util/DataResolver');
const { setPosition } = require('../util/Util');
Expand Down Expand Up @@ -218,6 +219,9 @@ class GuildChannelManager extends CachedManager {
* The default auto archive duration for all new threads in this channel
* @property {?string} [rtcRegion] The RTC region of the channel
* @property {?VideoQualityMode} [videoQualityMode] The camera video quality mode of the channel
* @property {GuildForumTag[]} [availableTags] The tags to set as available in a forum channel
* @property {DefaultReactionEmoji} [defaultReactionEmoji] The emoji to set as the default reaction emoji
* @property {number} [defaultThreadRateLimitPerUser] The rate limit per user (slowmode) to set on forum posts
* @property {string} [reason] Reason for editing this channel
*/

Expand Down Expand Up @@ -274,6 +278,11 @@ class GuildChannelManager extends CachedManager {
rate_limit_per_user: data.rateLimitPerUser,
default_auto_archive_duration: data.defaultAutoArchiveDuration,
permission_overwrites,
available_tags: data.availableTags?.map(availableTag => transformGuildForumTag(availableTag)),
default_reaction_emoji: data.defaultReactionEmoji
? transformGuildDefaultReaction(data.defaultReactionEmoji)
: undefined,
default_thread_rate_limit_per_user: data.defaultThreadRateLimitPerUser,
},
reason: data.reason,
});
Expand Down Expand Up @@ -418,7 +427,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
77 changes: 77 additions & 0 deletions packages/discord.js/src/managers/GuildForumThreadManager.js
@@ -0,0 +1,77 @@
'use strict';

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

/**
* Manages API methods for threads in forum channels and stores their cache.
* @extends {ThreadManager}
*/
class GuildForumThreadManager extends ThreadManager {
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved
/**
* @typedef {BaseMessageOptions} GuildForumThreadCreateOptions
* @property {stickers} [stickers] The stickers to send with the message
* @property {BitFieldResolvable} [flags] The flags to send with the message
*/

/**
* Options for creating a thread.
* @typedef {StartThreadOptions} GuildForumThreadCreateOptions
* @property {GuildForumThreadCreateOptions|MessagePayload} message The message associated with the thread post
* @property {Snowflake[]} [appliedTags] The tags to apply to the thread
*/

/**
* Creates a new thread in the channel.
* @param {GuildForumThreadCreateOptions} [options] Options to create a new thread
* @returns {Promise<ThreadChannel>}
* @example
* // Create a new forum post
* forum.threads
* .create({
* name: 'Food Talk',
* autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
* message: {
* content: 'Discuss your favorite food!',
* },
* reason: 'Needed a separate thread for food',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
message,
reason,
rateLimitPerUser,
appliedTags,
} = {}) {
if (!message) {
throw new TypeError(ErrorCodes.GuildForumMessageRequired);
}

const messagePayload =
message instanceof MessagePayload ? message.resolveBody() : MessagePayload.create(this, message).resolveBody();
kyranet marked this conversation as resolved.
Show resolved Hide resolved

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

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

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

module.exports = GuildForumThreadManager;
88 changes: 88 additions & 0 deletions packages/discord.js/src/managers/GuildTextThreadManager.js
@@ -0,0 +1,88 @@
'use strict';

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

/**
* Manages API methods for {@link ThreadChannel} objects and stores their cache.
* @extends {CachedManager}
*/
class GuildTextThreadManager extends ThreadManager {
/**
* Options for creating a thread. <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 the `type` of thread gets inferred automatically and cannot be changed.</warn>
* @property {ThreadChannelTypes|number} [type] The type of thread to create.
* Defaults to {@link ChannelType.GuildPublicThread} if created in a {@link TextChannel}
SpaceEEC marked this conversation as resolved.
Show resolved Hide resolved
* <warn>When creating threads in a {@link NewsChannel} this is ignored and is always
* {@link ChannelType.GuildNewsThread}</warn>
SpaceEEC marked this conversation as resolved.
Show resolved Hide resolved
* @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>
SpaceEEC marked this conversation as resolved.
Show resolved Hide resolved
*/

/**
* 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: ThreadAutoArchiveDuration.OneHour,
* reason: 'Needed a separate thread for food',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
* @example
* // Create a new private thread
* channel.threads
* .create({
* name: 'mod-talk',
* autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
* type: ChannelType.GuildPrivateThread,
SpaceEEC marked this conversation as resolved.
Show resolved Hide resolved
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
startMessage,
type,
invitable,
reason,
rateLimitPerUser,
} = {}) {
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError(ErrorCodes.InvalidType, 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === ChannelType.GuildNews ? ChannelType.GuildNewsThread : ChannelType.GuildPublicThread;
SpaceEEC marked this conversation as resolved.
Show resolved Hide resolved
let startMessageId;
if (startMessage) {
startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError(ErrorCodes.InvalidType, 'startMessage', 'MessageResolvable');
} else if (this.channel.type !== ChannelType.GuildNews) {
SpaceEEC marked this conversation as resolved.
Show resolved Hide resolved
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,
SpaceEEC marked this conversation as resolved.
Show resolved Hide resolved
rate_limit_per_user: rateLimitPerUser,
},
reason,
});

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

module.exports = GuildTextThreadManager;
82 changes: 10 additions & 72 deletions packages/discord.js/src/managers/ThreadManager.js
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, ErrorCodes } = require('../errors');
const ThreadChannel = require('../structures/ThreadChannel');

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

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

/**
* Data that can be resolved to a Thread Channel object. This can be:
* * A ThreadChannel object
* * A Snowflake
* @typedef {ThreadChannel|Snowflake} ThreadChannelResolvable
*/
suneettipirneni marked this conversation as resolved.
Show resolved Hide resolved

/**
* The cache of this Manager
* @type {Collection<Snowflake, ThreadChannel>}
Expand All @@ -35,13 +42,6 @@ class ThreadManager extends CachedManager {
return thread;
}

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

/**
* Resolves a {@link ThreadChannelResolvable} to a {@link ThreadChannel} object.
* @method resolve
Expand Down Expand Up @@ -74,68 +74,6 @@ class ThreadManager extends CachedManager {
* <info>Can only be set when type will be {@link ChannelType.PrivateThread}</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: ThreadAutoArchiveDuration.OneHour,
* reason: 'Needed a separate thread for food',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
* @example
* // Create a new private thread
* channel.threads
* .create({
* name: 'mod-talk',
* autoArchiveDuration: ThreadAutoArchiveDuration.OneHour,
* type: ChannelType.PrivateThread,
* reason: 'Needed a separate thread for moderation',
* })
* .then(threadChannel => console.log(threadChannel))
* .catch(console.error);
*/
async create({
name,
autoArchiveDuration = this.channel.defaultAutoArchiveDuration,
startMessage,
type,
invitable,
reason,
rateLimitPerUser,
} = {}) {
if (type && typeof type !== 'string' && typeof type !== 'number') {
throw new TypeError(ErrorCodes.InvalidType, 'type', 'ThreadChannelType or Number');
}
let resolvedType =
this.channel.type === ChannelType.GuildAnnouncement ? ChannelType.AnnouncementThread : ChannelType.PublicThread;
let startMessageId;
if (startMessage) {
startMessageId = this.channel.messages.resolveId(startMessage);
if (!startMessageId) throw new TypeError(ErrorCodes.InvalidType, 'startMessage', 'MessageResolvable');
} else if (this.channel.type !== ChannelType.GuildAnnouncement) {
resolvedType = type ?? resolvedType;
}

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

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

/**
* The options for fetching multiple threads, the properties are mutually exclusive
* @typedef {Object} FetchThreadsOptions
Expand Down