import {Registry} from "../events"; import * as hex from "../crypto/hex"; export const kIPCAvatarChannel = "avatars"; export const kLoadingAvatarImage = "img/loading_image.svg"; export const kDefaultAvatarImage = "img/style/avatar.png"; export type AvatarState = "unset" | "loading" | "errored" | "loaded"; export interface AvatarStateData { "unset": {}, "loading": {}, "errored": { message: string }, "loaded": { url: string } } interface AvatarEvents { avatar_changed: { newAvatarHash: string }, avatar_state_changed: { oldState: AvatarState, newState: AvatarState, newStateData: AvatarStateData[keyof AvatarStateData] } } export abstract class ClientAvatar { readonly events: Registry; readonly clientAvatarId: string; /* the base64 unique id thing from a-m */ private currentAvatarHash: string | "unknown"; /* the client avatars flag */ private state: AvatarState = "loading"; private stateData: AvatarStateData[AvatarState] = {}; loadingTimestamp: number = 0; constructor(clientAvatarId: string) { this.clientAvatarId = clientAvatarId; this.events = new Registry(); this.events.on("avatar_state_changed", event => { this.state = event.newState; this.stateData = event.newStateData; }); this.events.on("avatar_changed", event => this.currentAvatarHash = event.newAvatarHash); } destroy() { this.setState("unset", {}); this.events.destroy(); } protected setState(state: T, data: AvatarStateData[T], force?: boolean) { if(this.state === state && !force) return; this.destroyStateData(this.state, this.stateData); this.events.fire("avatar_state_changed", { newState: state, oldState: this.state, newStateData: data }); } public getTypedStateData(state: T) : AvatarStateData[T] { if(this.state !== state) throw "invalid avatar state"; return this.stateData as any; } public setUnset() { this.setState("unset", {}); } public setLoading() { this.setState("loading", {}); } public setLoaded(data: AvatarStateData["loaded"]) { this.setState("loaded", data); } public setErrored(data: AvatarStateData["errored"]) { this.setState("errored", data); } async awaitLoaded() { if(this.state !== "loading") return; await new Promise(resolve => this.events.on("avatar_state_changed", event => event.newState !== "loading" && resolve())); } getState() : AvatarState { return this.state; } getStateData() : AvatarStateData[AvatarState] { return this.stateData; } getAvatarHash() : string | "unknown" { return this.currentAvatarHash; } getAvatarUrl() { if(this.state === "loaded") return this.getTypedStateData("loaded").url || kDefaultAvatarImage; return kDefaultAvatarImage; } getLoadError() { return this.getTypedStateData("errored").message; } protected abstract destroyStateData(state: AvatarState, data: AvatarStateData[AvatarState]); } export abstract class AbstractAvatarManager { abstract resolveAvatar(clientAvatarId: string, avatarHash?: string) : ClientAvatar; abstract resolveClientAvatar(client: { id?: number, database_id?: number, clientUniqueId: string }); } export abstract class AbstractAvatarManagerFactory { abstract hasManager(handlerId: string) : boolean; abstract getManager(handlerId: string) : AbstractAvatarManager; } let globalAvatarManagerFactory: AbstractAvatarManagerFactory; export function setGlobalAvatarManagerFactory(factory: AbstractAvatarManagerFactory) { if(globalAvatarManagerFactory) throw "global avatar manager factory has already been set"; globalAvatarManagerFactory = factory; } export function getGlobalAvatarManagerFactory() { return globalAvatarManagerFactory; } export function uniqueId2AvatarId(unique_id: string) { function str2ab(str) { let buf = new ArrayBuffer(str.length); // 2 bytes for each char let bufView = new Uint8Array(buf); for (let i=0, strLen = str.length; i= '0' && c <= '9') offset = c.charCodeAt(0) - '0'.charCodeAt(0); else if(c >= 'A' && c <= 'F') offset = c.charCodeAt(0) - 'A'.charCodeAt(0) + 0x0A; else if(c >= 'a' && c <= 'f') offset = c.charCodeAt(0) - 'a'.charCodeAt(0) + 0x0A; result += String.fromCharCode('a'.charCodeAt(0) + offset); } return result; } catch (e) { //invalid base 64 (like music bot etc) return undefined; } }