[rooms] unset cloning on copy

This commit is contained in:
Devin Zuczek
2026-09-22 10:25:15 -04:00
parent c46341b331
commit 2dc374ab70
3 changed files with 38 additions and 6 deletions
+1 -1
View File
@@ -1719,7 +1719,7 @@ const app = new Hono<App>()
'caller. Cloning is the only way to make a room, so the per-account room cap is',
'enforced here — it counts the rooms the account created, minus their auto-provisioned',
'dorm (`MAX_ROOMS_PER_ACCOUNT`; 0 lifts the cap). The clone starts with no tags,',
'`IsRRO` cleared, and PRIVATE accessibility — a new room is unpublished until its',
'zeroed `Stats`, `IsRRO`/`IsDeveloperOwned`/`CloningAllowed` cleared, and PRIVATE accessibility — a new room is unpublished until its',
'owner sets its accessibility, so it never lands in the public feeds on creation.',
'',
'Rejections — a blank or taken name, the cap, a source that disallows cloning — are',
@@ -1309,11 +1309,20 @@ describe('rooms endpoints', () => {
CreatorAccountId: number
Tags?: Array<{ Tag: string }>
IsRRO: boolean
IsDeveloperOwned: boolean
CloningAllowed: boolean
Accessibility: number
Roles: Array<{ AccountId: number; Role: number; InvitedRole: number }>
Stats: Record<string, number>
} | null
}
// Give the source every flag and counter the clone must NOT inherit.
await env.DB.prepare(
`UPDATE room SET data = json_set(data, '$.IsDeveloperOwned', json('true'),
'$.CloningAllowed', json('true'), '$.Stats.VisitorCount', 42) WHERE room_id = 24`
).run()
// Clone MakerRoom (base, RoomId 24) → a fresh room owned by the caller (801).
const ok = await post(24, 'MyMakerClone')
expect(ok.success).toBe(true)
@@ -1327,6 +1336,16 @@ describe('rooms endpoints', () => {
expect(ok.value!.Tags).toEqual([])
// IsRRO is cleared so the client doesn't render a virtual "RRO" tag on the clone.
expect(ok.value!.IsRRO).toBe(false)
// Nor is it developer-owned, and the new owner opts it into cloning themselves.
expect(ok.value!.IsDeveloperOwned).toBe(false)
expect(ok.value!.CloningAllowed).toBe(false)
// No engagement carries over — not even the blob-stored VisitorCount.
expect(ok.value!.Stats).toEqual({
CheerCount: 0,
FavoriteCount: 0,
VisitorCount: 0,
VisitCount: 0,
})
// A new room is unpublished: Private (0), never the source's visibility.
expect(ok.value!.Accessibility).toBe(0)
// Ownership is reset to the cloner: sole owner (Role 255), and none of the
@@ -1338,8 +1357,16 @@ describe('rooms endpoints', () => {
// It persists and is fetchable by its new id.
const fetched = (await (await SELF.fetch(`${ORIGIN}/rooms/${ok.value!.RoomId}`)).json()) as {
Name: string
Tags: unknown[]
IsDeveloperOwned: boolean
CloningAllowed: boolean
Stats: Record<string, number>
}
expect(fetched.Name).toBe('MyMakerClone')
expect(fetched.Tags).toEqual([])
expect(fetched.IsDeveloperOwned).toBe(false)
expect(fetched.CloningAllowed).toBe(false)
expect(fetched.Stats.VisitorCount).toBe(0)
// Duplicate name is rejected.
const dup = await post(24, 'MyMakerClone')
+10 -5
View File
@@ -661,8 +661,8 @@ export async function deleteRoomLeaderboard(
* name, and the new owner. The clone starts with an empty tag set — the source's
* tags (including the `base` template tag) do not carry over, so the owner tags the
* clone from scratch — `IsRRO` is cleared so the client doesn't render a virtual
* "RRO" tag on it, and it starts PRIVATE rather than inheriting the source's
* visibility. Returns the new room, or null when the source isn't in D1 or disallows
* "RRO" tag on it, `IsDeveloperOwned`/`CloningAllowed` are cleared, its `Stats` start
* at zero, and it starts PRIVATE rather than inheriting the source's visibility. Returns the new room, or null when the source isn't in D1 or disallows
* cloning.
*/
export async function cloneRoom(
@@ -702,15 +702,20 @@ export async function cloneRoom(
// A user clone is not a Rec Room Original — clear the inherited flag, or the
// client renders a virtual "RRO" tag on the clone.
IsRRO: false,
// Neither is the cloner a developer, and a copy doesn't inherit the source's consent
// to be copied: the new owner opts their room into cloning themselves.
IsDeveloperOwned: false,
CloningAllowed: false,
// A brand-new room is unpublished: the owner publishes it by setting the room's
// accessibility. Inheriting the source's would put the clone straight into the
// public feeds (hot/search/recommendations/similar all key on Accessibility === 1)
// the moment it was made — every clone of a PUBLIC source, template or player room.
Accessibility: Accessibility.Private,
Roles: roles,
// A fresh room has no engagement of its own — don't inherit the source's counters
// (the derived ones are recomputed per read, but the clone is returned as-is here).
Stats: storedStats(source.Stats),
// A fresh room has no engagement of its own — don't inherit any of the source's
// counters. `storedStats` alone would keep the blob-owned `VisitorCount`, which
// `hotScore` ranks on.
Stats: { ...ZERO_STATS },
CreatedAt: new Date().toISOString(),
}