diff --git a/mod.ts b/mod.ts index 1d67f800..498b1a6e 100644 --- a/mod.ts +++ b/mod.ts @@ -67,6 +67,10 @@ export { GuildIntegration } from './src/structures/guild.ts' export { CategoryChannel } from './src/structures/guildCategoryChannel.ts' +export { + GuildForumChannel, + GuildForumTag +} from './src/structures/guildForumChannel.ts' export { NewsChannel } from './src/structures/guildNewsChannel.ts' export { VoiceChannel } from './src/structures/guildVoiceChannel.ts' export { Invite } from './src/structures/invite.ts' @@ -87,8 +91,7 @@ export { Snowflake } from './src/utils/snowflake.ts' export { TextChannel } from './src/structures/textChannel.ts' export { GuildTextBasedChannel, - GuildTextChannel, - checkGuildTextBasedChannel + GuildTextChannel } from './src/structures/guildTextChannel.ts' export type { AllMessageOptions } from './src/structures/textChannel.ts' export { MessageReaction } from './src/structures/messageReaction.ts' diff --git a/src/managers/channelThreads.ts b/src/managers/channelThreads.ts index 628bb803..96a2389d 100644 --- a/src/managers/channelThreads.ts +++ b/src/managers/channelThreads.ts @@ -6,23 +6,23 @@ import type { ThreadChannel, ThreadMember } from '../structures/threadChannel.ts' +import type { BaseManager, Message } from '../../mod.ts' import type { CreateThreadOptions, - GuildTextChannel -} from '../structures/guildTextChannel.ts' -import type { BaseManager, Message } from '../../mod.ts' + GuildThreadAvailableChannel +} from '../structures/guildThreadAvailableChannel.ts' export class ChannelThreadsManager extends BaseChildManager< ThreadChannelPayload, ThreadChannel > { - channel: GuildTextChannel + channel: GuildThreadAvailableChannel declare parent: BaseManager constructor( client: Client, parent: ThreadsManager, - channel: GuildTextChannel + channel: GuildThreadAvailableChannel ) { super( client, @@ -60,7 +60,7 @@ export class ChannelThreadsManager extends BaseChildManager< async start( options: CreateThreadOptions, - message: string | Message + message?: string | Message ): Promise { return this.channel.startThread(options, message) } diff --git a/src/managers/emojis.ts b/src/managers/emojis.ts index f99f9e12..9d2a6d11 100644 --- a/src/managers/emojis.ts +++ b/src/managers/emojis.ts @@ -1,3 +1,4 @@ +import type { Guild } from '../../mod.ts' import type { Client } from '../client/mod.ts' import { Emoji } from '../structures/emoji.ts' import type { EmojiPayload } from '../types/emoji.ts' @@ -32,4 +33,20 @@ export class EmojisManager extends BaseManager { .catch((e) => reject(e)) }) } + + /** Try to get Emoji from cache, if not found then fetch */ + async resolve( + key: string, + guild?: string | Guild + ): Promise { + const cacheValue = await this.get(key) + if (cacheValue !== undefined) return cacheValue + else { + if (guild !== undefined) { + const guildID = typeof guild === 'string' ? guild : guild.id + const fetchValue = await this.fetch(guildID, key).catch(() => undefined) + if (fetchValue !== undefined) return fetchValue + } + } + } } diff --git a/src/rest/endpoints.ts b/src/rest/endpoints.ts index 152dbfd9..1ed55dea 100644 --- a/src/rest/endpoints.ts +++ b/src/rest/endpoints.ts @@ -1366,9 +1366,9 @@ The `emoji` must be [URL Encoded](https://en.wikipedia.org/wiki/Percent-encoding } /** - * Creates a new public thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. + * Creates a new thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. */ - async startPublicThread( + async startPublicThreadFromMessage( channelId: string, messageId: string, payload: CreateThreadPayload @@ -1379,6 +1379,33 @@ The `emoji` must be [URL Encoded](https://en.wikipedia.org/wiki/Percent-encoding ) } + // Exist for backwards compatibility + /** + * Creates a new public thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. + */ + async startPublicThread( + channelId: string, + messageId: string, + payload: CreateThreadPayload + ): Promise { + return await this.startPublicThreadFromMessage( + channelId, + messageId, + payload + ) + } + + /** + * Creates a new thread from an existing message. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. + */ + async startThreadWithoutMessage( + channelId: string, + payload: CreateThreadPayload + ): Promise { + return this.rest.post(`/channels/${channelId}/threads`, payload) + } + + // Exist for backwards compatibility /** * Creates a new private thread. Returns a channel on success, and a 400 BAD REQUEST on invalid parameters. Fires a Thread Create Gateway event. */ @@ -1386,7 +1413,7 @@ The `emoji` must be [URL Encoded](https://en.wikipedia.org/wiki/Percent-encoding channelId: string, payload: CreateThreadPayload ): Promise { - return this.rest.post(`/channels/${channelId}/threads`, payload) + return await this.startThreadWithoutMessage(channelId, payload) } /** diff --git a/src/structures/channel.ts b/src/structures/channel.ts index fcb34850..0525b19c 100644 --- a/src/structures/channel.ts +++ b/src/structures/channel.ts @@ -43,9 +43,12 @@ import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' import type { StageVoiceChannel } from '../structures/guildVoiceStageChannel.ts' import type { TextChannel } from '../structures/textChannel.ts' import type { ThreadChannel } from '../structures/threadChannel.ts' +import { CreateInviteOptions } from '../managers/invites.ts' +import { Invite } from './invite.ts' export class Channel extends SnowflakeBase { type!: ChannelTypes + flags!: number static cacheName = 'channel' @@ -53,6 +56,10 @@ export class Channel extends SnowflakeBase { return `<#${this.id}>` } + toString(): string { + return this.mention + } + constructor(client: Client, data: ChannelPayload) { super(client, data) this.readFromData(data) @@ -61,6 +68,7 @@ export class Channel extends SnowflakeBase { readFromData(data: ChannelPayload): void { this.type = data.type ?? this.type this.id = data.id ?? this.id + this.flags = data.flags ?? this.flags } isDM(): this is DMChannel { @@ -435,4 +443,9 @@ export class GuildChannel extends Channel { async setPosition(position: number): Promise { return await this.edit({ position }) } + + /** Create an Invite for this Channel */ + async createInvite(options?: CreateInviteOptions): Promise { + return this.guild.invites.create(this.id, options) + } } diff --git a/src/structures/guildForumChannel.ts b/src/structures/guildForumChannel.ts new file mode 100644 index 00000000..c063ea33 --- /dev/null +++ b/src/structures/guildForumChannel.ts @@ -0,0 +1,191 @@ +import { Client } from '../client/client.ts' +import type { AllMessageOptions } from '../managers/channels.ts' +import { + CreateThreadInForumPayload, + GuildForumChannelPayload, + GuildForumSortOrderTypes, + GuildForumTagPayload, + ModifyGuildForumChannelOption, + ModifyGuildForumChannelPayload, + ThreadChannelPayload +} from '../types/channel.ts' +import { CHANNEL } from '../types/endpoint.ts' +import { transformComponent } from '../utils/components.ts' +import { Embed } from './embed.ts' +import { Emoji } from './emoji.ts' +import { Guild } from './guild.ts' +import { GuildThreadAvailableChannel } from './guildThreadAvailableChannel.ts' +import { Message } from './message.ts' +import { ThreadChannel } from './threadChannel.ts' + +export interface CreateThreadInForumOptions { + /** 2-100 character channel name */ + name: string + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + autoArchiveDuration?: number + slowmode?: number | null + message: string | AllMessageOptions + appliedTags?: string[] | GuildForumTag[] +} + +export class GuildForumTag { + id!: string + name!: string + moderated!: boolean + emojiID!: string + emojiName!: string | null + + constructor(data: GuildForumTagPayload) { + this.readFromData(data) + } + + readFromData(data: GuildForumTagPayload): void { + this.id = data.id ?? this.id + this.name = data.name ?? this.name + this.moderated = data.moderated ?? this.moderated + this.emojiID = data.emoji_id ?? this.emojiID + this.emojiName = data.emoji_name ?? this.emojiName + } +} + +export class GuildForumChannel extends GuildThreadAvailableChannel { + availableTags!: GuildForumTag[] + defaultReactionEmoji!: Emoji + defaultSortOrder!: GuildForumSortOrderTypes + + constructor(client: Client, data: GuildForumChannelPayload, guild: Guild) { + super(client, data, guild) + this.readFromData(data) + } + + readFromData(data: GuildForumChannelPayload): void { + super.readFromData(data) + this.availableTags = + data.available_tags?.map((tag) => new GuildForumTag(tag)) ?? + this.availableTags + this.defaultReactionEmoji = + data.default_reaction_emoji !== null + ? new Emoji(this.client, { + id: data.default_reaction_emoji.emoji_id, + name: data.default_reaction_emoji.emoji_name + }) + : this.defaultReactionEmoji + this.defaultSortOrder = data.default_sort_order ?? this.defaultSortOrder + } + + async edit( + options?: ModifyGuildForumChannelOption + ): Promise { + if (options?.defaultReactionEmoji !== undefined) { + if (options.defaultReactionEmoji instanceof Emoji) { + options.defaultReactionEmoji = { + emoji_id: options.defaultReactionEmoji.id, + emoji_name: options.defaultReactionEmoji.name + } + } + } + if (options?.availableTags !== undefined) { + options.availableTags = options.availableTags?.map((tag) => { + if (tag instanceof GuildForumTag) { + return { + id: tag.id, + name: tag.name, + moderated: tag.moderated, + emoji_id: tag.emojiID, + emoji_name: tag.emojiName + } + } + return tag + }) + } + + const body: ModifyGuildForumChannelPayload = { + name: options?.name, + position: options?.position, + permission_overwrites: options?.permissionOverwrites, + parent_id: options?.parentID, + nsfw: options?.nsfw, + topic: options?.topic, + rate_limit_per_user: options?.slowmode, + default_auto_archive_duration: options?.defaultAutoArchiveDuration, + default_thread_rate_limit_per_user: options?.defaultThreadSlowmode, + default_sort_order: options?.defaultSortOrder, + default_reaction_emoji: options?.defaultReactionEmoji, + available_tags: options?.availableTags + } + + const resp = await this.client.rest.patch(CHANNEL(this.id), body) + + return new GuildForumChannel(this.client, resp, this.guild) + } + + override async startThread( + options: CreateThreadInForumOptions, + message?: string | AllMessageOptions | Message + ): Promise { + if (options.message !== undefined) { + message = options.message + } + if (message instanceof Message) { + message = { + content: message.content, + embeds: message.embeds.map((embed) => new Embed(embed)), + components: message.components + } + } else if (message instanceof Embed) { + message = { + embed: message + } + } else if (Array.isArray(message)) { + message = { + embeds: message + } + } else if (typeof message === 'string') { + message = { + content: message + } + } + + const messageObject = { + content: message?.content, + embed: message?.embed, + embeds: message?.embeds, + file: message?.file, + files: message?.files, + allowed_mentions: message?.allowedMentions, + components: + message?.components !== undefined + ? typeof message.components === 'function' + ? message.components() + : transformComponent(message.components) + : undefined + } + + if ( + messageObject.content === undefined && + messageObject.embed === undefined + ) { + messageObject.content = '' + } + + const body: CreateThreadInForumPayload = { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.slowmode, + message: messageObject, + applied_tags: options.appliedTags?.map((tag) => { + if (tag instanceof GuildForumTag) { + return tag.id + } + return tag + }) + } + + const resp: ThreadChannelPayload = await this.client.rest.api.channels[ + this.id + ].threads.post(body) + const thread = new ThreadChannel(this.client, resp, this.guild) + this.threads.set(thread.id, resp) + return thread + } +} diff --git a/src/structures/guildNewsChannel.ts b/src/structures/guildNewsChannel.ts index e6f8f5e1..1c96fb25 100644 --- a/src/structures/guildNewsChannel.ts +++ b/src/structures/guildNewsChannel.ts @@ -1,3 +1,8 @@ +import { Mixin } from '../../deps.ts' import { GuildTextBasedChannel } from './guildTextChannel.ts' +import { GuildThreadAvailableChannel } from './guildThreadAvailableChannel.ts' -export class NewsChannel extends GuildTextBasedChannel {} +export class NewsChannel extends Mixin( + GuildTextBasedChannel, + GuildThreadAvailableChannel +) {} diff --git a/src/structures/guildTextChannel.ts b/src/structures/guildTextChannel.ts index 1687bb0b..7e54225a 100644 --- a/src/structures/guildTextChannel.ts +++ b/src/structures/guildTextChannel.ts @@ -4,46 +4,16 @@ import { GuildChannel } from './channel.ts' import type { Client } from '../client/mod.ts' import type { GuildTextBasedChannelPayload, - GuildTextChannelPayload, ModifyGuildTextBasedChannelOption, - ModifyGuildTextBasedChannelPayload, - ModifyGuildTextChannelOption, - ModifyGuildTextChannelPayload + ModifyGuildTextBasedChannelPayload } from '../types/channel.ts' -import { ChannelTypes } from '../types/channel.ts' import type { Guild } from './guild.ts' import { CHANNEL } from '../types/endpoint.ts' import type { Message } from './message.ts' -import type { CreateInviteOptions } from '../managers/invites.ts' -import type { Invite } from './invite.ts' -import type { CategoryChannel } from './guildCategoryChannel.ts' -import type { ThreadChannel, ThreadMember } from './threadChannel.ts' -import { ChannelThreadsManager } from '../managers/channelThreads.ts' - -const GUILD_TEXT_BASED_CHANNEL_TYPES: ChannelTypes[] = [ - ChannelTypes.GUILD_TEXT, - ChannelTypes.GUILD_NEWS -] - -export interface CreateThreadOptions { - /** 2-100 character channel name */ - name: string - /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ - autoArchiveDuration: number -} +import { GuildThreadAvailableChannel } from './guildThreadAvailableChannel.ts' /** Represents a Text Channel but in a Guild */ export class GuildTextBasedChannel extends Mixin(TextChannel, GuildChannel) { - topic?: string - - get mention(): string { - return `<#${this.id}>` - } - - toString(): string { - return this.mention - } - constructor( client: Client, data: GuildTextBasedChannelPayload, @@ -55,7 +25,6 @@ export class GuildTextBasedChannel extends Mixin(TextChannel, GuildChannel) { readFromData(data: GuildTextBasedChannelPayload): void { super.readFromData(data) - this.topic = data.topic ?? this.topic } /** Edit the Guild Text Channel */ @@ -67,9 +36,7 @@ export class GuildTextBasedChannel extends Mixin(TextChannel, GuildChannel) { position: options?.position, permission_overwrites: options?.permissionOverwrites, parent_id: options?.parentID, - nsfw: options?.nsfw, - topic: options?.topic - // rate_limit_per_user: options?.slowmode + nsfw: options?.nsfw } const resp = await this.client.rest.patch(CHANNEL(this.id), body) @@ -112,196 +79,10 @@ export class GuildTextBasedChannel extends Mixin(TextChannel, GuildChannel) { return this } - - /** Create an Invite for this Channel */ - async createInvite(options?: CreateInviteOptions): Promise { - return this.guild.invites.create(this.id, options) - } - - /** Edit topic of the channel */ - async setTopic(topic: string): Promise { - return await this.edit({ topic }) - } - - /** Edit category of the channel */ - async setCategory( - category: CategoryChannel | string - ): Promise { - return await this.edit({ - parentID: typeof category === 'object' ? category.id : category - }) - } } -export const checkGuildTextBasedChannel = ( - channel: TextChannel -): channel is GuildTextBasedChannel => - GUILD_TEXT_BASED_CHANNEL_TYPES.includes(channel.type) - -export class GuildTextChannel extends GuildTextBasedChannel { - slowmode!: number - threads: ChannelThreadsManager - - constructor(client: Client, data: GuildTextChannelPayload, guild: Guild) { - super(client, data, guild) - this.readFromData(data) - this.threads = new ChannelThreadsManager( - this.client, - this.guild.threads, - this - ) - } - - readFromData(data: GuildTextChannelPayload): void { - super.readFromData(data) - this.slowmode = data.rate_limit_per_user ?? this.slowmode - } - - /** Edit the Guild Text Channel */ - async edit( - options?: ModifyGuildTextChannelOption - ): Promise { - const body: ModifyGuildTextChannelPayload = { - name: options?.name, - position: options?.position, - permission_overwrites: options?.permissionOverwrites, - parent_id: options?.parentID, - nsfw: options?.nsfw, - topic: options?.topic, - rate_limit_per_user: options?.slowmode - } - - const resp = await this.client.rest.patch(CHANNEL(this.id), body) - - return new GuildTextChannel(this.client, resp, this.guild) - } - - /** Edit Slowmode of the channel */ - async setSlowmode(slowmode?: number | null): Promise { - return await this.edit({ slowmode: slowmode ?? null }) - } - - async startThread( - options: CreateThreadOptions, - message: Message | string - ): Promise { - const payload = await this.client.rest.endpoints.startPublicThread( - this.id, - typeof message === 'string' ? message : message.id, - { name: options.name, auto_archive_duration: options.autoArchiveDuration } - ) - await this.client.channels.set(payload.id, payload) - return (await this.client.channels.get(payload.id))! - } - - async startPrivateThread( - options: CreateThreadOptions - ): Promise { - const payload = await this.client.rest.endpoints.startPrivateThread( - this.id, - { name: options.name, auto_archive_duration: options.autoArchiveDuration } - ) - await this.client.channels.set(payload.id, payload) - return (await this.client.channels.get(payload.id))! - } - - async fetchArchivedThreads( - type: 'public' | 'private' = 'public', - params: { before?: string; limit?: number } = {} - ): Promise<{ - threads: ThreadChannel[] - members: ThreadMember[] - hasMore: boolean - }> { - const data = - type === 'public' - ? await this.client.rest.endpoints.getPublicArchivedThreads( - this.id, - params - ) - : await this.client.rest.endpoints.getPrivateArchivedThreads( - this.id, - params - ) - - const threads: ThreadChannel[] = [] - const members: ThreadMember[] = [] - - for (const d of data.threads) { - await this.threads.set(d.id, d) - threads.push((await this.threads.get(d.id))!) - } - - for (const d of data.members) { - const thread = - threads.find((e) => e.id === d.id) ?? (await this.threads.get(d.id)) - if (thread !== undefined) { - await thread.members.set(d.user_id, d) - members.push((await thread.members.get(d.user_id))!) - } - } - - return { - threads, - members, - hasMore: data.has_more - } - } - - async fetchPublicArchivedThreads( - params: { before?: string; limit?: number } = {} - ): Promise<{ - threads: ThreadChannel[] - members: ThreadMember[] - hasMore: boolean - }> { - return await this.fetchArchivedThreads('public', params) - } - - async fetchPrivateArchivedThreads( - params: { before?: string; limit?: number } = {} - ): Promise<{ - threads: ThreadChannel[] - members: ThreadMember[] - hasMore: boolean - }> { - return await this.fetchArchivedThreads('private', params) - } - - async fetchJoinedPrivateArchivedThreads( - params: { before?: string; limit?: number } = {} - ): Promise<{ - threads: ThreadChannel[] - members: ThreadMember[] - hasMore: boolean - }> { - const data = - await this.client.rest.endpoints.getJoinedPrivateArchivedThreads( - this.id, - params - ) - - const threads: ThreadChannel[] = [] - const members: ThreadMember[] = [] - - for (const d of data.threads) { - await this.threads.set(d.id, d) - threads.push((await this.threads.get(d.id))!) - } - - for (const d of data.members) { - const thread = - threads.find((e) => e.id === d.id) ?? (await this.threads.get(d.id)) - if (thread !== undefined) { - await thread.members.set(d.user_id, d) - members.push((await thread.members.get(d.user_id))!) - } - } - - return { - threads, - members, - hasMore: data.has_more - } - } -} +// Still exist for API compatibility +export class GuildTextChannel extends Mixin( + GuildTextBasedChannel, + GuildThreadAvailableChannel +) {} diff --git a/src/structures/guildThreadAvailableChannel.ts b/src/structures/guildThreadAvailableChannel.ts new file mode 100644 index 00000000..dc7a2ef4 --- /dev/null +++ b/src/structures/guildThreadAvailableChannel.ts @@ -0,0 +1,238 @@ +import { Client } from '../client/client.ts' +import { ChannelThreadsManager } from '../managers/channelThreads.ts' +import { + ChannelTypes, + GuildThreadAvailableChannelPayload, + ModifyGuildThreadAvailableChannelOption, + ModifyGuildThreadAvailableChannelPayload +} from '../types/channel.ts' +import { CHANNEL } from '../types/endpoint.ts' +import { GuildChannel } from './channel.ts' +import { Guild } from './guild.ts' +import { Message } from './message.ts' +import { ThreadChannel, ThreadMember } from './threadChannel.ts' + +export interface CreateThreadOptions { + /** 2-100 character channel name */ + name: string + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + autoArchiveDuration?: number + slowmode?: number | null + type?: ChannelTypes + invitable?: boolean +} + +export class GuildThreadAvailableChannel extends GuildChannel { + topic?: string + slowmode!: number + defaultThreadSlowmode?: number + defaultAutoArchiveDuration?: number + threads: ChannelThreadsManager + + constructor( + client: Client, + data: GuildThreadAvailableChannelPayload, + guild: Guild + ) { + super(client, data, guild) + this.readFromData(data) + this.threads = new ChannelThreadsManager( + this.client, + this.guild.threads, + this + ) + } + + readFromData(data: GuildThreadAvailableChannelPayload): void { + super.readFromData(data) + this.defaultThreadSlowmode = + data.default_thread_rate_limit_per_user ?? this.defaultThreadSlowmode + this.defaultAutoArchiveDuration = + data.default_auto_archive_duration ?? this.defaultAutoArchiveDuration + this.topic = data.topic ?? this.topic + } + + /** Edit the Guild Text Channel */ + async edit( + options?: ModifyGuildThreadAvailableChannelOption + ): Promise { + const body: ModifyGuildThreadAvailableChannelPayload = { + name: options?.name, + position: options?.position, + permission_overwrites: options?.permissionOverwrites, + parent_id: options?.parentID, + nsfw: options?.nsfw, + topic: options?.topic, + rate_limit_per_user: options?.slowmode, + default_auto_archive_duration: options?.defaultAutoArchiveDuration, + default_thread_rate_limit_per_user: options?.defaultThreadSlowmode + } + + const resp = await this.client.rest.patch(CHANNEL(this.id), body) + + return new GuildThreadAvailableChannel(this.client, resp, this.guild) + } + + /** Edit topic of the channel */ + async setTopic(topic: string): Promise { + return await this.edit({ topic }) + } + + /** Edit Slowmode of the channel */ + async setSlowmode( + slowmode?: number | null + ): Promise { + return await this.edit({ slowmode: slowmode ?? null }) + } + + /** Edit Default Slowmode of the threads in the channel */ + async setDefaultThreadSlowmode( + slowmode?: number | null + ): Promise { + return await this.edit({ defaultThreadSlowmode: slowmode ?? null }) + } + + /** Edit Default Auto Archive Duration of threads */ + async setDefaultAutoArchiveDuration( + slowmode?: number | null + ): Promise { + return await this.edit({ defaultAutoArchiveDuration: slowmode ?? null }) + } + + async startThread( + options: CreateThreadOptions, + message?: Message | string + ): Promise { + const payload = + message !== undefined + ? await this.client.rest.endpoints.startPublicThreadFromMessage( + this.id, + typeof message === 'string' ? message : message.id, + { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.slowmode + } + ) + : await this.client.rest.endpoints.startThreadWithoutMessage(this.id, { + name: options.name, + auto_archive_duration: options.autoArchiveDuration, + rate_limit_per_user: options.slowmode, + invitable: options.invitable, + type: options.type + }) + await this.client.channels.set(payload.id, payload) + return (await this.client.channels.get(payload.id))! + } + + async startPrivateThread( + options: CreateThreadOptions + ): Promise { + const payload = await this.client.rest.endpoints.startPrivateThread( + this.id, + { name: options.name, auto_archive_duration: options.autoArchiveDuration } + ) + await this.client.channels.set(payload.id, payload) + return (await this.client.channels.get(payload.id))! + } + + async fetchArchivedThreads( + type: 'public' | 'private' = 'public', + params: { before?: string; limit?: number } = {} + ): Promise<{ + threads: ThreadChannel[] + members: ThreadMember[] + hasMore: boolean + }> { + const data = + type === 'public' + ? await this.client.rest.endpoints.getPublicArchivedThreads( + this.id, + params + ) + : await this.client.rest.endpoints.getPrivateArchivedThreads( + this.id, + params + ) + + const threads: ThreadChannel[] = [] + const members: ThreadMember[] = [] + + for (const d of data.threads) { + await this.threads.set(d.id, d) + threads.push((await this.threads.get(d.id))!) + } + + for (const d of data.members) { + const thread = + threads.find((e) => e.id === d.id) ?? (await this.threads.get(d.id)) + if (thread !== undefined) { + await thread.members.set(d.user_id, d) + members.push((await thread.members.get(d.user_id))!) + } + } + + return { + threads, + members, + hasMore: data.has_more + } + } + + async fetchPublicArchivedThreads( + params: { before?: string; limit?: number } = {} + ): Promise<{ + threads: ThreadChannel[] + members: ThreadMember[] + hasMore: boolean + }> { + return await this.fetchArchivedThreads('public', params) + } + + async fetchPrivateArchivedThreads( + params: { before?: string; limit?: number } = {} + ): Promise<{ + threads: ThreadChannel[] + members: ThreadMember[] + hasMore: boolean + }> { + return await this.fetchArchivedThreads('private', params) + } + + async fetchJoinedPrivateArchivedThreads( + params: { before?: string; limit?: number } = {} + ): Promise<{ + threads: ThreadChannel[] + members: ThreadMember[] + hasMore: boolean + }> { + const data = + await this.client.rest.endpoints.getJoinedPrivateArchivedThreads( + this.id, + params + ) + + const threads: ThreadChannel[] = [] + const members: ThreadMember[] = [] + + for (const d of data.threads) { + await this.threads.set(d.id, d) + threads.push((await this.threads.get(d.id))!) + } + + for (const d of data.members) { + const thread = + threads.find((e) => e.id === d.id) ?? (await this.threads.get(d.id)) + if (thread !== undefined) { + await thread.members.set(d.user_id, d) + members.push((await thread.members.get(d.user_id))!) + } + } + + return { + threads, + members, + hasMore: data.has_more + } + } +} diff --git a/src/structures/message.ts b/src/structures/message.ts index 3e6ce89b..eb35d034 100644 --- a/src/structures/message.ts +++ b/src/structures/message.ts @@ -16,7 +16,6 @@ import { CHANNEL_MESSAGE } from '../types/endpoint.ts' import { MessageMentions } from './messageMentions.ts' import type { TextChannel } from './textChannel.ts' import type { - CreateThreadOptions, GuildTextBasedChannel, GuildTextChannel } from './guildTextChannel.ts' @@ -29,6 +28,7 @@ import { encodeText } from '../utils/encoding.ts' import { MessageComponentData } from '../types/messageComponents.ts' import { transformComponentPayload } from '../utils/components.ts' import type { ThreadChannel } from './threadChannel.ts' +import { CreateThreadOptions } from './guildThreadAvailableChannel.ts' type AllMessageOptions = MessageOptions | Embed diff --git a/src/structures/threadChannel.ts b/src/structures/threadChannel.ts index cea3751e..735a7c1f 100644 --- a/src/structures/threadChannel.ts +++ b/src/structures/threadChannel.ts @@ -89,6 +89,9 @@ export class ThreadChannel extends GuildTextBasedChannel { members: ThreadMembersManager slowmode: number = 0 owner!: UserResolvable + totalMessageSent!: number + // currently there's no way to grab the tag by id + appliedTags: string[] = [] constructor(client: Client, data: ThreadChannelPayload, guild: Guild) { super(client, data, guild) @@ -106,6 +109,8 @@ export class ThreadChannel extends GuildTextBasedChannel { : undefined this.slowmode = data.rate_limit_per_user ?? this.slowmode this.owner = new UserResolvable(this.client, data.owner_id) ?? this.owner + this.totalMessageSent = data.total_message_sent ?? this.totalMessageSent + this.appliedTags = data.applied_tags ?? this.appliedTags } readFromData(data: ThreadChannelPayload): this { diff --git a/src/types/channel.ts b/src/types/channel.ts index eb7620c4..1732cacc 100644 --- a/src/types/channel.ts +++ b/src/types/channel.ts @@ -11,10 +11,13 @@ import type { MessageComponentData, MessageComponentPayload } from './messageComponents.ts' +import type { Emoji } from '../structures/emoji.ts' +import type { GuildForumTag } from '../structures/guildForumChannel.ts' export interface ChannelPayload { id: string type: ChannelTypes + flags: number } export interface TextChannelPayload extends ChannelPayload { @@ -33,8 +36,19 @@ export interface GuildChannelPayload extends ChannelPayload { export interface GuildTextBasedChannelPayload extends TextChannelPayload, - GuildChannelPayload { + GuildChannelPayload {} + +export interface GuildThreadAvailableChannelPayload + extends GuildChannelPayload { topic?: string + rate_limit_per_user: number + default_thread_rate_limit_per_user?: number + default_auto_archive_duration?: number +} + +export enum ChannelFlags { + PINNED = 1 << 1, + REQUIRE_TAG = 1 << 4 } export interface ThreadChannelPayload @@ -46,13 +60,17 @@ export interface ThreadChannelPayload thread_metadata: ThreadMetadataPayload rate_limit_per_user: number owner_id: string + total_message_sent: number + applied_tags?: string[] } -export interface GuildTextChannelPayload extends GuildTextBasedChannelPayload { - rate_limit_per_user: number -} +export interface GuildTextChannelPayload + extends GuildTextBasedChannelPayload, + GuildThreadAvailableChannelPayload {} -export interface GuildNewsChannelPayload extends GuildTextBasedChannelPayload {} +export interface GuildNewsChannelPayload + extends GuildTextBasedChannelPayload, + GuildThreadAvailableChannelPayload {} export interface GuildVoiceChannelPayload extends GuildChannelPayload { bitrate: string @@ -63,6 +81,31 @@ export interface GuildVoiceChannelPayload extends GuildChannelPayload { export interface GuildStageChannelPayload extends Omit {} +export interface GuildForumTagPayload { + id: string + name: string + moderated: boolean + emoji_id: string + emoji_name: string | null +} + +export interface GuildForumDefaultReactionPayload { + emoji_id: string | null + emoji_name: string | null +} + +export enum GuildForumSortOrderTypes { + LATEST_ACTIVITY = 0, + CREATION_DATE = 1 +} + +export interface GuildForumChannelPayload + extends GuildThreadAvailableChannelPayload { + available_tags: GuildForumTagPayload[] + default_reaction_emoji: GuildForumDefaultReactionPayload | null + default_sort_order: GuildForumSortOrderTypes | null +} + export interface DMChannelPayload extends TextChannelPayload { recipients: UserPayload[] } @@ -91,14 +134,20 @@ export interface ModifyGuildCategoryChannelPayload export interface ModifyGuildTextBasedChannelPayload extends ModifyChannelPayload { type?: number - topic?: string | null } -export interface ModifyGuildTextChannelPayload - extends ModifyGuildTextBasedChannelPayload { +export interface ModifyGuildThreadAvailableChannelPayload + extends ModifyChannelPayload { + topic?: string | null rate_limit_per_user?: number | null + default_thread_rate_limit_per_user?: number | null + default_auto_archive_duration?: number | null } +export interface ModifyGuildTextChannelPayload + extends ModifyGuildTextBasedChannelPayload, + ModifyGuildThreadAvailableChannelPayload {} + export interface ModifyThreadChannelPayload extends ModifyGuildTextBasedChannelPayload { archived?: boolean @@ -107,13 +156,21 @@ export interface ModifyThreadChannelPayload } export interface ModifyGuildNewsChannelPayload - extends ModifyGuildTextBasedChannelPayload {} + extends ModifyGuildTextBasedChannelPayload, + ModifyGuildThreadAvailableChannelPayload {} export interface ModifyVoiceChannelPayload extends ModifyChannelPayload { bitrate?: number | null user_limit?: number | null } +export interface ModifyGuildForumChannelPayload + extends ModifyGuildThreadAvailableChannelPayload { + default_reaction_emoji?: GuildForumDefaultReactionPayload | null + default_sort_order?: GuildForumSortOrderTypes | null + available_tags?: GuildForumTagPayload[] | null +} + export interface ModifyChannelOption { name?: string position?: number | null @@ -126,14 +183,20 @@ export interface ModifyGuildCategoryChannelOption extends ModifyChannelOption {} export interface ModifyGuildTextBasedChannelOption extends ModifyChannelOption { type?: number - topic?: string | null } -export interface ModifyGuildTextChannelOption - extends ModifyGuildTextBasedChannelOption { +export interface ModifyGuildThreadAvailableChannelOption + extends ModifyChannelOption { + topic?: string | null slowmode?: number | null + defaultThreadSlowmode?: number | null + defaultAutoArchiveDuration?: number | null } +export interface ModifyGuildTextChannelOption + extends ModifyGuildTextBasedChannelOption, + ModifyGuildThreadAvailableChannelOption {} + export interface ModifyThreadChannelOption extends ModifyGuildTextChannelOption { archived?: boolean @@ -142,13 +205,21 @@ export interface ModifyThreadChannelOption } export interface ModifyGuildNewsChannelOption - extends ModifyGuildTextBasedChannelOption {} + extends ModifyGuildTextBasedChannelOption, + ModifyGuildThreadAvailableChannelOption {} export interface ModifyVoiceChannelOption extends ModifyChannelOption { bitrate?: number | null userLimit?: number | null } +export interface ModifyGuildForumChannelOption + extends ModifyGuildThreadAvailableChannelOption { + defaultReactionEmoji?: Emoji | GuildForumDefaultReactionPayload | null + defaultSortOrder?: GuildForumSortOrderTypes | null + availableTags?: GuildForumTag[] | GuildForumTagPayload[] | null +} + export enum OverwriteType { ROLE = 0, USER = 1 @@ -194,7 +265,9 @@ export enum ChannelTypes { NEWS_THREAD = 10, PUBLIC_THREAD = 11, PRIVATE_THREAD = 12, - GUILD_STAGE_VOICE = 13 + GUILD_STAGE_VOICE = 13, + GUILD_DIRECTORY = 14, + GUILD_FORUM = 15 } export interface MessagePayload { @@ -530,5 +603,18 @@ export interface CreateThreadPayload { /** 2-100 character channel name */ name: string /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ - auto_archive_duration: number + auto_archive_duration?: number + rate_limit_per_user?: number | null + type?: ChannelTypes + invitable?: boolean +} + +export interface CreateThreadInForumPayload { + /** 2-100 character channel name */ + name: string + /** duration in minutes to automatically archive the thread after recent activity, can be set to: 60, 1440, 4320, 10080 */ + auto_archive_duration?: number + rate_limit_per_user?: number | null + message: CreateMessagePayload + applied_tags?: string[] } diff --git a/src/types/guild.ts b/src/types/guild.ts index 6d5314cb..c3943993 100644 --- a/src/types/guild.ts +++ b/src/types/guild.ts @@ -8,6 +8,7 @@ import type { GuildTextChannel, GuildTextBasedChannel } from '../structures/guildTextChannel.ts' +import type { GuildForumChannel } from '../structures/guildForumChannel.ts' import type { ApplicationPayload } from './application.ts' import type { ChannelPayload, @@ -18,7 +19,8 @@ import type { GuildTextChannelPayload, GuildVoiceChannelPayload, ThreadChannelPayload, - MessageStickerPayload + MessageStickerPayload, + GuildForumChannelPayload } from './channel.ts' import type { EmojiPayload } from './emoji.ts' import type { PresenceUpdatePayload } from './gateway.ts' @@ -190,6 +192,11 @@ export interface GuildWidgetPayload { presence_count: number } +export type GuildThreadAvailableChannelPayloads = + | GuildTextChannelPayload + | GuildNewsChannelPayload + | GuildForumChannelPayload + export type GuildTextBasedPayloads = | GuildTextBasedChannelPayload | GuildTextChannelPayload @@ -197,11 +204,18 @@ export type GuildTextBasedPayloads = export type GuildChannelPayloads = | GuildTextBasedPayloads + | GuildThreadAvailableChannelPayloads | GuildVoiceChannelPayload | GuildCategoryChannelPayload +export type GuildThreadAvailableChannels = + | GuildTextChannel + | NewsChannel + | GuildForumChannel + export type GuildTextBasedChannels = | GuildTextBasedChannel + | GuildThreadAvailableChannels | GuildTextChannel | NewsChannel diff --git a/src/utils/channel.ts b/src/utils/channel.ts index 7caeef8d..1830463f 100644 --- a/src/utils/channel.ts +++ b/src/utils/channel.ts @@ -5,6 +5,7 @@ import { DMChannelPayload, GroupDMChannelPayload, GuildCategoryChannelPayload, + GuildForumChannelPayload, GuildNewsChannelPayload, GuildStageChannelPayload, GuildTextBasedChannelPayload, @@ -28,6 +29,7 @@ import { Channel, GuildChannel } from '../structures/channel.ts' import { StoreChannel } from '../structures/guildStoreChannel.ts' import { StageVoiceChannel } from '../structures/guildStageVoiceChannel.ts' import { ThreadChannel } from '../structures/threadChannel.ts' +import { GuildForumChannel } from '../structures/guildForumChannel.ts' export type EveryTextChannelTypes = | TextChannel @@ -38,6 +40,16 @@ export type EveryTextChannelTypes = | GroupDMChannel | ThreadChannel +export type EveryGuildThreadAvailableChannelTypes = + | GuildTextChannel + | NewsChannel + | GuildForumChannel + +export type EveryGuildThreadAvailableChannelPayloadTypes = + | GuildTextChannelPayload + | GuildNewsChannelPayload + | GuildForumChannelPayload + export type EveryTextChannelPayloadTypes = | TextChannelPayload | GuildNewsChannelPayload @@ -54,6 +66,7 @@ export type EveryChannelTypes = | VoiceChannel | StageVoiceChannel | EveryTextChannelTypes + | EveryGuildThreadAvailableChannelTypes export type EveryChannelPayloadTypes = | ChannelPayload @@ -61,6 +74,7 @@ export type EveryChannelPayloadTypes = | GuildVoiceChannelPayload | GuildStageChannelPayload | EveryTextChannelPayloadTypes + | EveryGuildThreadAvailableChannelPayloadTypes /** Get appropriate Channel structure by its type */ const getChannelByType = ( @@ -115,6 +129,14 @@ const getChannelByType = ( if (guild === undefined) throw new Error('No Guild was provided to construct Channel') return new ThreadChannel(client, data as ThreadChannelPayload, guild) + case ChannelTypes.GUILD_FORUM: + if (guild === undefined) + throw new Error('No Guild was provided to construct Channel') + return new GuildForumChannel( + client, + data as GuildForumChannelPayload, + guild + ) } } diff --git a/src/utils/channelTypes.ts b/src/utils/channelTypes.ts index 0b0ce30e..17d3b064 100644 --- a/src/utils/channelTypes.ts +++ b/src/utils/channelTypes.ts @@ -8,6 +8,7 @@ import type { GuildTextBasedChannel, GuildTextChannel } from '../structures/guildTextChannel.ts' +import { GuildThreadAvailableChannel } from '../structures/guildThreadAvailableChannel.ts' import type { VoiceChannel } from '../structures/guildVoiceChannel.ts' import type { StageVoiceChannel } from '../structures/guildVoiceStageChannel.ts' import type { TextChannel } from '../structures/textChannel.ts' @@ -74,7 +75,8 @@ export function isGuildChannel(channel: Channel): channel is GuildChannel { channel.type === ChannelTypes.GUILD_STAGE_VOICE || channel.type === ChannelTypes.NEWS_THREAD || channel.type === ChannelTypes.PRIVATE_THREAD || - channel.type === ChannelTypes.PUBLIC_THREAD + channel.type === ChannelTypes.PUBLIC_THREAD || + channel.type === ChannelTypes.GUILD_FORUM ) } @@ -97,3 +99,13 @@ export function isTextChannel(channel: Channel): channel is TextChannel { channel.type === ChannelTypes.PUBLIC_THREAD ) } + +export function isThreadAvailableChannel( + channel: Channel +): channel is GuildThreadAvailableChannel { + return ( + channel.type === ChannelTypes.GUILD_TEXT || + channel.type === ChannelTypes.GUILD_NEWS || + channel.type === ChannelTypes.GUILD_FORUM + ) +} diff --git a/src/utils/permissions.ts b/src/utils/permissions.ts index 91df3b6e..b4514b19 100644 --- a/src/utils/permissions.ts +++ b/src/utils/permissions.ts @@ -18,6 +18,7 @@ export class Permissions extends BitField { any(permission: PermissionResolvable, checkAdmin = true): boolean { return ( + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.any(permission) ) @@ -25,6 +26,7 @@ export class Permissions extends BitField { has(permission: PermissionResolvable, checkAdmin = true): boolean { return ( + // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions (checkAdmin && super.has(this.flags().ADMINISTRATOR)) || super.has(permission) ) diff --git a/test/forum.ts b/test/forum.ts new file mode 100644 index 00000000..86a6f50e --- /dev/null +++ b/test/forum.ts @@ -0,0 +1,28 @@ +import { Client, GuildForumChannel, Intents } from '../mod.ts' +// import type { } from '../mod.ts' +import { TOKEN } from './config.ts' + +const client = new Client() + +client.on('ready', async () => { + const guild = await client.guilds.resolve('GUILD_ID') + if (guild !== undefined) { + console.log('found guild') + const channel = await guild.channels.resolve('CHANNEL_ID') + if (channel !== undefined && channel instanceof GuildForumChannel) { + console.log('found channel') + const threads = await channel.threads.array() + console.log(threads) + const thread = await channel.startThread({ + name: 'also test', + autoArchiveDuration: 60, + message: { + content: 'test' + } + }) + thread.send('it works') + } + } +}) + +client.connect(TOKEN, Intents.NonPrivileged) diff --git a/test/index.ts b/test/index.ts index d2ab921e..e4154f0d 100644 --- a/test/index.ts +++ b/test/index.ts @@ -8,12 +8,12 @@ import { EveryChannelTypes, ChannelTypes, GuildTextChannel, - checkGuildTextBasedChannel, Permissions, Collector, MessageAttachment, OverrideType, - ColorUtil + ColorUtil, + isGuildBasedTextChannel } from '../mod.ts' // import { TOKEN } from './config.ts' @@ -219,7 +219,7 @@ client.on('messageCreate', async (msg: Message) => { vs.channel?.join() } else if (msg.content === '!getOverwrites') { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!checkGuildTextBasedChannel(msg.channel)) { + if (!isGuildBasedTextChannel(msg.channel)) { return msg.channel.send("This isn't a guild text channel!") } // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion @@ -251,7 +251,7 @@ client.on('messageCreate', async (msg: Message) => { msg.channel.send(`Your permissions:\n${permissions.toArray().join('\n')}`) } else if (msg.content === '!addBasicOverwrites') { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!checkGuildTextBasedChannel(msg.channel)) { + if (!isGuildBasedTextChannel(msg.channel)) { return msg.channel.send("This isn't a guild text channel!") } if (msg.member !== undefined) { @@ -263,7 +263,7 @@ client.on('messageCreate', async (msg: Message) => { } } else if (msg.content === '!updateBasicOverwrites') { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (!checkGuildTextBasedChannel(msg.channel)) { + if (!isGuildBasedTextChannel(msg.channel)) { return msg.channel.send("This isn't a guild text channel!") } if (msg.member !== undefined) {