Phase 3, part 1 (`docs/design.md`, "Next"): cases and notifications. File blocks, API tokens and two-factor sign-in come as separate PRs.
**Cases**
- One queue (Console → Cases) for everything someone has to decide. Each case has:
- a thread of notes, where internal notes are visible only to those deciding
- an assignee (moderators assign cases to moderators)
- a decision, Resolve or Decline, with a written resolution that the person who opened it sees
- **Reports:** anyone signed in can report a product or catalog from its page (the API also takes rooms and people).
- Limits: 10 an hour, one open report per person and thing, and you can't report yourself.
- Reports about a product go to the teams of the catalogs hosting it, as well as to moderators.
- Only the reporter and moderators know who reported. The audit entry for a report isn't tagged with what it's about, so a product's creator can't find out from their audit log either.
- **Rating reviews:** a creator lowering a rating from adult to general now opens a review instead of being refused. The rest of the edit still saves. Resolving the review lowers the rating, recorded in the product's history.
- **Removal requests:** a catalog's team can ask moderators to remove a product it hosts, everywhere. Resolving it removes the product.
- **Deletion requests:** a catalog's owner asks for deletion from the catalog's console page, and a moderator decides, as agreed. Resolving it marks the catalog `deleted`: its key stops working, nothing is available from it, and its changes feed says so.
- **Appeals:**
- From the audit log, the person an action affected can appeal it, once: their product removed, delisted or edited by someone else; their catalog suspended or rejected; a ban from a catalog or a declined request to join.
- Another moderator decides. An admin may decide an appeal of their own action, and the audit log flags it.
- Resolving records the decision; the moderator then undoes the action where it's done. For that, there's now a Restore for removed products (Console → Products, for moderators); the design mentioned one, but it was never built.
- The console shows a "Cases" tab, and a to-do with the number of open cases the person could decide.
**Notifications**
- A bell in the header with the unread count, and a notifications page with "Mark all as read".
- Each is also sent to the person's proxies over the realtime connection (`{"type": "notification"}`), so the client can show them later.
- They cover:
- a moderator edited or locked your product, or undid a change (with the reason)
- your product was removed or restored
- your case was decided
- someone replied on your case
- a catalog decided your membership
**Also changed**
- A product's creator and moderators can now see it when it's adult, even without an adult account. Before, a creator who made their own product adult couldn't open its page any more, and neither could moderators. Getting and wearing adult products still need an adult account.
**Not done yet** (in `docs/console.md`): email notifications (the hub sends no email yet), canned reasons, private moderator notes on people and products, and second approval for the most destructive actions.
**Migration** `0012_cases`: the `moderation_case`, `case_note` and `notification` tables. (`case` is a reserved word in SQL, hence the table's name.)
**Checks**
- 51 API tests pass. The 7 in the new `cases.test.ts` cover:
- who sees and decides each kind of case, and that the reporter stays private
- rate limits and duplicate reports
- the thread and internal notes, and assigning
- what deciding each kind of case does
- appeals by the person affected only, decided by another moderator
- restoring a removed product
- the notifications
- Type checks pass.
- On a local hub I went through every flow with the website's own forms: report from a product page; the catalog owner's note and decision; assigning, an internal note and a decision as admin; removal by a moderator, the creator's appeal from the audit log, the admin's decision and Restore; a creator's rating review and the decision lowering the rating; a deletion request and its decline; the notifications, the bell's count, and Mark all as read.
- Fixed along the way:
- The catalog's team could see who reported.
- A missing Resolve/Decline choice was treated as Decline; now it's refused.
- The Unassign button sent the typed name instead of nothing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Phase 3, part 1 (`docs/design.md`, "Next"): cases and notifications. File blocks, API tokens and two-factor sign-in come as separate PRs.
**Cases**
- One queue (Console → Cases) for everything someone has to decide. Each case has:
- a thread of notes, where internal notes are visible only to those deciding
- an assignee (moderators assign cases to moderators)
- a decision, Resolve or Decline, with a written resolution that the person who opened it sees
- **Reports:** anyone signed in can report a product or catalog from its page (the API also takes rooms and people).
- Limits: 10 an hour, one open report per person and thing, and you can't report yourself.
- Reports about a product go to the teams of the catalogs hosting it, as well as to moderators.
- Only the reporter and moderators know who reported. The audit entry for a report isn't tagged with what it's about, so a product's creator can't find out from their audit log either.
- **Rating reviews:** a creator lowering a rating from adult to general now opens a review instead of being refused. The rest of the edit still saves. Resolving the review lowers the rating, recorded in the product's history.
- **Removal requests:** a catalog's team can ask moderators to remove a product it hosts, everywhere. Resolving it removes the product.
- **Deletion requests:** a catalog's owner asks for deletion from the catalog's console page, and a moderator decides, as agreed. Resolving it marks the catalog `deleted`: its key stops working, nothing is available from it, and its changes feed says so.
- **Appeals:**
- From the audit log, the person an action affected can appeal it, once: their product removed, delisted or edited by someone else; their catalog suspended or rejected; a ban from a catalog or a declined request to join.
- Another moderator decides. An admin may decide an appeal of their own action, and the audit log flags it.
- Resolving records the decision; the moderator then undoes the action where it's done. For that, there's now a Restore for removed products (Console → Products, for moderators); the design mentioned one, but it was never built.
- The console shows a "Cases" tab, and a to-do with the number of open cases the person could decide.
**Notifications**
- A bell in the header with the unread count, and a notifications page with "Mark all as read".
- Each is also sent to the person's proxies over the realtime connection (`{"type": "notification"}`), so the client can show them later.
- They cover:
- a moderator edited or locked your product, or undid a change (with the reason)
- your product was removed or restored
- your case was decided
- someone replied on your case
- a catalog decided your membership
**Also changed**
- A product's creator and moderators can now see it when it's adult, even without an adult account. Before, a creator who made their own product adult couldn't open its page any more, and neither could moderators. Getting and wearing adult products still need an adult account.
**Not done yet** (in `docs/console.md`): email notifications (the hub sends no email yet), canned reasons, private moderator notes on people and products, and second approval for the most destructive actions.
**Migration** `0012_cases`: the `moderation_case`, `case_note` and `notification` tables. (`case` is a reserved word in SQL, hence the table's name.)
**Checks**
- 51 API tests pass. The 7 in the new `cases.test.ts` cover:
- who sees and decides each kind of case, and that the reporter stays private
- rate limits and duplicate reports
- the thread and internal notes, and assigning
- what deciding each kind of case does
- appeals by the person affected only, decided by another moderator
- restoring a removed product
- the notifications
- Type checks pass.
- On a local hub I went through every flow with the website's own forms: report from a product page; the catalog owner's note and decision; assigning, an internal note and a decision as admin; removal by a moderator, the creator's appeal from the audit log, the admin's decision and Restore; a creator's rating review and the decision lowering the rating; a deletion request and its decline; the notifications, the bell's count, and Mark all as read.
- Fixed along the way:
- The catalog's team could see who reported.
- A missing Resolve/Decline choice was treated as Decline; now it's refused.
- The Unassign button sent the typed name instead of nothing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: dhkatz <[email protected]>
183 lines
19 KiB
Markdown
183 lines
19 KiB
Markdown
# Products, catalogs and the library: the contract
|
|
|
|
How the hub (registry), catalogs (`t5de-catalog`, file hosting) and T5DE's proxy work together. The design and the
|
|
reasons are in `design.md` ("Products and catalogs", "Revisions", "In the client's shop and inventory"); this is
|
|
what each side sends.
|
|
|
|
## Words
|
|
|
|
- **Product**: a T5 product, owned by its creator (a hub person). ID: 10 characters of `a-z2-7` (random), e.g.
|
|
`k3mq7xa2pd`. Valid as a pack ID, so the proxy can store a downloaded product as a pack.
|
|
- **Revision**: a product's files, numbered 1, 2, 3... Never changes. Its **file list** is
|
|
`[{name, sha256, size}]`, sorted by name; `sha256` is lowercase hex.
|
|
- **Catalog**: a server storing files by sha256 and serving them. Registered on the hub; hidden until a hub
|
|
moderator approves it.
|
|
- **Library**: the products a person "got" (free), per person (all their IMVU accounts), optionally staying on a
|
|
revision.
|
|
- **Draft**: a new product before its creator publishes it: only its creator and moderators see it; it can't be
|
|
got, worn or placed. Drafts never published are deleted after 30 days.
|
|
- **Unlisted**: a published product left out of search, lists and creator pages for others; anyone with its link
|
|
can see and get it (membership rules apply).
|
|
- **Member**: a person a catalog with approval or invite membership has let in. A product is **available** to a
|
|
person if a catalog hosting it is approved, not suspended, and open or has them as a member (and hasn't banned
|
|
them).
|
|
- **Imported product**: a product a catalog's importer brought from IMVU with its creator's grant (imports.md).
|
|
`imvuProduct` is set; otherwise it's like any product.
|
|
|
|
Who may do what (creators, catalog teams, moderators) is in `console.md` ("Permissions").
|
|
|
|
## Tokens
|
|
|
|
The hub signs tokens with its own Ed25519 key (EdDSA JWTs, compact form), published as a JWKS at
|
|
`GET /.well-known/t5de-keys.json` (`kid` in each token's header). Catalogs fetch it (and again when they see an
|
|
unknown `kid`). Claims always include `iss` (the hub's API URL), `typ` (below) and `iat`.
|
|
|
|
| `typ` | Other claims | Lifetime | Use |
|
|
| --- | --- | --- | --- |
|
|
| `t5de-upload` | `sub` (person ID), `catalog` (catalog ID), `product` (optional: a new revision of this product) | 30 min | the creator's browser uploads to the catalog |
|
|
| `t5de-access` | `sub` (person ID), `adult` (bool) | 1 h | the proxy fetches adult files from catalogs |
|
|
| `t5de-revision` | `product`, `revision`, `rating`, `files` (the file list), `parent` (IMVU product ID or 0) | none (`exp` absent) | the hub vouches for a revision's files |
|
|
| `t5de-import` | `sub` (the job ID), `catalog`, `imvuProduct`, `imvuRevision` | 30 min | an importer uploads an imported product to its catalog |
|
|
|
|
## Catalog API (`t5de-catalog`)
|
|
|
|
Served at the catalog's public URL. CORS: the hub website's origin may call `POST /api/v1/uploads`.
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| `GET /health` | `{ok: true}` |
|
|
| `POST /api/v1/uploads` | `Authorization: Bearer <t5de-upload token>`; multipart form: `file` (the product as a zip: Create Mode's `.chkn`, files at the root) and `notes` (text, required for a new revision of an existing product, optional for the first). The catalog validates the files (t5de-api `pack` rules: names, limits, `index.xml` with `ProductRating` GA/AP), stores each file by sha256, then registers the revision with the hub (below) and answers the hub's answer: 201 `{product, revision}`. Errors: `{error, message}`, 400 for bad files, 401 for a bad token, 502 if the hub refused or failed. |
|
|
| `GET /api/v1/files/<sha256>` | the file's bytes, `Cache-Control: public, max-age=31536000, immutable` and `ETag` = the hash. A file used only by adult revisions needs `Authorization: Bearer <t5de-access token>` with `adult: true` (else 401/403, and `Cache-Control: private`). 404 if the catalog doesn't have it. |
|
|
| `GET /api/v1/revisions/<product>/<revision>` | `{product, revision, rating, files}`, for revisions the catalog hosts |
|
|
|
|
Imports: `POST /api/v1/uploads` also takes `Authorization: Bearer <t5de-import token>` (from the catalog's
|
|
importer). Then `notes` is optional, and the catalog registers the revision with `importToken` instead of
|
|
`uploadToken`. Validation is the same.
|
|
|
|
Changes: the catalog polls the hub's changes feed (below) every minute and acts on it: deletes the files of
|
|
revisions withdrawn or removed (files still used by another revision it hosts stay), and fetches revisions it's
|
|
asked to mirror.
|
|
|
|
Configuration (environment): `CATALOG_ADDR` (listen address, default `127.0.0.1:4102`), `CATALOG_DATA` (data folder:
|
|
SQLite database and files), `CATALOG_PUBLIC_URL`, `HUB_URL` (the hub's API), `CATALOG_ID` and `CATALOG_KEY` (from
|
|
registering on the hub), `WEB_ORIGIN` (the hub website, for CORS).
|
|
|
|
## Hub API
|
|
|
|
### For catalogs (server to server)
|
|
|
|
`Authorization: Catalog <catalog ID>:<key>`. Only approved catalogs.
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| `POST /api/v1/catalog/revisions` | `{uploadToken, files, rating ("general" or "adult"), parent (IMVU product ID or 0), name (from index.xml, for a new product), notes}` → 201 `{product, revision, draft}`. The upload token must be for this catalog. Without `product` in the token, a new product is made as a **draft** (owner: the token's person, name from `name`; the creator publishes it on the website). With it, a new revision of that product (the person must own it; `notes` required). A file list identical to the latest revision's is refused (409 `unchanged`). Files the hub blocks are refused (409 `blocked`). The catalog's upload policy (`anyone`, `members`, `team`) is checked when the upload token is made (403 `forbidden`). With `importToken` instead of `uploadToken`: an imported revision (imports.md); the hub checks the job and that a grant still covers it, makes the product (published, owned by the grant's creator) or adds the revision, and marks the job done. |
|
|
| `GET /api/v1/catalog/changes?after=<cursor>` | `{changes, cursor}`: what the catalog must act on, oldest first: `{type: "delete", product, revision, files}` (withdrawn, removed by moderation, blocked: delete those files unless another hosted revision uses them), `{type: "mirror", product, revision, files, from: [catalog URLs]}` (fetch each file from one of `from`, check its sha256, then confirm below), `{type: "status", status}` (the catalog was suspended or reinstated). Start with no `after`; keep the cursor. |
|
|
| `POST /api/v1/catalog/hosting` | `{product, revision}`: the catalog now hosts a revision it was asked to mirror (it has every file) → 204 |
|
|
| `POST /api/v1/catalog/stats` | optional: `{day, downloads, bytes}` for a past day |
|
|
|
|
### For importers (server to server)
|
|
|
|
`Authorization: Importer <catalog ID>:<key>` (the key from enabling imports on the catalog; rotated like the
|
|
catalog's). Only approved catalogs with imports on and not paused.
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| `POST /api/v1/importer/jobs/claim` | `{limit}` (1-20) → `{jobs: [{id, kind, imvuProduct, lastRevision}]}`; claimed jobs not reported within 30 minutes go back to the queue |
|
|
| `POST /api/v1/importer/jobs/:id/token` | `{imvuRevision}` → `{token, url}`: a `t5de-import` token and the catalog's upload URL |
|
|
| `POST /api/v1/importer/jobs/:id` | `{status: "done" \| "failed" \| "unchanged", imvuRevision?, reason?}` |
|
|
|
|
### For people (website, signed in; the proxy with its bearer token)
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| `POST /api/v1/catalogs` | register a catalog: `{name, url, contact, visibility ("public"/"private"), membership ("open"/"approval"/"invite")}` → 201 `{catalog, key}` (the key is shown once) |
|
|
| `GET /api/v1/catalogs` | (anyone) approved public catalogs; the person's own, any status |
|
|
| `GET /api/v1/catalogs/:id` | (anyone, if public or they're a member or on its team) `{catalog, membership?: {status}, permissions}` |
|
|
| `PATCH /api/v1/catalogs/:id` | (`catalog:edit`) `{name?, url?, contact?, visibility?, membership?, maxRating? ("general"/"adult"), uploads? ("anyone"/"members"/"team"), mirrors? ("auto"/"approve"/"off")}`. A new `url` must answer `/health`. |
|
|
| `POST /api/v1/catalogs/:id/key` | (`catalog:rotate-key`) → `{key}`: a new key; the old one stops working at once |
|
|
| `POST /api/v1/catalogs/:id/importer` | (`import:run`) `{enabled}` → `{key?}`: turns imports on (a new importer key, shown once) or off; `POST .../importer/key` rotates it; `{paused}` pauses |
|
|
| `GET /api/v1/catalogs/:id/team`, `PUT/DELETE .../team/:person` | (`team:manage`) `{role: "operator"}` |
|
|
| `POST /api/v1/catalogs/:id/transfer` | (`catalog:transfer`) `{to}`: offered to the person, who accepts on their side (`POST .../transfer/accept`) |
|
|
| `POST /api/v1/catalogs/:id/delete` | (`catalog:delete-request`) `{reason}` → `{case}`: opens a `catalog-deletion` case (409 `requested` if one is open); nothing changes until a moderator decides (console.md). Resolved, the catalog's status is `deleted`: nothing is available from it, and its key stops working |
|
|
| `POST /api/v1/catalogs/:id/join` | join an open catalog, or ask to join an approval one; `{invite}` to use an invite code. `DELETE` leaves. |
|
|
| `GET /api/v1/catalogs/:id/members?status=` | (`member:approve`) members, requests and bans |
|
|
| `PUT /api/v1/catalogs/:id/members/:person` | `{status: "member" \| "banned"}` (`member:approve` / `member:ban`); `DELETE` removes (`member:remove`) |
|
|
| `POST /api/v1/catalogs/:id/invites` | (`member:invite`) `{maxUses?, expiresIn?}` → `{invite: {code, url}}`; `GET` lists, `DELETE .../invites/:code` revokes |
|
|
| `GET /api/v1/catalogs/:id/products` | the products it hosts (its team also sees drafts' counts, delisted ones) |
|
|
| `DELETE /api/v1/catalogs/:id/products/:product` | (`hosting:delist`) `{reason}`: the catalog stops hosting every revision of it; it's in the changes feed. The product stays available from other catalogs hosting it. |
|
|
| `POST /api/v1/catalogs/:id/suspend`, `.../reinstate` | (`catalog:suspend`) `{reason}`: a suspended catalog's products aren't available from it, it can't register revisions, and its team sees why |
|
|
| `POST /api/v1/products/upload` | `{catalog, product?}` → `{token, url}`: an upload token and the catalog's upload URL (the catalog's upload policy and rating ceiling apply) |
|
|
| `GET /api/v1/products?q=&creator=&category=&catalog=&tag=&limit=` | (anyone) `{products}`, newest first: published and listed (and the person's own unlisted ones); adult ones only for people with an adult account. `category`: an IMVU category ID in its category path (its own if chosen, else its latest revision's parent's; e.g. 106 Clothing, 128 Tops). `tag`: one tag; `catalog`: those the catalog hosts. `limit`: 1-200, default 60 |
|
|
| `GET /api/v1/products/:id` | (anyone, adult rule as above, except that its creator and moderators always see it; drafts only for their creator and moderators; getting and wearing adult products still need an adult account) `{product, revisions, inLibrary?, mine?, available, permissions}` |
|
|
| `PATCH /api/v1/products/:id` | (`product:edit`) `{name?, description?, rating?, tags?, listed?, category?, reason?}` → `{product, fields}` (the fields that changed; only those go in the history). Description is HTML, cleaned by the allowlist. Tags: up to 10, lowercased, each up to 24 letters, digits, spaces or dashes. `category`: the ID of one of its category choices (below), or null for its parent's. Rating may go general → adult; adult → general without `product:rate-lower` opens a `rating-review` case for moderators instead (one at a time; its ID is the answer's `review`; the other fields still change), and no revision that isn't retracted may be rated adult (400 `files_adult`). `reason` is required when the person isn't the creator (400 `reason_needed`), and shown to them. Locked fields need `product:edit-locked` (403 `locked`). The creator can't edit a product they deleted; moderators can. |
|
|
| `GET /api/v1/products/:id/categories` | (`product:edit`) `{choices, parent}`: the category paths it may be filed under, and its parent's. The choices are every IMVU category path the hub has seen on a revision's parent, and each step of them, under the same top-level category as its parent's (any, if that's unknown). Keeping the top level keeps it the same kind of product: the proxy tells clothing, furniture and rooms apart by category. IMVU's own category tree would replace the seen paths if one turns up. |
|
|
| `POST /api/v1/products/:id/publish` | (`product:publish`) a draft becomes published (409 `published` if it is already) |
|
|
| `GET /api/v1/products/:id/history` | (`audit:read` at the product: its creator, moderators) the info history, newest first: `{entries: [{id, at, kind ("edit", "revert", "lock", "publish"), by, byCreator, changes: {field: {from, to}}, reason, revertOf}]}` |
|
|
| `POST /api/v1/products/:id/history/:entry/revert` | (`product:edit`) `{reason?}`: puts back what an edit (or revert) changed, as a new `revert` entry, by the same rules as an edit (locks, reasons, ratings); 409 `unchanged` if the info is already as it was |
|
|
| `PUT /api/v1/products/:id/locks` | (`product:lock`) `{fields, reason}`: the fields locked from now on (any of name, description, rating, tags, listed, category; the rest are unlocked). `reason` required; kept in the history |
|
|
| `POST /api/v1/products/:id/transfer` | (`product:transfer`) `{to}`; the other person accepts (`.../transfer/accept`) |
|
|
| `POST /api/v1/products/:id/restore` | (`product:restore`) undo a creator's delete |
|
|
| `POST /api/v1/products/:id/hosting` | (`hosting:choose`) `{catalog, revision?}`: ask another catalog to mirror it (every revision, or one); the catalog's `mirrors` setting decides (auto, or its team approves) |
|
|
| `POST /api/v1/products/:id/revisions/:n/retract` | (creator) |
|
|
| `DELETE /api/v1/products/:id` | (creator) deletes it: no longer listed, and nobody new can get it; people who have it keep it (its page, files and library entries stay, and rooms keep showing it). Its creator can't edit it or add revisions after (until restored). 204 |
|
|
| `POST /api/v1/products/:id/remove` | (`product:remove`) `{reason}`: removes it everywhere: its page and files answer 410 `removed`, it leaves libraries and rooms, and catalogs delete its files. `.../unremove` (`product:unremove`) `{reason?}` undoes it (for now removal only hides it, so its files and library entries come back). Its creator is told both. |
|
|
| `PUT /api/v1/blocks/:sha256` | (`product:remove`) `{reason}`: blocks a file network-wide: revisions with it can't be registered, and products using it are removed. `DELETE` unblocks. |
|
|
| `GET /api/v1/library` | `{library: [{product, revision, stayOn}]}`: the person's products that are available to them, each with the revision to use (products no longer available, e.g. after leaving a catalog, stay in the table but aren't listed until they are again) |
|
|
| `PUT /api/v1/library/:id` | get a product (add it): it must be published and available to the person (403 `members_only`, with the catalogs they could join) |
|
|
| `DELETE /api/v1/library/:id` | remove it |
|
|
| `PATCH /api/v1/library/:id` | `{stayOn: n or null}` |
|
|
| `GET /api/v1/products/:id/files` | for the proxy: `{product, revision, rating, parent, name, files, catalogs: [url], token}` for the revision this person uses (their pin, else the latest that isn't retracted); `token` is the `t5de-revision` token |
|
|
| `POST /api/v1/access` | for the proxy: `{token}`, a `t5de-access` token (`adult` if any of the person's IMVU accounts counts as adult) |
|
|
|
|
The realtime connection also tells the proxy `{"type": "library"}` when the person's library changes.
|
|
|
|
A product (in lists, its page, the library) is `{id, name, description, rating, latest, creator, creatorUsername,
|
|
parent, image, categories, category, status ("draft"/"published"), listed, tags, locked, deleted, removed, publishedAt,
|
|
imvuProduct, createdAt, updatedAt}` (`imvuProduct` comes with imports). The product's page also has `permissions`: what the
|
|
person may do to it (the website shows actions by them). `category` is the path chosen for it (null: its parent's), and `categories` the one it's filed under (`category` if
|
|
chosen); `parent`, `image` and the parent's categories are the latest revision's:
|
|
`parent` is the IMVU product it derives from (0 if none); `image` (an https address on `*.imvu.com`, or null) and
|
|
`categories` (`[{id, name}]`, outermost first, e.g. Clothing / Clothing for Female / Tops / Coats & Jackets; `[]` if
|
|
unknown) are that IMVU product's, from IMVU's public API (`GET https://api.imvu.com/product/product-<id>`).
|
|
|
|
### Parent info
|
|
|
|
The hub looks up a revision's parent when the revision is registered (after it's stored; an upload never waits for or
|
|
fails on it) and stores it with the revision (`product_revision.parent_info`: `{found, image, categories, gender,
|
|
checkedAt}`; `found` is false when IMVU shows no such product publicly). Info another revision got for the same
|
|
parent in the last 7 days is reused. Revisions still without it (IMVU was unavailable) are looked up at startup and
|
|
every 10 minutes, 20 at a time.
|
|
|
|
### Console (moderators)
|
|
|
|
`GET /api/v1/console/catalogs`, `POST /api/v1/console/catalogs/:id/approve`, `.../reject`.
|
|
|
|
## The proxy
|
|
|
|
- Fetches the library at connect and on `library` messages; for each product, `GET /api/v1/products/:id/files`,
|
|
downloads missing files from a listed catalog (`GET <catalog>/api/v1/files/<sha256>`, with an access token for
|
|
adult revisions), checks each sha256, and writes the product as a pack in its cache
|
|
(`%APPDATA%\T5DE\proxy\hub-products\<id>\`: `t5de-product.json` with the hub's name and rating, and `files/`).
|
|
From there it's served like any pack.
|
|
- Products others wear in a room that it doesn't have are fetched the same way, then given to the client. Seeing
|
|
needs no membership (design.md, "Seeing versus wearing"): `/files` answers for any published product the person
|
|
may see by rating. Wearing does: the hub relays a person's outfit with only the products available to them.
|
|
- Library products appear in the client's inventory (see the proxy's README).
|
|
|
|
## Decorations
|
|
|
|
T5 furniture and pose products placed in an ordinary IMVU room (design.md, "Decorations over IMVU rooms"). IMVU
|
|
keeps the room without them; the hub keeps them (`room_decoration`, routes in api.md), and only T5DE users see them.
|
|
|
|
- **Saving**: when the room's owner saves the room (`test.setRoomState`) or the client broadcasts a furniture change
|
|
in it, the proxy takes the hub products out before it goes to IMVU, and `PUT /api/v1/decorations/<instance>` the
|
|
room's full list of them (`[furniture instance ID, hub product ID, placement]`).
|
|
- **Loading**: when the client loads an IMVU room (`test.getRoomState`), the proxy `GET`s its decorations and adds
|
|
them to the room's contents (as local IDs, fetching the products as for outfits); the `hash` tells it whether
|
|
what it has is current. In the room, an incoming furniture broadcast from IMVU gets the decorations added back
|
|
the same way.
|
|
- **Changes**: after a `PUT`, every proxy with a chat in that room gets `{"type": "decor", "room": instance}` on the
|
|
realtime connection and `GET`s again.
|
|
- Viewers only get products they may see: registered, with a revision that isn't retracted, adult ones only for
|
|
adult accounts. Furniture instance IDs are the proxy's to choose; they must not clash with IMVU's in that room.
|