Presence: count an account as soon as it's linked; accept an empty outfit
CI / test (push) Successful in 36s
CI / test (push) Successful in 36s
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7a06ba49c3
commit
339ba8e2d6
3 files changed
+45
-5
No files matched your search
+4
-1
@@ -350,8 +350,10 @@ export function createApp({ db, auth, imvu, realtime: rt }: Deps) {
|
|||||||
target: String(found.id),
|
target: String(found.id),
|
||||||
detail: moved ? { imvu: found.username, movedFrom: prev!.userId } : { imvu: found.username },
|
detail: moved ? { imvu: found.username, movedFrom: prev!.userId } : { imvu: found.username },
|
||||||
});
|
});
|
||||||
return { link, moved };
|
return { link, moved, previousOwner: prev?.userId };
|
||||||
});
|
});
|
||||||
|
if (result.moved) realtime.unlinked(result.previousOwner!, found.id);
|
||||||
|
realtime.linked(me.id, found.id);
|
||||||
return { link: linkView(result.link), moved: result.moved };
|
return { link: linkView(result.link), moved: result.moved };
|
||||||
},
|
},
|
||||||
{ signedIn: true, params: imvuIdParam },
|
{ signedIn: true, params: imvuIdParam },
|
||||||
@@ -403,6 +405,7 @@ export function createApp({ db, auth, imvu, realtime: rt }: Deps) {
|
|||||||
async ({ me, params, set }) => {
|
async ({ me, params, set }) => {
|
||||||
const l = await ownLink(me, params.imvu);
|
const l = await ownLink(me, params.imvu);
|
||||||
await db.delete(imvuLink).where(eq(imvuLink.imvuId, l.imvuId));
|
await db.delete(imvuLink).where(eq(imvuLink.imvuId, l.imvuId));
|
||||||
|
realtime.unlinked(me.id, l.imvuId);
|
||||||
await audited(me.id, "link.remove", String(l.imvuId), { imvu: l.username });
|
await audited(me.id, "link.remove", String(l.imvuId), { imvu: l.username });
|
||||||
set.status = 204;
|
set.status = 204;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export type Item = { id: string; name: string };
|
|||||||
|
|
||||||
export type Online = { userId: string; imvuId: number; since: Date; outfit: Item[] };
|
export type Online = { userId: string; imvuId: number; since: Date; outfit: Item[] };
|
||||||
|
|
||||||
type Session = { userId: string; imvuId: number; since: Date; outfit: Item[]; close: () => void };
|
// requested: the IMVU account the client said it's using, even before it's linked (it counts once it is).
|
||||||
|
type Session = { userId: string; imvuId: number; requested: number; since: Date; outfit: Item[]; close: () => void };
|
||||||
|
|
||||||
export type Owners = (imvuId: number) => Promise<string | undefined>;
|
export type Owners = (imvuId: number) => Promise<string | undefined>;
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ export class Realtime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
open(id: string, userId: string, close: () => void) {
|
open(id: string, userId: string, close: () => void) {
|
||||||
this.sessions.set(id, { userId, imvuId: 0, since: new Date(), outfit: [], close });
|
this.sessions.set(id, { userId, imvuId: 0, requested: 0, since: new Date(), outfit: [], close });
|
||||||
}
|
}
|
||||||
|
|
||||||
close(id: string) {
|
close(id: string) {
|
||||||
@@ -57,6 +58,32 @@ export class Realtime {
|
|||||||
if (s?.imvuId) this.changed();
|
if (s?.imvuId) this.changed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** A person just linked an IMVU account: their clients already using it count now. */
|
||||||
|
linked(userId: string, imvuId: number) {
|
||||||
|
let changed = false;
|
||||||
|
for (const s of this.sessions.values()) {
|
||||||
|
if (s.userId === userId && s.requested === imvuId && s.imvuId !== imvuId) {
|
||||||
|
s.imvuId = imvuId;
|
||||||
|
s.since = new Date();
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) this.changed();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An IMVU account was unlinked (or moved): it no longer counts for that person's clients. */
|
||||||
|
unlinked(userId: string, imvuId: number) {
|
||||||
|
let changed = false;
|
||||||
|
for (const s of this.sessions.values()) {
|
||||||
|
if (s.userId === userId && s.imvuId === imvuId) {
|
||||||
|
s.imvuId = 0;
|
||||||
|
s.outfit = [];
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) this.changed();
|
||||||
|
}
|
||||||
|
|
||||||
/** Closes a person's connections (banned, or signed out of their computers). */
|
/** Closes a person's connections (banned, or signed out of their computers). */
|
||||||
disconnect(userId: string) {
|
disconnect(userId: string) {
|
||||||
for (const s of this.sessions.values()) if (s.userId === userId) s.close();
|
for (const s of this.sessions.values()) if (s.userId === userId) s.close();
|
||||||
@@ -70,6 +97,7 @@ export class Realtime {
|
|||||||
if (!m || typeof m !== "object") return { type: "error", message: "not a message" };
|
if (!m || typeof m !== "object") return { type: "error", message: "not a message" };
|
||||||
if (m.type === "session") {
|
if (m.type === "session") {
|
||||||
const imvuId = Number(m.imvu_id);
|
const imvuId = Number(m.imvu_id);
|
||||||
|
s.requested = Number.isSafeInteger(imvuId) && imvuId > 0 ? imvuId : 0;
|
||||||
const owner = Number.isSafeInteger(imvuId) && imvuId > 0 ? await this.owner(imvuId) : undefined;
|
const owner = Number.isSafeInteger(imvuId) && imvuId > 0 ? await this.owner(imvuId) : undefined;
|
||||||
if (owner !== s.userId) {
|
if (owner !== s.userId) {
|
||||||
// Not this person's account: no presence, no outfit for it.
|
// Not this person's account: no presence, no outfit for it.
|
||||||
@@ -85,8 +113,10 @@ export class Realtime {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m.type === "outfit") {
|
if (m.type === "outfit") {
|
||||||
if (!Array.isArray(m.items) || m.items.length > 64) return { type: "error", message: "too many items" };
|
const items = m.items ?? [];
|
||||||
s.outfit = m.items
|
if (!Array.isArray(items)) return { type: "error", message: "items must be a list" };
|
||||||
|
if (items.length > 64) return { type: "error", message: "too many items" };
|
||||||
|
s.outfit = items
|
||||||
.filter((it): it is Item => typeof it?.id === "string" && typeof it?.name === "string")
|
.filter((it): it is Item => typeof it?.id === "string" && typeof it?.name === "string")
|
||||||
.filter((it) => it.id && it.id.length <= 128 && it.name.length <= 128)
|
.filter((it) => it.id && it.id.length <= 128 && it.name.length <= 128)
|
||||||
.map((it) => ({ id: it.id, name: it.name }));
|
.map((it) => ({ id: it.id, name: it.name }));
|
||||||
|
|||||||
@@ -213,6 +213,13 @@ describe("T5DE's proxy", () => {
|
|||||||
await new Promise((r) => (ws.onopen = r));
|
await new Promise((r) => (ws.onopen = r));
|
||||||
ws.send(JSON.stringify({ type: "session", imvu_id: 42 }));
|
ws.send(JSON.stringify({ type: "session", imvu_id: 42 }));
|
||||||
await waitFor(() => messages.some((m) => m.message === "not_linked"));
|
await waitFor(() => messages.some((m) => m.message === "not_linked"));
|
||||||
|
// An account linked while the client runs counts at once.
|
||||||
|
ws.send(JSON.stringify({ type: "session", imvu_id: 78 }));
|
||||||
|
await waitFor(() => messages.filter((m) => m.message === "not_linked").length === 2);
|
||||||
|
await link(erin, { id: 78, username: "ErinAlt", ageVerified: false, accessPass: false });
|
||||||
|
await waitFor(() => hub.realtime.online().some((o) => o.imvuId === 78));
|
||||||
|
await erin.call("DELETE", "/api/v1/account/links/78");
|
||||||
|
await waitFor(() => !hub.realtime.online().some((o) => o.imvuId === 78));
|
||||||
ws.send(JSON.stringify({ type: "session", imvu_id: 77 }));
|
ws.send(JSON.stringify({ type: "session", imvu_id: 77 }));
|
||||||
ws.send(JSON.stringify({ type: "outfit", items: [{ id: "blue-coat", name: "Blue Coat" }] }));
|
ws.send(JSON.stringify({ type: "outfit", items: [{ id: "blue-coat", name: "Blue Coat" }] }));
|
||||||
await waitFor(() => hub.realtime.online().some((o) => o.imvuId === 77 && o.outfit.length === 1));
|
await waitFor(() => hub.realtime.online().some((o) => o.imvuId === 77 && o.outfit.length === 1));
|
||||||
|
|||||||
Reference in new issue
Block a user