From 6084d6aef2eba8b0a1328db2356f0c0594267f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Conor=E2=84=A2?= <35053522+iiFDCT@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:36:57 +0000 Subject: [PATCH] feat(ForumChannels): Support Forum Channels (#1452) Co-authored-by: bsian03 --- esm.mjs | 1 + index.d.ts | 122 ++++++++++++++--- index.js | 1 + lib/Client.js | 131 +++++++++++++++--- lib/Constants.js | 20 ++- lib/gateway/Shard.js | 50 ++++++- lib/structures/Channel.js | 4 + lib/structures/ForumChannel.js | 176 +++++++++++++++++++++++++ lib/structures/Guild.js | 16 ++- lib/structures/GuildChannel.js | 20 ++- lib/structures/Message.js | 4 +- lib/structures/PrivateThreadChannel.js | 1 + lib/structures/PublicThreadChannel.js | 16 +++ lib/structures/TextChannel.js | 31 ++++- lib/structures/TextVoiceChannel.js | 2 +- lib/structures/ThreadChannel.js | 9 +- lib/util/emitDeprecation.js | 1 + 17 files changed, 546 insertions(+), 59 deletions(-) create mode 100644 lib/structures/ForumChannel.js diff --git a/esm.mjs b/esm.mjs index 5968e4143..ccc054643 100644 --- a/esm.mjs +++ b/esm.mjs @@ -22,6 +22,7 @@ export const { DiscordHTTPError, DiscordRESTError, ExtendedUser, + ForumChannel, GroupChannel, Guild, GuildChannel, diff --git a/index.d.ts b/index.d.ts index 8dea9642e..d89bbf3fb 100644 --- a/index.d.ts +++ b/index.d.ts @@ -50,7 +50,7 @@ declare namespace Eris { // Channel type AnyChannel = AnyGuildChannel | PrivateChannel; - type AnyGuildChannel = GuildTextableChannel | AnyVoiceChannel | CategoryChannel; + type AnyGuildChannel = GuildTextableChannel | AnyVoiceChannel | CategoryChannel | ForumChannel; type AnyThreadChannel = NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel | ThreadChannel; type AnyVoiceChannel = TextVoiceChannel | StageChannel; type ChannelTypeConversion = @@ -59,7 +59,8 @@ declare namespace Eris { T extends Constants["ChannelTypes"]["GUILD_CATEGORY"] ? CategoryChannel : T extends Constants["ChannelTypes"]["GUILD_NEWS"] ? NewsChannel : T extends Constants["ChannelTypes"]["GUILD_STAGE_VOICE"] ? StageChannel : - never; + T extends Constants["ChannelTypes"]["GUILD_FORUM"] ? ForumChannel : + never; type GuildTextableChannel = TextChannel | TextVoiceChannel | NewsChannel; type GuildTextableWithThreads = GuildTextableChannel | AnyThreadChannel; type InviteChannel = InvitePartialChannel | Exclude; @@ -170,8 +171,11 @@ declare namespace Eris { type StickerFormats = Constants["StickerFormats"][keyof Constants["StickerFormats"]]; type StickerTypes = Constants["StickerTypes"][keyof Constants["StickerTypes"]]; - // Thread + // Thread/Forum type AutoArchiveDuration = 60 | 1440 | 4320 | 10080; + type ChannelFlags = Constants["ChannelFlags"][keyof Constants["ChannelFlags"]]; + type ForumLayoutTypes = Constants["ForumLayoutTypes"][keyof Constants["ForumLayoutTypes"]]; + type SortOrderTypes = Constants["SortOrderTypes"][keyof Constants["SortOrderTypes"]]; // User type PremiumTypes = Constants["PremiumTypes"][keyof Constants["PremiumTypes"]]; @@ -347,6 +351,7 @@ declare namespace Eris { /** valid for KEYWORD_PRESET */ presets: AutoModerationKeywordPresetType[]; } + // Channel interface ChannelFollow { channel_id: string; @@ -359,7 +364,13 @@ declare namespace Eris { position: number; } interface CreateChannelOptions { + availableTags?: ForumTag[]; bitrate?: number; + defaultAutoArchiveDuration?: AutoArchiveDuration; + defaultForumLayout?: ForumLayoutTypes; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultSortOrder?: SortOrderTypes; + defaultThreadRateLimitPerUser?: number; nsfw?: boolean; parentID?: string; permissionOverwrites?: Overwrite[]; @@ -370,9 +381,16 @@ declare namespace Eris { userLimit?: number; } interface EditChannelOptions extends Omit { + appliedTags?: string[]; archived?: boolean; autoArchiveDuration?: AutoArchiveDuration; + availableTags?: ForumTag[]; defaultAutoArchiveDuration?: AutoArchiveDuration; + defaultForumLayout?: ForumLayoutTypes; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultSortOrder?: SortOrderTypes; + defaultThreadRateLimitPerUser?: number; + flags?: number; icon?: string; invitable?: boolean; locked?: boolean; @@ -673,6 +691,14 @@ declare namespace Eris { ringing: string[]; unavailable: boolean; } + interface OldForumChannel extends OldGuildChannel { + availableTags: ForumTag[]; + defaultAutoArchiveDuration: AutoArchiveDuration; + defaultForumLayout: ForumLayoutTypes; + defaultReactionEmoji: DefaultReactionEmoji; + defaultSortOrder: SortOrderTypes; + defaultThreadRateLimitPerUser: number; + } interface OldGroupChannel { icon: string; name: string; @@ -720,6 +746,7 @@ declare namespace Eris { } interface OldGuildChannel { bitrate?: number; + flags?: number; name: string; nsfw?: boolean; parentID: string | null; @@ -796,6 +823,8 @@ declare namespace Eris { videoQualityMode: VideoQualityMode; } interface OldThread { + appliedTags: string[]; + autoArchiveDuration: number; name: string; rateLimitPerUser: number; threadMetadata: ThreadMetadata; @@ -826,7 +855,7 @@ declare namespace Eris { channelPinUpdate: [channel: TextableChannel, timestamp: number, oldTimestamp: number]; channelRecipientAdd: [channel: GroupChannel, user: User]; channelRecipientRemove: [channel: GroupChannel, user: User]; - channelUpdate: [channel: AnyGuildChannel, oldChannel: OldGuildChannel | OldGuildTextChannel | OldVoiceChannel] + channelUpdate: [channel: AnyGuildChannel, oldChannel: OldGuildChannel | OldForumChannel | OldGuildTextChannel | OldVoiceChannel] | [channel: GroupChannel, oldChannel: OldGroupChannel]; connect: [id: number]; debug: [message: string, id?: number]; @@ -1626,14 +1655,29 @@ declare namespace Eris { guild_connections?: true; } - // Thread + // Forum/Thread interface CreateThreadOptions { - autoArchiveDuration: AutoArchiveDuration; + autoArchiveDuration?: AutoArchiveDuration; name: string; + rateLimitPerUser?: number; + reason?: string; + } + interface CreateForumThreadOptions extends CreateThreadOptions { + appliedTags?: string[]; + message: Omit & FileContent[]; } interface CreateThreadWithoutMessageOptions extends CreateThreadOptions { - invitable: T extends PrivateThreadChannel["type"] ? boolean : never; - type: T; + invitable?: T extends PrivateThreadChannel["type"] ? boolean : never; + type?: T; + } + interface DefaultReactionEmoji { + emoji_id?: string; + emoji_name?: string; + } + interface ForumTag extends DefaultReactionEmoji { + id: string; + name: string; + moderated: boolean; } interface GetArchivedThreadsOptions { before?: Date; @@ -1912,6 +1956,10 @@ declare namespace Eris { DANGER: 4; LINK: 5; }; + ChannelFlags: { + PINNED: 1, + REQUIRE_TAG: 16 + }, ChannelTypes: { GUILD_TEXT: 0; DM: 1; @@ -1926,6 +1974,8 @@ declare namespace Eris { GUILD_STAGE_VOICE: 13; /** @deprecated */ GUILD_STAGE: 13; + + GUILD_FORUM: 15; }; ComponentTypes: { ACTION_ROW: 1; @@ -1937,10 +1987,19 @@ declare namespace Eris { NONE: 0; EVERYONE: 1; }; + ForumLayoutTypes: { + NOT_SET: 0, + LIST_VIEW: 1, + GALLERY_VIEW: 2 + }; DefaultMessageNotificationLevels: { ALL_MESSAGES: 0; ONLY_MENTIONS: 1; }; + SortOrderTypes: { + LATEST_ACTIVITY: 0, + CREATION_DATE: 1 + }; ExplicitContentFilterLevels: { DISABLED: 0; MEMBERS_WITHOUT_ROLES: 1; @@ -2645,7 +2704,7 @@ declare namespace Eris { ): Promise>; createChannelWebhook( channelID: string, - options: { name: string; avatar?: string | null }, + options: WebhookCreateOptions, reason?: string ): Promise; createCommand(command: ApplicationCommandCreateOptions): Promise>; @@ -2661,8 +2720,11 @@ declare namespace Eris { createMessage(channelID: string, content: MessageContent, file?: FileContent | FileContent[]): Promise; createRole(guildID: string, options?: Role | RoleOptions, reason?: string): Promise; createStageInstance(channelID: string, options: StageInstanceOptions): Promise; + createThread(channelID: string, options: CreateForumThreadOptions, file?: FileContent | FileContent[]): Promise>; + createThread(channelID: string, options: CreateThreadWithoutMessageOptions, file?: FileContent | FileContent[]): Promise; createThreadWithMessage(channelID: string, messageID: string, options: CreateThreadOptions): Promise; - createThreadWithoutMessage(channelID: string, options: CreateThreadWithoutMessageOptions): Promise; + /** @deprecated */ + createThreadWithoutMessage(channelID: string, options: CreateThreadWithoutMessageOptions): Promise; crosspostMessage(channelID: string, messageID: string): Promise; deleteAutoModerationRule(guildID: string, ruleID: string, reason?: string): Promise; deleteChannel(channelID: string, reason?: string): Promise; @@ -2768,7 +2830,7 @@ declare namespace Eris { followChannel(channelID: string, webhookChannelID: string): Promise; getActiveGuildThreads(guildID: string): Promise; getArchivedThreads(channelID: string, type: "private", options?: GetArchivedThreadsOptions): Promise>; - getArchivedThreads(channelID: string, type: "public", options?: GetArchivedThreadsOptions): Promise>; + getArchivedThreads(channelID: string, type: "public", options?: GetArchivedThreadsOptions): Promise>>; getAutoModerationRule(guildID: string, ruleID: string): Promise; getAutoModerationRules(guildID: string): Promise; getBotGateway(): Promise<{ session_start_limit: { max_concurrency: number; remaining: number; reset_after: number; total: number }; shards: number; url: string }>; @@ -3029,6 +3091,25 @@ declare namespace Eris { verified: boolean; } + export class ForumChannel extends GuildChannel { + availableTags: ForumTag[]; + defaultAutoArchiveDuration: AutoArchiveDuration; + defaultForumLayout: ForumLayoutTypes; + defaultReactionEmoji: DefaultReactionEmoji; + defaultSortOrder: SortOrderTypes; + defaultThreadRateLimitPerUser: number; + lastMessageID: string; + rateLimitPerUser: number; + threads: PublicThreadChannel[]; + topic?: string; + createInvite(options?: CreateInviteOptions, reason?: string): Promise>; + createThread(options: CreateForumThreadOptions, file?: FileContent | FileContent[]): Promise>; + createWebhook(options: WebhookCreateOptions, reason?: string): Promise; + getArchivedThreads(options?: GetArchivedThreadsOptions): Promise>>; + getInvites(): Promise[]>; + getWebhooks(): Promise; + } + export class GroupChannel extends PrivateChannel { icon: string | null; iconURL: string | null; @@ -3235,6 +3316,7 @@ declare namespace Eris { export class GuildChannel extends Channel { guild: Guild; name: string; + flags?: number; parentID: string | null; permissionOverwrites: Collection; position: number; @@ -3606,7 +3688,7 @@ declare namespace Eris { crosspostMessage(messageID: string): Promise>; editMessage(messageID: string, content: MessageContentEdit): Promise>; follow(webhookChannelID: string): Promise; - getInvites(): Promise<(Invite<"withMetadata", this>)[]>; + getInvites(): Promise[]>; getMessage(messageID: string): Promise>; getMessages(options?: GetMessagesOptions): Promise[]>; /** @deprecated */ @@ -3686,7 +3768,9 @@ declare namespace Eris { type: Constants["ChannelTypes"]["GUILD_PRIVATE_THREAD"]; } - export class PublicThreadChannel extends ThreadChannel { + /** Generic T is true if the PublicThreadChannel's parent channel is a Forum Channel */ + export class PublicThreadChannel extends ThreadChannel { + appliedTags: T extends true ? string[] : never; type: GuildPublicThreadChannelTypes; edit(options: Pick, reason?: string): Promise; } @@ -3896,8 +3980,10 @@ declare namespace Eris { addMessageReaction(messageID: string, reaction: string, userID: string): Promise; createInvite(options?: CreateInviteOptions, reason?: string): Promise>; createMessage(content: MessageContent, file?: FileContent | FileContent[]): Promise>; - createThreadWithMessage(messageID: string, options: CreateThreadOptions): Promise; - createThreadWithoutMessage(options: CreateThreadWithoutMessageOptions): Promise; + createThread(options: CreateThreadWithoutMessageOptions): Promise; + createThreadWithMessage(messageID: string, options: CreateThreadOptions): Promise; + /** @deprecated */ + createThreadWithoutMessage(options: CreateThreadWithoutMessageOptions): Promise; createWebhook(options: WebhookCreateOptions, reason?: string): Promise; deleteMessage(messageID: string, reason?: string): Promise; deleteMessages(messageIDs: string[], reason?: string): Promise; @@ -3905,7 +3991,7 @@ declare namespace Eris { editMessage(messageID: string, content: MessageContentEdit): Promise>; getArchivedThreads(type: "private", options?: GetArchivedThreadsOptions): Promise>; getArchivedThreads(type: "public", options?: GetArchivedThreadsOptions): Promise>; - getInvites(): Promise<(Invite<"withMetadata", this>)[]>; + getInvites(): Promise[]>; getJoinedPrivateArchivedThreads(options: GetArchivedThreadsOptions): Promise>; getMessage(messageID: string): Promise>; getMessageReaction(messageID: string, reaction: string, options?: GetMessageReactionOptions): Promise; @@ -3968,8 +4054,10 @@ declare namespace Eris { messageCount: number; messages: Collection>; ownerID: string; + parentID: string; rateLimitPerUser: number; threadMetadata: ThreadMetadata; + totalMessageSent: number; type: GuildThreadChannelTypes; constructor(data: BaseData, client: Client, messageLimit?: number); addMessageReaction(messageID: string, reaction: string): Promise; @@ -4054,7 +4142,7 @@ declare namespace Eris { type: GuildVoiceChannelTypes; voiceMembers: Collection; createInvite(options?: CreateInviteOptions, reason?: string): Promise>; - getInvites(): Promise<(Invite<"withMetadata", VoiceChannel>)[]>; + getInvites(): Promise[]>; join(options?: JoinVoiceChannelOptions): Promise; leave(): void; } diff --git a/index.js b/index.js index 50434f9fc..b9b0e2924 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,7 @@ Eris.Constants = require("./lib/Constants"); Eris.DiscordHTTPError = require("./lib/errors/DiscordHTTPError"); Eris.DiscordRESTError = require("./lib/errors/DiscordRESTError"); Eris.ExtendedUser = require("./lib/structures/ExtendedUser"); +Eris.ForumChannel = require("./lib/structures/ForumChannel"); Eris.GroupChannel = require("./lib/structures/GroupChannel"); Eris.Guild = require("./lib/structures/Guild"); Eris.GuildChannel = require("./lib/structures/GuildChannel"); diff --git a/lib/Client.js b/lib/Client.js index fc517aa5e..e5b639471 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -568,18 +568,24 @@ class Client extends EventEmitter { * Create a channel in a guild * @arg {String} guildID The ID of the guild to create the channel in * @arg {String} name The name of the channel - * @arg {String} [type=0] The type of the channel, either 0 (text), 2 (voice), 4 (category), 5 (news), or 13 (stage) + * @arg {Number} [type=0] The type of the channel, either 0 (text), 2 (voice), 4 (category), 5 (news), 13 (stage), or 15 (forum) * @arg {Object | String} [options] The properties the channel should have. If `options` is a string, it will be treated as `options.parentID` (see below). Passing a string is deprecated and will not be supported in future versions. + * @arg {Array} [options.availableTags] The available tags that can be applied to threads in a forum channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (forum channels only, max 20) * @arg {Number} [options.bitrate] The bitrate of the channel (voice channels only) + * @arg {Number} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (text/news/forum channels only) + * @arg {Number} [options.defaultForumLayout] The default forum layout type used to display posts in forum channels (forum channels only) + * @arg {Object} [options.defaultReactionEmoji] The emoji to show in the add reaction button on a thread in a forum channel (forum channels only) + * @arg {Number} [options.defaultSortOrder] The default sort order type used to order posts in forum channels (forum channels only) + * @arg {Number} [options.defaultThreadRateLimitPerUser] The initial rateLimitPerUser to set on newly created threads in a channel (text/forum channels only) * @arg {Boolean} [options.nsfw] The nsfw status of the channel * @arg {String?} [options.parentID] The ID of the parent category channel for this channel * @arg {Array} [options.permissionOverwrites] An array containing permission overwrite objects * @arg {Number} [options.position] The sorting position of the channel - * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (text channels only) + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) (text/voice/stage/forum channels only) * @arg {String} [options.reason] The reason to be displayed in audit logs * @arg {String} [options.topic] The topic of the channel (text channels only) * @arg {Number} [options.userLimit] The channel user limit (voice channels only) - * @returns {Promise} + * @returns {Promise} */ createChannel(guildID, name, type, reason, options = {}) { if(typeof options === "string") { // This used to be parentID, back-compat @@ -601,7 +607,13 @@ class Client extends EventEmitter { return this.requestHandler.request("POST", Endpoints.GUILD_CHANNELS(guildID), true, { name: name, type: type, + available_tags: options.availableTags, bitrate: options.bitrate, + default_auto_archive_duration: options.defaultAutoArchiveDuration, + default_forum_layout: options.defaultForumLayout, + default_reaction_emoji: options.defaultReactionEmoji, + default_sort_order: options.defaultSortOrder, + default_thread_rate_limit_per_user: options.defaultThreadRateLimitPerUser, nsfw: options.nsfw, parent_id: options.parentID, permission_overwrites: options.permissionOverwrites, @@ -1042,33 +1054,108 @@ class Client extends EventEmitter { }).then((instance) => new StageInstance(instance, this)); } + /** + * Create a thread in a channel + * @arg {String} channelID The ID of the channel + * @arg {Object} options The thread options + * @arg {Array} [options.appliedTags] The IDs of the set of tags that have been applied to a thread in a forum channel (threads created in forum channels only, max 5) + * @arg {Number} [options.autoArchiveDuration] The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the thread (private threads only) + * @arg {Object} [options.message] The message to create with the thread (only use when creating a thread inside of a `GUILD_FORUM` channel). Note: When creating a forum channel thread, you must provide at least one of `content`, `embeds`, `stickerIDs`, `components`, or `files` + * @arg {Object} [options.message.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [options.message.allowedMentions.everyone] Whether or not to allow @everyone/@here + * @arg {Boolean} [options.message.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to + * @arg {Boolean | Array} [options.message.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow + * @arg {Boolean | Array} [options.message.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow + * @arg {Array} [options.message.attachments] An array of attachment objects with the filename and description + * @arg {String} [options.message.attachments[].description] The description of the file + * @arg {String} [options.message.attachments[].filename] The name of the file + * @arg {Number} options.message.attachments[].id The index of the file + * @arg {Array} [options.message.components] An array of component objects + * @arg {String} [options.message.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [options.message.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [options.message.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [options.message.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [options.message.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [options.message.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [options.message.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [options.message.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [options.message.components[].options[].description] The description for this option + * @arg {Object} [options.message.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} options.message.components[].options[].label The label for this option + * @arg {Number | String} options.message.components[].options[].value The value for this option + * @arg {String} [options.message.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [options.message.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} options.message.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [options.message.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [options.message.content] A string containing the message content + * @arg {Array} [options.message.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [options.message.stickerIDs] An array of IDs corresponding to stickers to send + * @arg {String} options.name The thread channel name + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @arg {Number} [options.type] The channel type of the thread to create. Either `10` (announcement thread, announcement channels only), `11` (public thread) or `12` (private thread). Note: Not required when creating a thread inside of a forum channel, it will always be public + * @arg {Object | Array} [file] A file object (or an Array of them). Only use when creating a thread inside of a forum channel + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + createThread(channelID, options, file) { + if(options.message) { + options.message.allowed_mentions = this._formatAllowedMentions(options.message.allowedMentions); + options.message.sticker_ids = options.message.stickerIDs; + } + let payload = { + applied_tags: options.appliedTags, + auto_archive_duration: options.autoArchiveDuration, + invitable: options.invitable, + message: options.message, + name: options.name, + rate_limit_per_user: options.rateLimitPerUser, + reason: options.reason, + type: options.type + }; + if(file) { + payload = {payload_json: payload}; + } + return this.requestHandler.request("POST", Endpoints.THREAD_WITHOUT_MESSAGE(channelID), true, payload, file).then((channel) => Channel.from(channel, this)); + } + /** * Create a thread with an existing message * @arg {String} channelID The ID of the channel * @arg {String} messageID The ID of the message to create the thread from * @arg {Object} options The thread options - * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {Number} [options.autoArchiveDuration] Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 * @arg {String} options.name The thread channel name + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs * @returns {Promise} */ createThreadWithMessage(channelID, messageID, options) { return this.requestHandler.request("POST", Endpoints.THREAD_WITH_MESSAGE(channelID, messageID), true, { + auto_archive_duration: options.autoArchiveDuration, name: options.name, - auto_archive_duration: options.autoArchiveDuration + rate_limit_per_user: options.rateLimitPerUser, + reason: options.reason }).then((channel) => Channel.from(channel, this)); } /** - * Create a thread without an existing message + * [DEPRECATED] Create a thread without an existing message. Use `createThread` instead * @arg {String} channelID The ID of the channel * @arg {Object} options The thread options - * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {Number} [options.autoArchiveDuration] Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the thread (private threads only) * @arg {String} options.name The thread channel name - * @arg {Number} [options.type] The channel type of the thread to create. It is recommended to explicitly set this property as this will be a required property in API v10 - * @returns {Promise} + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @arg {Number} [options.type] The channel type of the thread to create. Either `10` (announcement thread, announcement channels only), `11` (public thread) or `12` (private thread) + * @returns {Promise} */ createThreadWithoutMessage(channelID, options) { + emitDeprecation("CREATE_THREAD_WITHOUT_MESSAGE"); + this.emit("warn", "[DEPRECATED] createThreadWithoutMessage() is deprecated. Use createThread() instead."); return this.requestHandler.request("POST", Endpoints.THREAD_WITHOUT_MESSAGE(channelID), true, { auto_archive_duration: options.autoArchiveDuration, invitable: options.invitable, @@ -1432,10 +1519,17 @@ class Client extends EventEmitter { * Edit a channel's properties * @arg {String} channelID The ID of the channel * @arg {Object} options The properties to edit + * @arg {Array} [options.appliedTags] The IDs of the set of tags that have been applied to a thread in a forum channel (threads created in forum channels only, max 5) * @arg {Boolean} [options.archived] The archive status of the channel (thread channels only) * @arg {Number} [options.autoArchiveDuration] The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 (thread channels only) + * @arg {Array} [options.availableTags] The available tags that can be applied to threads in a forum channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (forum channels only, max 20) * @arg {Number} [options.bitrate] The bitrate of the channel (guild voice channels only) - * @arg {Number?} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (guild text/news channels only) + * @arg {Number} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (text/news/forum channels only) + * @arg {Number} [options.defaultForumLayout] The default forum layout type used to display posts in forum channels (forum channels only) + * @arg {Object} [options.defaultReactionEmoji] The emoji to show in the add reaction button on a thread in a forum channel (forum channels only) + * @arg {Number} [options.defaultSortOrder] The default sort order type used to order posts in forum channels (forum channels only) + * @arg {Number} [options.defaultThreadRateLimitPerUser] The initial rateLimitPerUser to set on newly created threads in a channel (text/forum channels only) + * @arg {Number} [options.flags] The flags for the channel combined as a bitfield (thread/forum channels only). Note: `PINNED` can only be set for threads in forum channels, and `REQUIRE_TAG` can only be set for forum channels * @arg {String} [options.icon] The icon of the channel as a base64 data URI (group channels only). Note: base64 strings alone are not base64 data URI strings * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the channel (private thread channels only) * @arg {Boolean} [options.locked] The lock status of the channel (thread channels only) @@ -1445,20 +1539,27 @@ class Client extends EventEmitter { * @arg {String?} [options.parentID] The ID of the parent channel category for this channel (guild text/voice channels only) * @arg {Array} [options.permissionOverwrites] An array containing permission overwrite objects * @arg {Number} [options.position] The sorting position of the channel (guild channels only) - * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (guild text and thread channels only) + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) (text/voice/stage/forum channels only) * @arg {String?} [options.rtcRegion] The RTC region ID of the channel (automatic if `null`) (guild voice channels only) * @arg {String} [options.topic] The topic of the channel (guild text channels only) * @arg {Number} [options.userLimit] The channel user limit (guild voice channels only) * @arg {Number} [options.videoQualityMode] The camera video quality mode of the channel (guild voice channels only). `1` is auto, `2` is 720p * @arg {String} [reason] The reason to be displayed in audit logs - * @returns {Promise} + * @returns {Promise} */ editChannel(channelID, options, reason) { return this.requestHandler.request("PATCH", Endpoints.CHANNEL(channelID), true, { + applied_tags: options.appliedTags, archived: options.archived, auto_archive_duration: options.autoArchiveDuration, + available_tags: options.availableTags, bitrate: options.bitrate, default_auto_archive_duration: options.defaultAutoArchiveDuration, + default_forum_layout: options.defaultForumLayout, + default_reaction_emoji: options.defaultReactionEmoji, + default_sort_order: options.defaultSortOrder, + default_thread_rate_limit_per_user: options.defaultThreadRateLimitPerUser, + flags: options.flags, icon: options.icon, invitable: options.invitable, locked: options.locked, @@ -2493,7 +2594,7 @@ class Client extends EventEmitter { /** * Get a Channel object from a channel ID * @arg {String} channelID The ID of the channel - * @returns {CategoryChannel | GroupChannel | PrivateChannel | TextChannel | TextVoiceChannel | NewsChannel | NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} + * @returns {CategoryChannel | ForumChannel | GroupChannel | NewsChannel | NewsThreadChannel | PrivateChannel | PrivateThreadChannel | PublicThreadChannel | TextChannel | TextVoiceChannel} */ getChannel(channelID) { if(!channelID) { @@ -3094,7 +3195,7 @@ class Client extends EventEmitter { /** * Get a channel's data via the REST API. REST mode is required to use this endpoint. * @arg {String} channelID The ID of the channel - * @returns {Promise} + * @returns {Promise} */ getRESTChannel(channelID) { if(!this.options.restMode) { @@ -3122,7 +3223,7 @@ class Client extends EventEmitter { /** * Get a guild's channels via the REST API. REST mode is required to use this endpoint. * @arg {String} guildID The ID of the guild - * @returns {Promise} + * @returns {Promise} */ getRESTGuildChannels(guildID) { if(!this.options.restMode) { diff --git a/lib/Constants.js b/lib/Constants.js index 64e2c7add..0e4615449 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -164,6 +164,11 @@ module.exports.ButtonStyles = { LINK: 5 }; +module.exports.ChannelFlags = { + PINNED: 1 << 1, + REQUIRE_TAG: 1 << 4 +}; + module.exports.ChannelTypes = { GUILD_TEXT: 0, DM: 1, @@ -175,7 +180,9 @@ module.exports.ChannelTypes = { GUILD_NEWS_THREAD: 10, GUILD_PUBLIC_THREAD: 11, GUILD_PRIVATE_THREAD: 12, - GUILD_STAGE_VOICE: 13, GUILD_STAGE: 13 // [DEPRECATED] + GUILD_STAGE_VOICE: 13, GUILD_STAGE: 13, // [DEPRECATED] + + GUILD_FORUM: 15 }; module.exports.ComponentTypes = { @@ -190,11 +197,22 @@ module.exports.ConnectionVisibilityTypes = { EVERYONE: 1 }; +module.exports.ForumLayoutTypes = { + NOT_SET: 0, + LIST_VIEW: 1, + GALLERY_VIEW: 2 +}; + module.exports.DefaultMessageNotificationLevels = { ALL_MESSAGES: 0, ONLY_MENTIONS: 1 }; +module.exports.SortOrderTypes = { + LATEST_ACTIVITY: 0, + CREATION_DATE: 1 +}; + module.exports.ExplicitContentFilterLevels = { DISABLED: 0, MEMBERS_WITHOUT_ROLES: 1, diff --git a/lib/gateway/Shard.js b/lib/gateway/Shard.js index 90cd20322..ff671a37a 100644 --- a/lib/gateway/Shard.js +++ b/lib/gateway/Shard.js @@ -5,6 +5,7 @@ const Base = require("../structures/Base"); const Bucket = require("../util/Bucket"); const Call = require("../structures/Call"); const Channel = require("../structures/Channel"); +const ForumChannel = require("../structures/ForumChannel"); const GroupChannel = require("../structures/GroupChannel"); const GuildChannel = require("../structures/GuildChannel"); const Message = require("../structures/Message"); @@ -994,6 +995,10 @@ class Shard extends EventEmitter { const channel = this.client.getChannel(packet.d.channel_id); if(channel) { // MESSAGE_CREATE just when deleting o.o channel.lastMessageID = packet.d.id; + if(channel instanceof ThreadChannel) { + channel.messageCount++; + channel.totalMessageSent++; + } /** * Fired when a message is created * @event Client#messageCreate @@ -1057,7 +1062,9 @@ class Shard extends EventEmitter { } case "MESSAGE_DELETE": { const channel = this.client.getChannel(packet.d.channel_id); - + if(channel instanceof ThreadChannel) { + channel.messageCount--; + } /** * Fired when a cached message is deleted * @event Client#messageDelete @@ -1075,7 +1082,9 @@ class Shard extends EventEmitter { } case "MESSAGE_DELETE_BULK": { const channel = this.client.getChannel(packet.d.channel_id); - + if(channel instanceof ThreadChannel) { + channel.messageCount -= packet.d.ids.length; + } /** * Fired when a bulk delete occurs * @event Client#messageDeleteBulk @@ -1680,7 +1689,7 @@ class Shard extends EventEmitter { /** * Fired when a channel is created * @event Client#channelCreate - * @prop {TextChannel | TextVoiceChannel | CategoryChannel | NewsChannel | GuildChannel} channel The channel + * @prop {ForumChannel | TextChannel | TextVoiceChannel | CategoryChannel | NewsChannel | GuildChannel} channel The channel */ this.emit("channelCreate", channel); } else { @@ -1703,7 +1712,14 @@ class Shard extends EventEmitter { }; } else if(channel instanceof GuildChannel) { oldChannel = { + availableTags: channel.availableTags, bitrate: channel.bitrate, + defaultAutoArchiveDuration: channel.defaultAutoArchiveDuration, + defaultForumLayout: channel.defaultForumLayout, + defaultReactionEmoji: channel.defaultReactionEmoji, + defaultSortOrder: channel.defaultSortOrder, + defaultThreadRateLimitPerUser: channel.defaultThreadRateLimitPerUser, + flags: channel.flags, name: channel.name, nsfw: channel.nsfw, parentID: channel.parentID, @@ -1751,15 +1767,22 @@ class Shard extends EventEmitter { /** * Fired when a channel is updated * @event Client#channelUpdate - * @prop {TextChannel | TextVoiceChannel | CategoryChannel | NewsChannel | GuildChannel | PrivateChannel} channel The updated channel + * @prop {ForumChannel | TextChannel | TextVoiceChannel | CategoryChannel | NewsChannel | GuildChannel | PrivateChannel} channel The updated channel * @prop {Object} oldChannel The old channel data + * @prop {Array} oldChannel.availableTags The available tags that can be applied to threads in a forum channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (forum channels only, max 20) * @prop {Number} oldChannel.bitrate The bitrate of the channel (voice channels only) + * @prop {Number} oldChannel.defaultAutoArchiveDuration The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (text/news/forum channels only) + * @prop {Number} oldChannel.defaultForumLayout The default forum layout type used to display posts in forum channels (forum channels only) + * @prop {Object} oldChannel.defaultReactionEmoji The emoji to show in the add reaction button on a thread in a forum channel (forum channels only) + * @prop {Number} oldChannel.defaultSortOrder The default sort order type used to order posts in forum channels (forum channels only) + * @prop {Number} oldChannel.defaultThreadRateLimitPerUser The initial rateLimitPerUser to set on newly created threads in a channel (text/forum channels only) + * @prop {Number?} oldChannel.flags The flags for the channel combined as a bitfield (thread/forum channels only) * @prop {String} oldChannel.name The name of the channel * @prop {Boolean} oldChannel.nsfw Whether the channel is NSFW or not (text channels only) * @prop {String?} oldChannel.parentID The ID of the category this channel belongs to (guild channels only) * @prop {Collection} oldChannel.permissionOverwrites Collection of PermissionOverwrites in this channel (guild channels only) * @prop {Number} oldChannel.position The position of the channel (guild channels only) - * @prop {Number?} oldChannel.rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled (text channels only) + * @prop {Number?} oldChannel.rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) (text/voice/stage/forum channels only) * @prop {String?} oldChannel.rtcRegion The RTC region ID of the channel (automatic when `null`) (voice channels only) * @prop {String?} oldChannel.topic The topic of the channel (text channels only) * @prop {Number} oldChannel.type The type of the old channel (text/news channels only) @@ -1778,7 +1801,7 @@ class Shard extends EventEmitter { /** * Fired when a channel is deleted * @event Client#channelDelete - * @prop {PrivateChannel | TextChannel | NewsChannel | TextVoiceChannel | CategoryChannel} channel The channel + * @prop {ForumChannel | PrivateChannel | TextChannel | NewsChannel | TextVoiceChannel | CategoryChannel} channel The channel */ this.emit("channelDelete", channel); } @@ -2306,6 +2329,13 @@ class Shard extends EventEmitter { } channel.guild.threads.add(channel, this.client); this.client.threadGuildMap[packet.d.id] = packet.d.guild_id; + + const parent = channel.guild.channels.get(channel.parentID); + + if(parent instanceof ForumChannel) { + parent.lastMessageID = channel.id; + } + /** * Fired when a channel is created * @event Client#threadCreate @@ -2327,6 +2357,9 @@ class Shard extends EventEmitter { break; } const oldChannel = { + appliedTags: channel.appliedTags, + autoArchiveDuration: channel.autoArchiveDuration, + flags: channel.flags, name: channel.name, rateLimitPerUser: channel.rateLimitPerUser, threadMetadata: channel.threadMetadata @@ -2338,8 +2371,11 @@ class Shard extends EventEmitter { * @event Client#threadUpdate * @prop {NewsThreadChannel | PrivateThreadChannel | PublicThreadChannel} channel The updated channel * @prop {Object?} oldChannel The old thread channel. This will be null if the channel was uncached + * @prop {Array?} oldChannel.appliedTags The IDs of the set of tags that have been applied to a thread in a forum channel + * @prop {Number} oldChannel.autoArchiveDuration The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @prop {Number} oldChannel.flags The flags for the channel combined as a bitfield * @prop {String} oldChannel.name The name of the channel - * @prop {Number} oldChannel.rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled + * @prop {Number} oldChannel.rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) * @prop {Object} oldChannel.threadMetadata Metadata for the thread * @prop {Boolean} oldChannel.threadMetadata.archived Whether the thread is archived * @prop {Number} oldChannel.threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity diff --git a/lib/structures/Channel.js b/lib/structures/Channel.js index 556b306bc..91bab6894 100644 --- a/lib/structures/Channel.js +++ b/lib/structures/Channel.js @@ -54,6 +54,9 @@ class Channel extends Base { case ChannelTypes.GUILD_STAGE_VOICE: { return new StageChannel(data, client); } + case ChannelTypes.GUILD_FORUM: { + return new ForumChannel(data, client); + } } if(data.guild_id) { if(data.last_message_id !== undefined) { @@ -80,6 +83,7 @@ module.exports = Channel; // Circular import const CategoryChannel = require("./CategoryChannel"); const GuildChannel = require("./GuildChannel"); +const ForumChannel = require("./ForumChannel"); const GroupChannel = require("./GroupChannel"); const NewsChannel = require("./NewsChannel"); const NewsThreadChannel = require("./NewsThreadChannel"); diff --git a/lib/structures/ForumChannel.js b/lib/structures/ForumChannel.js new file mode 100644 index 000000000..71276c94e --- /dev/null +++ b/lib/structures/ForumChannel.js @@ -0,0 +1,176 @@ +"use strict"; + +const GuildChannel = require("./GuildChannel"); + +/** + * Represents a guild forum channel. See GuildChannel for more properties and methods. + * @extends GuildChannel + * @prop {Array} availableTags The available tags that can be applied to threads in the forum channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (max 20) + * @prop {Number} defaultAutoArchiveDuration The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) + * @prop {Number} defaultForumLayout The default forum layout type used to display posts in the forum channel + * @prop {Object} defaultReactionEmoji The emoji to show in the add reaction button on a thread in the forum channel + * @prop {Number} defaultSortOrder The default sort order type used to order posts in the forum channel + * @prop {Number} defaultThreadRateLimitPerUser The initial rateLimitPerUser to set on newly created threads in the forum channel + * @prop {String} lastMessageID The ID of the most recently created thread in the forum channel + * @prop {Number} rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @prop {Array} threads An array of threads in the forum channel + * @prop {String?} topic The topic of the channel + */ +class ForumChannel extends GuildChannel { + constructor(data, client) { + super(data, client); + this.lastMessageID = data.last_message_id || null; + this.rateLimitPerUser = data.rate_limit_per_user == null ? null : data.rate_limit_per_user; + this.update(data); + } + + update(data) { + super.update(data); + if(data.topic !== undefined) { + this.topic = data.topic; + } + if(data.available_tags !== undefined) { + this.availableTags = data.available_tags; + } + if(data.default_auto_archive_duration !== undefined) { + this.defaultAutoArchiveDuration = data.default_auto_archive_duration; + } + if(data.default_forum_layout !== undefined) { + this.defaultForumLayout = data.default_forum_layout; + } + if(data.default_reaction_emoji !== undefined) { + this.defaultReactionEmoji = data.default_reaction_emoji; + } + if(data.default_sort_order !== undefined) { + this.defaultSortOrder = data.default_sort_order; + } + if(data.default_thread_rate_limit_per_user !== undefined) { + this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user; + } + if(data.rate_limit_per_user !== undefined) { + this.rateLimitPerUser = data.rate_limit_per_user; + } + } + + get threads() { + return this.guild.threads.filter((thread) => thread.parentID === this.id); + } + + /** + * Create an invite for the channel + * @arg {Object} [options] Invite generation options + * @arg {Number} [options.maxAge] How long the invite should last in seconds + * @arg {Number} [options.maxUses] How many uses the invite should last for + * @arg {Boolean} [options.temporary] Whether the invite grants temporary membership or not + * @arg {Boolean} [options.unique] Whether the invite is unique or not + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} + */ + createInvite(options, reason) { + return this.client.createChannelInvite.call(this.client, this.id, options, reason); + } + + /** + * Create a thread inside the forum channel + * @arg {Object} options The thread options + * @arg {Array} [options.appliedTags] The IDs of the set of tags that have been applied to a thread in a forum channel (threads created in forum channels only, max 5) + * @arg {Number} [options.autoArchiveDuration] The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 (thread channels only) + * @arg {Object} [options.message] The message to create with the thread. Note: When creating a forum channel thread, you must provide at least one of `content`, `embeds`, `stickerIDs`, `components`, or `files` + * @arg {Object} [options.message.allowedMentions] A list of mentions to allow (overrides default) + * @arg {Boolean} [options.message.allowedMentions.everyone] Whether or not to allow @everyone/@here + * @arg {Boolean} [options.message.allowedMentions.repliedUser] Whether or not to mention the author of the message being replied to + * @arg {Boolean | Array} [options.message.allowedMentions.roles] Whether or not to allow all role mentions, or an array of specific role mentions to allow + * @arg {Boolean | Array} [options.message.allowedMentions.users] Whether or not to allow all user mentions, or an array of specific user mentions to allow + * @arg {Array} [options.message.attachments] An array of attachment objects with the filename and description + * @arg {String} [options.message.attachments[].description] The description of the file + * @arg {String} [options.message.attachments[].filename] The name of the file + * @arg {Number} options.message.attachments[].id The index of the file + * @arg {Array} [options.message.components] An array of component objects + * @arg {String} [options.message.components[].custom_id] The ID of the component (type 2 style 0-4 and type 3 only) + * @arg {Boolean} [options.message.components[].disabled] Whether the component is disabled (type 2 and 3 only) + * @arg {Object} [options.message.components[].emoji] The emoji to be displayed in the component (type 2) + * @arg {String} [options.message.components[].label] The label to be displayed in the component (type 2) + * @arg {Number} [options.message.components[].max_values] The maximum number of items that can be chosen (1-25, default 1) + * @arg {Number} [options.message.components[].min_values] The minimum number of items that must be chosen (0-25, default 1) + * @arg {Array} [options.message.components[].options] The options for this component (type 3 only) + * @arg {Boolean} [options.message.components[].options[].default] Whether this option should be the default value selected + * @arg {String} [options.message.components[].options[].description] The description for this option + * @arg {Object} [options.message.components[].options[].emoji] The emoji to be displayed in this option + * @arg {String} options.message.components[].options[].label The label for this option + * @arg {Number | String} options.message.components[].options[].value The value for this option + * @arg {String} [options.message.components[].placeholder] The placeholder text for the component when no option is selected (type 3 only) + * @arg {Number} [options.message.components[].style] The style of the component (type 2 only) - If 0-4, `custom_id` is required; if 5, `url` is required + * @arg {Number} options.message.components[].type The type of component - If 1, it is a collection and a `components` array (nested) is required; if 2, it is a button; if 3, it is a select menu + * @arg {String} [options.message.components[].url] The URL that the component should open for users (type 2 style 5 only) + * @arg {String} [options.message.content] A string containing the message content + * @arg {Array} [options.message.embeds] An array of embed objects. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#embed-object) for object structure + * @arg {Array} [options.message.stickerIDs] An array of IDs corresponding to stickers to send + * @arg {String} options.name The thread channel name + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @arg {Object | Array} [file] A file object (or an Array of them) + * @arg {Buffer} file.file A buffer containing file data + * @arg {String} file.name What to name the file + * @returns {Promise} + */ + createThread(options, file) { + return this.client.createThread.call(this.client, this.id, options, file); + } + + /** + * Create a channel webhook + * @arg {Object} options Webhook options + * @arg {String} [options.avatar] The default avatar as a base64 data URI. Note: base64 strings alone are not base64 data URI strings + * @arg {String} options.name The default name + * @arg {String} [reason] The reason to be displayed in audit logs + * @returns {Promise} Resolves with a webhook object + */ + createWebhook(options, reason) { + return this.client.createChannelWebhook.call(this.client, this.id, options, reason); + } + + /** + * Get all archived threads in this channel + * @arg {Object} [options] Additional options when requesting archived threads + * @arg {Date} [options.before] List of threads to return before the timestamp + * @arg {Number} [options.limit] Maximum number of threads to return + * @returns {Promise} An object containing an array of `threads`, an array of `members` and whether the response `hasMore` threads that could be returned in a subsequent call + */ + getArchivedThreads(options) { + return this.client.getArchivedThreads.call(this.client, this.id, "public", options); + } + + /** + * Get all invites in the channel + * @returns {Promise>} + */ + getInvites() { + return this.client.getChannelInvites.call(this.client, this.id); + } + + /** + * Get all the webhooks in the channel + * @returns {Promise>} Resolves with an array of webhook objects + */ + getWebhooks() { + return this.client.getChannelWebhooks.call(this.client, this.id); + } + + toJSON(props = []) { + return super.toJSON([ + "availableTags", + "defaultAutoArchiveDuration", + "defaultForumLayout", + "defaultReactionEmoji", + "defaultSortOrder", + "defaultThreadRateLimitPerUser", + "nsfw", + "position", + "rateLimitPerUser", + "topic", + ...props + ]); + } +} + +module.exports = ForumChannel; diff --git a/lib/structures/Guild.js b/lib/structures/Guild.js index f538eef5e..33385fef7 100644 --- a/lib/structures/Guild.js +++ b/lib/structures/Guild.js @@ -435,18 +435,24 @@ class Guild extends Base { /** * Create a channel in the guild * @arg {String} name The name of the channel - * @arg {Number} [type=0] The type of the channel, either 0 (text), 2 (voice), 4 (category), 5 (news) or 13 (stage) + * @arg {Number} [type=0] The type of the channel, either 0 (text), 2 (voice), 4 (category), 5 (news), 13 (stage), or 15 (forum) * @arg {Object | String} [options] The properties the channel should have. If `options` is a string, it will be treated as `options.parentID` (see below). Passing a string is deprecated and will not be supported in future versions. + * @arg {Array} [options.availableTags] The available tags that can be applied to threads in a forum channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (forum channels only, max 20) * @arg {Number} [options.bitrate] The bitrate of the channel (voice channels only) + * @arg {Number} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (text/news/forum channels only) + * @arg {Number} [options.defaultForumLayout] The default forum layout type used to display posts in forum channels (forum channels only) + * @arg {Object} [options.defaultReactionEmoji] The emoji to show in the add reaction button on a thread in a forum channel (forum channels only) + * @arg {Number} [options.defaultSortOrder] The default sort order type used to order posts in forum channels (forum channels only) + * @arg {Number} [options.defaultThreadRateLimitPerUser] The initial rateLimitPerUser to set on newly created threads in a channel (text/forum channels only) * @arg {Boolean} [options.nsfw] The nsfw status of the channel * @arg {String?} [options.parentID] The ID of the parent category channel for this channel - * @arg {Array} [options.permissionOverwrites] An array containing permission overwrite objects + * @arg {Array} [options.permissionOverwrites] An array containing permission overwrite objects * @arg {Number} [options.position] The sorting position of the channel - * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (text channels only) + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) (text/voice/stage/forum channels only) * @arg {String} [options.reason] The reason to be displayed in audit logs * @arg {String} [options.topic] The topic of the channel (text channels only) * @arg {Number} [options.userLimit] The channel user limit (voice channels only) - * @returns {Promise} + * @returns {Promise} */ createChannel(name, type, reason, options) { return this._client.createChannel.call(this._client, this.id, name, type, reason, options); @@ -1154,7 +1160,7 @@ class Guild extends Base { /** * Get a guild's channels via the REST API. REST mode is required to use this endpoint. - * @returns {Promise | TextVoiceChannel | NewsChannel | StageChannel>>} + * @returns {Promise | TextVoiceChannel>} */ getRESTChannels() { return this._client.getRESTGuildChannels.call(this._client, this.id); diff --git a/lib/structures/GuildChannel.js b/lib/structures/GuildChannel.js index 2c8fe80ad..ac6326584 100644 --- a/lib/structures/GuildChannel.js +++ b/lib/structures/GuildChannel.js @@ -7,6 +7,7 @@ const {Permissions} = require("../Constants"); /** * Represents a guild channel. You also probably want to look at CategoryChannel, NewsChannel, TextChannel, ThreadChannel, and TextVoiceChannel. See Channel for extra properties. * @extends Channel + * @prop {Number?} flags Channel flags (see constants) (thread/forum channels only) * @prop {Guild} guild The guild that owns the channel * @prop {String} id The ID of the channel * @prop {String} name The name of the channel @@ -34,6 +35,9 @@ class GuildChannel extends Channel { if(data.parent_id !== undefined) { this.parentID = data.parent_id; } + if(data.flags !== undefined) { + this.flags = data.flags; + } } /** @@ -58,24 +62,31 @@ class GuildChannel extends Channel { /** * Edit the channel's properties * @arg {Object} options The properties to edit + * @arg {Array} [options.appliedTags] The IDs of the set of tags that have been applied to a thread in a forum channel (threads created in forum channels only, max 5) * @arg {Boolean} [options.archived] The archive status of the channel (thread channels only) * @arg {Number} [options.autoArchiveDuration] The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 (thread channels only) + * @arg {Array} [options.availableTags] The available tags that can be applied to threads in a forum channel. See [the official Discord API documentation entry](https://discord.com/developers/docs/resources/channel#forum-tag-object) for object structure (forum channels only, max 20) * @arg {Number} [options.bitrate] The bitrate of the channel (guild voice channels only) - * @arg {Number?} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (guild text/news channels only) + * @arg {Number} [options.defaultAutoArchiveDuration] The default duration of newly created threads in minutes to automatically archive the thread after inactivity (60, 1440, 4320, 10080) (text/news/forum channels only) + * @arg {Number} [options.defaultForumLayout] The default forum layout type used to display posts in forum channels (forum channels only) + * @arg {Object} [options.defaultReactionEmoji] The emoji to show in the add reaction button on a thread in a forum channel (forum channels only) + * @arg {Number} [options.defaultSortOrder] The default sort order type used to order posts in forum channels (forum channels only) + * @arg {Number} [options.defaultThreadRateLimitPerUser] The initial rateLimitPerUser to set on newly created threads in a channel (text/forum channels only) + * @arg {Number} [options.flags] The flags for the channel combined as a bitfield (thread/forum channels only). Note: `PINNED` can only be set for threads in forum channels, and `REQUIRE_TAG` can only be set for forum channels * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the channel (private thread channels only) * @arg {Boolean} [options.locked] The lock status of the channel (thread channels only) * @arg {String} [options.name] The name of the channel * @arg {Boolean} [options.nsfw] The nsfw status of the channel - * @arg {Number?} [options.parentID] The ID of the parent channel category for this channel (guild text/voice channels only) or the channel ID where the thread originated from (thread channels only) + * @arg {String?} [options.parentID] The ID of the parent channel category for this channel (guild text/voice channels only) or the channel ID where the thread originated from (thread channels only) * @arg {Array} [options.permissionOverwrites] An array containing permission overwrite objects * @arg {Number} [options.position] The sorting position of the channel - * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (does not affect bots or users with manageMessages/manageChannel permissions) (guild text and thread channels only) + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) (text/voice/stage/forum channels only) * @arg {String?} [options.rtcRegion] The RTC region ID of the channel (automatic if `null`) (guild voice channels only) * @arg {String} [options.topic] The topic of the channel (guild text channels only) * @arg {Number} [options.userLimit] The channel user limit (guild voice channels only) * @arg {Number} [options.videoQualityMode] The camera video quality mode of the channel (guild voice channels only). `1` is auto, `2` is 720p * @arg {String} [reason] The reason to be displayed in audit logs - * @returns {Promise} + * @returns {Promise} */ edit(options, reason) { return this.client.editChannel.call(this.client, this.id, options, reason); @@ -141,6 +152,7 @@ class GuildChannel extends Channel { toJSON(props = []) { return super.toJSON([ "parentID", + "flags", ...props ]); } diff --git a/lib/structures/Message.js b/lib/structures/Message.js index ea643b177..051d56917 100644 --- a/lib/structures/Message.js +++ b/lib/structures/Message.js @@ -399,8 +399,10 @@ class Message extends Base { /** * Create a thread with this message * @arg {Object} options The thread options - * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {Number} [options.autoArchiveDuration] Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 * @arg {String} options.name The thread channel name + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs * @returns {Promise} */ createThreadWithMessage(options) { diff --git a/lib/structures/PrivateThreadChannel.js b/lib/structures/PrivateThreadChannel.js index 5fdee7816..6be08db15 100644 --- a/lib/structures/PrivateThreadChannel.js +++ b/lib/structures/PrivateThreadChannel.js @@ -20,6 +20,7 @@ class PrivateThreadChannel extends ThreadChannel { } update(data) { + super.update(data); if(data.thread_metadata !== undefined) { this.threadMetadata = { archiveTimestamp: Date.parse(data.thread_metadata.archive_timestamp), diff --git a/lib/structures/PublicThreadChannel.js b/lib/structures/PublicThreadChannel.js index ae9c545de..6138fd139 100644 --- a/lib/structures/PublicThreadChannel.js +++ b/lib/structures/PublicThreadChannel.js @@ -5,10 +5,26 @@ const ThreadChannel = require("./ThreadChannel"); /** * Represents a public thread channel. See ThreadChannel for extra properties. * @extends ThreadChannel + * @prop {Array?} appliedTags The IDs of the set of tags that have been applied to a thread in a forum channel (threads created in forum channels only, max 5) */ class PublicThreadChannel extends ThreadChannel { constructor(data, client, messageLimit) { super(data, client, messageLimit); + this.update(data); + } + + update(data) { + super.update(data); + if(data.applied_tags !== undefined) { + this.appliedTags = data.applied_tags; + } + } + + toJSON(props = []) { + return super.toJSON([ + "appliedTags", + ...props + ]); } } diff --git a/lib/structures/TextChannel.js b/lib/structures/TextChannel.js index 462f5739d..82eb678e9 100644 --- a/lib/structures/TextChannel.js +++ b/lib/structures/TextChannel.js @@ -15,7 +15,7 @@ const PermissionOverwrite = require("./PermissionOverwrite"); * @prop {Boolean} nsfw Whether the channel is an NSFW channel or not * @prop {Collection} permissionOverwrites Collection of PermissionOverwrites in this channel * @prop {Number} position The position of the channel - * @prop {Number} rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled + * @prop {Number} rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) * @prop {String?} topic The topic of the channel */ class TextChannel extends GuildChannel { @@ -127,12 +127,29 @@ class TextChannel extends GuildChannel { return this.client.createMessage.call(this.client, this.id, content, file); } + /** + * Create a thread without an existing message + * @arg {Object} options The thread options + * @arg {Number} [options.autoArchiveDuration] The duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 (thread channels only) + * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the thread (private threads only) + * @arg {String} options.name The thread channel name + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @arg {Number} [options.type] The channel type of the thread to create. Either `10` (announcement thread, announcement channels only), `11` (public thread) or `12` (private thread) + * @returns {Promise} + */ + createThread(options) { + return this.client.createThread.call(this.client, this.id, options); + } + /** * Create a thread with an existing message * @arg {String} messageID The ID of the message to create the thread from * @arg {Object} options The thread options - * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {Number} [options.autoArchiveDuration] Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 * @arg {String} options.name The thread channel name + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs * @returns {Promise} */ createThreadWithMessage(messageID, options) { @@ -140,13 +157,15 @@ class TextChannel extends GuildChannel { } /** - * Create a thread without an existing message + * [DEPRECATED] Create a thread without an existing message. Use `createThread` instead * @arg {Object} options The thread options - * @arg {Number} options.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 + * @arg {Number} [options.autoArchiveDuration] Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 * @arg {Boolean} [options.invitable] Whether non-moderators can add other non-moderators to the thread (private threads only) * @arg {String} options.name The thread channel name - * @arg {Number} [options.type] The channel type of the thread to create. It is recommended to explicitly set this property as this will be a required property in API v10 - * @returns {Promise} + * @arg {Number} [options.rateLimitPerUser] The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) + * @arg {String} [options.reason] The reason to be displayed in audit logs + * @arg {Number} [options.type] The channel type of the thread to create. Either `10` (announcement thread, announcement channels only), `11` (public thread) or `12` (private thread) + * @returns {Promise} */ createThreadWithoutMessage(options) { return this.client.createThreadWithoutMessage.call(this.client, this.id, options); diff --git a/lib/structures/TextVoiceChannel.js b/lib/structures/TextVoiceChannel.js index fc19d7895..045efd144 100644 --- a/lib/structures/TextVoiceChannel.js +++ b/lib/structures/TextVoiceChannel.js @@ -10,7 +10,7 @@ const Message = require("./Message"); * @prop {String} lastMessageID The ID of the last message in this channel * @prop {Collection} messages Collection of Messages in this channel * @prop {Boolean} nsfw Whether the channel is an NSFW channel or not - * @prop {Number} rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled + * @prop {Number} rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) * @prop {Number?} userLimit The max number of users that can join the channel * @prop {Number?} videoQualityMode The camera video quality mode of the voice channel. `1` is auto, `2` is 720p */ diff --git a/lib/structures/ThreadChannel.js b/lib/structures/ThreadChannel.js index da2b008e3..4b5dea10e 100644 --- a/lib/structures/ThreadChannel.js +++ b/lib/structures/ThreadChannel.js @@ -16,16 +16,17 @@ const ThreadMember = require("./ThreadMember"); * @prop {String} member.userID The ID of the user * @prop {Number} memberCount An approximate number of users in the thread (stops at 50) * @prop {Collection} members Collection of members in this channel - * @prop {Number} messageCount An approximate number of messages in the thread (stops at 50) + * @prop {Number} messageCount An approximate number of messages in the thread * @prop {Collection} messages Collection of Messages in this channel * @prop {String} ownerID The ID of the user that created the thread - * @prop {Number} rateLimitPerUser The ratelimit of the channel, in seconds. 0 means no ratelimit is enabled + * @prop {Number} rateLimitPerUser The time in seconds a user has to wait before sending another message (0-21600) (does not affect bots or users with manageMessages/manageChannel permissions) * @prop {Object} threadMetadata Metadata for the thread * @prop {Boolean} threadMetadata.archived Whether the thread is archived * @prop {Number} threadMetadata.archiveTimestamp Timestamp when the thread's archive status was last changed, used for calculating recent activity * @prop {Number} threadMetadata.autoArchiveDuration Duration in minutes to automatically archive the thread after recent activity, either 60, 1440, 4320 or 10080 * @prop {Number?} threadMetadata.createTimestamp Timestamp when the thread was created (only available for threads created after 09 January 2022) * @prop {Boolean} threadMetadata.locked Whether the thread is locked + * @prop {Number} totalMessageSent Total number of messages ever sent in a thread (similar to `messageCount`, however this number will not decrement when a message is deleted) */ class ThreadChannel extends GuildChannel { constructor(data, client, messageLimit) { @@ -62,6 +63,9 @@ class ThreadChannel extends GuildChannel { if(data.member !== undefined) { this.member = new ThreadMember(data.member, this.client); } + if(data.total_message_sent !== undefined) { + this.totalMessageSent = data.total_message_sent; + } } /** @@ -348,6 +352,7 @@ class ThreadChannel extends GuildChannel { "rateLimitPerUser", "threadMetadata", "member", + "totalMessageSent", ...props ]); } diff --git a/lib/util/emitDeprecation.js b/lib/util/emitDeprecation.js index 362765a88..67b0d6af4 100644 --- a/lib/util/emitDeprecation.js +++ b/lib/util/emitDeprecation.js @@ -1,5 +1,6 @@ const warningMessages = { CREATE_CHANNEL_OPTIONS: "Passing parentID or reason string arguments directly to createChannel() is deprecated. Use an options object instead.", + CREATE_THREAD_WITHOUT_MESSAGE: "createThreadWithoutMessage() is deprecated. Use createThread() instead.", DEFAULT_PERMISSION: "Passing a defaultPermission for application commands is deprecated. Use defaultMemberPermissions instead.", DELETE_MESSAGE_DAYS: "Passing the deleteMessageDays parameter to banGuildMember is deprecated. Use an options object with deleteMessageSeconds instead.", DM_REACTION_REMOVE: "Passing a userID when using removeMessageReaction() in a PrivateChannel is deprecated. This behavior is no longer supported by Discord.",