From a0074f0c4ea2ac2118766e3e1d08f15e3ca818aa Mon Sep 17 00:00:00 2001 From: Kodie Date: Thu, 19 Sep 2024 08:58:50 +1200 Subject: [PATCH] refactor: add partial type parameters to structures (#116) * refactor: add partial type parameters to structures * fix: correct some getter return types * chore: add changeset --- .changeset/tidy-chicken-clean.md | 5 + packages/carbon/src/abstracts/BaseChannel.ts | 87 +++-- .../carbon/src/abstracts/BaseGuildChannel.ts | 65 ++-- .../src/abstracts/BaseGuildTextChannel.ts | 37 ++- .../carbon/src/abstracts/BaseInteraction.ts | 4 +- .../src/abstracts/GuildThreadOnlyChannel.ts | 68 ++-- packages/carbon/src/classes/Client.ts | 2 +- .../carbon/src/internals/OptionsHandler.ts | 31 +- packages/carbon/src/structures/DmChannel.ts | 18 +- .../carbon/src/structures/GroupDmChannel.ts | 132 ++++---- packages/carbon/src/structures/Guild.ts | 145 +++++---- .../structures/GuildAnnouncementChannel.ts | 13 +- .../src/structures/GuildCategoryChannel.ts | 11 +- .../src/structures/GuildForumChannel.ts | 14 +- .../src/structures/GuildMediaChannel.ts | 10 +- packages/carbon/src/structures/GuildMember.ts | 167 ++++++---- .../structures/GuildStageOrVoiceChannel.ts | 59 ++-- .../carbon/src/structures/GuildTextChannel.ts | 26 +- .../src/structures/GuildThreadChannel.ts | 100 ++++-- packages/carbon/src/structures/Message.ts | 308 +++++++++++------- packages/carbon/src/structures/Role.ts | 172 ++++++---- packages/carbon/src/structures/User.ts | 164 ++++++---- packages/carbon/src/utils.ts | 2 + 23 files changed, 1004 insertions(+), 636 deletions(-) create mode 100644 .changeset/tidy-chicken-clean.md diff --git a/.changeset/tidy-chicken-clean.md b/.changeset/tidy-chicken-clean.md new file mode 100644 index 00000000..2b8f1c51 --- /dev/null +++ b/.changeset/tidy-chicken-clean.md @@ -0,0 +1,5 @@ +--- +"@buape/carbon": patch +--- + +refactor: add partial type parameters to structures to improve field types diff --git a/packages/carbon/src/abstracts/BaseChannel.ts b/packages/carbon/src/abstracts/BaseChannel.ts index 897b48a5..7fa58700 100644 --- a/packages/carbon/src/abstracts/BaseChannel.ts +++ b/packages/carbon/src/abstracts/BaseChannel.ts @@ -1,62 +1,79 @@ import { type APIChannel, + type ChannelFlags, type ChannelType, Routes } from "discord-api-types/v10" import type { Client } from "../classes/Client.js" +import type { IfPartial } from "../utils.js" import { Base } from "./Base.js" -export abstract class BaseChannel extends Base { - /** - * The id of the channel. - */ - id: string - /** - * Whether the channel is a partial channel (meaning it does not have all the data). - * If this is true, you should use {@link BaseChannel.fetch} to get the full data of the channel. - */ - partial: boolean - /** - * The type of the channel. - */ - type?: Type - /** - * The flags of the channel in a bitfield. - * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-flags - */ - flags?: number | null - /** - * The raw data of the channel. - */ - protected rawData: Extract | null = null - +export abstract class BaseChannel< + Type extends ChannelType, + IsPartial extends boolean = false +> extends Base { constructor( client: Client, - rawDataOrId: Extract | string + rawDataOrId: IsPartial extends true + ? string + : Extract ) { super(client) if (typeof rawDataOrId === "string") { this.id = rawDataOrId - this.partial = true } else { - this.rawData = rawDataOrId + this.rawData = rawDataOrId as never this.id = rawDataOrId.id - this.partial = false - this.setData(rawDataOrId) + this.setData(rawDataOrId as never) } } + /** + * The raw data of the channel. + */ + protected rawData: Extract | null = null protected setData(data: Extract) { if (!data) throw new Error("Cannot set data without having data... smh") this.rawData = data - this.type = data.type - this.partial = false - this.setSpecificData(data) + } + protected setField( + field: keyof Extract, + value: unknown + ) { + if (!this.rawData) + throw new Error("Cannot set field without having data... smh") + this.rawData[field] = value + } + + /** + * The id of the channel. + */ + readonly id: string + + /** + * Whether the channel is a partial channel (meaning it does not have all the data). + * If this is true, you should use {@link BaseChannel.fetch} to get the full data of the channel. + */ + get partial(): IsPartial { + return (this.rawData === null) as never + } + + /** + * The type of the channel. + */ + get type(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.type } - protected abstract setSpecificData( - data: Extract - ): void + /** + * The flags of the channel in a bitfield. + * @see https://discord.com/developers/docs/resources/channel#channel-object-channel-flags + */ + get flags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.flags + } /** * Fetches the channel from the API. diff --git a/packages/carbon/src/abstracts/BaseGuildChannel.ts b/packages/carbon/src/abstracts/BaseGuildChannel.ts index b9f77ea3..262da766 100644 --- a/packages/carbon/src/abstracts/BaseGuildChannel.ts +++ b/packages/carbon/src/abstracts/BaseGuildChannel.ts @@ -1,5 +1,4 @@ import { - type APIChannel, type APIGuildChannel, type APIMessage, type GuildChannelType, @@ -10,49 +9,63 @@ import { } from "discord-api-types/v10" import { Guild } from "../structures/Guild.js" import type { GuildCategoryChannel } from "../structures/GuildCategoryChannel.js" +import type { IfPartial } from "../utils.js" import { BaseChannel } from "./BaseChannel.js" export abstract class BaseGuildChannel< - Type extends GuildChannelType -> extends BaseChannel { + Type extends GuildChannelType, + IsPartial extends boolean = false +> extends BaseChannel { + // @ts-expect-error + declare rawData: APIGuildChannel | null + /** * The name of the channel. */ - name?: string + get name(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.name as never + } + /** * The ID of the guild this channel is in */ - guildId?: string + get guildId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.guild_id as never + } + /** * The position of the channel in the channel list. */ - position?: number + get position(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.position + } + /** * The ID of the parent category for the channel. */ - parentId?: string | null + get parentId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.parent_id ?? null + } + /** * Whether the channel is marked as nsfw. */ - nsfw?: boolean + get nsfw(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.nsfw ?? false + } /** * The guild this channel is in */ - get guild() { + get guild(): IfPartial> { + if (!this.rawData) return undefined as never if (!this.guildId) throw new Error("Cannot get guild without guild ID") - return new Guild(this.client, this.guildId) - } - - protected override setData(data: APIGuildChannel): void { - this.rawData = data as Extract | null - this.partial = false - this.name = data.name - this.guildId = data.guild_id - this.position = data.position - this.parentId = data.parent_id - this.nsfw = data.nsfw - this.setSpecificData(data as Extract) + return new Guild(this.client, this.guildId) } /** @@ -65,7 +78,7 @@ export abstract class BaseGuildChannel< name } }) - this.name = name + this.setField("name", name) } /** @@ -78,7 +91,7 @@ export abstract class BaseGuildChannel< position } }) - this.position = position + this.setField("position", position) } /** @@ -92,14 +105,14 @@ export abstract class BaseGuildChannel< parent_id: parent } }) - this.parentId = parent + this.setField("parent_id", parent) } else { await this.client.rest.patch(Routes.channel(this.id), { body: { parent_id: parent.id } }) - this.parentId = parent.id + this.setField("parent_id", parent.id) } } @@ -113,7 +126,7 @@ export abstract class BaseGuildChannel< nsfw } }) - this.nsfw = nsfw + this.setField("nsfw", nsfw) } /** diff --git a/packages/carbon/src/abstracts/BaseGuildTextChannel.ts b/packages/carbon/src/abstracts/BaseGuildTextChannel.ts index 62692426..7e43e251 100644 --- a/packages/carbon/src/abstracts/BaseGuildTextChannel.ts +++ b/packages/carbon/src/abstracts/BaseGuildTextChannel.ts @@ -8,36 +8,40 @@ import { } from "discord-api-types/v10" import { GuildThreadChannel } from "../structures/GuildThreadChannel.js" import { Message } from "../structures/Message.js" +import type { IfPartial } from "../utils.js" import { BaseGuildChannel } from "./BaseGuildChannel.js" export abstract class BaseGuildTextChannel< - Type extends GuildTextChannelType -> extends BaseGuildChannel { + Type extends GuildTextChannelType, + IsPartial extends boolean = false +> extends BaseGuildChannel { + declare rawData: APIGuildTextChannel | null + /** * The ID of the last message sent in the channel. * * @remarks * This might not always resolve to a message. The ID still stays a part of the channel's data, even if the message is deleted. */ - lastMessageId?: string | null + get lastMessageId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.last_message_id ?? null + } /** * The timestamp of the last pin in the channel. */ - lastPinTimestamp?: string | null + get lastPinTimestamp(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.last_pin_timestamp ?? null + } /** * The rate limit per user for the channel, in seconds. */ - rateLimitPerUser?: number | null - - protected setSpecificData(data: APIGuildTextChannel): void { - this.lastMessageId = data.last_message_id - this.lastPinTimestamp = data.last_pin_timestamp - this.rateLimitPerUser = data.rate_limit_per_user - this.setMoreSpecificData(data) + get rateLimitPerUser(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.rate_limit_per_user } - protected abstract setMoreSpecificData(data: APIGuildTextChannel): void - /** * The last message sent in the channel. * @@ -46,11 +50,12 @@ export abstract class BaseGuildTextChannel< * This will always return a partial message, so you can use {@link Message.fetch} to get the full message data. * */ - get lastMessage() { + get lastMessage(): IfPartial | null> { + if (!this.rawData) return undefined as never if (!this.lastMessageId) return null - return new Message(this.client, { + return new Message(this.client, { id: this.lastMessageId, - channel_id: this.id + channelId: this.id }) } diff --git a/packages/carbon/src/abstracts/BaseInteraction.ts b/packages/carbon/src/abstracts/BaseInteraction.ts index 482afd16..6963e5dc 100644 --- a/packages/carbon/src/abstracts/BaseInteraction.ts +++ b/packages/carbon/src/abstracts/BaseInteraction.ts @@ -120,9 +120,9 @@ export abstract class BaseInteraction extends Base { return new Message(this.client, this.rawData.message) } - get guild(): Guild | null { + get guild(): Guild | null { if (!this.rawData.guild_id) return null - return new Guild(this.client, this.rawData.guild_id) + return new Guild(this.client, this.rawData.guild_id) } get user(): User | null { diff --git a/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts b/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts index 9ef4da47..b51a02bd 100644 --- a/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts +++ b/packages/carbon/src/abstracts/GuildThreadOnlyChannel.ts @@ -4,51 +4,70 @@ import type { APIMessage, APIThreadOnlyChannel, ChannelType, - SortOrderType + SortOrderType, + ThreadChannelType } from "discord-api-types/v10" import { GuildThreadChannel } from "../structures/GuildThreadChannel.js" +import type { IfPartial } from "../utils.js" import { BaseGuildChannel } from "./BaseGuildChannel.js" export abstract class GuildThreadOnlyChannel< - Type extends ChannelType.GuildForum | ChannelType.GuildMedia -> extends BaseGuildChannel { + Type extends ChannelType.GuildForum | ChannelType.GuildMedia, + IsPartial extends boolean = false +> extends BaseGuildChannel { + declare rawData: APIThreadOnlyChannel | null + /** * The topic of the channel. */ - topic?: string | null + get topic(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.topic ?? null + } + /** * The default auto archive duration of the channel. */ - defaultAutoArchiveDuration?: number | null + get defaultAutoArchiveDuration(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.default_auto_archive_duration ?? null + } + /** - * The available tags to set on posts in the channel. + * The default thread rate limit per user for the channel. */ - availableTags?: APIGuildForumTag[] + get defaultThreadRateLimitPerUser(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.default_thread_rate_limit_per_user ?? null + } + /** - * The default thread rate limit per user for the channel. + * The available tags to set on posts in the channel. */ - defaultThreadRateLimitPerUser?: number | null + get availableTags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.available_tags ?? [] + } + /** * The default reaction emoji for the channel. */ - defaultReactionEmoji?: APIGuildForumDefaultReactionEmoji | null + get defaultReactionEmoji(): IfPartial< + IsPartial, + APIGuildForumDefaultReactionEmoji | null + > { + if (!this.rawData) return undefined as never + return this.rawData.default_reaction_emoji + } + /** * The default sort order for the channel, by latest activity or by creation date. */ - defaultSortOrder?: SortOrderType | null - - protected setSpecificData(data: APIThreadOnlyChannel): void { - this.topic = data.topic - this.defaultAutoArchiveDuration = data.default_auto_archive_duration - this.availableTags = data.available_tags - this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user - this.defaultReactionEmoji = data.default_reaction_emoji - this.defaultSortOrder = data.default_sort_order - this.setMoreSpecificData(data) + get defaultSortOrder(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.default_sort_order } - protected abstract setMoreSpecificData(data: APIThreadOnlyChannel): void - /** * You cannot send a message directly to a forum or media channel, so this method throws an error. * Use {@link GuildThreadChannel.send} instead, or the alias {@link GuildThreadOnlyChannel.sendToPost} instead, to send a message to the channel's posts. @@ -65,7 +84,10 @@ export abstract class GuildThreadOnlyChannel< * This is an alias for {@link GuildThreadChannel.send} that will fetch the channel, but if you already have the channel, you can use {@link GuildThreadChannel.send} instead. */ async sendToPost(message: APIMessage, postId: string): Promise { - const channel = new GuildThreadChannel(this.client, postId) + const channel = new GuildThreadChannel( + this.client, + postId + ) await channel.send(message) } } diff --git a/packages/carbon/src/classes/Client.ts b/packages/carbon/src/classes/Client.ts index 4be57f84..2e4fb95f 100644 --- a/packages/carbon/src/classes/Client.ts +++ b/packages/carbon/src/classes/Client.ts @@ -364,7 +364,7 @@ export class Client { const member = (await this.rest.get( Routes.guildMember(guildId, id) )) as APIGuildMember - return new GuildMember(this, member, new Guild(this, guildId)) + return new GuildMember(this, member, new Guild(this, guildId)) } // ======================== End Fetchers ================================================ diff --git a/packages/carbon/src/internals/OptionsHandler.ts b/packages/carbon/src/internals/OptionsHandler.ts index c1b8008d..605a8695 100644 --- a/packages/carbon/src/internals/OptionsHandler.ts +++ b/packages/carbon/src/internals/OptionsHandler.ts @@ -136,9 +136,9 @@ export class OptionsHandler extends Base { * @param required Whether the option is required. * @returns The value of the option, or undefined if the option was not provided and it is not required. */ - public getUser(key: string, required?: false): User | undefined - public getUser(key: string, required: true): User - public getUser(key: string, required = false): User | undefined { + public getUser(key: string, required?: false): User | undefined + public getUser(key: string, required: true): User + public getUser(key: string, required = false): User | undefined { const id = this.raw.find( (x) => x.name === key && x.type === ApplicationCommandOptionType.User )?.value @@ -146,7 +146,7 @@ export class OptionsHandler extends Base { if (!id || typeof id !== "string") throw new Error(`Missing required option: ${key}`) } else if (!id || typeof id !== "string") return undefined - return new User(this.client, id) + return new User(this.client, id) } /** @@ -184,9 +184,9 @@ export class OptionsHandler extends Base { * @param required Whether the option is required. * @returns The value of the option, or undefined if the option was not provided and it is not required. */ - public getRole(key: string, required?: false): Role | undefined - public getRole(key: string, required: true): Role - public getRole(key: string, required = false): Role | undefined { + public getRole(key: string, required?: false): Role | undefined + public getRole(key: string, required: true): Role + public getRole(key: string, required = false): Role | undefined { const id = this.raw.find( (x) => x.name === key && x.type === ApplicationCommandOptionType.Role )?.value @@ -194,7 +194,7 @@ export class OptionsHandler extends Base { if (!id || typeof id !== "string") throw new Error(`Missing required option: ${key}`) } else if (!id || typeof id !== "string") return undefined - return new Role(this.client, id) + return new Role(this.client, id) } /** @@ -206,12 +206,15 @@ export class OptionsHandler extends Base { public async getMentionable( key: string, required?: false - ): Promise - public async getMentionable(key: string, required: true): Promise + ): Promise | undefined> + public async getMentionable( + key: string, + required: true + ): Promise> public async getMentionable( key: string, required = false - ): Promise { + ): Promise | undefined> { const id = this.raw.find( (x) => x.name === key && x.type === ApplicationCommandOptionType.Mentionable @@ -222,11 +225,11 @@ export class OptionsHandler extends Base { } else if (!id || typeof id !== "string") return undefined try { - const user = new User(this.client, id) + const user = new User(this.client, id) await user.fetch() - return user + return user as unknown as User } catch { - return new Role(this.client, id) + return new Role(this.client, id) } } } diff --git a/packages/carbon/src/structures/DmChannel.ts b/packages/carbon/src/structures/DmChannel.ts index 240b9a80..520f0166 100644 --- a/packages/carbon/src/structures/DmChannel.ts +++ b/packages/carbon/src/structures/DmChannel.ts @@ -1,23 +1,27 @@ import { type APIDMChannel, type APIMessage, - ChannelType, + type ChannelType, Routes } from "discord-api-types/v10" import { BaseChannel } from "../abstracts/BaseChannel.js" +import type { IfPartial } from "../utils.js" /** * Represents a DM between two users. */ -export class DmChannel extends BaseChannel { +export class DmChannel extends BaseChannel< + ChannelType.DM, + IsPartial +> { + declare rawData: APIDMChannel | null + /** * The name of the channel. This is always null for DM channels. */ - name?: null = null - type: ChannelType.DM = ChannelType.DM - - protected setSpecificData(data: APIDMChannel) { - this.name = data.name + get name(): IfPartial { + if (!this.rawData) return undefined as never + return null } /** diff --git a/packages/carbon/src/structures/GroupDmChannel.ts b/packages/carbon/src/structures/GroupDmChannel.ts index 5fc120ba..6f898405 100644 --- a/packages/carbon/src/structures/GroupDmChannel.ts +++ b/packages/carbon/src/structures/GroupDmChannel.ts @@ -1,74 +1,96 @@ import { type APIGroupDMChannel, - ChannelType, + type ChannelType, Routes } from "discord-api-types/v10" import { BaseChannel } from "../abstracts/BaseChannel.js" +import type { IfPartial } from "../utils.js" import { Message } from "./Message.js" import { User } from "./User.js" /** * Represents a group DM channel. */ -export class GroupDmChannel extends BaseChannel { +export class GroupDmChannel< + IsPartial extends boolean = false +> extends BaseChannel { + declare rawData: APIGroupDMChannel | null + /** * The name of the channel. */ - name?: string | null + get name(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.name + } + /** * The recipients of the channel. */ - recipients?: User[] - type: ChannelType.GroupDM = ChannelType.GroupDM + get recipients(): IfPartial[]> { + if (!this.rawData) return undefined as never + const recipients = this.rawData.recipients ?? [] + return recipients.map((u) => new User(this.client, u)) + } + /** * The ID of the application that created the channel, if it was created by a bot. */ - applicationId?: string | null + get applicationId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.application_id ?? null + } + /** * The icon hash of the channel. */ - icon?: string | null + get icon(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.icon ?? null + } + + /** + * Get the URL of the channel's icon. + */ + get iconUrl(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.icon) return null + return `https://cdn.discordapp.com/channel-icons/${this.id}/${this.icon}.png` + } + /** * The ID of the user who created the channel. */ - ownerId?: string | null + get ownerId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.owner_id ?? null + } + /** * The ID of the last message sent in the channel. * * @remarks * This might not always resolve to a message. The ID still stays a part of the channel's data, even if the message is deleted. */ - lastMessageId?: string | null - /** - * Whether the channel is managed by an Oauth2 application. - */ - managed?: boolean | null - - protected setSpecificData(data: APIGroupDMChannel) { - this.name = data.name - this.recipients = data.recipients?.map((x) => new User(this.client, x)) - this.applicationId = data.application_id - this.icon = data.icon - this.ownerId = data.owner_id - this.lastMessageId = data.last_message_id - this.managed = data.managed + get lastMessageId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.last_message_id ?? null } /** - * Get the URL of the channel's icon. + * Whether the channel is managed by an Oauth2 application. */ - get iconUrl(): string | null { - return this.icon - ? `https://cdn.discordapp.com/channel-icons/${this.id}/${this.icon}.png` - : null + get managed(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.managed ?? false } /** * Get the owner of the channel. */ - get owner(): User { + get owner(): User { if (!this.ownerId) throw new Error("Cannot get owner without owner ID") - return new User(this.client, this.ownerId) + return new User(this.client, this.ownerId) } /** @@ -81,9 +103,9 @@ export class GroupDmChannel extends BaseChannel { */ get lastMessage() { if (!this.lastMessageId) return null - return new Message(this.client, { + return new Message(this.client, { id: this.lastMessageId, - channel_id: this.id + channelId: this.id }) } @@ -97,36 +119,26 @@ export class GroupDmChannel extends BaseChannel { name } }) - this.name = name + this.setField("name", name) } - async addRecipient(user: User | string) { - await this.client.rest.put( - Routes.channelRecipient( - this.id, - typeof user === "string" ? user : user.id - ) - ) - if (this.recipients) - this.recipients.push( - typeof user === "string" ? new User(this.client, user) : user - ) - else - this.recipients = [ - typeof user === "string" ? new User(this.client, user) : user - ] - } + // TODO: Do these even work without access token? - async removeRecipient(user: User | string) { - await this.client.rest.delete( - Routes.channelRecipient( - this.id, - typeof user === "string" ? user : user.id - ) - ) - if (this.recipients) - this.recipients = this.recipients.filter( - (x) => x.id !== (typeof user === "string" ? user : user.id) - ) - } + // async addRecipient(user: User | string) { + // await this.client.rest.put( + // Routes.channelRecipient( + // this.id, + // typeof user === "string" ? user : user.id + // ) + // ) + // } + + // async removeRecipient(user: User | string) { + // await this.client.rest.delete( + // Routes.channelRecipient( + // this.id, + // typeof user === "string" ? user : user.id + // ) + // ) + // } } diff --git a/packages/carbon/src/structures/Guild.ts b/packages/carbon/src/structures/Guild.ts index 4d4b2c63..34cd34db 100644 --- a/packages/carbon/src/structures/Guild.ts +++ b/packages/carbon/src/structures/Guild.ts @@ -9,67 +9,117 @@ import { import { Base } from "../abstracts/Base.js" import type { Client } from "../classes/Client.js" import { channelFactory } from "../factories/channelFactory.js" +import type { IfPartial } from "../utils.js" import { GuildMember } from "./GuildMember.js" import { Role } from "./Role.js" -export class Guild extends Base { +export class Guild extends Base { + constructor( + client: Client, + rawDataOrId: IsPartial extends true ? string : APIGuild + ) { + super(client) + if (typeof rawDataOrId === "string") { + this.id = rawDataOrId + } else { + this.rawData = rawDataOrId + this.id = rawDataOrId.id + this.setData(rawDataOrId) + } + } + + private rawData: APIGuild | null = null + private setData(data: typeof this.rawData) { + if (!data) throw new Error("Cannot set data without having data... smh") + this.rawData = data + } + // private setField(key: keyof APIGuild, value: unknown) { + // if (!this.rawData) + // throw new Error("Cannot set field without having data... smh") + // Reflect.set(this.rawData, key, value) + // } + /** * The ID of the guild */ - id: string + readonly id: string + + /** + * Whether the guild is a partial guild (meaning it does not have all the data). + * If this is true, you should use {@link Guild.fetch} to get the full data of the guild. + */ + get partial(): IfPartial { + return (this.rawData === null) as never + } + /** * The name of the guild. */ - name?: string + get name(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.name as never + } + /** * The description of the guild. */ - description?: string | null + get description(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.description as never + } + /** * The icon hash of the guild. * You can use {@link Guild.iconUrl} to get the URL of the icon. */ - icon?: string | null + get icon(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.icon + } + + /** + * Get the URL of the guild's icon + */ + get iconUrl(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.icon) return null + return `https://cdn.discordapp.com/icons/${this.id}/${this.icon}.png` + } + /** * The splash hash of the guild. * You can use {@link Guild.splashUrl} to get the URL of the splash. */ - splash?: string | null + get splash(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.splash + } + /** - * The ID of the owner of the guild. + * Get the URL of the guild's splash */ - ownerId?: string + get splashUrl(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.splash) return null + return `https://cdn.discordapp.com/splashes/${this.id}/${this.splash}.png` + } /** - * Whether the guild is a partial guild (meaning it does not have all the data). - * If this is true, you should use {@link Guild.fetch} to get the full data of the guild. + * The ID of the owner of the guild. */ - partial: boolean - - private rawData: APIGuild | null = null - - constructor(client: Client, rawDataOrId: APIGuild | string) { - super(client) - if (typeof rawDataOrId === "string") { - this.id = rawDataOrId - this.partial = true - } else { - this.rawData = rawDataOrId - this.id = rawDataOrId.id - this.partial = false - this.setData(rawDataOrId) - } + get ownerId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.owner_id } - private setData(data: typeof this.rawData) { - if (!data) throw new Error("Cannot set data without having data... smh") - this.rawData = data - this.name = data.name - this.description = data.description - this.icon = data.icon - this.splash = data.splash - this.ownerId = data.owner_id - this.partial = false + /** + * Get all roles in the guild + */ + get roles(): IfPartial { + if (!this.rawData) return undefined as never + const roles = this.rawData?.roles + if (!roles) throw new Error("Cannot get roles without having data... smh") + return roles.map((role) => new Role(this.client, role)) } /** @@ -115,33 +165,6 @@ export class Guild extends Base { return new GuildMember(this.client, member, this) } - /** - * Get the URL of the guild's icon - */ - get iconUrl(): string | null { - return this.icon - ? `https://cdn.discordapp.com/icons/${this.id}/${this.icon}.png` - : null - } - - /** - * Get the URL of the guild's splash - */ - get splashUrl(): string | null { - return this.splash - ? `https://cdn.discordapp.com/splashes/${this.id}/${this.splash}.png` - : null - } - - /** - * Get all roles in the guild - */ - get roles() { - const roles = this.rawData?.roles - if (!roles) throw new Error("Cannot get roles without having data... smh") - return roles.map((role) => new Role(this.client, role)) - } - /** * Fetch a channel from the guild by ID */ diff --git a/packages/carbon/src/structures/GuildAnnouncementChannel.ts b/packages/carbon/src/structures/GuildAnnouncementChannel.ts index 77d76f77..44a21826 100644 --- a/packages/carbon/src/structures/GuildAnnouncementChannel.ts +++ b/packages/carbon/src/structures/GuildAnnouncementChannel.ts @@ -1,13 +1,18 @@ -import { ChannelType, Routes } from "discord-api-types/v10" +import { + type APIGuildTextChannel, + type ChannelType, + Routes +} from "discord-api-types/v10" import { BaseGuildTextChannel } from "../abstracts/BaseGuildTextChannel.js" import type { GuildTextChannel } from "./GuildTextChannel.js" /** * Represents a guild announcement channel. */ -export class GuildAnnouncementChannel extends BaseGuildTextChannel { - type: ChannelType.GuildAnnouncement = ChannelType.GuildAnnouncement - protected setMoreSpecificData() {} +export class GuildAnnouncementChannel< + IsPartial extends boolean = false +> extends BaseGuildTextChannel { + declare rawData: APIGuildTextChannel | null async follow(targetChannel: GuildTextChannel | string) { await this.client.rest.put(Routes.channelFollowers(this.id), { diff --git a/packages/carbon/src/structures/GuildCategoryChannel.ts b/packages/carbon/src/structures/GuildCategoryChannel.ts index cd281e19..4a2a0dbf 100644 --- a/packages/carbon/src/structures/GuildCategoryChannel.ts +++ b/packages/carbon/src/structures/GuildCategoryChannel.ts @@ -1,11 +1,16 @@ -import type { ChannelType } from "discord-api-types/v10" +import type { + APIGuildCategoryChannel, + ChannelType +} from "discord-api-types/v10" import { BaseGuildChannel } from "../abstracts/BaseGuildChannel.js" /** * Represents a guild category channel. */ -export class GuildCategoryChannel extends BaseGuildChannel { - protected setSpecificData() {} +export class GuildCategoryChannel< + IsPartial extends boolean = false +> extends BaseGuildChannel { + declare rawData: APIGuildCategoryChannel | null /** * You cannot send a message to a category channel, so this method throws an error diff --git a/packages/carbon/src/structures/GuildForumChannel.ts b/packages/carbon/src/structures/GuildForumChannel.ts index 98d74d1e..847b42b3 100644 --- a/packages/carbon/src/structures/GuildForumChannel.ts +++ b/packages/carbon/src/structures/GuildForumChannel.ts @@ -4,17 +4,21 @@ import type { ForumLayoutType } from "discord-api-types/v10" import { GuildThreadOnlyChannel } from "../abstracts/GuildThreadOnlyChannel.js" +import type { IfPartial } from "../utils.js" /** * Represents a guild forum channel. */ -export class GuildForumChannel extends GuildThreadOnlyChannel { +export class GuildForumChannel< + IsPartial extends boolean = false +> extends GuildThreadOnlyChannel { + declare rawData: APIGuildForumChannel | null + /** * The default forum layout of the channel. */ - defaultForumLayout?: ForumLayoutType - - protected setMoreSpecificData(data: APIGuildForumChannel) { - this.defaultForumLayout = data.default_forum_layout + get defaultForumLayout(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.default_forum_layout as never } } diff --git a/packages/carbon/src/structures/GuildMediaChannel.ts b/packages/carbon/src/structures/GuildMediaChannel.ts index 8cf5cf5c..d0c394db 100644 --- a/packages/carbon/src/structures/GuildMediaChannel.ts +++ b/packages/carbon/src/structures/GuildMediaChannel.ts @@ -1,13 +1,9 @@ -import type { ChannelType, ForumLayoutType } from "discord-api-types/v10" +import type { APIGuildMediaChannel, ChannelType } from "discord-api-types/v10" import { GuildThreadOnlyChannel } from "../abstracts/GuildThreadOnlyChannel.js" /** - * Represents a guild media channel (a forum channel ) + * Represents a guild media channel (a forum channel) */ export class GuildMediaChannel extends GuildThreadOnlyChannel { - /** - * The default forum layout of the channel. - */ - defaultForumLayout?: ForumLayoutType - protected setMoreSpecificData() {} + declare rawData: APIGuildMediaChannel | null } diff --git a/packages/carbon/src/structures/GuildMember.ts b/packages/carbon/src/structures/GuildMember.ts index 8adec3d5..ac13266c 100644 --- a/packages/carbon/src/structures/GuildMember.ts +++ b/packages/carbon/src/structures/GuildMember.ts @@ -1,85 +1,139 @@ import type { APIGuildMember, GuildMemberFlags } from "discord-api-types/v10" import { Base } from "../abstracts/Base.js" import type { Client } from "../classes/Client.js" +import type { IfPartial } from "../utils.js" import type { Guild } from "./Guild.js" import { Role } from "./Role.js" import { User } from "./User.js" -export class GuildMember extends Base { +export class GuildMember< + // This currently can never be partial, so we don't need to worry about it + IsPartial extends false = false, + IsGuildPartial extends boolean = false +> extends Base { + constructor( + client: Client, + rawData: APIGuildMember, + guild: Guild + ) { + super(client) + this.rawData = rawData + this.guild = guild + this.user = new User(client, rawData.user) + this.setData(rawData) + } + + private rawData: APIGuildMember | null = null + private setData(data: typeof this.rawData) { + if (!data) throw new Error("Cannot set data without having data... smh") + this.rawData = data + } + private setField(key: keyof APIGuildMember, value: unknown) { + if (!this.rawData) + throw new Error("Cannot set field without having data... smh") + Reflect.set(this.rawData, key, value) + } + + /** + * The guild object of the member. + */ + guild: Guild + + /** + * The user object of the member. + */ + user: User + /** * The guild-specific nickname of the member. */ - nickname?: string | null + get nickname(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.nick ?? null + } + /** * The guild-specific avatar hash of the member. * You can use {@link GuildMember.avatarUrl} to get the URL of the avatar. */ - avatar?: string | null + get avatar(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.avatar ?? null + } + + /** + * Get the URL of the member's guild-specific avatar + */ + get avatarUrl(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.user || !this.avatar) return null + return `https://cdn.discordapp.com/guilds/${this.guild.id}/users/${this.user.id}/${this.avatar}.png` + } + /** * Is this member muted in Voice Channels? */ - mute?: boolean | null + get mute(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.mute + } + /** * Is this member deafened in Voice Channels? */ - deaf?: boolean | null + get deaf(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.deaf + } + /** * The date since this member boosted the guild, if applicable. */ - premiumSince?: string | null + get premiumSince(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.premium_since ?? null + } + /** * The flags of the member. * @see https://discord.com/developers/docs/resources/guild#guild-member-object-guild-member-flags */ - flags?: GuildMemberFlags | null + get flags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.flags + } + /** * The roles of the member */ - roles?: Role[] | null + get roles(): IfPartial[]> { + if (!this.rawData) return undefined as never + const roles = this.rawData.roles ?? [] + return roles.map((role) => new Role(this.client, role)) + } + /** * The joined date of the member */ - joinedAt?: string | null + get joinedAt(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.joined_at + } + /** * The date when the member's communication privileges (timeout) will be reinstated */ - communicationDisabledUntil?: string | null + get communicationDisabledUntil(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.communication_disabled_until ?? null + } + /** * Is this member yet to pass the guild's Membership Screening requirements? */ - pending?: boolean | null - /** - * The guild object of the member - */ - guild: Guild - /** - * The user object of the member - */ - user?: User | null - - private rawData: APIGuildMember | null = null - - constructor(client: Client, rawData: APIGuildMember, guild: Guild) { - super(client) - this.rawData = rawData - this.guild = guild - this.setData(rawData) - } - - private setData(data: typeof this.rawData) { - if (!data) throw new Error("Cannot set data without having data... smh") - this.rawData = data - this.nickname = data.nick - this.avatar = data.avatar - this.mute = data.mute - this.deaf = data.deaf - this.premiumSince = data.premium_since - this.flags = data.flags - this.roles = data.roles?.map((roleId) => new Role(this.client, roleId)) - this.joinedAt = data.joined_at - this.communicationDisabledUntil = data.communication_disabled_until - this.pending = data.pending - this.user = data.user ? new User(this.client, data.user) : null + get pending(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.pending ?? false } /** @@ -94,7 +148,7 @@ export class GuildMember extends Base { } } ) - this.nickname = nickname + this.setField("nick", nickname) } /** @@ -105,7 +159,7 @@ export class GuildMember extends Base { `/guilds/${this.guild?.id}/members/${this.user?.id}/roles/${roleId}`, {} ) - this.roles?.push(new Role(this.client, roleId)) + this.roles?.push(new Role(this.client, roleId)) } /** @@ -115,7 +169,8 @@ export class GuildMember extends Base { await this.client.rest.delete( `/guilds/${this.guild?.id}/members/${this.user?.id}/roles/${roleId}` ) - this.roles = this.roles?.filter((role) => role.id !== roleId) + const roles = this.roles?.filter((role) => role.id !== roleId) + if (roles) this.setField("roles", roles) } /** @@ -156,7 +211,7 @@ export class GuildMember extends Base { } } ) - this.mute = true + this.setField("mute", true) } /** @@ -171,7 +226,7 @@ export class GuildMember extends Base { } } ) - this.mute = false + this.setField("mute", false) } /** @@ -186,7 +241,7 @@ export class GuildMember extends Base { } } ) - this.deaf = true + this.setField("deaf", true) } /** @@ -201,7 +256,7 @@ export class GuildMember extends Base { } } ) - this.deaf = false + this.setField("deaf", false) } /** @@ -216,16 +271,6 @@ export class GuildMember extends Base { } } ) - this.communicationDisabledUntil = communicationDisabledUntil - } - - /** - * Get the URL of the member's guild-specific avatar - */ - get avatarUrl(): string | null { - if (!this.user) return null - return this.avatar - ? `https://cdn.discordapp.com/guilds/${this.guild.id}/users/${this.user?.id}/${this.avatar}.png` - : null + this.setField("communication_disabled_until", communicationDisabledUntil) } } diff --git a/packages/carbon/src/structures/GuildStageOrVoiceChannel.ts b/packages/carbon/src/structures/GuildStageOrVoiceChannel.ts index 9db921a6..eaa39262 100644 --- a/packages/carbon/src/structures/GuildStageOrVoiceChannel.ts +++ b/packages/carbon/src/structures/GuildStageOrVoiceChannel.ts @@ -1,46 +1,61 @@ -import type { - APIGuildStageVoiceChannel, - APIGuildVoiceChannel, - ChannelType, +import { + type APIGuildStageVoiceChannel, + type APIGuildVoiceChannel, + type ChannelType, VideoQualityMode } from "discord-api-types/v10" import { BaseGuildChannel } from "../abstracts/BaseGuildChannel.js" +import type { IfPartial } from "../utils.js" export abstract class GuildStageOrVoiceChannel< - Type extends ChannelType.GuildStageVoice | ChannelType.GuildVoice -> extends BaseGuildChannel { + Type extends ChannelType.GuildStageVoice | ChannelType.GuildVoice, + IsPartial extends boolean = false +> extends BaseGuildChannel { + // @ts-expect-error + declare rawData: APIGuildStageVoiceChannel | APIGuildVoiceChannel | null + /** * The bitrate of the channel. */ - bitrate?: number | null + get bitrate(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.bitrate + } + /** * The user limit of the channel. */ - userLimit?: number | null + get userLimit(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.user_limit + } + /** * The RTC region of the channel. * This is automatic when set to `null`. */ - rtcRegion?: string | null + get rtcRegion(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.rtc_region ?? null + } + /** * The video quality mode of the channel. * 1 when not present. */ - videoQualityMode?: VideoQualityMode | null - - protected setSpecificData( - data: APIGuildStageVoiceChannel | APIGuildVoiceChannel - ) { - this.bitrate = data.bitrate - this.userLimit = data.user_limit - this.rtcRegion = data.rtc_region - this.videoQualityMode = data.video_quality_mode + get videoQualityMode(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.video_quality_mode ?? VideoQualityMode.Auto } } -export class GuildStageChannel extends BaseGuildChannel { - protected setSpecificData() {} +export class GuildStageChannel< + IsPartial extends boolean = false +> extends GuildStageOrVoiceChannel { + declare rawData: APIGuildStageVoiceChannel | null } -export class GuildVoiceChannel extends BaseGuildChannel { - protected setSpecificData() {} +export class GuildVoiceChannel< + IsPartial extends boolean = false +> extends GuildStageOrVoiceChannel { + declare rawData: APIGuildVoiceChannel | null } diff --git a/packages/carbon/src/structures/GuildTextChannel.ts b/packages/carbon/src/structures/GuildTextChannel.ts index 1dbd3918..5cc1f940 100644 --- a/packages/carbon/src/structures/GuildTextChannel.ts +++ b/packages/carbon/src/structures/GuildTextChannel.ts @@ -1,21 +1,25 @@ -import { type APIGuildTextChannel, ChannelType } from "discord-api-types/v10" +import type { APIGuildTextChannel, ChannelType } from "discord-api-types/v10" import { BaseGuildTextChannel } from "../abstracts/BaseGuildTextChannel.js" +import type { IfPartial } from "../utils.js" + +export class GuildTextChannel< + IsPartial extends boolean = false +> extends BaseGuildTextChannel { + declare rawData: APIGuildTextChannel | null -export class GuildTextChannel extends BaseGuildTextChannel { - type: ChannelType.GuildText = ChannelType.GuildText /** * The default auto archive duration of threads in the channel. */ - defaultAutoArchiveDuration?: number | null + get defaultAutoArchiveDuration(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.default_auto_archive_duration ?? null + } + /** * The default thread rate limit per user of the channel. */ - defaultThreadRateLimitPerUser?: number | null - - protected setMoreSpecificData( - data: APIGuildTextChannel - ) { - this.defaultAutoArchiveDuration = data.default_auto_archive_duration - this.defaultThreadRateLimitPerUser = data.default_thread_rate_limit_per_user + get defaultThreadRateLimitPerUser(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.default_thread_rate_limit_per_user ?? null } } diff --git a/packages/carbon/src/structures/GuildThreadChannel.ts b/packages/carbon/src/structures/GuildThreadChannel.ts index c765e759..7d06c2e9 100644 --- a/packages/carbon/src/structures/GuildThreadChannel.ts +++ b/packages/carbon/src/structures/GuildThreadChannel.ts @@ -4,70 +4,104 @@ import { type ThreadChannelType } from "discord-api-types/v10" import { BaseGuildChannel } from "../abstracts/BaseGuildChannel.js" +import type { IfPartial } from "../utils.js" export class GuildThreadChannel< - Type extends ThreadChannelType -> extends BaseGuildChannel { + Type extends ThreadChannelType, + IsPartial extends boolean = false +> extends BaseGuildChannel { + // @ts-expect-error + declare rawData: APIThreadChannel | null + /** * Whether the thread is archived. */ - archived?: boolean + get archived(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.thread_metadata?.archived + } + /** * The duration until the thread is auto archived. */ - autoArchiveDuration?: number + get autoArchiveDuration(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.thread_metadata?.auto_archive_duration + } + /** * The timestamp of when the thread was archived. */ - archiveTimestamp?: string + get archiveTimestamp(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.thread_metadata?.archive_timestamp + } + /** * Whether the thread is locked. */ - locked?: boolean + get locked(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.thread_metadata?.locked + } + /** * Whether non-moderators can add other non-moderators to a thread; only available on private threads */ - invitable?: boolean + get invitable(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.thread_metadata?.invitable + } + /** * The timestamp of when the thread was created. */ - createTimestamp?: string + get createTimestamp(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.thread_metadata?.create_timestamp + } + /** * The number of messages in the thread. */ - messageCount?: number + get messageCount(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.message_count + } + /** * The number of members in the thread. * * @remarks * This is only accurate until 50, after that, Discord stops counting. */ - memberCount?: number + get memberCount(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.member_count + } + /** * The ID of the owner of the thread. */ - ownerId?: string + get ownerId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.owner_id + } + /** * The number of messages sent in the thread. */ - totalMessageSent?: number + get totalMessageSent(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.total_message_sent + } + /** * The tags applied to the thread. */ - appliedTags?: string[] - - protected setSpecificData(data: APIThreadChannel) { - this.archived = data.thread_metadata?.archived - this.autoArchiveDuration = data.thread_metadata?.auto_archive_duration - this.archiveTimestamp = data.thread_metadata?.archive_timestamp - this.locked = data.thread_metadata?.locked - this.invitable = data.thread_metadata?.invitable - this.createTimestamp = data.thread_metadata?.create_timestamp - this.messageCount = data.message_count - this.memberCount = data.member_count - this.ownerId = data.owner_id - this.totalMessageSent = data.total_message_sent - this.appliedTags = data.applied_tags + get appliedTags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.applied_tags } /** @@ -105,7 +139,7 @@ export class GuildThreadChannel< await this.client.rest.patch(Routes.channel(this.id), { body: { archive: true } }) - this.archived = true + Reflect.set(this.rawData?.thread_metadata ?? {}, "archived", true) } /** @@ -115,7 +149,7 @@ export class GuildThreadChannel< await this.client.rest.patch(Routes.channel(this.id), { body: { archive: false } }) - this.archived = false + Reflect.set(this.rawData?.thread_metadata ?? {}, "archived", false) } /** @@ -125,7 +159,11 @@ export class GuildThreadChannel< await this.client.rest.patch(Routes.channel(this.id), { body: { auto_archive_duration: duration } }) - this.autoArchiveDuration = duration + Reflect.set( + this.rawData?.thread_metadata ?? {}, + "auto_archive_duration", + duration + ) } /** @@ -135,7 +173,7 @@ export class GuildThreadChannel< await this.client.rest.put(Routes.channel(this.id), { body: { locked: true } }) - this.locked = true + Reflect.set(this.rawData?.thread_metadata ?? {}, "locked", true) } /** @@ -145,6 +183,6 @@ export class GuildThreadChannel< await this.client.rest.put(Routes.channel(this.id), { body: { locked: false } }) - this.locked = false + Reflect.set(this.rawData?.thread_metadata ?? {}, "locked", false) } } diff --git a/packages/carbon/src/structures/Message.ts b/packages/carbon/src/structures/Message.ts index bfc28118..9c0fbd1b 100644 --- a/packages/carbon/src/structures/Message.ts +++ b/packages/carbon/src/structures/Message.ts @@ -7,7 +7,7 @@ import { type APIMessageReference, type APIPoll, type APIReaction, - type APISticker, + type APIStickerItem, type APIThreadChannel, type ChannelType, type MessageFlags, @@ -20,140 +20,266 @@ import type { Client } from "../classes/Client.js" import { Embed } from "../classes/Embed.js" import type { Row } from "../classes/Row.js" import { channelFactory } from "../factories/channelFactory.js" +import type { IfPartial } from "../utils.js" import { GuildThreadChannel } from "./GuildThreadChannel.js" import { Role } from "./Role.js" import { User } from "./User.js" -export class Message extends Base { +export class Message extends Base { + constructor( + client: Client, + rawDataOrIds: IsPartial extends true + ? { id: string; channelId: string } + : APIMessage + ) { + super(client) + if (Object.keys(rawDataOrIds).length === 2) { + this.id = rawDataOrIds.id + this.channelId = rawDataOrIds["channelId" as never] + } else { + const data = rawDataOrIds as APIMessage + this.id = data.id + this.channelId = data.channel_id + this.setData(data) + } + } + + private rawData: APIMessage | null = null + private setData(data: typeof this.rawData) { + this.rawData = data + if (!data) throw new Error("Cannot set data without having data... smh") + } + // private setField(key: keyof APIMessage, value: unknown) { + // if (!this.rawData) + // throw new Error("Cannot set field without having data... smh") + // Reflect.set(this.rawData, key, value) + // } + /** * The ID of the message */ - protected id: string + readonly id: string + /** * The ID of the channel the message is in */ - protected channelId: string + readonly channelId: string + /** * Whether the message is a partial message (meaning it does not have all the data). * If this is true, you should use {@link Message.fetch} to get the full data of the message. */ - protected partial: boolean + get partial(): IsPartial { + return (this.rawData === null) as never + } + /** * If this message is a response to an interaction, this is the ID of the interaction's application */ - protected applicationId?: string + get applicationId(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.application_id + } + /** * The attachments of the message */ - protected attachments?: APIAttachment[] + get attachments(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.attachments ?? [] + } + /** * The components of the message */ - protected components?: APIMessage["components"] + get components(): IfPartial< + IsPartial, + NonNullable + > { + if (!this.rawData) return undefined as never + return this.rawData.components ?? [] + } + /** * The content of the message */ - protected content?: string + get content(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.content ?? "" + } + + get embeds(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.rawData?.embeds) return [] + return this.rawData.embeds.map((embed) => new Embed(embed)) + } + /** * If this message was edited, this is the timestamp of the edit */ - protected editedTimestamp?: string | null + get editedTimestamp(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.edited_timestamp as never + } + /** * The flags of the message */ - protected flags?: MessageFlags + get flags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.flags as never + } + /** * The interaction metadata of the message */ - protected interactionMetadata?: APIMessageInteractionMetadata + get interactionMetadata(): IfPartial< + IsPartial, + APIMessageInteractionMetadata | undefined + > { + if (!this.rawData) return undefined as never + return this.rawData.interaction_metadata + } + /** * Whether the message mentions everyone */ - protected mentionedEveryone?: boolean + get mentionedEveryone(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.mention_everyone + } + + /** + * The users mentioned in the message + */ + get mentionedUsers(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.rawData?.mentions) return [] + return this.rawData.mentions.map( + (mention) => new User(this.client, mention) + ) + } + + /** + * The roles mentioned in the message + */ + get mentionedRoles(): IfPartial[]> { + if (!this.rawData) return undefined as never + if (!this.rawData?.mention_roles) return [] + return this.rawData.mention_roles.map( + (mention) => new Role(this.client, mention) + ) + } + /** * The data about the referenced message. You can use {@link Message.referencedMessage} to get the referenced message itself. */ - protected messageReference?: APIMessageReference + get messageReference(): IfPartial< + IsPartial, + APIMessageReference | undefined + > { + if (!this.rawData) return undefined as never + return this.rawData.message_reference + } + + /** + * The referenced message itself + */ + get referencedMessage(): IfPartial { + if (!this.rawData?.referenced_message) return null as never + return new Message(this.client, this.rawData?.referenced_message) + } + /** * Whether the message is pinned */ - protected pinned?: boolean + get pinned(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.pinned + } + /** * The poll contained in the message */ - protected poll?: APIPoll + get poll(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.poll + } + /** * The approximate position of the message in the channel */ - protected position?: number + get position(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.position + } + /** * The reactions on the message */ - protected reactions?: APIReaction[] + get reactions(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.reactions ?? [] + } + /** * The stickers in the message */ - protected stickers?: APISticker[] + get stickers(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.sticker_items ?? [] + } + /** * The timestamp of the original message */ - protected timestamp?: string + get timestamp(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.timestamp + } + /** * Whether the message is a TTS message */ - protected tts?: boolean + get tts(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.tts + } + /** * The type of the message */ - protected type?: MessageType + get type(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.type + } + /** - * If a webhook is used to send the message, this is the ID of the webhook + * Get the author of the message */ - protected webhookId?: string - - private rawData: APIMessage | null = null - - constructor( - client: Client, - rawDataOrIds: - | APIMessage - | { - id: string - channel_id: string - } - ) { - super(client) - this.id = rawDataOrIds.id - this.channelId = rawDataOrIds.channel_id - if (Object.keys(rawDataOrIds).length === 2) { - this.partial = true - } else { - this.partial = false - this.setData(rawDataOrIds as APIMessage) - } + get author(): IfPartial { + if (!this.rawData) return null as never + if (this.rawData?.webhook_id) return null as never // TODO: Add webhook user + return new User(this.client, this.rawData.author) } - private setData(data: typeof this.rawData) { - this.rawData = data - if (!data) throw new Error("Cannot set data without having data... smh") - this.applicationId = data.application_id - this.attachments = data.attachments - this.components = data.components - this.content = data.content - this.editedTimestamp = data.edited_timestamp - this.flags = data.flags - this.interactionMetadata = data.interaction_metadata - this.mentionedEveryone = data.mention_everyone - this.messageReference = data.message_reference - this.pinned = data.pinned - this.poll = data.poll - this.position = data.position - this.reactions = data.reactions - this.stickers = data.stickers - this.timestamp = data.timestamp - this.tts = data.tts - this.type = data.type - this.webhookId = data.webhook_id + /** + * Get the thread associated with this message, if there is one + */ + get thread(): IfPartial< + IsPartial, + GuildThreadChannel< + ChannelType.PublicThread | ChannelType.AnnouncementThread + > | null + > { + if (!this.rawData) return null as never + if (!this.rawData?.thread) return null + return channelFactory( + this.client, + this.rawData?.thread + ) as GuildThreadChannel< + ChannelType.PublicThread | ChannelType.AnnouncementThread + > } /** @@ -179,20 +305,6 @@ export class Message extends Base { ) } - /** - * Get the author of the message - */ - get author(): User | null { - if (this.rawData?.webhook_id) return null // TODO: Add webhook user - // Check if we have an additional property on the author object, in which case we have a full user object - if (this.rawData?.author.username) - return new User(this.client, this.rawData.author) - // This means we only have a partial user object - if (this.rawData?.author.id) - return new User(this.client, this.rawData.author.id) - return null - } - /** * Get the channel the message was sent in */ @@ -231,23 +343,6 @@ export class Message extends Base { return new GuildThreadChannel(this.client, thread) } - get thread(): GuildThreadChannel< - ChannelType.PublicThread | ChannelType.AnnouncementThread - > | null { - if (!this.rawData?.thread) return null - return channelFactory( - this.client, - this.rawData?.thread - ) as GuildThreadChannel< - ChannelType.PublicThread | ChannelType.AnnouncementThread - > - } - - get embeds(): Embed[] { - if (!this.rawData?.embeds) return [] - return this.rawData.embeds.map((embed) => new Embed(embed)) - } - async edit(data: { content?: string embeds?: Embed[] @@ -266,23 +361,4 @@ export class Message extends Base { } ) } - - get mentionedUsers(): User[] { - if (!this.rawData?.mentions) return [] - return this.rawData.mentions.map( - (mention) => new User(this.client, mention) - ) - } - - get mentionedRoles(): Role[] { - if (!this.rawData?.mention_roles) return [] - return this.rawData.mention_roles.map( - (mention) => new Role(this.client, mention) - ) - } - - get referencedMessage(): Message | null { - if (!this.rawData?.referenced_message) return null - return new Message(this.client, this.rawData?.referenced_message) - } } diff --git a/packages/carbon/src/structures/Role.ts b/packages/carbon/src/structures/Role.ts index 20c81b30..3e65574a 100644 --- a/packages/carbon/src/structures/Role.ts +++ b/packages/carbon/src/structures/Role.ts @@ -6,96 +6,145 @@ import { } from "discord-api-types/v10" import { Base } from "../abstracts/Base.js" import type { Client } from "../classes/Client.js" +import type { IfPartial } from "../utils.js" + +export class Role extends Base { + constructor( + client: Client, + rawDataOrId: IsPartial extends true ? string : APIRole + ) { + super(client) + if (typeof rawDataOrId === "string") { + this.id = rawDataOrId + } else { + this.rawData = rawDataOrId + this.id = rawDataOrId.id + this.setData(rawDataOrId) + } + } + + private rawData: APIRole | null = null + private setData(data: typeof this.rawData) { + if (!data) throw new Error("Cannot set data without having data... smh") + this.rawData = data + } + private setField(key: keyof APIRole, value: unknown) { + if (!this.rawData) + throw new Error("Cannot set field without having data... smh") + Reflect.set(this.rawData, key, value) + } -export class Role extends Base { /** * The ID of the role. */ - id: string + readonly id: string + + /** + * Whether the role is a partial role (meaning it does not have all the data). + * If this is true, you should use {@link Role.fetch} to get the full data of the role. + */ + get partial(): IsPartial { + return (this.rawData === null) as never + } + /** * The name of the role. */ - name?: string | null + get name(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.name + } + /** * The color of the role. */ - color?: number | null + get color(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.color + } + /** * The icon hash of the role. * You can use {@link Role.iconUrl} to get the URL of the icon. */ - icon?: string | null + get icon(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.icon ?? null + } + + /** + * Get the URL of the role's icon + */ + get iconUrl(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.icon) return null as never + return `https://cdn.discordapp.com/role-icons/${this.id}/${this.icon}.png` + } + /** * If this role is mentionable. */ - mentionable?: boolean | null + get mentionable(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.mentionable + } + /** * If this role is hoisted. */ - hoisted?: boolean | null + get hoisted(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.hoist + } + /** * The position of the role. */ - position?: number | null + get position(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.position + } + /** * The permissions of the role. */ - permissions?: string | null + get permissions(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.permissions + } + /** * If this role is managed by an integration. */ - managed?: boolean | null + get managed(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.managed + } + /** * The unicode emoji for the role. */ - unicodeEmoji?: string | null + get unicodeEmoji(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.unicode_emoji ?? null + } + /** * The flags of this role. * @see https://discord.com/developers/docs/topics/permissions#role-object-role-flags */ - flags?: RoleFlags | null + get flags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.flags + } + /** * The tags of this role. * @see https://discord.com/developers/docs/topics/permissions#role-object-role-tags-structure */ - tags?: APIRoleTags | null - - /** - * Whether the role is a partial role (meaning it does not have all the data). - * If this is true, you should use {@link Role.fetch} to get the full data of the role. - */ - partial: boolean - - private rawData: APIRole | null = null - - constructor(client: Client, rawDataOrId: APIRole | string) { - super(client) - if (typeof rawDataOrId === "string") { - this.id = rawDataOrId - this.partial = true - } else { - this.rawData = rawDataOrId - this.id = rawDataOrId.id - this.partial = false - this.setData(rawDataOrId) - } - } - - private setData(data: typeof this.rawData) { - if (!data) throw new Error("Cannot set data without having data... smh") - this.rawData = data - this.name = data.name - this.color = data.color - this.icon = data.icon - this.mentionable = data.mentionable - this.hoisted = data.hoist - this.position = data.position - this.permissions = data.permissions - this.managed = data.managed - this.unicodeEmoji = data.unicode_emoji - this.flags = data.flags - this.tags = data.tags - this.partial = false + get tags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.tags } /** @@ -121,7 +170,7 @@ export class Role extends Base { name } }) - this.name = name + this.setField("name", name) } /** @@ -131,7 +180,7 @@ export class Role extends Base { await this.client.rest.patch(Routes.guildRole(guildId, this.id), { body: { color } }) - this.color = color + this.setField("color", color) } /** @@ -142,7 +191,7 @@ export class Role extends Base { await this.client.rest.patch(Routes.guildRole(guildId, this.id), { body: { icon } }) - this.icon = icon + this.setField("icon", icon) } /** @@ -152,7 +201,7 @@ export class Role extends Base { await this.client.rest.patch(Routes.guildRole(guildId, this.id), { body: { mentionable } }) - this.mentionable = mentionable + this.setField("mentionable", mentionable) } /** @@ -162,7 +211,7 @@ export class Role extends Base { await this.client.rest.patch(Routes.guildRole(guildId, this.id), { body: { hoist: hoisted } }) - this.hoisted = hoisted + this.setField("hoist", hoisted) } /** @@ -172,7 +221,7 @@ export class Role extends Base { await this.client.rest.patch(Routes.guildRole(guildId, this.id), { body: { position } }) - this.position = position + this.setField("position", position) } /** @@ -183,19 +232,10 @@ export class Role extends Base { await this.client.rest.patch(Routes.guildRole(guildId, this.id), { body: { permissions } }) - this.permissions = permissions + this.setField("permissions", permissions) } async delete(guildId: string) { await this.client.rest.delete(Routes.guildRole(guildId, this.id)) } - - /** - * Get the URL of the role's icon - */ - get iconUrl(): string | null { - return this.icon - ? `https://cdn.discordapp.com/role-icons/${this.id}/${this.icon}.png` - : null - } } diff --git a/packages/carbon/src/structures/User.ts b/packages/carbon/src/structures/User.ts index 6967ef11..bf4df38c 100644 --- a/packages/carbon/src/structures/User.ts +++ b/packages/carbon/src/structures/User.ts @@ -8,87 +8,139 @@ import { } from "discord-api-types/v10" import { Base } from "../abstracts/Base.js" import type { Client } from "../classes/Client.js" +import type { IfPartial } from "../utils.js" import { Message } from "./Message.js" -export class User extends Base { +export class User extends Base { + constructor( + client: Client, + rawDataOrId: IsPartial extends true ? string : APIUser + ) { + super(client) + if (typeof rawDataOrId === "string") { + this.id = rawDataOrId + } else { + this.rawData = rawDataOrId + this.id = rawDataOrId.id + this.setData(rawDataOrId) + } + } + + private rawData: APIUser | null = null + private setData(data: typeof this.rawData) { + if (!data) throw new Error("Cannot set data without having data... smh") + this.rawData = data + } + // private setField(key: keyof APIUser, value: unknown) { + // if (!this.rawData) throw new Error("Cannot set field without having data... smh") + // Reflect.set(this.rawData, key, value) + // } + /** * The ID of the user */ - id: string + readonly id: string + + /** + * Whether the user is a partial user (meaning it does not have all the data). + * If this is true, you should use {@link User.fetch} to get the full data of the user. + */ + get partial(): IsPartial { + return (this.rawData === null) as never + } + /** * The username of the user. */ - username?: string + get username(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.username + } + /** * The global name of the user. */ - globalName?: string | null + get globalName(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.global_name + } + /** * The discriminator of the user. */ - discriminator?: string - /** - * The avatar hash of the user. - * You can use {@link User.avatarUrl} to get the URL of the avatar. - */ - avatar?: string | null + get discriminator(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.discriminator + } + /** * Is this user a bot? */ - bot?: boolean + get bot(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.bot ?? false + } + /** * Is this user a system user? */ - system?: boolean + get system(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.system ?? false + } + /** * The public flags of the user. (Bitfield) * @see https://discord.com/developers/docs/resources/user#user-object-user-flags */ - flags?: UserFlags + get flags(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.public_flags + } + /** - * The banner hash of the user. - * You can use {@link User.bannerUrl} to get the URL of the banner. + * The avatar hash of the user. + * You can use {@link User.avatarUrl} to get the URL of the avatar. */ - banner?: string | null + + get avatar(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.avatar + } + /** - * The accent color of the user. + * Get the URL of the user's avatar */ - accentColor?: number | null + get avatarUrl(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.avatar) return null + return `https://cdn.discordapp.com/avatars/${this.id}/${this.avatar}.png` + } /** - * Whether the user is a partial user (meaning it does not have all the data). - * If this is true, you should use {@link User.fetch} to get the full data of the user. + * The banner hash of the user. + * You can use {@link User.bannerUrl} to get the URL of the banner. */ - partial: boolean - - private rawData: APIUser | null = null + get banner(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.banner ?? null + } - constructor(client: Client, rawDataOrId: APIUser | string) { - super(client) - if (typeof rawDataOrId === "string") { - this.id = rawDataOrId - this.partial = true - } else { - this.rawData = rawDataOrId - this.id = rawDataOrId.id - this.partial = false - this.setData(rawDataOrId) - } + /** + * Get the URL of the user's banner + */ + get bannerUrl(): IfPartial { + if (!this.rawData) return undefined as never + if (!this.banner) return null + return `https://cdn.discordapp.com/banners/${this.id}/${this.banner}.png` } - private setData(data: typeof this.rawData) { - if (!data) throw new Error("Cannot set data without having data... smh") - this.rawData = data - this.username = data.username - this.globalName = data.global_name - this.discriminator = data.discriminator - this.avatar = data.avatar - this.bot = data.bot - this.system = data.system - this.flags = data.public_flags - this.banner = data.banner - this.accentColor = data.accent_color - this.partial = false + /** + * The accent color of the user. + */ + get accentColor(): IfPartial { + if (!this.rawData) return undefined as never + return this.rawData.accent_color ?? null } /** @@ -132,22 +184,4 @@ export class User extends Base { )) as APIMessage return new Message(this.client, message) } - - /** - * Get the URL of the user's avatar - */ - get avatarUrl(): string | null { - return this.avatar - ? `https://cdn.discordapp.com/avatars/${this.id}/${this.avatar}.png` - : null - } - - /** - * Get the URL of the user's banner - */ - get bannerUrl(): string | null { - return this.banner - ? `https://cdn.discordapp.com/banners/${this.id}/${this.banner}.png` - : null - } } diff --git a/packages/carbon/src/utils.ts b/packages/carbon/src/utils.ts index 917bddbf..2c29aa07 100644 --- a/packages/carbon/src/utils.ts +++ b/packages/carbon/src/utils.ts @@ -1,3 +1,5 @@ +export type IfPartial = T extends true ? V : U + export const splitCustomId = ( customId: string ): [string, Record] => {