diff --git a/packages/structures/src/bitfields/GuildMemberFlagsBitField.ts b/packages/structures/src/bitfields/GuildMemberFlagsBitField.ts new file mode 100644 index 000000000000..fb037fc94cbc --- /dev/null +++ b/packages/structures/src/bitfields/GuildMemberFlagsBitField.ts @@ -0,0 +1,16 @@ +import { GuildMemberFlags } from 'discord-api-types/v10'; +import { BitField } from './BitField.js'; + +/** + * Data structure that makes it easy to interact with a {@link GuildMember#flags} bitfield. + */ +export class GuildMemberFlagsBitField extends BitField { + /** + * Numeric guild member flags. + */ + public static override readonly Flags = GuildMemberFlags; + + public override toJSON() { + return super.toJSON(true); + } +} diff --git a/packages/structures/src/bitfields/GuildSystemChannelFlagsBitField.ts b/packages/structures/src/bitfields/GuildSystemChannelFlagsBitField.ts new file mode 100644 index 000000000000..2216f9222a88 --- /dev/null +++ b/packages/structures/src/bitfields/GuildSystemChannelFlagsBitField.ts @@ -0,0 +1,16 @@ +import { GuildSystemChannelFlags } from 'discord-api-types/v10'; +import { BitField } from './BitField.js'; + +/** + * Data structure that makes it easy to interact with a {@link Guild#systemChannelFlags} bitfield. + */ +export class GuildSystemChannelFlagsBitField extends BitField { + /** + * Numeric system channel flags. + */ + public static override readonly Flags = GuildSystemChannelFlags; + + public override toJSON() { + return super.toJSON(true); + } +} diff --git a/packages/structures/src/bitfields/index.ts b/packages/structures/src/bitfields/index.ts index 694b4815b026..04d6b8d38ecb 100644 --- a/packages/structures/src/bitfields/index.ts +++ b/packages/structures/src/bitfields/index.ts @@ -2,6 +2,8 @@ export * from './BitField.js'; export * from './AttachmentFlagsBitField.js'; export * from './ChannelFlagsBitField.js'; +export * from './GuildMemberFlagsBitField.js'; +export * from './GuildSystemChannelFlagsBitField.js'; export * from './MessageFlagsBitField.js'; export * from './PermissionsBitField.js'; export * from './SKUFlagsBitField.js'; diff --git a/packages/structures/src/guild/Ban.ts b/packages/structures/src/guild/Ban.ts new file mode 100644 index 000000000000..4f85a102822b --- /dev/null +++ b/packages/structures/src/guild/Ban.ts @@ -0,0 +1,31 @@ +import type { APIBan } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { kData } from '../utils/symbols.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a ban on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks has substructure `User`, which needs to be instantiated and stored by any extending classes using it. + */ +export class Ban extends Structure { + /** + * The template used for removing data from the raw data stored for each ban. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the ban. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The reason for the ban. + */ + public get reason() { + return this[kData].reason; + } +} diff --git a/packages/structures/src/guild/Guild.ts b/packages/structures/src/guild/Guild.ts new file mode 100644 index 000000000000..1473f327779e --- /dev/null +++ b/packages/structures/src/guild/Guild.ts @@ -0,0 +1,420 @@ +import { DiscordSnowflake } from '@sapphire/snowflake'; +import { + CDNRoutes, + ImageFormat, + RouteBases, + type APIGuild, + type GuildBannerFormat, + type GuildDiscoverySplashFormat, + type GuildIconFormat, + type GuildSystemChannelFlags, + type GuildSplashFormat, +} from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { GuildSystemChannelFlagsBitField } from '../bitfields/GuildSystemChannelFlagsBitField.js'; +import { PermissionsBitField } from '../bitfields/PermissionsBitField.js'; +import { kData, kPermissions } from '../utils/symbols.js'; +import { isFieldSet, isIdSet } from '../utils/type-guards.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a guild on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks has substructures `GuildWelcomeScreen`, `GuildIncidentsData`, `Role`, `Emoji` and `Sticker` which needs to be instantiated and stored by any extending classes using it. + */ +export class Guild extends Structure { + protected [kPermissions]: bigint | null = null; + + /** + * @param data - The raw data from the API for the guild. + */ + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * The template used for removing data from the raw data stored for each `Guild` + * + * @remarks This template has defaults, if you want to remove additional data and keep the defaults, + * use `Object.defineProperties`. + */ + public static override readonly DataTemplate: Partial = { + set permissions(_: string) {}, + }; + + /** + * The id of the guild. + */ + public get id() { + return this[kData].id; + } + + /** + * The name of the guild + */ + public get name() { + return this[kData].name; + } + + /** + * The icon hash of the guild. + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get icon() { + return this[kData].icon; + } + + /** + * Get the URL to the guild icon. + * + * @param format - the file format to use + */ + public iconURL(format: GuildIconFormat = ImageFormat.WebP) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'icon', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildIcon(this[kData].id.toString(), this[kData].icon, format)}` + : null; + } + + /** + * The icon hash of the guild, when returned in the template object + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get iconHash() { + return this[kData].icon_hash; + } + + /** + * The splash hash of the guild. + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get splash() { + return this[kData].splash; + } + + /** + * Get the URL to the guild splash. + * + * @param format - the file format to use + */ + public splashURL(format: GuildSplashFormat = ImageFormat.WebP) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'splash', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildSplash(this[kData].id.toString(), this[kData].splash, format)}` + : null; + } + + /** + * The discovery splash hash of the guild. Only present for guilds with the "DISCOVERABLE" feature. + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get discoverySplash() { + return this[kData].discovery_splash; + } + + /** + * Get the URL to the guild discovery splash. + * + * @param format - the file format to use + */ + public discoverySplashURL(format: GuildDiscoverySplashFormat = ImageFormat.WebP) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'discovery_splash', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildDiscoverySplash(this[kData].id.toString(), this[kData].discovery_splash, format)}` + : null; + } + + /** + * Returns `true` if the user is the owner of the guild. + * + * @remarks This field is only received from https://discord.com/developers/docs/resources/user#get-current-user-guilds + */ + public get owner() { + return this[kData].owner; + } + + /** + * The id of the owner of the guild. + */ + public get ownerId() { + return this[kData].owner_id; + } + + /** + * Total permissions for the user in the guild (excluding overwrites) + * + * @remarks This field is only received from https://discord.com/developers/docs/resources/user#get-current-user-guilds + * @see {@link https://en.wikipedia.org/wiki/Bit_field} + */ + public get permissions() { + const permissions = this[kPermissions]; + return typeof permissions === 'bigint' ? new PermissionsBitField(permissions) : null; + } + + /** + * The id of the afk channel. + */ + public get afkChannelId() { + return this[kData].afk_channel_id; + } + + /** + * The afk timeout (in seconds). + * Can be set to: `60`, `300`, `900`, `1800`, `3600` + */ + public get afkTimeout() { + return this[kData].afk_timeout; + } + + /** + * Returns `true` if the guild's widget is enabled. + */ + public get widgetEnabled() { + return this[kData].widget_enabled; + } + + /** + * The channel id that the widget will generate an invite to, or `null` if set to no invite. + */ + public get widgetChannelId() { + return this[kData].widget_channel_id; + } + + /** + * The verification level required for the guild. + * + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-verification-level} + */ + public get verificationLevel() { + return this[kData].verification_level; + } + + /** + * The default message notifications level. + * + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-default-message-notification-level} + */ + public get defaultMessageNotifications() { + return this[kData].default_message_notifications; + } + + /** + * The explicit content filter level. + * + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-explicit-content-filter-level} + */ + public get explicitContentFilter() { + return this[kData].explicit_content_filter; + } + + /** + * The required MFA level for the guild. + * + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-mfa-level} + */ + public get mfaLevel() { + return this[kData].mfa_level; + } + + /** + * Application id of the guild creator, if it is bot-created. + */ + public get applicationId() { + return this[kData].application_id; + } + + /** + * The id of the channel where guild notices (such as welcome messages and boost events) are posted. + */ + public get systemChannelId() { + return this[kData].system_channel_id; + } + + /** + * The system channel flags + * + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-system-channel-flags} + */ + public get systemChannelFlags() { + return isFieldSet(this[kData], 'system_channel_flags', 'number') + ? new GuildSystemChannelFlagsBitField(this[kData].system_channel_flags as GuildSystemChannelFlags) + : null; + } + + /** + * The id of the channel where community guilds can display rules and/or guidelines. + */ + public get rulesChannelId() { + return this[kData].rules_channel_id; + } + + /** + * The maximum number of presences for the guild. (`null` is always returned, apart for the largest of guilds.) + */ + public get maxPresences() { + return this[kData].max_presences; + } + + /** + * The maximum number of members for the guild. + */ + public get maxMembers() { + return this[kData].max_members; + } + + /** + * The vanity URL code for the guild. + */ + public get vanityURLCode() { + return this[kData].vanity_url_code; + } + + /** + * The description for the guild. + */ + public get description() { + return this[kData].description; + } + + /** + * The banner hash for the guild. + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get banner() { + return this[kData].banner; + } + + /** + * Get the URL to the guild banner. + * + * @param format - the file format to use + */ + public bannerURL(format: GuildBannerFormat = ImageFormat.WebP) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'banner', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildBanner(this[kData].id.toString(), this[kData].banner, format)}` + : null; + } + + /** + * The premium tier (server boost level) for the guild. + * + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-premium-tier} + */ + public get premiumTier() { + return this[kData].premium_tier; + } + + /** + * The number of boosts this guild currently has. + */ + public get premiumSubscriptionCount() { + return this[kData].premium_subscription_count; + } + + /** + * The preferred locale of a community guild. Used in server discover and notices in discord. + * + */ + public get preferredLocale() { + return this[kData].preferred_locale; + } + + /** + * The id of the channel where admins and moderators of Community guilds receive notices from Discord. + */ + public get publicUpdatesChannelId() { + return this[kData].public_updates_channel_id; + } + + /** + * The maximum amount of users in a video channel. + */ + public get maxVideoChannelUsers() { + return this[kData].max_video_channel_users; + } + + /** + * The maximum amount of users in a stage video channel. + */ + public get maxStageVideoChannelUsers() { + return this[kData].max_stage_video_channel_users; + } + + /** + * Approximate number of members in this guild, returned from the `GET /guilds/` and `/users/@me/guilds` endpoints when `with_counts` is `true` + */ + public get approximateMemberCount() { + return this[kData].approximate_member_count; + } + + /** + * Approximate number of non-offline members in this guild, returned from the `GET /guilds/` and `/users/@me/guilds` endpoints when `with_counts` is `true` + */ + public get approximatePresenceCount() { + return this[kData].approximate_presence_count; + } + + /** + * The guild's NSFW level. + * + * @see {@link https://discord.com/developers/docs/resources/guild#guild-object-guild-nsfw-level} + */ + public get nsfwLevel() { + return this[kData].nsfw_level; + } + + /** + * Whether the guild has the boost progress bar enabled. + */ + public get premiumProgressBarEnabled() { + return this[kData].premium_progress_bar_enabled; + } + + /** + * The id of the channel where admins and moderators of Community guilds receive safety alerts from discord. + */ + public get safetyAlertsChannelId() { + return this[kData].safety_alerts_channel_id; + } + + /** + * The timestamp the guild was created at. + */ + public get createdTimestamp() { + return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null; + } + + /** + * The time the guild was created at. + */ + public get createdAt() { + const createdTimestamp = this.createdTimestamp; + return createdTimestamp ? new Date(createdTimestamp) : null; + } + + /** + * {@inheritDoc Structure.optimizeData} + */ + protected override optimizeData(data: Partial): void { + if (data.permissions) { + this[kPermissions] = BigInt(data.permissions); + } + } + + /** + * {@inheritDoc Structure.toJSON} + */ + public override toJSON() { + const clone = super.toJSON(); + const permissions = this[kPermissions]; + if (permissions) { + clone.permissions = permissions.toString(); + } + + return clone; + } +} diff --git a/packages/structures/src/guild/GuildIntegration.ts b/packages/structures/src/guild/GuildIntegration.ts new file mode 100644 index 000000000000..00edc4dbb0e3 --- /dev/null +++ b/packages/structures/src/guild/GuildIntegration.ts @@ -0,0 +1,185 @@ +import { DiscordSnowflake } from '@sapphire/snowflake'; +import type { APIGuildIntegration } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { dateToDiscordISOTimestamp } from '../utils/optimization.js'; +import { kData, kSyncedAt } from '../utils/symbols.js'; +import { isIdSet } from '../utils/type-guards.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a guild integration on Discord.. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks has substructures `User`, `IntegrationAccount`, and `Application`, which needs to be instantiated and stored by any extending classes using it. + */ +export class GuildIntegration extends Structure< + APIGuildIntegration, + Omitted +> { + /** + * The template used for removing data from the raw data stored for each `GuildIntegration` + * + * @remarks This template has defaults, if you want to remove additional data and keep the defaults, + * use `Object.defineProperties`. + */ + public static override readonly DataTemplate: Partial = { + set synced_at(_: string) {}, + }; + + protected [kSyncedAt]: number | null = null; + + /** + * @param data - The raw data from the API for the guild integration. + */ + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * The id of the integration. + */ + public get id() { + return this[kData].id; + } + + /** + * The name of the integration. + */ + public get name() { + return this[kData].name; + } + + /** + * The integration type. + */ + public get type() { + return this[kData].type; + } + + /** + * Whether the integration is enabled. + */ + public get enabled() { + return this[kData].enabled; + } + + /** + * Whether the integration is syncing. + * + * @remarks This field is not provided for `discord` bot integration. + */ + public get syncing() { + return this[kData].syncing; + } + + /** + * The date at which this integration was last synced at. + */ + public get syncedAt() { + const timestamp = this.syncedTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp at which this integration was last synced at. + */ + public get syncedTimestamp() { + return this[kSyncedAt]; + } + + /** + * ID that this integration uses for "subscribers". + * + * @remarks This field is not provided for `discord` bot integration. + */ + public get roleId() { + return this[kData].role_id; + } + + /** + * Whether emoticons should be synced for this integration. (`twitch` only currently.) + * + * @remarks This field is not provided for `discord` bot integration. + */ + public get enableEmoticons() { + return this[kData].enable_emoticons; + } + + /** + * The behavior of expiring subscribers. + * + * @remarks This field is not provided for `discord` bot integration. + * @see {@link https://discord.com/developers/docs/resources/guild#integration-object-integration-expire-behaviors} + */ + public get expireBehavior() { + return this[kData].expire_behavior; + } + + /** + * The grace period (in days) before expiring subscribers + * + * @remarks This field is not provided for `discord` bot integration. + */ + public get expireGracePeriod() { + return this[kData].expire_grace_period; + } + + /** + * How many subscribers this integration has. + * + * @remarks This field is not provided for `discord` bot integration. + */ + public get subscriberCount() { + return this[kData].subscriber_count; + } + + /** + * Whether the integration has been revoked. + * + * @remarks This field is not provided for `discord` bot integration. + */ + public get revoked() { + return this[kData].revoked; + } + + /** + * The timestamp the integration was created at. + */ + public get createdTimestamp() { + return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null; + } + + /** + * The time the integration was created at. + */ + public get createdAt() { + const createdTimestamp = this.createdTimestamp; + return createdTimestamp ? new Date(createdTimestamp) : null; + } + + /** + * {@inheritDoc Structure.optimizeData} + */ + protected override optimizeData(data: Partial) { + if (data.synced_at) { + this[kSyncedAt] = Date.parse(data.synced_at); + } + } + + /** + * + * {@inheritDoc Structure.toJSON} + */ + public override toJSON() { + const clone = super.toJSON(); + + const syncedAt = this[kSyncedAt]; + + if (syncedAt) { + clone.synced_at = dateToDiscordISOTimestamp(new Date(syncedAt)); + } + + return clone; + } +} diff --git a/packages/structures/src/guild/GuildMember.ts b/packages/structures/src/guild/GuildMember.ts new file mode 100644 index 000000000000..97225df4b791 --- /dev/null +++ b/packages/structures/src/guild/GuildMember.ts @@ -0,0 +1,195 @@ +import type { APIInteractionGuildMember, GuildMemberFlags } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { GuildMemberFlagsBitField } from '../bitfields/GuildMemberFlagsBitField.js'; +import { dateToDiscordISOTimestamp } from '../utils/optimization.js'; +import { kCommunicationDisabledUntil, kData, kJoinedAt, kPermissions, kPremiumSince } from '../utils/symbols.js'; +import { isFieldSet } from '../utils/type-guards.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a guild member on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks Intentionally does not export `roles`so extending classes can map this array to `Role[]`. + * @remarks has substructures `User` and `AvatarDecorationData`, which needs to be instantiated and stored by any extending classes using it. + */ +export class GuildMember< + Omitted extends keyof APIInteractionGuildMember | '' = 'communication_disabled_until' | 'joined_at' | 'premium_since', +> extends Structure { + /** + * @param data - The raw data from the API for the guild member. + */ + + /** + * The template used for removing data from the raw data stored for each `GuildMember` + * + * @remarks This template has defaults, if you want to remove additional data and keep the defaults, + * use `Object.defineProperties`. + */ + public static override readonly DataTemplate: Partial = { + set communication_disabled_until(_: string) {}, + set joined_at(_: string) {}, + set premium_since(_: string) {}, + set permissions(_: string) {}, + }; + + protected [kCommunicationDisabledUntil]: number | null = null; + + protected [kJoinedAt]: number | null = null; + + protected [kPremiumSince]: number | null = null; + + protected [kPermissions]: bigint | null = null; + + /** + * @param data - The raw data from the API for the guild member. + */ + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * The user's guild nickname. + */ + public get nick() { + return this[kData].nick; + } + + /** + * The member's guild avatar hash. + */ + public get avatar() { + return this[kData].avatar; + } + + /** + * The member's guild banner hash. + */ + public get banner() { + return this[kData].banner; + } + + /** + * Whether the user is deafened in voice channels. + */ + public get deaf() { + return this[kData].deaf; + } + + /** + * Whether the user is muted in voice channels. + */ + public get mute() { + return this[kData].mute; + } + + /** + * Guild member flags represented as a bit set. + * + */ + public get flags() { + return isFieldSet(this[kData], 'flags', 'number') + ? new GuildMemberFlagsBitField(this[kData].flags as GuildMemberFlags) + : null; + } + + public get permissions() { + return this[kData].permissions; + } + + /** + * The time this member's timeout will be removed. + */ + public get communicationsDisabledUntil() { + const timestamp = this.communicationsDisabledUntilTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp this member's timeout will be removed. + */ + public get communicationsDisabledUntilTimestamp() { + return this[kCommunicationDisabledUntil]; + } + + /** + * The time this member joined the guild. + */ + public get joinedAt() { + const timestamp = this.joinedTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp this member joined the guild at. + */ + public get joinedTimestamp() { + return this[kJoinedAt]; + } + + /** + * The last time this member started boosting the guild. + */ + public get premiumSince() { + const timestamp = this.premiumSinceTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The last timestamp this member started boosting the guild. + */ + public get premiumSinceTimestamp() { + return this[kPremiumSince]; + } + + /** + * {@inheritDoc Structure.optimizeData} + */ + protected override optimizeData(data: Partial): void { + if (data.communication_disabled_until) { + this[kCommunicationDisabledUntil] = Date.parse(data.communication_disabled_until); + } + + if (data.joined_at) { + this[kJoinedAt] = Date.parse(data.joined_at); + } + + if (data.premium_since) { + this[kPremiumSince] = Date.parse(data.premium_since); + } + + if (data.permissions) { + this[kPermissions] = BigInt(data.permissions); + } + } + + /** + * {@inheritDoc Structure.toJSON} + */ + public override toJSON() { + const clone = super.toJSON(); + + const communicationDisabledUntil = this[kCommunicationDisabledUntil]; + const joinedAt = this[kJoinedAt]; + const premiumSince = this[kPremiumSince]; + const permissions = this[kPermissions]; + if (communicationDisabledUntil) { + clone.communication_disabled_until = dateToDiscordISOTimestamp(new Date(communicationDisabledUntil)); + } + + if (joinedAt) { + clone.joined_at = dateToDiscordISOTimestamp(new Date(joinedAt)); + } + + if (premiumSince) { + clone.premium_since = dateToDiscordISOTimestamp(new Date(premiumSince)); + } + + if (permissions) { + clone.permissions = permissions.toString(); + } + + return clone; + } +} diff --git a/packages/structures/src/guild/GuildPreview.ts b/packages/structures/src/guild/GuildPreview.ts new file mode 100644 index 000000000000..77ff641a0a7e --- /dev/null +++ b/packages/structures/src/guild/GuildPreview.ts @@ -0,0 +1,145 @@ +import { DiscordSnowflake } from '@sapphire/snowflake'; +import { + CDNRoutes, + ImageFormat, + RouteBases, + type APIGuildPreview, + type GuildDiscoverySplashFormat, + type GuildIconFormat, + type GuildSplashFormat, +} from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { kData } from '../utils/symbols.js'; +import { isFieldSet, isIdSet } from '../utils/type-guards.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a guild preview on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks Intentionally does not export `emojis` or `stickers`, + * so extending classes can map each array to `Emoji[]` and `Sticker[]` respectively. + */ +export class GuildPreview extends Structure { + /** + * The template used for removing data from the raw data stored for each preview. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the guild preview. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The id of the guild. + */ + public get id() { + return this[kData].id; + } + + /** + * The name of the guild. + */ + public get name() { + return this[kData].name; + } + + /** + * The icon hash of the guild. + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get icon() { + return this[kData].icon; + } + + /** + * The splash hash of the guild. + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get splash() { + return this[kData].splash; + } + + /** + * The discovery splash hash of the guild. Only present for guilds with the "DISCOVERABLE" feature. + * + * @see {@link https://discord.com/developers/docs/reference#image-formatting} + */ + public get discoverySplash() { + return this[kData].discovery_splash; + } + + /** + *et the URL to the guild icon. + * + *param format - the file format to use + */ + public iconURL(format: GuildIconFormat = ImageFormat.WebP) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'icon', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildIcon(this[kData].id.toString(), this[kData].icon, format)}` + : null; + } + + /** + * Get the URL to the guild discovery splash. + * + * @param format - the file format to use + */ + public discoverySplashURL(format: GuildDiscoverySplashFormat = ImageFormat.WebP) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'discovery_splash', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildDiscoverySplash(this[kData].id.toString(), this[kData].discovery_splash, format)}` + : null; + } + + /** + * Get the URL to the guild splash. + * + * @param format - the file format to use + */ + public splashURL(format: GuildSplashFormat = ImageFormat.WebP) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'splash', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildSplash(this[kData].id.toString(), this[kData].splash, format)}` + : null; + } + + /** + * Approximate number of members in this guild. + */ + public get approximateMemberCount() { + return this[kData].approximate_member_count; + } + + /** + * Approximate number of non-offline members in this guild. + */ + public get approximatePresenceCount() { + return this[kData].approximate_presence_count; + } + + /** + * The description for the guild. + */ + public get description() { + return this[kData].description; + } + + /** + * The timestamp the guild was created at. + */ + public get createdTimestamp() { + return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null; + } + + /** + * The time the guild was created at. + */ + public get createdAt() { + const createdTimestamp = this.createdTimestamp; + return createdTimestamp ? new Date(createdTimestamp) : null; + } +} diff --git a/packages/structures/src/guild/GuildTemplate.ts b/packages/structures/src/guild/GuildTemplate.ts new file mode 100644 index 000000000000..c8fbb5852245 --- /dev/null +++ b/packages/structures/src/guild/GuildTemplate.ts @@ -0,0 +1,152 @@ +import type { APITemplate } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { dateToDiscordISOTimestamp } from '../utils/optimization.js'; +import { kCreatedAt, kData, kUpdatedAt } from '../utils/symbols.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a guild template on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks has substructures `User` and `Guild`, which need to be instantiated and stored by any extending classes using it. + */ +export class GuildTemplate extends Structure< + APITemplate, + Omitted +> { + /** + * The template used for removing data from the raw data stored for each `GuildTemplate` + * + * @remarks This template has defaults, if you want to remove additional data and keep the defaults, + * use `Object.defineProperties`. + */ + public static override readonly DataTemplate: Partial = { + set created_at(_: string) {}, + set updated_at(_: string) {}, + }; + + protected [kCreatedAt]: number | null = null; + + protected [kUpdatedAt]: number | null = null; + + /** + * @param data - The raw data from the API for the guild template. + */ + + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * The template code (unique ID) + */ + public get code() { + return this[kData].code; + } + + /** + * The name of the template. + */ + public get name() { + return this[kData].name; + } + + /** + * The description for the template. + */ + public get description() { + return this[kData].description; + } + + /** + * The number of times this template has been used. + */ + public get usageCount() { + return this[kData].usage_count; + } + + /** + * The id of the user who created the template. + */ + public get creatorId() { + return this[kData].creator_id; + } + + /** + * The id of the guild this template is based on. + */ + public get sourceGuildId() { + return this[kData].source_guild_id; + } + + /** + * Whether this template has unsynced changes. + */ + public get isDirty() { + return this[kData].is_dirty; + } + + /** + * The time when this template was created at. + */ + public get createdAt() { + const timestamp = this.createdTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp when this template was created at. + */ + public get createdTimestamp() { + return this[kCreatedAt]; + } + + /** + * The time when this template was last synced to the guild + */ + public get updatedAt() { + const timestamp = this.updatedTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp when this template was last synced to the guild. + */ + public get updatedTimestamp() { + return this[kUpdatedAt]; + } + + /** + * {@inheritDoc Structure.optimizeData} + */ + protected override optimizeData(data: Partial) { + if (data.created_at) { + this[kCreatedAt] = Date.parse(data.created_at); + } + + if (data.updated_at) { + this[kUpdatedAt] = Date.parse(data.updated_at); + } + } + + /** + * {@inheritDoc Structure.toJSON} + */ + public override toJSON() { + const clone = super.toJSON(); + + const createdAt = this[kCreatedAt]; + const updatedAt = this[kUpdatedAt]; + + if (createdAt) { + clone.created_at = dateToDiscordISOTimestamp(new Date(createdAt)); + } + + if (updatedAt) { + clone.updated_at = dateToDiscordISOTimestamp(new Date(updatedAt)); + } + + return clone; + } +} diff --git a/packages/structures/src/guild/GuildWelcomeScreen.ts b/packages/structures/src/guild/GuildWelcomeScreen.ts new file mode 100644 index 000000000000..350edb033078 --- /dev/null +++ b/packages/structures/src/guild/GuildWelcomeScreen.ts @@ -0,0 +1,34 @@ +import type { APIGuildWelcomeScreen } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { kData } from '../utils/symbols.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a welcome screen on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks has substructure `GuildWelcomeScreenChannel`, which needs to be instantiated and stored by any extending classes using it. + */ +export class GuildWelcomeScreen extends Structure< + APIGuildWelcomeScreen, + Omitted +> { + /** + * The template used for removing data from the raw data stored for each welcome screen. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the welcome screen. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The description of the welcome screen. + */ + public get description() { + return this[kData].description; + } +} diff --git a/packages/structures/src/guild/GuildWelcomeScreenChannel.ts b/packages/structures/src/guild/GuildWelcomeScreenChannel.ts new file mode 100644 index 000000000000..ab621faf4968 --- /dev/null +++ b/packages/structures/src/guild/GuildWelcomeScreenChannel.ts @@ -0,0 +1,54 @@ +import type { APIGuildWelcomeScreenChannel } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { kData } from '../utils/symbols.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a welcome screen channel on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + */ +export class GuildWelcomeScreenChannel extends Structure< + APIGuildWelcomeScreenChannel, + Omitted +> { + /** + * The template used for removing data from the raw data stored for each welcome screen channel. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the welcome screen channel. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The channel's ID. + */ + public get channelId() { + return this[kData].channel_id; + } + + /** + * The description of the welcome screen channel. + */ + public get description() { + return this[kData].description; + } + + /** + * The ID of the emoji shown (if the emoji is custom). + */ + public get emojiId() { + return this[kData].emoji_id; + } + + /** + * The name of the emoji if custom, the unicode character if standard, or `null` if no emoji is set. + */ + public get emojiName() { + return this[kData].emoji_name; + } +} diff --git a/packages/structures/src/guild/GuildWidget.ts b/packages/structures/src/guild/GuildWidget.ts new file mode 100644 index 000000000000..723bcef0e016 --- /dev/null +++ b/packages/structures/src/guild/GuildWidget.ts @@ -0,0 +1,70 @@ +import { DiscordSnowflake } from '@sapphire/snowflake'; +import type { APIGuildWidget } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { kData } from '../utils/symbols.js'; +import { isIdSet } from '../utils/type-guards.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents a guild widget on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks Intentionally does not export `channels`, or `members`, + * so extending classes can map each array to `GuildWidgetChannel[]`, and `GuildWidgetMember[]` respectively. + */ +export class GuildWidget extends Structure { + /** + * The template used for removing data from the raw data stored for each guild widget. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the guild widget. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The id of the guild. + */ + public get id() { + return this[kData].id; + } + + /** + * The name of the guild. + */ + public get name() { + return this[kData].name; + } + + /** + * Instant invite for the guild's specified widget invite channel. + */ + public get instantInvite() { + return this[kData].instant_invite; + } + + /** + * Number of online members in this guild. + */ + public get presenceCount() { + return this[kData].presence_count; + } + + /** + * The timestamp the guild was created at. + */ + public get createdTimestamp() { + return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null; + } + + /** + * The time the guild was created at. + */ + public get createdAt() { + const createdTimestamp = this.createdTimestamp; + return createdTimestamp ? new Date(createdTimestamp) : null; + } +} diff --git a/packages/structures/src/guild/IncidentsData.ts b/packages/structures/src/guild/IncidentsData.ts new file mode 100644 index 000000000000..b3f70a8a4bc1 --- /dev/null +++ b/packages/structures/src/guild/IncidentsData.ts @@ -0,0 +1,158 @@ +import type { APIIncidentsData } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { dateToDiscordISOTimestamp } from '../utils/optimization.js'; +import { kDmSpamDetectedAt, kDmsDisabledUntil, kInvitesDisabledUntil, kRaidDetectedAt } from '../utils/symbols.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents incident data on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + */ +export class IncidentsData< + Omitted extends keyof APIIncidentsData | '' = + | 'dm_spam_detected_at' + | 'dms_disabled_until' + | 'invites_disabled_until' + | 'raid_detected_at', +> extends Structure { + /** + * The template used for removing data from the raw data stored for each `IncidentsData` + * + * @remarks This template has defaults, if you want to remove additional data and keep the defaults, + * use `Object.defineProperties`. + */ + public static override readonly DataTemplate: Partial = { + set dm_spam_detected_at(_: string) {}, + set dms_disabled_until(_: string) {}, + set invites_disabled_until(_: string) {}, + set raid_detected_at(_: string) {}, + }; + + protected [kDmSpamDetectedAt]: number | null = null; + + protected [kDmsDisabledUntil]: number | null = null; + + protected [kInvitesDisabledUntil]: number | null = null; + + protected [kRaidDetectedAt]: number | null = null; + + /** + * @param data - The raw data from the API for the incidents data. + */ + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * When direct message spam was detected. + */ + public get dmSpamDetectedAt() { + const timestamp = this.dmSpamDetectedTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp direct message spam was detected. + */ + public get dmSpamDetectedTimestamp() { + return this[kDmSpamDetectedAt]; + } + + /** + * When direct messages will be enabled again. + */ + public get dmsDisabledUntil() { + const timestamp = this.dmsDisabledUntilTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp direct messages will be enabled again. + */ + public get dmsDisabledUntilTimestamp() { + return this[kDmsDisabledUntil]; + } + + /** + * When invites will be enabled again. + */ + public get invitesDisabledUntil() { + const timestamp = this.invitesDisabledUntilTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp invites will be enabled again. + */ + public get invitesDisabledUntilTimestamp() { + return this[kInvitesDisabledUntil]; + } + + /** + * When a raid was detected. + */ + public get raidDetectedAt() { + const timestamp = this.raidDetectedTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp a raid was detected. + */ + public get raidDetectedTimestamp() { + return this[kRaidDetectedAt]; + } + + /** + * {@inheritDoc Structure.optimizeData} + */ + protected override optimizeData(data: Partial): void { + if (data.dm_spam_detected_at) { + this[kDmSpamDetectedAt] = Date.parse(data.dm_spam_detected_at); + } + + if (data.dms_disabled_until) { + this[kDmsDisabledUntil] = Date.parse(data.dms_disabled_until); + } + + if (data.invites_disabled_until) { + this[kInvitesDisabledUntil] = Date.parse(data.invites_disabled_until); + } + + if (data.raid_detected_at) { + this[kRaidDetectedAt] = Date.parse(data.raid_detected_at); + } + } + + /** + * {@inheritDoc Structure.toJSON} + */ + public override toJSON() { + const clone = super.toJSON(); + + const dmSpamDetectedAt = this[kDmSpamDetectedAt]; + const dmsDisabledUntil = this[kDmsDisabledUntil]; + const invitesDisabledUntil = this[kInvitesDisabledUntil]; + const raidDetectedAt = this[kRaidDetectedAt]; + + if (dmSpamDetectedAt) { + clone.dm_spam_detected_at = dateToDiscordISOTimestamp(new Date(dmSpamDetectedAt)); + } + + if (dmsDisabledUntil) { + clone.dms_disabled_until = dateToDiscordISOTimestamp(new Date(dmsDisabledUntil)); + } + + if (invitesDisabledUntil) { + clone.invites_disabled_until = dateToDiscordISOTimestamp(new Date(invitesDisabledUntil)); + } + + if (raidDetectedAt) { + clone.raid_detected_at = dateToDiscordISOTimestamp(new Date(raidDetectedAt)); + } + + return clone; + } +} diff --git a/packages/structures/src/guild/IntegrationAccount.ts b/packages/structures/src/guild/IntegrationAccount.ts new file mode 100644 index 000000000000..a37f44d3fad4 --- /dev/null +++ b/packages/structures/src/guild/IntegrationAccount.ts @@ -0,0 +1,40 @@ +import type { APIIntegrationAccount } from 'discord-api-types/v10'; +import { Structure } from '../Structure.js'; +import { kData } from '../utils/symbols.js'; +import type { Partialize } from '../utils/types.js'; + +/** + * Represents an integration's account on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + */ +export class IntegrationAccount extends Structure< + APIIntegrationAccount, + Omitted +> { + /** + * The template used for removing data from the raw data stored for each integration account. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the integration account. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The id of the account. + */ + public get id() { + return this[kData].id; + } + + /** + * The name of the account. + */ + public get name() { + return this[kData].name; + } +} diff --git a/packages/structures/src/guild/index.ts b/packages/structures/src/guild/index.ts new file mode 100644 index 000000000000..97b8c43f753b --- /dev/null +++ b/packages/structures/src/guild/index.ts @@ -0,0 +1,12 @@ +export * from './Ban.js'; +export * from './Guild.js'; +export * from './GuildIntegration.js'; +export * from './GuildMember.js'; +export * from './GuildPreview.js'; +export * from './GuildTemplate.js'; +export * from './GuildWelcomeScreen.js'; +export * from './GuildWelcomeScreenChannel.js'; +export * from './GuildWidget.js'; +export * from './IncidentsData.js'; +export * from './IntegrationAccount.js'; +export * from './scheduled-event/index.js'; diff --git a/packages/structures/src/guild/scheduled-event/GuildScheduledEvent.ts b/packages/structures/src/guild/scheduled-event/GuildScheduledEvent.ts new file mode 100644 index 000000000000..13dff0d1a8b9 --- /dev/null +++ b/packages/structures/src/guild/scheduled-event/GuildScheduledEvent.ts @@ -0,0 +1,222 @@ +import { DiscordSnowflake } from '@sapphire/snowflake'; +import { + CDNRoutes, + RouteBases, + type APIGuildScheduledEvent, + type GuildScheduledEventCoverFormat, +} from 'discord-api-types/v10'; +import { Structure } from '../../Structure.js'; +import { dateToDiscordISOTimestamp } from '../../utils/optimization.js'; +import { kData, kScheduledEndTime, kScheduledStartTime } from '../../utils/symbols.js'; +import { isFieldSet, isIdSet } from '../../utils/type-guards.js'; +import type { Partialize } from '../../utils/types.js'; + +/** + * Represents a guild scheduled event on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks has substructures `User`, `GuildScheduledEventRecurrenceRule` and `GuildScheduledEventEntityMetadata`, which need to be instantiated and stored by any extending classes using it. + */ +export class GuildScheduledEvent< + Omitted extends keyof APIGuildScheduledEvent | '' = 'scheduled_end_time' | 'scheduled_start_time', +> extends Structure { + /** + * The template used for removing data from the raw data stored for each `GuildScheduledEvent` + * + * @remarks This template has defaults, if you want to remove additional data and keep the defaults, + * use `Object.defineProperties`. + */ + public static override readonly DataTemplate: Partial = { + set scheduled_end_time(_: string) {}, + set scheduled_start_time(_: string) {}, + }; + + protected [kScheduledEndTime]: number | null = null; + + protected [kScheduledStartTime]: number | null = null; + + /** + *param data - The raw data from the API for the scheduled event. + */ + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * The id of the scheduled event. + */ + public get id() { + return this[kData].id; + } + + /** + * The guild id which the scheduled event belongs to. + */ + public get guildId() { + return this[kData].guild_id; + } + + /** + * + * The channel id in which the scheduled event will be hosted, or `null` if {@link entityType} is {@link GuildScheduledEventEntityType.External} + * + * @see {@link https://discord.com/developers/docs/resources/guild-scheduled-event#guild-scheduled-event-object-field-requirements-by-entity-type} + */ + public get channelId() { + return this[kData].channel_id; + } + + /** + * The id of the user that created the scheduled event. + */ + public get creatorId() { + return this[kData].creator_id; + } + + /** + * The name of the scheduled event. + */ + public get name() { + return this[kData].name; + } + + /** + * The description of the scheduled event + */ + public get description() { + return this[kData].description; + } + + /** + * The privacy level of the scheduled event. + */ + public get privacyLevel() { + return this[kData].privacy_level; + } + + /** + * The status of the scheduled event. + */ + public get status() { + return this[kData].status; + } + + /** + * The type of the scheduled event. + */ + public get entityType() { + return this[kData].entity_type; + } + + /** + * The id of the entity associated with a guild scheduled event. + */ + public get entityId() { + return this[kData].entity_id; + } + + /** + * The number of users subscribed to the scheduled event. + */ + public get userCount() { + return this[kData].user_count; + } + + /** + * The cover image hash of the scheduled event. + */ + public get image() { + return this[kData].image; + } + + /** + * Get the URL to the scheduled event cover image. + * + * @param format - the file format to use. + */ + public imageURL(format: GuildScheduledEventCoverFormat) { + return isIdSet(this[kData].id) && isFieldSet(this[kData], 'image', 'string') + ? `${RouteBases.cdn}${CDNRoutes.guildScheduledEventCover(this[kData].id.toString(), this[kData].image, format)}` + : null; + } + + /** + * The time the guild scheduled event will start at. + */ + public get scheduledStartAt() { + const timestamp = this.scheduledStartTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp the guild scheduled event will start it. + */ + public get scheduledStartTimestamp() { + return this[kScheduledStartTime]; + } + + /** + * The time the guild scheduled event will end at. + */ + public get scheduledEndAt() { + const timestamp = this.scheduledEndTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The time at which the guild event will end, or `null` if the event does not have a scheduled time to end. + * + */ + public get scheduledEndTimestamp() { + return this[kScheduledEndTime]; + } + + /** + * The timestamp the scheduled event was created at. + */ + public get createdTimestamp() { + return isIdSet(this.id) ? DiscordSnowflake.timestampFrom(this.id) : null; + } + + /** + * The time the scheduled event was created at. + */ + public get createdAt() { + const createdTimestamp = this.createdTimestamp; + return createdTimestamp ? new Date(createdTimestamp) : null; + } + + /** + * {@inheritDoc Structure.optimizeData} + */ + protected override optimizeData(data: Partial): void { + if (data.scheduled_end_time) { + this[kScheduledEndTime] = Date.parse(data.scheduled_end_time); + } + + if (data.scheduled_start_time) { + this[kScheduledStartTime] = Date.parse(data.scheduled_start_time); + } + } + + /** + * {@inheritDoc Structure.toJSON} + */ + public override toJSON() { + const clone = super.toJSON(); + + const scheduledEndTime = this[kScheduledEndTime]; + const scheduledStartTime = this[kScheduledStartTime]; + + if (scheduledEndTime) { + clone.scheduled_end_time = dateToDiscordISOTimestamp(new Date(scheduledEndTime)); + } + + if (scheduledStartTime) { + clone.scheduled_start_time = dateToDiscordISOTimestamp(new Date(scheduledStartTime)); + } + + return clone; + } +} diff --git a/packages/structures/src/guild/scheduled-event/GuildScheduledEventEntityMetadata.ts b/packages/structures/src/guild/scheduled-event/GuildScheduledEventEntityMetadata.ts new file mode 100644 index 000000000000..1823c258d40b --- /dev/null +++ b/packages/structures/src/guild/scheduled-event/GuildScheduledEventEntityMetadata.ts @@ -0,0 +1,32 @@ +import type { APIGuildScheduledEventEntityMetadata } from 'discord-api-types/v10'; +import { Structure } from '../../Structure.js'; +import { kData } from '../../utils/symbols.js'; +import type { Partialize } from '../../utils/types.js'; + +/** + * Represents the additional entity metadata of a scheduled event on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + */ +export class GuildScheduledEventEntityMetadata< + Omitted extends keyof APIGuildScheduledEventEntityMetadata | '' = '', +> extends Structure { + /** + * The template used for removing data from the entity metadata. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the entity metadata. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The location of the event. + */ + public get location() { + return this[kData].location; + } +} diff --git a/packages/structures/src/guild/scheduled-event/GuildScheduledEventRecurrenceRule.ts b/packages/structures/src/guild/scheduled-event/GuildScheduledEventRecurrenceRule.ts new file mode 100644 index 000000000000..bbc150531dda --- /dev/null +++ b/packages/structures/src/guild/scheduled-event/GuildScheduledEventRecurrenceRule.ts @@ -0,0 +1,154 @@ +import type { + APIGuildScheduledEventRecurrenceRule, + GuildScheduledEventRecurrenceRuleFrequency, +} from 'discord-api-types/v10'; +import { Structure } from '../../Structure.js'; +import { dateToDiscordISOTimestamp } from '../../utils/optimization.js'; +import { kData, kEnd, kStart } from '../../utils/symbols.js'; +import type { Partialize } from '../../utils/types.js'; + +/** + * Represents the recurrence rule of a scheduled event on Discord. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + * @remarks has substructure `GuildScheduledEventRecurenceRuleNWeekday` which needs to be instantiated and stored by an extending class using it. + */ +export class GuildScheduledEventRecurrenceRule< + Omitted extends keyof APIGuildScheduledEventRecurrenceRule | '' = 'end' | 'start', +> extends Structure { + /** + * The template used for removing data from the raw data stored for each `GuildScheduledEventRecurrenceRule` + * + * @remarks This template has defaults, if you want to remove additional data and keep the defaults, + * use `Object.defineProperties`. + */ + public static override readonly DataTemplate: Partial = { + set start(_: string) {}, + set end(_: string) {}, + }; + + protected [kStart]: number | null = null; + + protected [kEnd]: number | null = null; + + /** + * @param data - The raw data from the API for the recurrence rule. + */ + public constructor(data: Partialize) { + super(data); + this.optimizeData(data); + } + + /** + * How often the event occurs. + */ + public get frequency() { + return this[kData].frequency; + } + + /** + * The spacing between events, defined by `frequency`. + * For example, a `frequency` of {@link GuildScheduledEventRecurrenceRuleFrequency.Weekly} and an `interval` of `2` would be "every-other-week" + */ + public get interval() { + return this[kData].interval; + } + + /** + * Set of specific days within a week for the event to recur on. + */ + public get byWeekday() { + return this[kData].by_weekday; + } + + /** + * Set of specific months to recur on. + */ + public get byMonth() { + return this[kData].by_month; + } + + /** + * Set of specific dates within a month to recur on. + */ + public get byMonthDay() { + return this[kData].by_month_day; + } + + /** + * Set of days within a year to recur on. (1-364) + */ + public get byYearDay() { + return this[kData].by_year_day; + } + + /** + * The total amount of times that the event is allowed to recur before stopping. + */ + public get count() { + return this[kData].count; + } + + /** + * The time the recurrence rule interval starts at. + */ + public get startAt() { + const timestamp = this.startTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp the recurrence rule interval starts at. + */ + public get startTimestamp() { + return this[kStart]; + } + + /** + * The time the recurrence rule interval ends at. + */ + public get endAt() { + const timestamp = this.endTimestamp; + return timestamp ? new Date(timestamp) : null; + } + + /** + * The timestamp the recurrence rule interval ends at. + */ + public get endTimestamp() { + return this[kEnd]; + } + + /** + * {@inheritDoc Structure.optimizeData} + */ + protected override optimizeData(data: Partial) { + if (data.start) { + this[kStart] = Date.parse(data.start); + } + + if (data.end) { + this[kEnd] = Date.parse(data.end); + } + } + + /** + * {@inheritDoc Structure.toJSON} + */ + public override toJSON() { + const clone = super.toJSON(); + + const start = this[kStart]; + const end = this[kEnd]; + + if (start) { + clone.start = dateToDiscordISOTimestamp(new Date(start)); + } + + if (end) { + clone.end = dateToDiscordISOTimestamp(new Date(end)); + } + + return clone; + } +} diff --git a/packages/structures/src/guild/scheduled-event/GuildScheduledEventRecurrenceRuleNWeekday.ts b/packages/structures/src/guild/scheduled-event/GuildScheduledEventRecurrenceRuleNWeekday.ts new file mode 100644 index 000000000000..5478efa89264 --- /dev/null +++ b/packages/structures/src/guild/scheduled-event/GuildScheduledEventRecurrenceRuleNWeekday.ts @@ -0,0 +1,40 @@ +/* eslint-disable id-length */ +import type { APIGuildScheduledEventRecurrenceRuleNWeekday } from 'discord-api-types/v10'; +import { Structure } from '../../Structure.js'; +import { kData } from '../../utils/symbols.js'; +import type { Partialize } from '../../utils/types.js'; + +/** + * Represents the N_Weekday structure of a scheduled event's recurrence rule. + * + * @typeParam Omitted - Specify the properties that will not be stored in the raw data field as a union, implement via `DataTemplate` + */ +export class GuildScheduledEventRecurrenceRuleNWeekday< + Omitted extends keyof APIGuildScheduledEventRecurrenceRuleNWeekday | '' = '' | '', +> extends Structure { + /** + * The template used for removing data from the raw data stored from the N weekday. + */ + public static override readonly DataTemplate: Partial = {}; + + /** + * @param data - The raw data from the API for the recurrence rule's N weekday. + */ + public constructor(data: Partialize) { + super(data); + } + + /** + * The week to reoccur on. (1-5) + */ + public get n() { + return this[kData].n; + } + + /** + * The day within the week of reoccur on. + */ + public get day() { + return this[kData].day; + } +} diff --git a/packages/structures/src/guild/scheduled-event/index.ts b/packages/structures/src/guild/scheduled-event/index.ts new file mode 100644 index 000000000000..1e97947afb61 --- /dev/null +++ b/packages/structures/src/guild/scheduled-event/index.ts @@ -0,0 +1,4 @@ +export * from './GuildScheduledEvent.js'; +export * from './GuildScheduledEventEntityMetadata.js'; +export * from './GuildScheduledEventRecurrenceRule.js'; +export * from './GuildScheduledEventRecurrenceRuleNWeekday.js'; diff --git a/packages/structures/src/index.ts b/packages/structures/src/index.ts index f5bfbcecd00d..c036a3b25cde 100644 --- a/packages/structures/src/index.ts +++ b/packages/structures/src/index.ts @@ -3,6 +3,7 @@ export * from './bitfields/index.js'; export * from './channels/index.js'; export * from './emojis/index.js'; export * from './entitlements/index.js'; +export * from './guild/index.js'; export * from './interactions/index.js'; export * from './invites/index.js'; export * from './messages/index.js'; diff --git a/packages/structures/src/utils/symbols.ts b/packages/structures/src/utils/symbols.ts index 28fa474a6b89..d327ad62b33a 100644 --- a/packages/structures/src/utils/symbols.ts +++ b/packages/structures/src/utils/symbols.ts @@ -10,6 +10,27 @@ export const kArchiveTimestamp = Symbol.for('djs.structures.archiveTimestamp'); export const kStartsTimestamp = Symbol.for('djs.structures.startsTimestamp'); export const kEndsTimestamp = Symbol.for('djs.structures.endsTimestamp'); +export const kPermissions = Symbol.for('djs.structures.permissions'); + +export const kDmsDisabledUntil = Symbol.for('djs.structures.dmsDisabledUntil'); +export const kDmSpamDetectedAt = Symbol.for('djs.structures.dmSpamDetectedAt'); +export const kInvitesDisabledUntil = Symbol.for('djs.structures.invitesDisabledUntil'); +export const kRaidDetectedAt = Symbol.for('djs.structures.raidDetectedAt'); + +export const kJoinedAt = Symbol.for('djs.structures.joinedAt'); +export const kCommunicationDisabledUntil = Symbol.for('djs.structures.communicationDisabledUntil'); +export const kPremiumSince = Symbol.for('djs.structures.premiumSince'); + +export const kSyncedAt = Symbol.for('djs.structures.syncedAt'); + +export const kCreatedAt = Symbol.for('djs.structures.createdAt'); +export const kUpdatedAt = Symbol.for('djs.structures.updatedAt'); + +export const kScheduledEndTime = Symbol.for('djs.structures.scheduledEndTime'); +export const kScheduledStartTime = Symbol.for('djs.structures.scheduledStartTime'); + +export const kStart = Symbol.for('djs.structures.start'); +export const kEnd = Symbol.for('djs.structures.end'); export const kCurrentPeriodStartTimestamp = Symbol.for('djs.structures.currentPeriodStartTimestamp'); export const kCurrentPeriodEndTimestamp = Symbol.for('djs.structures.currentPeriodEndTimestamp'); export const kCanceledTimestamp = Symbol.for('djs.structures.canceledTimestamp');