From 773c0a38c77370e0b1f82bfccc3846b0a0d38449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcos=20Susa=C3=B1a?= Date: Tue, 16 Apr 2024 15:07:11 -0400 Subject: [PATCH] feat: thread methods and shorter (#184) * feat: threads methods * fix: typing * fix: fixes --- src/client/base.ts | 2 + src/common/index.ts | 1 + src/common/shorters/channels.ts | 30 +++----- src/common/shorters/guilds.ts | 9 +++ src/common/shorters/messages.ts | 12 +-- src/common/shorters/threads.ts | 125 ++++++++++++++++++++++++++++++++ src/structures/channels.ts | 19 +++-- 7 files changed, 163 insertions(+), 35 deletions(-) create mode 100644 src/common/shorters/threads.ts diff --git a/src/client/base.ts b/src/client/base.ts index 8ef0fc7..f7efb78 100644 --- a/src/client/base.ts +++ b/src/client/base.ts @@ -17,6 +17,7 @@ import { ReactionShorter, RoleShorter, TemplateShorter, + ThreadShorter, UsersShorter, WebhookShorter, filterSplit, @@ -50,6 +51,7 @@ export class BaseClient { roles = new RoleShorter(this); reactions = new ReactionShorter(this); emojis = new EmojiShorter(this); + threads = new ThreadShorter(this); debugger?: Logger; diff --git a/src/common/index.ts b/src/common/index.ts index 6d1e0bc..229e420 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -12,6 +12,7 @@ export * from './shorters/reactions'; export * from './shorters/roles'; export * from './shorters/templates'; export * from './shorters/users'; +export * from './shorters/threads'; export * from './shorters/webhook'; export * from './types/options'; export * from './types/resolvables'; diff --git a/src/common/shorters/channels.ts b/src/common/shorters/channels.ts index 0214119..446d926 100644 --- a/src/common/shorters/channels.ts +++ b/src/common/shorters/channels.ts @@ -5,9 +5,10 @@ import { type RESTPostAPIGuildForumThreadsJSONBody, } from 'discord-api-types/v10'; import { BaseChannel, Message, type GuildMember, type GuildRole } from '../../structures'; -import channelFrom, { type AllChannels, type ThreadChannel } from '../../structures/channels'; +import channelFrom, { type AllChannels } from '../../structures/channels'; import { PermissionsBitField } from '../../structures/extra/Permissions'; import { BaseShorter } from './base'; +import { MergeOptions } from '../it/utils'; export class ChannelShorter extends BaseShorter { /** @@ -35,8 +36,9 @@ export class ChannelShorter extends BaseShorter { * @returns A Promise that resolves to the deleted channel. */ async delete(id: string, optional: ChannelShorterOptionalParams = { guildId: '@me' }): Promise { - const res = await this.client.proxy.channels(id).delete({ reason: optional.reason }); - await this.client.cache.channels?.removeIfNI(BaseChannel.__intent__(optional.guildId!), res.id, optional.guildId!); + const options = MergeOptions({ guildId: '@me' }, optional); + const res = await this.client.proxy.channels(id).delete({ reason: options.reason }); + await this.client.cache.channels?.removeIfNI(BaseChannel.__intent__(options.guildId!), res.id, options.guildId!); return channelFrom(res, this.client); } @@ -52,18 +54,14 @@ export class ChannelShorter extends BaseShorter { body: RESTPatchAPIChannelJSONBody, optional: ChannelShorterOptionalParams = { guildId: '@me' }, ): Promise { - const res = await this.client.proxy.channels(id).patch({ body, reason: optional.reason }); - await this.client.cache.channels?.setIfNI( - BaseChannel.__intent__(optional.guildId!), - res.id, - optional.guildId!, - res, - ); + const options = MergeOptions({ guildId: '@me' }, optional); + const res = await this.client.proxy.channels(id).patch({ body, reason: options.reason }); + await this.client.cache.channels?.setIfNI(BaseChannel.__intent__(options.guildId!), res.id, options.guildId!, res); if (body.permission_overwrites && 'permission_overwrites' in res) await this.client.cache.overwrites?.setIfNI( - BaseChannel.__intent__(optional.guildId!), + BaseChannel.__intent__(options.guildId!), res.id, - optional.guildId!, + options.guildId!, res.permission_overwrites, ); return channelFrom(res, this.client); @@ -116,13 +114,7 @@ export class ChannelShorter extends BaseShorter { body: RESTPostAPIChannelThreadsJSONBody | RESTPostAPIGuildForumThreadsJSONBody, reason?: string, ) { - return ( - this.client.proxy - .channels(channelId) - .threads.post({ body, reason }) - // When testing this, discord returns the thread object, but in discord api types it does not. - .then(thread => channelFrom(thread, this.client) as ThreadChannel) - ); + return this.client.threads.create(channelId, body, reason); } async memberPermissions(channelId: string, member: GuildMember, checkAdmin = true): Promise { diff --git a/src/common/shorters/guilds.ts b/src/common/shorters/guilds.ts index 8a99050..2afcc70 100644 --- a/src/common/shorters/guilds.ts +++ b/src/common/shorters/guilds.ts @@ -185,6 +185,15 @@ export class GuildShorter extends BaseShorter { */ editPositions: (guildId: string, body: RESTPatchAPIGuildChannelPositionsJSONBody) => this.client.proxy.guilds(guildId).channels.patch({ body }), + + addFollower: async (channelId: string, webhook_channel_id: string, reason?: string) => { + return this.client.proxy.channels(channelId).followers.post({ + body: { + webhook_channel_id, + }, + reason, + }); + }, }; } diff --git a/src/common/shorters/messages.ts b/src/common/shorters/messages.ts index 486a82d..993b777 100644 --- a/src/common/shorters/messages.ts +++ b/src/common/shorters/messages.ts @@ -4,8 +4,8 @@ import type { RESTPostAPIChannelMessagesThreadsJSONBody, } from 'discord-api-types/v10'; import { resolveFiles } from '../../builders'; -import { Message, MessagesMethods, type ThreadChannel } from '../../structures'; -import channelFrom from '../../structures/channels'; +import { Message, MessagesMethods } from '../../structures'; + import type { MessageCreateBodyRequest, MessageUpdateBodyRequest } from '../types/write'; import { BaseShorter } from './base'; @@ -70,12 +70,6 @@ export class MessageShorter extends BaseShorter { messageId: string, options: RESTPostAPIChannelMessagesThreadsJSONBody & { reason?: string }, ) { - const { reason, ...body } = options; - - return this.client.proxy - .channels(channelId) - .messages(messageId) - .threads.post({ body, reason }) - .then(thread => channelFrom(thread, this.client) as ThreadChannel); + return this.client.threads.fromMessage(channelId, messageId, options); } } diff --git a/src/common/shorters/threads.ts b/src/common/shorters/threads.ts new file mode 100644 index 0000000..2df9e67 --- /dev/null +++ b/src/common/shorters/threads.ts @@ -0,0 +1,125 @@ +import type { + APIThreadMember, + RESTGetAPIChannelThreadMembersQuery, + RESTGetAPIChannelThreadsArchivedQuery, + RESTPatchAPIChannelJSONBody, + RESTPostAPIChannelMessagesThreadsJSONBody, + RESTPostAPIChannelThreadsJSONBody, + RESTPostAPIGuildForumThreadsJSONBody, +} from 'discord-api-types/v10'; +import type { ThreadChannel } from '../../structures'; +import channelFrom from '../../structures/channels'; +import { BaseShorter } from './base'; +import type { MakeRequired, When } from '../types/util'; + +export class ThreadShorter extends BaseShorter { + /** + * Creates a new thread in the channel (only guild based channels). + * @param channelId The ID of the parent channel. + * @param reason The reason for unpinning the message. + * @returns A promise that resolves when the thread is succesfully created. + */ + async create( + channelId: string, + body: RESTPostAPIChannelThreadsJSONBody | RESTPostAPIGuildForumThreadsJSONBody, + reason?: string, + ) { + return ( + this.client.proxy + .channels(channelId) + .threads.post({ body, reason }) + // When testing this, discord returns the thread object, but in discord api types it does not. + .then(thread => channelFrom(thread, this.client) as ThreadChannel) + ); + } + + async fromMessage( + channelId: string, + messageId: string, + options: RESTPostAPIChannelMessagesThreadsJSONBody & { reason?: string }, + ) { + const { reason, ...body } = options; + + return this.client.proxy + .channels(channelId) + .messages(messageId) + .threads.post({ body, reason }) + .then(thread => channelFrom(thread, this.client) as ThreadChannel); + } + + async join(threadId: string) { + return this.client.proxy.channels(threadId)['thread-members']('@me').put(); + } + + async leave(threadId: string) { + return this.client.proxy.channels(threadId)['thread-members']('@me').delete(); + } + + async lock(threadId: string, locked = true, reason?: string) { + return this.edit(threadId, { locked }, reason).then(x => channelFrom(x, this.client) as ThreadChannel); + } + + async edit(threadId: string, body: RESTPatchAPIChannelJSONBody, reason?: string) { + return this.client.channels.edit(threadId, body, { reason }); + } + + async removeMember(threadId: string, memberId: string) { + return this.client.proxy.channels(threadId)['thread-members'](memberId).delete(); + } + + async fetchMember( + threadId: string, + memberId: string, + with_member: WithMember, + ): Promise, GetAPIChannelThreadMemberResult>> { + return this.client.proxy.channels(threadId)['thread-members'](memberId).get({ + query: { + with_member, + }, + }) as never; + } + + async addMember(threadId: string, memberId: string) { + return this.client.proxy.channels(threadId)['thread-members'](memberId).put(); + } + + async listMembers( + threadId: string, + query?: T, + ): Promise> { + return this.client.proxy.channels(threadId)['thread-members'].get({ query }) as never; + } + + async listArchivedThreads( + channelId: string, + type: 'public' | 'private', + query?: RESTGetAPIChannelThreadsArchivedQuery, + ) { + const data = await this.client.proxy.channels(channelId).threads.archived[type].get({ query }); + + return { + threads: data.threads.map(thread => channelFrom(thread, this.client) as ThreadChannel), + members: data.members as GetAPIChannelThreadMemberResult[], + hasMore: data.has_more, + }; + } + + async listJoinedArchivedPrivate(channelId: string, query?: RESTGetAPIChannelThreadsArchivedQuery) { + const data = await this.client.proxy.channels(channelId).users('@me').threads.archived.private.get({ query }); + return { + threads: data.threads.map(thread => channelFrom(thread, this.client) as ThreadChannel), + members: data.members as GetAPIChannelThreadMemberResult[], + hasMore: data.has_more, + }; + } +} + +export type GetAPIChannelThreadMemberResult = MakeRequired; + +type InferWithMemberOnList = T extends { + with_member: infer B; +} + ? B extends true + ? Required[] + : GetAPIChannelThreadMemberResult[] + : GetAPIChannelThreadMemberResult[]; diff --git a/src/structures/channels.ts b/src/structures/channels.ts index 9c6be70..70c4873 100644 --- a/src/structures/channels.ts +++ b/src/structures/channels.ts @@ -455,6 +455,16 @@ export class ThreadChannel extends BaseChannel< channelId: this.parentId!, }); + async join() { + await this.client.threads.join(this.id); + return this; + } + + async leave() { + await this.client.threads.leave(this.id); + return this; + } + setRatelimitPerUser(rate_limit_per_user: number | null | undefined) { return this.edit({ rate_limit_per_user }); } @@ -506,13 +516,8 @@ export interface NewsChannel extends ObjectToLower, WebhookChann export class NewsChannel extends BaseChannel { declare type: ChannelType.GuildAnnouncement; - addFollower(webhook_channel_id: string, reason?: string) { - return this.api.channels(this.id).followers.post({ - body: { - webhook_channel_id, - }, - reason, - }); + addFollower(webhookChannelId: string, reason?: string) { + return this.client.guilds.channels.addFollower(this.id, webhookChannelId, reason); } }