support for 202507 endpoints (#37)

* [auth][api] accept the 20250424.01 client

* [2025] unstable

* 20250718.0

* correct one this time

* stubs

* more stubs

* more stubs

* [lists] add worker

* [ai] route stubs

* [api] player photo setting

* [econ] add roomEconConfig route

* [infra] update worker generators

* [worker] add cards/moderation/platformnotification workers

* [lists] updates to some endpoints

* [clubs] stub out announcement endpoint, for now

* [econ] stub out season endpoints for now

* [chat] apps/chat stub out party endpoint not sure the shape yet

* [api] stub out statsig and lockeditems

* [doc] new services

* [lists] stub the bulk endpoint

* [datacollection] add placeholder service until we can kill it

* [api] set gifting to lvl5

* update lock

* [cdn] enable cache

* [match] matchmake v2

* [lists] stub some lists

* [ai] stubs

* [rooms] new subroom save endpoint

* [econ] add bulk purchase endpoint

* [discovery] update featured creator to 1 for fun

* [api] add photo settings flag

* [chat] fixup chat permissions (sorta)

* [auth] restrictions endpoint

* [rooms] contributed endpoint

* [api] fix outfit endpoint

* [discovery] attempt to fix store

* [chat] privacy endpoints

* [api] cheered images

* [rooms] add xp endpoint (disbaled)

* [rooms] add xp endpoint (disabled)

* update images-db for cheers

* [rooms] add autocomplete endpoint

* [cdn/img] increase cache ttl for statics

* [api] bulk route for images

* [accounts] add banner image

* [api] add misc missing endpoints

* [discovery] remove AI tab

* [platformnotifications] stub some endpoints

* [lists] add some more lists

* [rooms] additional endpoints

* [chat] stub a few privacy endpoints

* [econ] stub some endpoints

* misc db fixes

* [api] tweak shape for images v6

* [rooms] dont show trending RROs
This commit is contained in:
devin
2026-08-18 23:07:24 -04:00
committed by Devin Zuczek
parent 66c09806f9
commit 178d3b5b0e
162 changed files with 114930 additions and 469 deletions
+202 -6
View File
@@ -137,6 +137,14 @@ export const BacktraceConfig = z.object({
VersionRegex: z.string(),
})
/**
* `GET /statsigUserProperties` — the reference server answers this with a single
* `success` carrying its `StatsigEnabled` config value, a BOOL rather than an int.
*/
export const StatsigUserProperties = z.object({
success: z.boolean().describe('The Statsig-enabled flag (true)'),
})
/**
* `GET /api/config/v2` — the big client config blob (a static asset), with
* `ShareBaseUrl` derived from the deploy-time base domain.
@@ -145,7 +153,13 @@ export const ApiConfigV2 = JsonObject.describe(
'The static client config, plus a ShareBaseUrl templated from the deploy domain'
)
/** `GET /api/versioncheck/v4` — whether the client's `?v=` build matches GAME_VERSION. */
/**
* `GET /api/versioncheck/islandedversions` — builds islanded onto their own matchmaking
* pool. Always empty here.
*/
export const IslandedVersions = z.array(z.string())
/** `GET /api/versioncheck/v4` — whether the client's `?v=` build is one we serve. */
export const VersionCheck = z.object({
VersionStatus: z.int().describe('0 = current, 1 = client on a different build'),
UpdateNotificationStage: z.int(),
@@ -199,6 +213,17 @@ export const SendMultipleMessagesRequest = z.object({
Data: z.string().optional().describe('The message payload; often empty'),
})
/**
* `POST /api/messages/v1/friendOnlineStatus` — how many of the caller's friends are
* online, wrapped in the client's `{ success, value }` envelope.
*/
export const FriendOnlineCountResponse = z.object({
success: z.boolean(),
value: z.object({
FriendsOnlineCount: z.int().describe('Friends with live presence right now'),
}),
})
/** The `{ Success, Message }` ack the flag toggles answer with. */
export const AckResponse = z.object({ Success: z.boolean(), Message: z.string() })
@@ -411,12 +436,118 @@ export const GenerateGiftRequest = z.object({
Xp: z.string().optional(),
})
/**
* `POST /api/customAvatarItems/v1/bulk` form body. A repeated form field, not a JSON
* array: the reference binds `[FromForm] List<string>`, so the client posts
* `customAvatarItemIds=a&customAvatarItemIds=b`.
*/
export const BulkCustomAvatarItemsRequest = z.object({
customAvatarItemIds: z
.array(z.string())
.describe('The ids to resolve; repeat the field once per id'),
})
/** A paginated custom-avatar-item page (no storage yet, so always empty). */
export const CustomAvatarItemsPage = z.object({
Results: JsonArray,
TotalResults: z.int(),
})
/**
* One custom-item save — the rebuilt version of a legacy avatar item. This is the
* official shape, recorded for documentation: nothing stores custom items yet, so we
* never actually emit one of these.
*/
export const CustomAvatarItemSave = z.object({
customAvatarItemSaveId: z.int().describe('The saves id'),
customAvatarItemId: z.string().describe('Guid of the custom item this save belongs to'),
unityAssetId: z.string().describe('Guid of the built Unity asset'),
createdAt: z.string().describe('ISO 8601 timestamp'),
thumbnailFileName: z.string(),
additionalConfiguration: z.string(),
unityAsset: z.string(),
unityAssetHash: z.string(),
})
/**
* The custom-item saves that replace a set of legacy avatar items, keyed by the legacy
* item's `AvatarItemDesc`. Nothing stores custom items yet, so the map is always empty —
* the value shape is documented rather than served.
*/
export const LegacyAvatarItemSaves = z.object({
customAvatarItemSavesByAvatarItemDesc: z.record(z.string(), CustomAvatarItemSave),
})
/**
* `GET /outfits/me` — the outfit envelope. Either the outfit stored in slot 0, served
* back exactly as it was saved, or (for a player who has never saved) the brand-new-
* account form, where every field that would carry an outfit is null/empty and
* `DataVersion` is 9.
*/
export const OutfitsMeResponse = z.object({
LegacyData: z.object({
SelectionsV1: z.string().nullable().describe('Semicolon-delimited legacy descriptors'),
SelectionsV2: z.string().nullable().describe('JSON-in-a-string: `{ selections: [...] }`'),
FaceFeatures: z.string().nullable().describe('JSON-in-a-string'),
SkinColor: z.string().nullable(),
HairColor: z.string().nullable(),
}),
Selections: JsonArray,
DataVersion: z.int().describe('9 in the new-account envelope; whatever was saved otherwise'),
CustomizationSettings: z
.string()
.nullable()
.describe('JSON-in-a-string: the same outfit in the newer structured form'),
ThumbnailFileName: z.string().nullable(),
Name: z.string().nullable(),
Accessibility: z.int(),
Slot: z.int().describe('0 — the outfit being worn'),
})
/**
* `PUT /outfits/me` JSON body — the outfit the client is saving, in the newer envelope.
* The heavy fields are JSON-in-a-string, exactly as the client serialises them:
* `SelectionsV2` and `CustomizationSettings` are whole documents encoded as strings, and
* `FaceFeatures` likewise. Note the two formats overlap: `LegacyData` carries the old
* flat descriptors while `CustomizationSettings` carries the same outfit in the new
* structured form, and the client sends both. `Selections` arrives empty — the actual
* selections are inside those strings.
*/
export const OutfitsMeRequest = z.object({
DataVersion: z.int().describe('The clients outfit format version (2 in observed saves)'),
LegacyData: z.object({
SelectionsV1: z.string().nullable().describe('Semicolon-delimited legacy descriptors'),
SelectionsV2: z.string().nullable().describe('JSON-in-a-string: `{ selections: [...] }`'),
FaceFeatures: z.string().nullable().describe('JSON-in-a-string'),
SkinColor: z.string().nullable(),
HairColor: z.string().nullable(),
}),
CustomizationSettings: z
.string()
.nullable()
.describe('JSON-in-a-string: the same outfit in the newer structured form'),
Selections: JsonArray.describe('Empty in observed saves'),
Slot: z.int(),
Name: z.string().nullable(),
Accessibility: z.int(),
ThumbnailFileName: z.string().nullable(),
})
/**
* `PUT /outfits/me` — the base envelope, with NO `Value` key: three keys and that is the
* whole body. The save answers only whether it worked; the client keeps the outfit it just
* sent rather than re-rendering from a response, so nothing here echoes the outfit back.
*
* Note the mixed casing — `Success` and `Error` are PascalCase, `error_id` is snake_case.
* That is what the reference sends, and the client's decoder matches on the exact names, so
* do not "tidy" it into one convention.
*/
export const OutfitSaveResponse = z.object({
Success: z.boolean(),
Error: z.string().nullable().describe('Null on success'),
error_id: z.string().nullable().describe('Null on success. snake_case, unlike its siblings'),
})
/** The `{ success, value }` envelope `isCreationAllowedForAccount` wraps its answer in. */
export const SuccessValueEnvelope = z.object({ success: z.boolean(), value: z.null() })
@@ -582,13 +713,27 @@ export const PlayerEventsPage = z.object({
// ---- Moderation ------------------------------------------------------------
/**
* `GET /api/PlayerReporting/v1/moderationBlockDetails` — always the "not blocked"
* answer (no ban storage yet). `ReportCategory` is -1 (no category) rather than 0,
* which is a real category; `Message` is null, not an empty string — the client
* distinguishes "no message" from a blank one.
* One row of `GET /api/PlayerReporting/v1/voteToKickReasons` — the label the client puts
* on a vote-to-kick button, and the report category the kick is filed under if it passes.
*/
export const VoteToKickReason = z.object({
Reason: z.string().describe('The label shown on the button'),
ReportCategory: z
.int()
.describe('The category the resulting report is filed under: 101, 102, 103 or 6'),
})
/**
* `GET|POST /api/PlayerReporting/v1/moderationBlockDetails` — always the "not blocked"
* answer (no ban storage yet), mirroring the reference server's stub
* `ReturnModerationBlockDetails()`. `ReportCategory` is `Unknown` (-1) rather than 0,
* which is a real category, and `Message` is null — the client distinguishes "no
* message" from a blank one, so we send null where the reference sends an empty string.
* `IsVoiceModAutoban`/`TimeoutStartedAt` are on the DTO but unset by that stub, so
* they carry their C# defaults (false / null).
*/
export const ModerationBlockDetails = z.object({
ReportCategory: z.int().describe('-1 = no category (0 is a real one)'),
ReportCategory: z.int().describe('-1 = ReportCategory.Unknown (0 is a real category)'),
Duration: z.int(),
GameSessionId: z.int(),
IsBan: z.boolean(),
@@ -687,6 +832,29 @@ export const SavedImageDto = z.object({
CommentCount: z.int(),
})
/**
* `GET /api/images/v6` — an image's metadata by bucket key. A third projection of the same
* row: renamed like `ImagesPlayer` (`SavedImageId`/`SavedImageType`, no `TaggedPlayerIds`)
* but carrying `ClubId`, and with no nullable fields — `RoomId`, `PlayerEventId` and
* `ClubId` are 0 where the row holds null, `Description` is `""`. Don't unify it with the
* other two.
*/
export const ImageMetadataDto = z.object({
SavedImageId: z.int(),
ImageName: z.string().describe('The bucket key the img worker serves it back by'),
PlayerId: z.int(),
RoomId: z.int().describe('0 when the photo was not taken in a room'),
PlayerEventId: z.int().describe('0 when it belongs to no event'),
ClubId: z.int().describe('Always 0 — nothing here associates an image with a club'),
Description: z.string().describe('Empty string, never null'),
Accessibility: z.int(),
AccessibilityLocked: z.boolean(),
SavedImageType: z.int().describe('1 = share camera, 3 = room, 4 = profile, …'),
CreatedAt: z.string(),
CheerCount: z.int(),
CommentCount: z.int(),
})
/**
* The client's `ImagesPlayer` projection — the same record with `Id` → `SavedImageId`,
* `Type` → `SavedImageType` and no `TaggedPlayerIds`. The player photo lists and feed
@@ -745,12 +913,40 @@ export const UploadImageResponse = z.object({
/** `DELETE /api/images/v1/deletesaved` JSON body. */
export const DeleteImageRequest = z.object({ ImageName: z.string() })
/**
* `POST /api/images/v5/cheered/bulk` form body — the saved-image ids to report cheer state
* for, as a REPEATED `id` field (`id=651&id=570&…`), one value per id. The client sends a
* whole photo-grid page this way, around a hundred ids at a time.
*/
export const CheeredBulkRequest = z.object({
id: z.string().describe('Repeated once per image id; each value may also be comma-separated'),
})
/** `POST /api/images/v1/cheer` JSON body. */
export const CheerImageRequest = z.object({
SavedImageId: z.int(),
Cheer: z.boolean().describe('True to cheer, false to un-cheer'),
})
/**
* `PUT /api/players/v1/playerPhotoTaggingSetting` JSON body — who may tag the caller in
* photos. The value is an opaque enum ordinal: it is stored and served back untouched, so
* whatever the client means by a given number survives a round trip without this server
* needing to know the enum.
*/
export const PhotoTaggingSettingRequest = z.object({
Setting: z.int().describe('The preferences enum ordinal, stored verbatim'),
})
/**
* `GET|PUT /api/players/v1/playerPhotoTaggingSetting` — a BARE JSON integer, not an
* envelope and not a `{ value }` wrapper. Both routes answer the setting the player now
* has: the reference's GET and its PUT both `Ok(...)` the stored value.
*/
export const PhotoTaggingSettingResponse = z
.int()
.describe('The callers photo-tagging preference; 0 until they set one')
/** The bare `{ success: true }` ack the image writes answer with. */
export const SuccessResponse = z.object({ success: z.boolean() })