The realtime connection now carries the IMVU chats a client is in: the hub tells each proxy in a chat which other T5DE users are there and what community products they wear (adult products only to adult accounts), on joining and on every change. Rooms T5DE users enter are looked up on IMVU; those that are public, listed and whose owner turned on "List my public rooms" appear on the new Rooms page, with the T5DE users in them who show they're online. AP rooms are shown to adults only. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7db9fc0370
commit
6ba4d83317
18 files changed
+1838
-50
No files matched your search
+59
-6
@@ -1,5 +1,6 @@
|
||||
// IMVU's public user API: who an account is, its tagline (for linking) and whether it's age verified or has Access
|
||||
// Pass. The hub calls it rarely (linking, and age checks) and depends on it for nothing else.
|
||||
// IMVU's public API: who an account is, its tagline (for linking) and whether it's age verified or has Access Pass;
|
||||
// and rooms (for the rooms list). The hub calls it rarely (linking, age checks, rooms T5DE users enter) and depends
|
||||
// on it for nothing else.
|
||||
|
||||
export type ImvuUser = {
|
||||
id: number;
|
||||
@@ -9,6 +10,21 @@ export type ImvuUser = {
|
||||
accessPass: boolean;
|
||||
};
|
||||
|
||||
export type ImvuRoom = {
|
||||
instanceId: string; // "<owner>-<n>"
|
||||
ownerId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
imageURL: string;
|
||||
privacy: string; // "public", ...
|
||||
type: string; // "listed", "unlisted", ...
|
||||
contentRating: string; // "GA", "AP"
|
||||
accessPass: boolean;
|
||||
occupancy: number;
|
||||
capacity: number;
|
||||
expired: boolean;
|
||||
};
|
||||
|
||||
export class NoSuchImvuUser extends Error {
|
||||
constructor() {
|
||||
super("no such IMVU account");
|
||||
@@ -31,11 +47,22 @@ export class ImvuClient {
|
||||
byUsername(username: string): Promise<ImvuUser> {
|
||||
const name = username.trim();
|
||||
if (!name || name.length > 64) return Promise.reject(new NoSuchImvuUser());
|
||||
return this.get(`/user?username=${encodeURIComponent(name)}`);
|
||||
return this.get(`/user?username=${encodeURIComponent(name)}`).then(parse);
|
||||
}
|
||||
|
||||
byId(id: number): Promise<ImvuUser> {
|
||||
return this.get(`/user/user-${id}`);
|
||||
return this.get(`/user/user-${id}`).then(parse);
|
||||
}
|
||||
|
||||
/** A room by its instance ID ("<owner>-<n>"); undefined if there's no such room. */
|
||||
async room(instanceId: string): Promise<ImvuRoom | undefined> {
|
||||
if (!/^[0-9]{1,12}-[0-9]{1,12}$/.test(instanceId)) return undefined;
|
||||
try {
|
||||
return parseRoom(await this.get(`/room/room-${instanceId}`), instanceId);
|
||||
} catch (e) {
|
||||
if (e instanceof NoSuchImvuUser) return undefined;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async wait() {
|
||||
@@ -45,7 +72,7 @@ export class ImvuClient {
|
||||
if (at > now) await Bun.sleep(at - now);
|
||||
}
|
||||
|
||||
private async get(path: string): Promise<ImvuUser> {
|
||||
private async get(path: string): Promise<unknown> {
|
||||
await this.wait();
|
||||
let response: Response;
|
||||
try {
|
||||
@@ -58,10 +85,36 @@ export class ImvuClient {
|
||||
}
|
||||
if (response.status === 404) throw new NoSuchImvuUser();
|
||||
if (!response.ok) throw new ImvuUnavailable(`IMVU answered ${response.status}`);
|
||||
return parse(await response.json());
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export function parseRoom(answer: unknown, instanceId: string): ImvuRoom | undefined {
|
||||
const den = (answer as { denormalized?: Record<string, { data?: Record<string, unknown> }> })?.denormalized ?? {};
|
||||
const entry = Object.entries(den).find(([key]) => key.endsWith(`/room/room-${instanceId}`));
|
||||
const d = entry?.[1].data;
|
||||
if (!d) return undefined;
|
||||
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
||||
const num = (v: unknown) => (typeof v === "number" && Number.isFinite(v) ? v : 0);
|
||||
let image = str(d.image_url);
|
||||
if (image.startsWith("//")) image = "https:" + image;
|
||||
if (!image.startsWith("https://")) image = "";
|
||||
return {
|
||||
instanceId,
|
||||
ownerId: num(d.customers_id) || Number(instanceId.split("-")[0]),
|
||||
name: str(d.name).slice(0, 200),
|
||||
description: str(d.description).slice(0, 2000),
|
||||
imageURL: image,
|
||||
privacy: str(d.privacy),
|
||||
type: str(d.type),
|
||||
contentRating: str(d.content_rating),
|
||||
accessPass: d.is_ap === true,
|
||||
occupancy: num(d.occupancy),
|
||||
capacity: num(d.capacity),
|
||||
expired: d.is_expired === true,
|
||||
};
|
||||
}
|
||||
|
||||
// IMVU's "denormalized" answers: the user is the one /user/user-<id> entry.
|
||||
export function parse(answer: unknown): ImvuUser {
|
||||
const den = (answer as { denormalized?: Record<string, { data?: Record<string, unknown> }> })?.denormalized ?? {};
|
||||
|
||||
Reference in new issue
Block a user