/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import type {UserRecord} from '~/records/UserRecord'; import DeveloperOptionsStore from '~/stores/DeveloperOptionsStore'; import {buildMediaProxyURL} from '~/utils/MediaProxyUtils'; import {cdnUrl, mediaUrl} from '~/utils/UrlUtils'; const DEFAULT_AVATAR_PRIMARY_COLORS = [0x4641d9, 0xf0b100, 0x00bba7, 0x2b7fff, 0xad46ff, 0x6a7282]; const DEFAULT_AVATAR_COUNT = DEFAULT_AVATAR_PRIMARY_COLORS.length; const getDefaultAvatar = (index: number): string => cdnUrl(`avatars/${index}.png`); const getDefaultAvatarIndex = (id: string) => Number(BigInt(id) % BigInt(DEFAULT_AVATAR_COUNT)); export const getDefaultAvatarPrimaryColor = (id: string) => DEFAULT_AVATAR_PRIMARY_COLORS[getDefaultAvatarIndex(id)]; type AvatarOptions = Pick; type BannerOptions = Pick; interface IconOptions { id: string; icon: string | null; } const getMediaURL = ({ path, id, hash, size, format, }: { path: string; id: string; hash: string; size?: number; format: string; }) => { if (DeveloperOptionsStore.forceRenderPlaceholders) { return ''; } const basePath = `${path}/${id}/${hash}.${format}`; return size ? mediaUrl(`${basePath}?size=${size}`) : mediaUrl(basePath); }; const getGuildMemberMediaURL = ({ path, guildId, userId, hash, size, format, }: { path: string; guildId: string; userId: string; hash: string; size?: number; format: string; }) => { if (DeveloperOptionsStore.forceRenderPlaceholders) { return ''; } const basePath = `guilds/${guildId}/users/${userId}/${path}/${hash}.${format}`; return size ? mediaUrl(`${basePath}?size=${size}`) : mediaUrl(basePath); }; const parseAvatar = (avatar: string) => { const animated = avatar.startsWith('a_'); const hash = animated ? avatar.slice(2) : avatar; return { animated, hash, }; }; export const getUserAvatarURL = ({id, avatar}: AvatarOptions, animated = false) => { if (!avatar) { return getDefaultAvatar(getDefaultAvatarIndex(id)); } const parsedAvatar = parseAvatar(avatar); const shouldAnimate = parsedAvatar.animated ? animated : false; return getMediaURL({ path: 'avatars', id, hash: parsedAvatar.hash, size: 160, format: shouldAnimate ? 'gif' : 'webp', }); }; export const getUserAvatarURLWithProxy = ( {id, avatar}: AvatarOptions, mediaProxyEndpoint: string, animated = false, ) => { if (!avatar) { return getDefaultAvatar(getDefaultAvatarIndex(id)); } if (DeveloperOptionsStore.forceRenderPlaceholders) { return ''; } const parsedAvatar = parseAvatar(avatar); const shouldAnimate = parsedAvatar.animated ? animated : false; const format = shouldAnimate ? 'gif' : 'webp'; return buildMediaProxyURL(`${mediaProxyEndpoint}/avatars/${id}/${parsedAvatar.hash}.${format}?size=160`); }; export const getWebhookAvatarURL = ({id, avatar}: {id: string; avatar: string | null}, animated = false) => { if (!avatar) { return getDefaultAvatar(getDefaultAvatarIndex(id)); } const parsedAvatar = parseAvatar(avatar); const shouldAnimate = parsedAvatar.animated ? animated : false; return getMediaURL({ path: 'avatars', id, hash: parsedAvatar.hash, size: 160, format: shouldAnimate ? 'gif' : 'webp', }); }; export const getUserBannerURL = ({id, banner}: BannerOptions, animated = false, size = 1024) => { if (!banner) { return null; } const parsedBanner = parseAvatar(banner); const shouldAnimate = parsedBanner.animated ? animated : false; return getMediaURL({ path: 'banners', id, hash: parsedBanner.hash, size, format: shouldAnimate ? 'gif' : 'webp', }); }; export const getGuildIconURL = ({id, icon}: IconOptions, animated = false) => { if (!icon) { return null; } const parsedIcon = parseAvatar(icon); const shouldAnimate = parsedIcon.animated ? animated : false; return getMediaURL({ path: 'icons', id, hash: parsedIcon.hash, size: 160, format: shouldAnimate ? 'gif' : 'webp', }); }; export const getGuildBannerURL = ({id, banner}: {id: string; banner: string | null}, animated = false) => { if (!banner) { return null; } const parsedBanner = parseAvatar(banner); const shouldAnimate = parsedBanner.animated ? animated : false; return getMediaURL({ path: 'banners', id, hash: parsedBanner.hash, size: 1024, format: shouldAnimate ? 'gif' : 'webp', }); }; export const getGuildSplashURL = ({id, splash}: {id: string; splash: string | null}, size = 1024) => { if (!splash) { return null; } const parsedSplash = parseAvatar(splash); return getMediaURL({ path: 'splashes', id, hash: parsedSplash.hash, size, format: 'webp', }); }; export const getGuildEmbedSplashURL = ({id, embedSplash}: {id: string; embedSplash: string | null}, size = 1024) => { if (!embedSplash) { return null; } const parsedEmbedSplash = parseAvatar(embedSplash); return getMediaURL({ path: 'embed-splashes', id, hash: parsedEmbedSplash.hash, size, format: 'webp', }); }; export const getChannelIconURL = ({id, icon}: IconOptions, size?: number) => { if (!icon) { return null; } const parsedIcon = parseAvatar(icon); return getMediaURL({ path: 'icons', id, hash: parsedIcon.hash, size: size || 160, format: 'webp', }); }; export const getGuildMemberAvatarURL = ({ guildId, userId, avatar, animated = false, }: { guildId: string; userId: string; avatar: string | null; animated?: boolean; }) => { if (!avatar) { return null; } const parsedAvatar = parseAvatar(avatar); const shouldAnimate = parsedAvatar.animated ? animated : false; return getGuildMemberMediaURL({ path: 'avatars', guildId, userId, hash: parsedAvatar.hash, size: 160, format: shouldAnimate ? 'gif' : 'webp', }); }; export const getGuildMemberBannerURL = ({ guildId, userId, banner, animated = false, size = 1024, }: { guildId: string; userId: string; banner: string | null; animated?: boolean; size?: number; }) => { if (!banner) { return null; } const parsedBanner = parseAvatar(banner); const shouldAnimate = parsedBanner.animated ? animated : false; return getGuildMemberMediaURL({ path: 'banners', guildId, userId, hash: parsedBanner.hash, size, format: shouldAnimate ? 'gif' : 'webp', }); }; export const getEmojiURL = ({id, animated}: {id: string; animated?: boolean}) => { if (DeveloperOptionsStore.forceRenderPlaceholders) { return ''; } return mediaUrl(`emojis/${id}.${animated ? 'gif' : 'webp'}`); }; type StickerSize = 160 | 320; export const getStickerURL = ({id, animated, size = 320}: {id: string; animated?: boolean; size?: StickerSize}) => { if (DeveloperOptionsStore.forceRenderPlaceholders) { return ''; } const safeSize: StickerSize = size === 320 ? 320 : 160; const ext = animated ? 'gif' : 'webp'; return mediaUrl(`stickers/${id}.${ext}?size=${safeSize}`); }; export const fileToBase64 = (file: File) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result as string); reader.onerror = reject; reader.readAsDataURL(file); });