// SPDX-License-Identifier: AGPL-3.0-or-later import RuntimeConfig from '@app/features/app/state/RuntimeConfig'; import DeveloperOptions from '@app/features/devtools/state/DeveloperOptions'; import {buildMediaProxyURL} from '@app/features/messaging/utils/MediaProxyUtils'; import {cdnUrl, mediaUrl, setPathQueryParams} from '@app/features/messaging/utils/MessagingUrlUtils'; import type {User} from '@app/features/user/models/User'; import { getDefaultAvatarIndex, getDefaultAvatarPrimaryColor as getSharedDefaultAvatarPrimaryColor, normalizeEndpoint, parseAvatarHash, } from '@app/features/user/utils/AvatarMediaUtils'; import { MEDIA_PROXY_AVATAR_SIZE_DEFAULT, MEDIA_PROXY_ICON_SIZE_DEFAULT, } from '@fluxer/constants/src/MediaProxyAssetSizes'; import type {MediaProxyImageSize} from '@fluxer/constants/src/MediaProxyImageSizes'; const getDefaultAvatar = (index: number): string => cdnUrl(`avatars/${index}.png`); export function getDefaultAvatarPrimaryColor(id: string) { return getSharedDefaultAvatarPrimaryColor(id); } type AvatarOptions = Pick; type BannerOptions = Pick; interface IconOptions { id: string; icon: string | null; } type MediaURLParams = { path: string; id: string; hash: string; size?: MediaProxyImageSize; format: string; animated?: boolean; endpoint?: string; }; const parseMediaHashForRequest = (value: string, animated = false) => { const {animated: isAnimated} = parseAvatarHash(value); return { hash: value, animated: isAnimated ? animated : undefined, }; }; const getMediaURL = ({path, id, hash, size, format, animated, endpoint}: MediaURLParams) => { if (DeveloperOptions.forceRenderPlaceholders) { return ''; } const baseEndpoint = endpoint ?? RuntimeConfig.mediaEndpoint; if (!baseEndpoint) { return ''; } const basePath = `${path}/${id}/${hash}.${format}`; const url = size ? setPathQueryParams(basePath, {size}) : basePath; const proxyOptions = animated === undefined ? undefined : {animated}; return buildMediaProxyURL(`${normalizeEndpoint(baseEndpoint)}/${url}`, proxyOptions); }; type GuildMemberMediaURLParams = { path: string; guildId: string; userId: string; hash: string; size?: MediaProxyImageSize; format: string; animated?: boolean; }; const getGuildMemberMediaURL = ({path, guildId, userId, hash, size, format, animated}: GuildMemberMediaURLParams) => { if (DeveloperOptions.forceRenderPlaceholders) { return ''; } const baseEndpoint = RuntimeConfig.mediaEndpoint; if (!baseEndpoint) { return ''; } const basePath = `guilds/${guildId}/users/${userId}/${path}/${hash}.${format}`; const url = size ? setPathQueryParams(basePath, {size}) : basePath; const proxyOptions = animated === undefined ? undefined : {animated}; return buildMediaProxyURL(`${normalizeEndpoint(baseEndpoint)}/${url}`, proxyOptions); }; const buildWebpMediaUrl = (params: Omit) => getMediaURL({...params, format: 'webp'}); const buildPngMediaUrl = (params: Omit) => getMediaURL({...params, format: 'png'}); const buildGuildMemberWebpUrl = (params: Omit) => getGuildMemberMediaURL({...params, format: 'webp'}); export function getUserAvatarURL( {id, avatar}: AvatarOptions, animated = false, size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT, ) { if (!avatar) { return getDefaultAvatar(getDefaultAvatarIndex(id)); } const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated); return buildWebpMediaUrl({ path: 'avatars', id, hash, size, animated: shouldAnimate, }); } export function getUserNotificationAvatarURL( {id, avatar}: AvatarOptions, size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT, ) { if (!avatar) { return getDefaultAvatar(getDefaultAvatarIndex(id)); } const {hash, animated} = parseMediaHashForRequest(avatar, false); return buildPngMediaUrl({ path: 'avatars', id, hash, size, animated, }); } export function getUserBannerURL({id, banner}: BannerOptions, animated = false, size: MediaProxyImageSize = 1024) { if (!banner) { return ''; } const {hash, animated: shouldAnimate} = parseMediaHashForRequest(banner, animated); return buildWebpMediaUrl({ path: 'banners', id, hash, size, animated: shouldAnimate, }); } export function getGuildIconURL({id, icon}: IconOptions, animated = false) { if (!icon) { return ''; } const {hash, animated: shouldAnimate} = parseMediaHashForRequest(icon, animated); return buildWebpMediaUrl({ path: 'icons', id, hash, size: MEDIA_PROXY_ICON_SIZE_DEFAULT, animated: shouldAnimate, }); } export function getGuildSplashURL( { id, splash, }: { id: string; splash: string | null; }, size: MediaProxyImageSize = 1024, ) { if (!splash) { return ''; } return buildWebpMediaUrl({ path: 'splashes', id, hash: splash, size, }); } export function getGuildDiscoverySplashURL({id, splash}: {id: string; splash: string | null}) { if (!splash) { return ''; } return buildWebpMediaUrl({ path: 'discovery-splashes', id, hash: splash, size: 1024, }); } export function getGuildBannerURL( { id, banner, }: { id: string; banner: string | null; }, animated = false, ) { if (!banner) { return ''; } const {hash, animated: shouldAnimate} = parseMediaHashForRequest(banner, animated); return buildWebpMediaUrl({ path: 'banners', id, hash, size: 1024, animated: shouldAnimate, }); } export function getGuildMemberAvatarURL({ guildId, userId, avatar, memberAvatar, animated = false, size = MEDIA_PROXY_AVATAR_SIZE_DEFAULT, }: { guildId: string; userId: string; avatar: string | null; memberAvatar?: string | null; animated?: boolean; size?: MediaProxyImageSize; }) { if (memberAvatar) { const {hash, animated: shouldAnimate} = parseMediaHashForRequest(memberAvatar, animated); return buildGuildMemberWebpUrl({ path: 'avatars', guildId, userId, hash, size, animated: shouldAnimate, }); } if (avatar) { const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated); return buildWebpMediaUrl({ path: 'avatars', id: userId, hash, size, animated: shouldAnimate, }); } return getDefaultAvatar(getDefaultAvatarIndex(userId)); } export function getGuildMemberDisplayAvatarURL({ guildId, user, memberAvatar, avatarUnset = false, animated = false, size = MEDIA_PROXY_AVATAR_SIZE_DEFAULT, }: { guildId: string; user: AvatarOptions; memberAvatar?: string | null; avatarUnset?: boolean; animated?: boolean; size?: MediaProxyImageSize; }) { if (avatarUnset) { return getUserAvatarURL({id: user.id, avatar: null}, animated, size); } return getGuildMemberAvatarURL({ guildId, userId: user.id, avatar: user.avatar, memberAvatar, animated, size, }); } export function getGuildMemberBannerURL({ guildId, userId, banner, memberBanner, animated = false, size = 1024, }: { guildId: string; userId: string; banner: string | null; memberBanner?: string | null; animated?: boolean; size?: MediaProxyImageSize; }) { if (memberBanner) { const {hash, animated: shouldAnimate} = parseMediaHashForRequest(memberBanner, animated); return buildGuildMemberWebpUrl({ path: 'banners', guildId, userId, hash, size, animated: shouldAnimate, }); } if (banner) { const {hash, animated: shouldAnimate} = parseMediaHashForRequest(banner, animated); return buildWebpMediaUrl({ path: 'banners', id: userId, hash, size, animated: shouldAnimate, }); } return ''; } export function getUserAvatarURLWithProxy( options: AvatarOptions, endpoint: string, animated = false, size: MediaProxyImageSize = MEDIA_PROXY_AVATAR_SIZE_DEFAULT, ) { if (!endpoint) { return getUserAvatarURL(options, animated, size); } const {id, avatar} = options; if (!avatar) { return getDefaultAvatar(getDefaultAvatarIndex(id)); } const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated); return buildWebpMediaUrl({ path: 'avatars', id, hash, size, animated: shouldAnimate, endpoint, }); } export function getGuildEmbedSplashURL( { id, embedSplash, }: { id: string; embedSplash: string | null; }, size: MediaProxyImageSize = 1024, ) { if (!embedSplash) { return ''; } return buildWebpMediaUrl({ path: 'embed-splashes', id, hash: embedSplash, size, }); } export function getChannelIconURL( {id, icon}: IconOptions, size: MediaProxyImageSize = MEDIA_PROXY_ICON_SIZE_DEFAULT, animated = false, ) { if (!icon) { return ''; } const {hash, animated: shouldAnimate} = parseMediaHashForRequest(icon, animated); return buildWebpMediaUrl({ path: 'icons', id, hash, size, animated: shouldAnimate, }); } export function getWebhookAvatarURL({id, avatar}: AvatarOptions, animated = false) { if (!avatar) { return getDefaultAvatar(getDefaultAvatarIndex(id)); } const {hash, animated: shouldAnimate} = parseMediaHashForRequest(avatar, animated); return buildWebpMediaUrl({ path: 'avatars', id, hash, size: MEDIA_PROXY_AVATAR_SIZE_DEFAULT, animated: shouldAnimate, }); } const EMOJI_URL_CACHE = new Map(); const EMOJI_URL_CACHE_LIMIT = 4096; export function getEmojiURL({id, animated}: {id: string; animated?: boolean}) { if (DeveloperOptions.forceRenderPlaceholders) { return ''; } const animatedFlag = Boolean(animated); const key = animatedFlag ? `a:${id}` : `s:${id}`; const cached = EMOJI_URL_CACHE.get(key); if (cached !== undefined) return cached; const result = mediaUrl(setPathQueryParams(`emojis/${id}.webp`, {v: 5}), {animated: animatedFlag}); if (EMOJI_URL_CACHE.size >= EMOJI_URL_CACHE_LIMIT) EMOJI_URL_CACHE.clear(); EMOJI_URL_CACHE.set(key, result); return result; } type StickerSize = 160 | 320; export function getStickerURL({id, animated, size = 320}: {id: string; animated?: boolean; size?: StickerSize}) { if (DeveloperOptions.forceRenderPlaceholders) { return ''; } const safeSize: StickerSize = size === 320 ? 320 : 160; return mediaUrl(setPathQueryParams(`stickers/${id}.webp`, {size: safeSize}), {animated: Boolean(animated)}); } export function fileToBase64(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(file); }); }