mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-28 00:18:58 -07:00
[clubs] add account clubs endpoint
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
|||||||
getHomeClub,
|
getHomeClub,
|
||||||
getMembership,
|
getMembership,
|
||||||
getOrCreateSubscriptionClub,
|
getOrCreateSubscriptionClub,
|
||||||
|
getPublicClubsByMember,
|
||||||
getSubscriptionClub,
|
getSubscriptionClub,
|
||||||
getSubscriptionMemberships,
|
getSubscriptionMemberships,
|
||||||
glyphLength,
|
glyphLength,
|
||||||
@@ -685,6 +686,35 @@ const app = new Hono<App>()
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The clubs shown on another player's profile: the PUBLIC clubs the account is a
|
||||||
|
// member of. Public — a private club is left out for everyone, the account's own
|
||||||
|
// owner included (they read theirs from /club/mine/member). An unknown account
|
||||||
|
// simply has no clubs.
|
||||||
|
.get(
|
||||||
|
'/account/:accountId{[0-9]+}/clubs',
|
||||||
|
describeRoute({
|
||||||
|
tags: ['Clubs'],
|
||||||
|
summary: 'The public clubs an account is a member of',
|
||||||
|
description: [
|
||||||
|
'The account’s memberships from `club_member`, oldest club first, PUBLIC clubs only',
|
||||||
|
'(private clubs, subscription clubs and pending/denied/banned rows excluded). Public',
|
||||||
|
'(no auth). An unknown account has `[]`.',
|
||||||
|
].join(' '),
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'accountId',
|
||||||
|
in: 'path',
|
||||||
|
required: true,
|
||||||
|
description: 'Account id (digits only)',
|
||||||
|
schema: { type: 'string' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
responses: { 200: json(ClubDto.array(), 'The account’s public clubs') },
|
||||||
|
}),
|
||||||
|
async (c) =>
|
||||||
|
c.json(await getPublicClubsByMember(c.env.DB, Number.parseInt(c.req.param('accountId'), 10)))
|
||||||
|
)
|
||||||
|
|
||||||
// Club search / browse. Public, non-subscription clubs; `category` filters to that
|
// Club search / browse. Public, non-subscription clubs; `category` filters to that
|
||||||
// category, `query` matches the name or description, `sort` picks the order (1 =
|
// category, `query` matches the name or description, `sort` picks the order (1 =
|
||||||
// newest, 2 = by name, default = most members first), and `count` caps the page
|
// newest, 2 = by name, default = most members first), and `count` caps the page
|
||||||
|
|||||||
@@ -369,6 +369,64 @@ describe('clubs endpoints', () => {
|
|||||||
expect(await res.json()).toEqual([])
|
expect(await res.json()).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('GET /account/:id/clubs lists only the PUBLIC clubs the account is a member of', async () => {
|
||||||
|
const club = (ClubId: number, Visibility: number, ClubType = 0) => ({
|
||||||
|
ClubId,
|
||||||
|
Name: `Club ${ClubId}`,
|
||||||
|
Description: '',
|
||||||
|
Category: 'Social',
|
||||||
|
Visibility,
|
||||||
|
Joinability: 0,
|
||||||
|
AllowJuniors: true,
|
||||||
|
MainImageName: '',
|
||||||
|
ClubType,
|
||||||
|
ClubhouseRoomId: null,
|
||||||
|
CreatorAccountId: 4500,
|
||||||
|
IsRRO: false,
|
||||||
|
MinLevel: 0,
|
||||||
|
State: 0,
|
||||||
|
MemberCount: 2,
|
||||||
|
CreatedAt: `2026-07-0${ClubId - 9100}T00:00:00Z`,
|
||||||
|
})
|
||||||
|
// 9101 public, 9102 private, 9103 public but only a pending request, 9104 a
|
||||||
|
// public subscription club, 9105 public.
|
||||||
|
const clubs = [club(9101, 1), club(9102, 0), club(9103, 1), club(9104, 1, 1), club(9105, 1)]
|
||||||
|
const insertClub = env.DB.prepare('INSERT INTO club (data) VALUES (?1)')
|
||||||
|
const insertMember = env.DB.prepare(
|
||||||
|
'INSERT INTO club_member (club_id, account_id, membership_type, created_at) VALUES (?1, 4501, ?2, ?3)'
|
||||||
|
)
|
||||||
|
await env.DB.batch([
|
||||||
|
...clubs.map((c) => insertClub.bind(JSON.stringify(c))),
|
||||||
|
...clubs.map((c) =>
|
||||||
|
insertMember.bind(c.ClubId, c.ClubId === 9103 ? 1 : 10, '2026-07-10T00:00:00Z')
|
||||||
|
),
|
||||||
|
])
|
||||||
|
const pending = await env.DB.prepare(
|
||||||
|
'SELECT membership_type FROM club_member WHERE club_id = 9103 AND account_id = 4501'
|
||||||
|
).first<{ membership_type: number }>()
|
||||||
|
expect(pending?.membership_type).toBeLessThan(10)
|
||||||
|
|
||||||
|
// No auth needed; the private, pending and subscription clubs are left out.
|
||||||
|
const res = await exports.default.fetch(`${ORIGIN}/account/4501/clubs`)
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const listed = (await res.json()) as Array<{ ClubId: number; Visibility: number }>
|
||||||
|
expect(listed.map((c) => c.ClubId)).toEqual([9101, 9105])
|
||||||
|
expect(listed[0]).toMatchObject({ Name: 'Club 9101', Visibility: 1, MemberCount: 2 })
|
||||||
|
|
||||||
|
// The member themself gets the same public-only list here.
|
||||||
|
const own = await exports.default.fetch(`${ORIGIN}/account/4501/clubs`, {
|
||||||
|
headers: await bearer('4501'),
|
||||||
|
})
|
||||||
|
expect(((await own.json()) as Array<{ ClubId: number }>).map((c) => c.ClubId)).toEqual([
|
||||||
|
9101, 9105,
|
||||||
|
])
|
||||||
|
|
||||||
|
// An account with no memberships has no clubs; a non-numeric id doesn't match.
|
||||||
|
const none = await exports.default.fetch(`${ORIGIN}/account/4599/clubs`)
|
||||||
|
expect(await none.json()).toEqual([])
|
||||||
|
expect((await exports.default.fetch(`${ORIGIN}/account/abc/clubs`)).status).toBe(404)
|
||||||
|
})
|
||||||
|
|
||||||
test('the my-clubs lists exclude subscription clubs (ClubType 1)', async () => {
|
test('the my-clubs lists exclude subscription clubs (ClubType 1)', async () => {
|
||||||
// A subscription club the player both created and is a member of. It's reached
|
// A subscription club the player both created and is a member of. It's reached
|
||||||
// through /subscription/*, so it must not show up among their clubs.
|
// through /subscription/*, so it must not show up among their clubs.
|
||||||
@@ -1646,6 +1704,7 @@ describe('clubs endpoints', () => {
|
|||||||
'DELETE /club/{clubId}',
|
'DELETE /club/{clubId}',
|
||||||
'DELETE /club/{clubId}/additionalimage/{index}',
|
'DELETE /club/{clubId}/additionalimage/{index}',
|
||||||
'DELETE /club/{clubId}/clubhouse',
|
'DELETE /club/{clubId}/clubhouse',
|
||||||
|
'GET /account/{accountId}/clubs',
|
||||||
'GET /announcements/club/{clubId}',
|
'GET /announcements/club/{clubId}',
|
||||||
'GET /announcements/v2/mine/unread',
|
'GET /announcements/v2/mine/unread',
|
||||||
'GET /announcements/v2/subscription/mine/unread',
|
'GET /announcements/v2/subscription/mine/unread',
|
||||||
|
|||||||
@@ -1079,6 +1079,26 @@ export async function getClubsByMember(db: D1Database, accountId: number): Promi
|
|||||||
return parseAll(results)
|
return parseAll(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The PUBLIC clubs an account is an actual member of — what anyone may see of another
|
||||||
|
* player's clubs — oldest club first. {@link getClubsByMember} minus the private ones.
|
||||||
|
*/
|
||||||
|
export async function getPublicClubsByMember(db: D1Database, accountId: number): Promise<Club[]> {
|
||||||
|
const { results } = await db
|
||||||
|
.prepare(
|
||||||
|
`SELECT c.data AS data
|
||||||
|
FROM club_member m
|
||||||
|
JOIN club c ON c.club_id = m.club_id
|
||||||
|
WHERE m.account_id = ?1 AND m.membership_type >= ?2
|
||||||
|
AND json_extract(c.data, '$.ClubType') != ?3
|
||||||
|
AND c.visibility = ?4
|
||||||
|
ORDER BY json_extract(c.data, '$.CreatedAt') ASC`
|
||||||
|
)
|
||||||
|
.bind(accountId, MEMBER_THRESHOLD, SUBSCRIPTION_CLUB_TYPE, ClubVisibility.Public)
|
||||||
|
.all<ClubRow>()
|
||||||
|
return parseAll(results)
|
||||||
|
}
|
||||||
|
|
||||||
/** Whether an account is an actual member of a club (Member tier or above). */
|
/** Whether an account is an actual member of a club (Member tier or above). */
|
||||||
export async function isClubMember(
|
export async function isClubMember(
|
||||||
db: D1Database,
|
db: D1Database,
|
||||||
|
|||||||
Reference in New Issue
Block a user