mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[clubs] add account clubs endpoint
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
getHomeClub,
|
||||
getMembership,
|
||||
getOrCreateSubscriptionClub,
|
||||
getPublicClubsByMember,
|
||||
getSubscriptionClub,
|
||||
getSubscriptionMemberships,
|
||||
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
|
||||
// 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
|
||||
|
||||
@@ -369,6 +369,64 @@ describe('clubs endpoints', () => {
|
||||
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 () => {
|
||||
// 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.
|
||||
@@ -1646,6 +1704,7 @@ describe('clubs endpoints', () => {
|
||||
'DELETE /club/{clubId}',
|
||||
'DELETE /club/{clubId}/additionalimage/{index}',
|
||||
'DELETE /club/{clubId}/clubhouse',
|
||||
'GET /account/{accountId}/clubs',
|
||||
'GET /announcements/club/{clubId}',
|
||||
'GET /announcements/v2/mine/unread',
|
||||
'GET /announcements/v2/subscription/mine/unread',
|
||||
|
||||
@@ -1079,6 +1079,26 @@ export async function getClubsByMember(db: D1Database, accountId: number): Promi
|
||||
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). */
|
||||
export async function isClubMember(
|
||||
db: D1Database,
|
||||
|
||||
Reference in New Issue
Block a user