mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-27 07:58:55 -07:00
[rooms] keep invited roles on the roles endpoint for the permissions list
This commit is contained in:
@@ -14,7 +14,6 @@ import {
|
|||||||
canManageRoomById,
|
canManageRoomById,
|
||||||
cloneRoom,
|
cloneRoom,
|
||||||
cloneSubRoom,
|
cloneSubRoom,
|
||||||
confirmedRoomRoles,
|
|
||||||
countRoomsByCreator,
|
countRoomsByCreator,
|
||||||
createNotification,
|
createNotification,
|
||||||
createStudioRoom,
|
createStudioRoom,
|
||||||
@@ -2659,15 +2658,14 @@ const app = new Hono<App>()
|
|||||||
summary: 'A room’s roles',
|
summary: 'A room’s roles',
|
||||||
description: [
|
description: [
|
||||||
'Everyone with a role in the room — its creator plus whoever has been granted Host,',
|
'Everyone with a role in the room — its creator plus whoever has been granted Host,',
|
||||||
'Moderator or CoOwner — as whole `RoomRole` records. This is the same array the',
|
'Moderator or CoOwner — as whole `RoomRole` records, PLUS every pending invite. It is',
|
||||||
'room DTO serves under `Roles`, on its own endpoint, so it is public exactly like the',
|
'public exactly like the room is: a role list says who runs a room, which the room',
|
||||||
'room is: a role list says who runs a room, which the room page already shows.',
|
'page already shows.',
|
||||||
'',
|
'',
|
||||||
'Held roles only. A player with a standing co-owner invite they have not answered',
|
'Unlike the room DTO’s `Roles`, this list INCLUDES unanswered co-owner invites (`Role`',
|
||||||
'(`Role` 0, the offer on `InvitedRole`) is not listed here or on the room DTO: they',
|
'0, the offer on `InvitedRole`): the client reads it to show the players with an',
|
||||||
'have no standing in the room until they accept, and the offer reaches them as a',
|
'invite pending. The room DTO leaves those entries out, because there an invited',
|
||||||
'`RoomCoOwnerInvited` message. A member who holds a role AND has a higher one pending',
|
'player read as a member before they had accepted anything.',
|
||||||
'is listed at the role they hold, `InvitedRole` included.',
|
|
||||||
'',
|
'',
|
||||||
'A bare array, NOT the `{ success, error, value }` envelope the role WRITE answers. An',
|
'A bare array, NOT the `{ success, error, value }` envelope the role WRITE answers. An',
|
||||||
'unknown room is an empty list rather than an error — it reads the same as a room',
|
'unknown room is an empty list rather than an error — it reads the same as a room',
|
||||||
@@ -2681,7 +2679,7 @@ const app = new Hono<App>()
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const roomId = Number.parseInt(c.req.param('roomId'), 10)
|
const roomId = Number.parseInt(c.req.param('roomId'), 10)
|
||||||
const room = await getRoomById(c.env.DB, roomId)
|
const room = await getRoomById(c.env.DB, roomId)
|
||||||
return c.json(room ? confirmedRoomRoles(room) : [])
|
return c.json(room ? roomRoles(room) : [])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -576,8 +576,9 @@ describe('rooms endpoints', () => {
|
|||||||
expect(ownedIds).toContain(30401)
|
expect(ownedIds).toContain(30401)
|
||||||
expect(ownedIds).not.toContain(30408)
|
expect(ownedIds).not.toContain(30408)
|
||||||
|
|
||||||
// Nor is the invited player part of the room anyone is shown: the room DTO and the
|
// Nor is the invited player part of the room as served: the room DTO carries only HELD
|
||||||
// role list carry only HELD roles. The stored room keeps the entry for the accept.
|
// roles. `GET /rooms/:id/roles` DOES list the invite — the client shows pending invites
|
||||||
|
// from it — and the stored room keeps the entry for the accept.
|
||||||
type Served = { Roles: Array<{ AccountId: number; Role: number; InvitedRole: number }> }
|
type Served = { Roles: Array<{ AccountId: number; Role: number; InvitedRole: number }> }
|
||||||
const servedRoles = async () =>
|
const servedRoles = async () =>
|
||||||
((await (await SELF.fetch(`${ORIGIN}/rooms/30408`)).json()) as Served).Roles
|
((await (await SELF.fetch(`${ORIGIN}/rooms/30408`)).json()) as Served).Roles
|
||||||
@@ -586,6 +587,7 @@ describe('rooms endpoints', () => {
|
|||||||
])
|
])
|
||||||
expect(await (await SELF.fetch(`${ORIGIN}/rooms/30408/roles`)).json()).toEqual([
|
expect(await (await SELF.fetch(`${ORIGIN}/rooms/30408/roles`)).json()).toEqual([
|
||||||
{ AccountId: 821, Role: 255, LastChangedByAccountId: null, InvitedRole: 0 },
|
{ AccountId: 821, Role: 255, LastChangedByAccountId: null, InvitedRole: 0 },
|
||||||
|
{ AccountId: 820, Role: 0, LastChangedByAccountId: 821, InvitedRole: 30 },
|
||||||
])
|
])
|
||||||
expect(
|
expect(
|
||||||
await env.DB.prepare(
|
await env.DB.prepare(
|
||||||
@@ -1729,8 +1731,7 @@ describe('rooms endpoints', () => {
|
|||||||
const envOf = async (res: Response) => (await res.json()) as RoomEnv
|
const envOf = async (res: Response) => (await res.json()) as RoomEnv
|
||||||
|
|
||||||
// A role entry as STORED, read off the `room_role` index — pending invites included.
|
// A role entry as STORED, read off the `room_role` index — pending invites included.
|
||||||
// The served room and `GET /rooms/:id/roles` carry only held roles, so an entry that is
|
// The served room carries only held roles; `GET /rooms/:id/roles` lists invites too.
|
||||||
// nothing but an unanswered invite (`Role` 0) can only be observed here.
|
|
||||||
type StoredRole = { Role: number; InvitedRole: number; LastChangedByAccountId: number | null }
|
type StoredRole = { Role: number; InvitedRole: number; LastChangedByAccountId: number | null }
|
||||||
const storedRoleOf = async (roomId: number, accountId: number) =>
|
const storedRoleOf = async (roomId: number, accountId: number) =>
|
||||||
(await env.DB.prepare(
|
(await env.DB.prepare(
|
||||||
@@ -2158,8 +2159,8 @@ describe('rooms endpoints', () => {
|
|||||||
error: 'You must provide a valid role!',
|
error: 'You must provide a valid role!',
|
||||||
})
|
})
|
||||||
expect(await storedRoleOf(2, 885)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
expect(await storedRoleOf(2, 885)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
||||||
// An entry that is only an invite is not a role the room serves.
|
// The role list still shows the invite as pending.
|
||||||
expect(await roleOf(885)).toBeUndefined()
|
expect(await roleOf(885)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('a revoke pushes a RoomUpdate to the room and to the player who lost the role', async () => {
|
it('a revoke pushes a RoomUpdate to the room and to the player who lost the role', async () => {
|
||||||
@@ -2218,11 +2219,11 @@ describe('rooms endpoints', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// RecCenter (room 2) is owned by account 1. The owner invites 850 to co-ownership,
|
// RecCenter (room 2) is owned by account 1. The owner invites 850 to co-ownership,
|
||||||
// leaving a pending offer and no role yet. The offer is stored on their entry but
|
// leaving a pending offer and no role yet. The role list shows the pending invite, but
|
||||||
// the room as SERVED does not list them: they are not a member until they accept.
|
// the room DTO does not list them: they are not a member until they accept.
|
||||||
expect((await putForm('/rooms/2/roles/850/invite', { role: '30' }, '1')).status).toBe(200)
|
expect((await putForm('/rooms/2/roles/850/invite', { role: '30' }, '1')).status).toBe(200)
|
||||||
expect(await storedRoleOf(2, 850)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
expect(await storedRoleOf(2, 850)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
||||||
expect(await roleOf(850)).toBeUndefined()
|
expect(await roleOf(850)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
||||||
const room2 = (await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as { Roles: Role[] }
|
const room2 = (await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as { Roles: Role[] }
|
||||||
expect(room2.Roles.some((r) => r.AccountId === 850)).toBe(false)
|
expect(room2.Roles.some((r) => r.AccountId === 850)).toBe(false)
|
||||||
|
|
||||||
@@ -2302,7 +2303,7 @@ describe('rooms endpoints', () => {
|
|||||||
// offer rather than an entry with nothing pending on it.
|
// offer rather than an entry with nothing pending on it.
|
||||||
expect((await putForm('/rooms/2/roles/851/invite', { role: '30' }, '1')).status).toBe(200)
|
expect((await putForm('/rooms/2/roles/851/invite', { role: '30' }, '1')).status).toBe(200)
|
||||||
expect(await storedRoleOf(2, 851)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
expect(await storedRoleOf(2, 851)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
||||||
expect(await roleOf(851)).toBeUndefined()
|
expect(await roleOf(851)).toMatchObject({ Role: 0, InvitedRole: 30 })
|
||||||
|
|
||||||
const declined = await putForm('/rooms/2/roles/851', { role: '0' }, '851')
|
const declined = await putForm('/rooms/2/roles/851', { role: '0' }, '851')
|
||||||
expect(declined.status).toBe(200)
|
expect(declined.status).toBe(200)
|
||||||
@@ -2570,14 +2571,23 @@ describe('rooms endpoints', () => {
|
|||||||
expect(await res.json()).toEqual({ success: true })
|
expect(await res.json()).toEqual({ success: true })
|
||||||
|
|
||||||
// A new entry, holding no role yet — only the pending offer, stamped with the inviter.
|
// A new entry, holding no role yet — only the pending offer, stamped with the inviter.
|
||||||
// STORED, not served: the room's role list carries held roles only, so the invited
|
// The role list shows it as a pending invite (the client lists pending invites from
|
||||||
// player does not appear on it until they accept.
|
// it); the room DTO's `Roles` does not, until they accept.
|
||||||
expect(await storedRoleOf(2, 187)).toEqual({
|
expect(await storedRoleOf(2, 187)).toEqual({
|
||||||
Role: 0,
|
Role: 0,
|
||||||
InvitedRole: 30,
|
InvitedRole: 30,
|
||||||
LastChangedByAccountId: 1,
|
LastChangedByAccountId: 1,
|
||||||
})
|
})
|
||||||
expect((await rolesOf(2)).find((r) => r.AccountId === 187)).toBeUndefined()
|
expect(await rolesOf(2)).toContainEqual({
|
||||||
|
AccountId: 187,
|
||||||
|
Role: 0,
|
||||||
|
InvitedRole: 30,
|
||||||
|
LastChangedByAccountId: 1,
|
||||||
|
})
|
||||||
|
const room2 = (await (await SELF.fetch(`${ORIGIN}/rooms/2`)).json()) as {
|
||||||
|
Roles: Array<{ AccountId: number }>
|
||||||
|
}
|
||||||
|
expect(room2.Roles.some((r) => r.AccountId === 187)).toBe(false)
|
||||||
|
|
||||||
// One MessageReceived to the INVITED player carrying a type 62 RoomCoOwnerInvited —
|
// One MessageReceived to the INVITED player carrying a type 62 RoomCoOwnerInvited —
|
||||||
// the entry alone is silent, this is what raises the invite on their client.
|
// the entry alone is silent, this is what raises the invite on their client.
|
||||||
|
|||||||
@@ -304,18 +304,6 @@ function holdsRole(entry: unknown): boolean {
|
|||||||
return Number((entry as Partial<RoomRole>).Role ?? Role.None) !== Role.None
|
return Number((entry as Partial<RoomRole>).Role ?? Role.None) !== Role.None
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A room's HELD roles — {@link roomRoles} without the pending invites. What the client is
|
|
||||||
* shown as the room's role list: an invited player has no standing in the room until they
|
|
||||||
* accept, and listing them as a member with `Role` 0 showed them on the room's member list
|
|
||||||
* before they had agreed to anything. The offer reaches them as a `RoomCoOwnerInvited`
|
|
||||||
* message instead, and the entry itself stays in the stored room for
|
|
||||||
* {@link answerRoomRoleInvite} to promote.
|
|
||||||
*/
|
|
||||||
export function confirmedRoomRoles(room: Room): RoomRole[] {
|
|
||||||
return roomRoles(room).filter((r) => r.Role !== Role.None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The room as SERVED, with pending invites dropped from its `Roles` — a shallow copy; the
|
* The room as SERVED, with pending invites dropped from its `Roles` — a shallow copy; the
|
||||||
* room passed in is untouched, so a caller can keep writing from it. Entries are kept in the
|
* room passed in is untouched, so a caller can keep writing from it. Entries are kept in the
|
||||||
|
|||||||
Reference in New Issue
Block a user