import axios from "axios"; import dotenv from "dotenv"; import FormData from "form-data"; dotenv.config(); const READY_API_KEY = process.env.READY_API_KEY; const READY_API_BASE = "https://api.readyplayer.me/v1"; const READY_MODELS_BASE = "https://models.readyplayer.me"; const READY_AVATARS_BASE = "https://avatars.readyplayer.me"; if (!READY_API_KEY) { console.warn("READY_API_KEY not found in environment variables"); } const getHeaders = (): Record => ({ "X-API-Key": READY_API_KEY || "", "Content-Type": "application/json", }); export interface ReadyPlayerMeUser { id: string; applicationIds: string[]; partners: string[]; createdAt: string; updatedAt: string; } export interface ReadyPlayerMeAsset { id: string; name: string; type: string; gender: string; modelUrl: string; iconUrl: string; organizationId: string; locked: boolean; applications?: any[]; hasApps: boolean; createdAt: string; updatedAt: string; } export interface ReadyPlayerMeToken { token: string; } export interface ReadyPlayerMeAvatarMetadata { id: string; url?: string; [key: string]: any; } export class ReadyPlayerMeClient { private apiKey: string; private applicationId?: string; constructor(applicationId?: string) { this.apiKey = READY_API_KEY || ""; this.applicationId = applicationId; } async createGuestUser(applicationId: string): Promise { try { const response = await axios.post<{ data: ReadyPlayerMeUser }>( `${READY_API_BASE}/users`, { data: { applicationId, }, }, { headers: getHeaders(), } ); return response.data.data; } catch (error: any) { console.error("Error creating Ready Player Me guest user:", error.response?.data || error.message); return null; } } async getAuthToken(userId: string, partner: string): Promise { try { const response = await axios.get<{ data: ReadyPlayerMeToken }>( `${READY_API_BASE}/auth/token`, { params: { userId, partner }, headers: getHeaders(), } ); return response.data.data.token; } catch (error: any) { console.error("Error getting Ready Player Me token:", error.response?.data || error.message); return null; } } async getAvatarGLB(avatarId: string, options?: { quality?: "low" | "medium" | "high" | "ultra"; lod?: number; textureAtlas?: number | "none"; textureFormat?: "webp" | "jpeg" | "png"; useDracoMeshCompression?: boolean; }): Promise { const params = new URLSearchParams(); if (options?.quality) params.append("quality", options.quality); if (options?.lod !== undefined) params.append("lod", options.lod.toString()); if (options?.textureAtlas) params.append("textureAtlas", options.textureAtlas.toString()); if (options?.textureFormat) params.append("textureFormat", options.textureFormat); if (options?.useDracoMeshCompression) params.append("useDracoMeshCompression", "true"); const queryString = params.toString(); return `${READY_AVATARS_BASE}/${avatarId}.glb${queryString ? `?${queryString}` : ""}`; } async getAvatar2DRender(avatarId: string, options?: { size?: number; quality?: number; camera?: "portrait" | "fullbody" | "fit"; background?: string; expression?: string; pose?: string; }): Promise { const params = new URLSearchParams(); if (options?.size) params.append("size", options.size.toString()); if (options?.quality) params.append("quality", options.quality.toString()); if (options?.camera) params.append("camera", options.camera); if (options?.background) params.append("background", options.background); if (options?.expression) params.append("expression", options.expression); if (options?.pose) params.append("pose", options.pose); const queryString = params.toString(); return `${READY_MODELS_BASE}/${avatarId}.png${queryString ? `?${queryString}` : ""}`; } async getAvatarMetadata(avatarId: string): Promise { try { const response = await axios.get<{ data: ReadyPlayerMeAvatarMetadata }>( `${READY_MODELS_BASE}/${avatarId}.json`, { headers: getHeaders(), } ); return response.data.data; } catch (error: any) { console.error("Error getting avatar metadata:", error.response?.data || error.message); return null; } } async uploadAssetFile(fileBuffer: Buffer, filename: string, mimetype: string): Promise { try { const formData = new FormData(); formData.append("file", fileBuffer, { filename, contentType: mimetype, }); const headers = { ...getHeaders(), ...formData.getHeaders(), }; console.log(`[Ready Player Me] Uploading file: ${filename} (${fileBuffer.length} bytes, ${mimetype})`); const response = await axios.post<{ data: { url: string } }>( `${READY_API_BASE}/temporary-media`, formData, { headers, maxContentLength: Infinity, maxBodyLength: Infinity, } ); if (response.data?.data?.url) { console.log(`[Ready Player Me] File uploaded successfully: ${response.data.data.url}`); return response.data.data.url; } else { console.error("[Ready Player Me] Upload response missing URL:", response.data); return null; } } catch (error: any) { console.error("[Ready Player Me] Error uploading asset file:"); console.error(" Status:", error.response?.status); console.error(" Status Text:", error.response?.statusText); console.error(" Response Data:", JSON.stringify(error.response?.data, null, 2)); console.error(" Error Message:", error.message); if (error.response?.data) { console.error(" Full Error:", JSON.stringify(error.response.data, null, 2)); } return null; } } async createAsset(data: { name: string; type: string; gender: "male" | "female" | "neutral"; modelUrl: string; iconUrl: string; organizationId: string; locked?: boolean; applications?: Array<{ id: string; organizationId: string; isVisibleInEditor?: boolean; }>; }): Promise { try { console.log(`[Ready Player Me] Creating asset: ${data.name} (type: ${data.type}, gender: ${data.gender})`); const response = await axios.post<{ data: ReadyPlayerMeAsset }>( `${READY_API_BASE}/assets`, { data }, { headers: getHeaders(), } ); if (response.data?.data) { console.log(`[Ready Player Me] Asset created successfully: ${response.data.data.id}`); return response.data.data; } else { console.error("[Ready Player Me] Create asset response missing data:", response.data); return null; } } catch (error: any) { console.error("[Ready Player Me] Error creating asset:"); console.error(" Status:", error.response?.status); console.error(" Status Text:", error.response?.statusText); console.error(" Response Data:", JSON.stringify(error.response?.data, null, 2)); console.error(" Error Message:", error.message); if (error.response?.data) { console.error(" Full Error:", JSON.stringify(error.response.data, null, 2)); } return null; } } async listAssets(filters?: { type?: string[]; gender?: string[]; name?: string; organizationId?: string; applicationIds?: string[]; limit?: number; page?: number; }): Promise<{ data: ReadyPlayerMeAsset[]; pagination: any } | null> { try { const params = new URLSearchParams(); if (filters?.type) { filters.type.forEach(t => params.append("type", t)); } if (filters?.gender) { filters.gender.forEach(g => params.append("gender", g)); } if (filters?.name) params.append("name", filters.name); if (filters?.organizationId) params.append("organizationId", filters.organizationId); if (filters?.applicationIds) { filters.applicationIds.forEach(id => params.append("applicationIds", id)); } if (filters?.limit) params.append("limit", filters.limit.toString()); if (filters?.page) params.append("page", filters.page.toString()); const headers: Record = { ...getHeaders() }; if (this.applicationId) { headers["X-APP-ID"] = this.applicationId; } const response = await axios.get<{ data: ReadyPlayerMeAsset[]; pagination: any }>( `${READY_API_BASE}/assets?${params.toString()}`, { headers } ); return response.data; } catch (error: any) { console.error("Error listing assets:", error.response?.data || error.message); return null; } } async updateAsset(assetId: string, data: { name?: string; type?: string; gender?: "male" | "female" | "neutral"; modelUrl?: string; iconUrl?: string; locked?: boolean; applications?: Array<{ id: string; organizationId: string; isVisibleInEditor?: boolean; }>; }): Promise { try { const response = await axios.patch<{ data: ReadyPlayerMeAsset }>( `${READY_API_BASE}/assets/${assetId}`, { data }, { headers: getHeaders(), } ); return response.data.data; } catch (error: any) { console.error("Error updating asset:", error.response?.data || error.message); return null; } } async equipAsset(avatarId: string, assetId: string): Promise { try { await axios.put( `${READY_API_BASE}/avatars/${avatarId}/equip`, { data: { assetId, }, }, { headers: getHeaders(), } ); return true; } catch (error: any) { console.error("Error equipping asset:", error.response?.data || error.message); return false; } } async unequipAsset(avatarId: string, assetId: string): Promise { try { await axios.put( `${READY_API_BASE}/avatars/${avatarId}/unequip`, { data: { assetId, }, }, { headers: getHeaders(), } ); return true; } catch (error: any) { console.error("Error unequipping asset:", error.response?.data || error.message); return false; } } async addAssetToApplication(assetId: string, applicationId: string, isVisibleInEditor: boolean = true): Promise { try { await axios.post( `${READY_API_BASE}/assets/${assetId}/application`, { data: { applicationId, isVisibleInEditor, }, }, { headers: getHeaders(), } ); return true; } catch (error: any) { console.error("Error adding asset to application:", error.response?.data || error.message); return false; } } async removeAssetFromApplication(assetId: string, applicationId: string): Promise { try { await axios.delete( `${READY_API_BASE}/assets/${assetId}/application`, { data: { applicationId, }, headers: getHeaders(), } ); return true; } catch (error: any) { console.error("Error removing asset from application:", error.response?.data || error.message); return false; } } async unlockAssetForUser(assetId: string, userId: string): Promise { try { await axios.put( `${READY_API_BASE}/assets/${assetId}/unlock`, { data: { userId, }, }, { headers: getHeaders(), } ); return true; } catch (error: any) { console.error("Error unlocking asset:", error.response?.data || error.message); return false; } } async lockAssetForUser(assetId: string, userId: string): Promise { try { await axios.put( `${READY_API_BASE}/assets/${assetId}/lock`, { data: { userId, }, }, { headers: getHeaders(), } ); return true; } catch (error: any) { console.error("Error locking asset:", error.response?.data || error.message); return false; } } } export const readyPlayerMeClient = new ReadyPlayerMeClient();