From 659f4512a873e86adf99d23db552593c8a2a4282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Susa=C3=B1a?= Date: Thu, 15 Sep 2022 02:11:03 -0400 Subject: [PATCH] feat: Guild Forums (#113) which closes #25 * feat: Guild Forums * fix --- packages/api-types/src/common.ts | 14 +++ packages/api-types/src/v10/index.ts | 14 +++ packages/core/src/structures/channels.ts | 126 ++++++++++++++++++++++- packages/core/src/structures/message.ts | 5 + 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/packages/api-types/src/common.ts b/packages/api-types/src/common.ts index fc2d605..401b75e 100644 --- a/packages/api-types/src/common.ts +++ b/packages/api-types/src/common.ts @@ -175,6 +175,20 @@ export interface BaseRole { unicodeEmoji?: string; } +/** https://discord.com/developers/docs/resources/channel#forum-tag-object */ +export interface DiscordForumTag { + /** the id of the tag */ + id: Snowflake; + /** the name of the tag (0-20 characters) */ + name: string; + /** whether this tag can only be added to or removed from threads by a member with the MANAGE_THREADS permission */ + moderated: boolean; + /** the id of a guild's custom emoji * */ + emoji_id: Snowflake | null; + /** he unicode character of the emoji */ + emoji_name: string | null; +} + /** https://discord.com/developers/docs/resources/guild#guild-object-guild-features */ export enum GuildFeatures { /** Guild has access to set an invite splash background */ diff --git a/packages/api-types/src/v10/index.ts b/packages/api-types/src/v10/index.ts index 43869be..dff8bca 100644 --- a/packages/api-types/src/v10/index.ts +++ b/packages/api-types/src/v10/index.ts @@ -12,6 +12,7 @@ import type { DefaultMessageNotificationLevels, EmbedTypes, ExplicitContentFilterLevels, + DiscordForumTag, GatewayEventNames, GuildFeatures, GuildNsfwLevel, @@ -754,6 +755,17 @@ export interface DiscordChannel { newly_created?: boolean; /** The recipients of the DM*/ recipients?: DiscordUser[]; + + /** number of messages ever sent in a thread */ + total_message_sent?: number; + /** the set of tags that can be used in a GUILD_FORUM channel */ + available_tags?: DiscordForumTag[]; + /** the IDs of the set of tags that have been applied to a thread in a GUILD_FORUM channel */ + applied_tags?: string[]; + /** the emoji to show in the add reaction button on a thread in a GUILD_FORUM channel */ + default_reaction_emoji?: { emoji_id: string; emoji_name: string | null }; + /** the initial rate_limit_per_user to set on newly created threads in a channel. this field is copied to the thread at creation time and does not live update. */ + default_thread_rate_limit_per_user?: number; } /** https://discord.com/developers/docs/topics/gateway#presence-update */ @@ -1051,6 +1063,8 @@ export interface DiscordMessage { components?: DiscordMessageComponents; /** Sent if the message contains stickers */ sticker_items?: DiscordStickerItem[]; + /** A generally increasing integer (there may be gaps or duplicates) */ + position?: number; } /** https://discord.com/developers/docs/resources/channel#channel-mention-object */ diff --git a/packages/core/src/structures/channels.ts b/packages/core/src/structures/channels.ts index 2cd50d5..5c7c70e 100644 --- a/packages/core/src/structures/channels.ts +++ b/packages/core/src/structures/channels.ts @@ -104,6 +104,11 @@ export abstract class BaseChannel implements Model { return this.type === ChannelTypes.GuildStageVoice; } + /** If the channel is a ForumChannel */ + isForum(): this is ForumChannel { + return this.type === ChannelTypes.GuildForum; + } + async fetch(channelId?: Snowflake): Promise { const channel = await this.session.rest.get(CHANNEL(channelId ?? this.id)); @@ -506,10 +511,20 @@ export interface EditNewsChannelOptions extends EditGuildChannelOptions { defaultAutoArchiveDuration?: number | null; } +export interface EditForumChannelOptions extends EditGuildChannelOptions { + availableTags?: ForumTag[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultThreadRateLimitPerUser?: number; +} + export interface EditGuildTextChannelOptions extends EditNewsChannelOptions { rateLimitPerUser?: number | null; } +export interface EditThreadChannelOptions extends EditGuildTextChannelOptions { + appliedTags: string[]; +} + export interface EditStageChannelOptions extends EditGuildChannelOptions { bitrate?: number | null; rtcRegion?: Snowflake | null; @@ -589,9 +604,16 @@ export class GuildChannel extends BaseChannel implements Model { async edit(options: EditNewsChannelOptions): Promise; async edit(options: EditStageChannelOptions): Promise; async edit(options: EditVoiceChannelOptions): Promise; + async edit(options: EditForumChannelOptions): Promise; + async edit(options: EditThreadChannelOptions): Promise; async edit( - options: EditGuildTextChannelOptions | EditNewsChannelOptions | EditVoiceChannelOptions, - ): Promise { + options: + | EditGuildTextChannelOptions + | EditNewsChannelOptions + | EditVoiceChannelOptions + | EditForumChannelOptions + | EditThreadChannelOptions + ): Promise { const channel = await this.session.rest.patch( CHANNEL(this.id), { @@ -607,12 +629,30 @@ export class GuildChannel extends BaseChannel implements Model { parent_id: 'parentId' in options ? options.parentId : undefined, rtc_region: 'rtcRegion' in options ? options.rtcRegion : undefined, video_quality_mode: 'videoQualityMode' in options ? options.videoQualityMode : undefined, + applied_tags: 'appliedTags' in options ? options.appliedTags : undefined, default_auto_archive_duration: 'defaultAutoArchiveDuration' in options ? options.defaultAutoArchiveDuration : undefined, + default_reaction_emoji: 'defaultReactionEmoji' in options + ? options.defaultReactionEmoji + : undefined, + default_thread_rate_limit_per_user: 'defaultThreadRateLimitPerUser' in options + ? options.defaultThreadRateLimitPerUser + : undefined, + available_tags: 'availableTags' in options + ? options.availableTags?.map(at => { + return { + id: at.id, + name: at.name, + moderated: at.moderated, + emoji_id: at.emojiId, + emoji_name: at.emojiName + }; + }) + : undefined }, ); - return ChannelFactory.from(this.session, channel); + return ChannelFactory.fromGuildChannel(this.session, channel); } /** @@ -832,6 +872,10 @@ export class ThreadChannel extends GuildChannel implements Model { if (data.member) { this.member = new ThreadMember(session, data.member); } + + if (data.total_message_sent) { this.totalMessageSent = data.total_message_sent; } + + if (data.applied_tags) { this.appliedTags = data.applied_tags; } } override type: ChannelTypes.GuildNewsThread | ChannelTypes.GuildPrivateThread | ChannelTypes.GuildPublicThread; @@ -843,6 +887,8 @@ export class ThreadChannel extends GuildChannel implements Model { memberCount?: number; member?: ThreadMember; ownerId?: Snowflake; + totalMessageSent?: number; + appliedTags?: string[]; async joinThread(): Promise { await this.session.rest.put(THREAD_ME(this.id), {}); @@ -871,12 +917,76 @@ export class ThreadChannel extends GuildChannel implements Model { return members.map(threadMember => new ThreadMember(this.session, threadMember)); } + + async setAppliedTags(tags: string[]) { + const thread = await this.edit({ appliedTags: tags }); + return thread; + } } export interface ThreadChannel extends Omit, Omit { } TextChannel.applyTo(ThreadChannel); +/** ForumChannel */ +export class ForumChannel extends GuildChannel { + constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { + super(session, data, guildId); + + if (data.available_tags) { + this.availableTags = data.available_tags.map(at => { + return { + id: at.id, + name: at.name, + moderated: at.moderated, + emojiId: at.emoji_id, + emojiName: at.emoji_name + }; + }); + } + if (data.default_reaction_emoji) { + this.defaultReactionEmoji = { + emojiId: data.default_reaction_emoji.emoji_id, + emojiName: data.default_reaction_emoji.emoji_name + }; + } + + this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user; + } + + availableTags?: ForumTag[]; + defaultReactionEmoji?: DefaultReactionEmoji; + defaultThreadRateLimitPerUser?: number; + + async setAvailableTags(tags: ForumTag[]) { + const forum = await this.edit({ availableTags: tags }); + return forum; + } + + async setDefaultReactionEmoji(emoji: DefaultReactionEmoji) { + const forum = await this.edit({ defaultReactionEmoji: emoji }); + return forum; + } + + async setDefaultThreadRateLimitPerUser(limit: number) { + const forum = await this.edit({ defaultThreadRateLimitPerUser: limit }); + return forum; + } +} + +export interface ForumTag { + id: Snowflake; + name: string; + moderated: boolean; + emojiId: Snowflake | null; + emojiName: string | null; +} + +export interface DefaultReactionEmoji { + emojiId: Snowflake; + emojiName: string | null; +} + export class GuildTextChannel extends GuildChannel { constructor(session: Session, data: DiscordChannel, guildId: Snowflake) { super(session, data, guildId); @@ -899,14 +1009,16 @@ export type Channel = | NewsChannel | ThreadChannel | StageChannel - | CategoryChannel; + | CategoryChannel + | ForumChannel; export type ChannelInGuild = | GuildTextChannel | VoiceChannel | StageChannel | NewsChannel - | ThreadChannel; + | ThreadChannel + | ForumChannel; export type ChannelWithMessages = | GuildTextChannel @@ -929,6 +1041,8 @@ export class ChannelFactory { case ChannelTypes.GuildPublicThread: case ChannelTypes.GuildPrivateThread: return new ThreadChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildForum: + return new ForumChannel(session, channel, channel.guild_id!); case ChannelTypes.GuildText: return new GuildTextChannel(session, channel, channel.guild_id!); case ChannelTypes.GuildNews: @@ -947,6 +1061,8 @@ export class ChannelFactory { case ChannelTypes.GuildPublicThread: case ChannelTypes.GuildPrivateThread: return new ThreadChannel(session, channel, channel.guild_id!); + case ChannelTypes.GuildForum: + return new ForumChannel(session, channel, channel.guild_id!); case ChannelTypes.GuildText: return new GuildTextChannel(session, channel, channel.guild_id!); case ChannelTypes.GuildNews: diff --git a/packages/core/src/structures/message.ts b/packages/core/src/structures/message.ts index f226116..75e363e 100644 --- a/packages/core/src/structures/message.ts +++ b/packages/core/src/structures/message.ts @@ -166,6 +166,8 @@ export class Message implements Model { ); this.embeds = data.embeds.map(NewEmbedR); + if (data.position) { this.position = data.position; } + if (data.interaction) { this.interaction = InteractionFactory.fromMessage( session, @@ -349,6 +351,9 @@ export class Message implements Model { /** sent with Rich Presence-related chat embeds */ activity?: MessageActivity; + /** Represents the approximate position of the message in a thread */ + position?: number; + /** gets the timestamp of this message, this does not requires the timestamp field */ get createdTimestamp(): number { return Snowflake.snowflakeToTimestamp(this.id);