diff --git a/src/helpers/guilds/guild_banner_url.ts b/src/helpers/guilds/guild_banner_url.ts index b539b27be..f083f7141 100644 --- a/src/helpers/guilds/guild_banner_url.ts +++ b/src/helpers/guilds/guild_banner_url.ts @@ -1,16 +1,30 @@ import type { DiscordImageFormat } from "../../types/misc/image_format.ts"; import type { DiscordImageSize } from "../../types/misc/image_size.ts"; import { endpoints } from "../../util/constants.ts"; +import { iconBigintToHash } from "../../util/hash.ts"; import { formatImageURL } from "../../util/utils.ts"; /** The full URL of the banner from Discords CDN. Undefined if no banner is set. */ export function guildBannerURL( id: bigint, - banner?: string, - size: DiscordImageSize = 128, - format?: DiscordImageFormat, + options: { + banner?: string | bigint; + size?: DiscordImageSize; + format?: DiscordImageFormat; + animated?: boolean; + }, ) { - return banner - ? formatImageURL(endpoints.GUILD_BANNER(id, banner), size, format) + return options.banner + ? formatImageURL( + endpoints.GUILD_BANNER( + id, + typeof options.banner === "string" ? options.banner : iconBigintToHash( + options.banner, + options.animated ?? true, + ), + ), + options.size || 128, + options.format, + ) : undefined; } diff --git a/src/helpers/guilds/guild_icon_url.ts b/src/helpers/guilds/guild_icon_url.ts index 5a4c2104a..e9fb44bbd 100644 --- a/src/helpers/guilds/guild_icon_url.ts +++ b/src/helpers/guilds/guild_icon_url.ts @@ -1,16 +1,30 @@ import type { DiscordImageFormat } from "../../types/misc/image_format.ts"; import type { DiscordImageSize } from "../../types/misc/image_size.ts"; import { endpoints } from "../../util/constants.ts"; +import { iconBigintToHash } from "../../util/hash.ts"; import { formatImageURL } from "../../util/utils.ts"; /** The full URL of the icon from Discords CDN. Undefined when no icon is set. */ export function guildIconURL( id: bigint, - icon?: string, - size: DiscordImageSize = 128, - format?: DiscordImageFormat, + options: { + icon?: string | bigint; + size?: DiscordImageSize; + format?: DiscordImageFormat; + animated?: boolean; + }, ) { - return icon - ? formatImageURL(endpoints.GUILD_ICON(id, icon), size, format) + return options.icon + ? formatImageURL( + endpoints.GUILD_ICON( + id, + typeof options.icon === "string" ? options.icon : iconBigintToHash( + options.icon, + options.animated ?? true, + ), + ), + options.size || 128, + options.format, + ) : undefined; } diff --git a/src/helpers/guilds/guild_splash_url.ts b/src/helpers/guilds/guild_splash_url.ts index b8aef6d56..70ec7969d 100644 --- a/src/helpers/guilds/guild_splash_url.ts +++ b/src/helpers/guilds/guild_splash_url.ts @@ -1,20 +1,30 @@ import type { DiscordImageFormat } from "../../types/misc/image_format.ts"; import type { DiscordImageSize } from "../../types/misc/image_size.ts"; import { endpoints } from "../../util/constants.ts"; +import { iconBigintToHash } from "../../util/hash.ts"; import { formatImageURL } from "../../util/utils.ts"; /** The full URL of the splash from Discords CDN. Undefined if no splash is set. */ export function guildSplashURL( id: bigint, - splash?: string, - size: DiscordImageSize = 128, - format?: DiscordImageFormat, + options: { + splash?: string | bigint; + size?: DiscordImageSize; + format?: DiscordImageFormat; + animated?: boolean; + }, ) { - return splash + return options.splash ? formatImageURL( - endpoints.GUILD_SPLASH(id, splash), - size, - format, + endpoints.GUILD_SPLASH( + id, + typeof options.splash === "string" ? options.splash : iconBigintToHash( + options.splash, + options.animated ?? true, + ), + ), + options.size || 128, + options.format, ) : undefined; } diff --git a/src/helpers/members/avatar_url.ts b/src/helpers/members/avatar_url.ts index 0f9ad36da..40b98558f 100644 --- a/src/helpers/members/avatar_url.ts +++ b/src/helpers/members/avatar_url.ts @@ -1,17 +1,31 @@ import type { DiscordImageFormat } from "../../types/misc/image_format.ts"; import type { DiscordImageSize } from "../../types/misc/image_size.ts"; import { endpoints } from "../../util/constants.ts"; +import { iconBigintToHash } from "../../util/hash.ts"; import { formatImageURL } from "../../util/utils.ts"; /** The users custom avatar or the default avatar if you don't have a member object. */ export function avatarURL( userId: bigint, discriminator: bigint, - avatar?: string | null, - size: DiscordImageSize = 128, - format?: DiscordImageFormat, + options: { + avatar?: string | bigint; + size?: DiscordImageSize; + format?: DiscordImageFormat; + animated?: boolean; + }, ) { - return avatar - ? formatImageURL(endpoints.USER_AVATAR(userId, avatar), size, format) + return options.avatar + ? formatImageURL( + endpoints.USER_AVATAR( + userId, + typeof options.avatar === "string" ? options.avatar : iconBigintToHash( + options.avatar, + options.animated ?? true, + ), + ), + options.size || 128, + options.format, + ) : endpoints.USER_DEFAULT_AVATAR(Number(discriminator) % 5); } diff --git a/src/structures/guild.ts b/src/structures/guild.ts index 75bc4a09b..96c3a6ae3 100644 --- a/src/structures/guild.ts +++ b/src/structures/guild.ts @@ -28,6 +28,7 @@ import type { DiscordImageSize } from "../types/misc/image_size.ts"; import { snowflakeToBigint } from "../util/bigint.ts"; import { cacheMembers } from "../util/cache_members.ts"; import { Collection } from "../util/collection.ts"; +import { iconHashToBigInt } from "../util/hash.ts"; import { createNewProp } from "../util/utils.ts"; import { DiscordenoChannel } from "./channel.ts"; import { DiscordenoMember } from "./member.ts"; @@ -58,6 +59,12 @@ export const guildToggles = { unavailable: 8n, /** Whether this server is an nsfw guild */ nsfw: 16n, + /** Whether this server's icon is animated */ + animatedIcon: 32n, + /** Whether this server's banner is animated. */ + animatedBanner: 64n, + /** Whether this server's splash is animated. */ + animatedSplash: 128n, }; const baseGuild: Partial = { @@ -98,10 +105,20 @@ const baseGuild: Partial = { return Boolean(this.features?.includes(DiscordGuildFeatures.Verified)); }, bannerURL(size, format) { - return guildBannerURL(this.id!, this.banner!, size, format); + return guildBannerURL(this.id!, { + banner: this.banner!, + size, + format, + animated: this.animatedBanner!, + }); }, splashURL(size, format) { - return guildSplashURL(this.id!, this.splash!, size, format); + return guildSplashURL(this.id!, { + splash: this.splash!, + size, + format, + animated: this.animatedSplash, + }); }, delete() { return deleteGuild(this.id!); @@ -128,7 +145,12 @@ const baseGuild: Partial = { return getInvites(this.id!); }, iconURL(size, format) { - return guildIconURL(this.id!, this.icon!, size, format); + return guildIconURL(this.id!, { + icon: this.icon!, + size, + format, + animated: this.animatedIcon!, + }); }, leave() { return leaveGuild(this.id!); @@ -148,6 +170,15 @@ const baseGuild: Partial = { get nsfw() { return Boolean(this.bitfield! & guildToggles.nsfw); }, + get animatedIcon() { + return Boolean(this.bitfield! & guildToggles.animatedIcon); + }, + get animatedBanner() { + return Boolean(this.bitfield! & guildToggles.animatedBanner); + }, + get animatedSplash() { + return Boolean(this.bitfield! & guildToggles.animatedSplash); + }, }; export async function createDiscordenoGuild( @@ -163,6 +194,9 @@ export async function createDiscordenoGuild( joinedAt = "", emojis, members = [], + icon, + splash, + banner, ...rest } = data; @@ -215,6 +249,20 @@ export async function createDiscordenoGuild( ); } + const hashes = [ + { name: "icon", toggle: guildToggles.animatedIcon, value: icon }, + { name: "banner", toggle: guildToggles.animatedBanner, value: banner }, + { name: "splash", toggle: guildToggles.animatedSplash, value: splash }, + ]; + + for (const hash of hashes) { + const transformed = hash.value ? iconHashToBigInt(hash.value) : undefined; + if (transformed) { + props.icon = createNewProp(hash.value); + if (transformed.animated) bitfield |= hash.toggle; + } + } + const guild: DiscordenoGuild = Object.create(baseGuild, { ...props, shardId: createNewProp(shardId), @@ -285,6 +333,12 @@ export interface DiscordenoGuild extends rulesChannelId?: bigint; /** The id of the channel where admins and moderators of Community guilds receive notices from Discord */ publicUpdatesChannelId?: bigint; + /** Whether this server's icon is animated */ + animatedIcon: boolean; + /** Whether this server's banner is animated. */ + animatedBanner: boolean; + /** Whether this server's splash is animated. */ + animatedSplash: boolean; /** The id of the shard this guild is bound to */ shardId: number; /** Total number of members in this guild */ diff --git a/src/structures/member.ts b/src/structures/member.ts index 3ee3ff30d..2805114b4 100644 --- a/src/structures/member.ts +++ b/src/structures/member.ts @@ -19,6 +19,7 @@ import type { DiscordImageSize } from "../types/misc/image_size.ts"; import type { User } from "../types/users/user.ts"; import { snowflakeToBigint } from "../util/bigint.ts"; import { Collection } from "../util/collection.ts"; +import { iconHashToBigInt } from "../util/hash.ts"; import { createNewProp } from "../util/utils.ts"; import { DiscordenoGuild } from "./guild.ts"; @@ -36,11 +37,16 @@ export const memberToggles = { mfaEnabled: 4n, /** Whether the email on this account has been verified */ verified: 8n, + /** Whether the users avatar is animated. */ + animatedAvatar: 16n, }; const baseMember: Partial = { get avatarURL() { - return avatarURL(this.id!, this.discriminator!, this.avatar!); + return avatarURL(this.id!, this.discriminator!, { + avatar: this.avatar!, + animated: this.animatedAvatar, + }); }, get mention() { return `<@!${this.id!}>`; @@ -54,9 +60,12 @@ const baseMember: Partial = { return avatarURL( this.id!, this.discriminator!, - this.avatar!, - options.size, - options.format, + { + avatar: this.avatar!, + size: options.size, + format: options.format, + animated: this.animatedAvatar!, + }, ); }, guild(guildId) { @@ -98,6 +107,9 @@ const baseMember: Partial = { get verified() { return Boolean(this.bitfield! & memberToggles.verified); }, + get animatedAvatar() { + return Boolean(this.bitfield! & memberToggles.animatedAvatar); + }, }; export async function createDiscordenoMember( @@ -126,6 +138,12 @@ export async function createDiscordenoMember( continue; } + if (key === "icon") { + const transformed = value ? iconHashToBigInt(value) : undefined; + if (transformed?.animated) bitfield |= memberToggles.animatedAvatar; + props.icon = createNewProp(transformed?.bigint); + } + props[key] = createNewProp( MEMBER_SNOWFLAKES.includes(key) ? value ? snowflakeToBigint(value) : undefined @@ -193,6 +211,8 @@ export interface DiscordenoMember extends mention: string; /** The username#discriminator tag for this member */ tag: string; + /** Whether or not the avatar is animated. */ + animatedAvatar: boolean; // METHODS diff --git a/src/util/hash.ts b/src/util/hash.ts new file mode 100644 index 000000000..c0aaf6a68 --- /dev/null +++ b/src/util/hash.ts @@ -0,0 +1,18 @@ +export function iconHashToBigInt(hash: string) { + let animated = false; + + if (hash.startsWith("a_")) { + animated = true; + hash = hash.substring(2); + } + + return { + animated, + bigint: BigInt(`0x${hash}`), + }; +} + +export function iconBigintToHash(icon: bigint, animated = true) { + const hash = icon.toString(16); + return `${animated ? "a_" : ""}${hash}`; +}