Skip to content

Commit 893e80c

Browse files
authored
Merge pull request #346 from TaloDev/player-groups-api
Player groups API
2 parents 7b39c39 + a754f53 commit 893e80c

File tree

13 files changed

+291
-29
lines changed

13 files changed

+291
-29
lines changed

src/config/api-routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import apiKeyMiddleware from '../middlewares/api-key-middleware'
1515
import playerAuthMiddleware from '../middlewares/player-auth-middleware'
1616
import PlayerAuthAPIService from '../services/api/player-auth-api.service'
1717
import continunityMiddleware from '../middlewares/continunity-middleware'
18+
import PlayerGroupAPIService from '../services/api/player-group-api.service'
1819

1920
export default (app: Koa) => {
2021
app.use(apiKeyMiddleware)
@@ -31,6 +32,7 @@ export default (app: Koa) => {
3132
app.use(playerAuthMiddleware)
3233
app.use(continunityMiddleware)
3334

35+
app.use(service('/v1/player-groups', new PlayerGroupAPIService()))
3436
app.use(service('/v1/health-check', new HealthCheckAPIService()))
3537
app.use(service('/v1/game-feedback', new GameFeedbackAPIService()))
3638
app.use(service('/v1/game-config', new GameConfigAPIService()))

src/docs/player-group-api.docs.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import PlayerGroupAPIService from '../services/api/player-group-api.service'
2+
import APIDocs from './api-docs'
3+
4+
const PlayerGroupAPIDocs: APIDocs<PlayerGroupAPIService> = {
5+
get: {
6+
description: 'Get a group and its members',
7+
params: {
8+
route: {
9+
id: 'The ID of the group'
10+
}
11+
},
12+
samples: [
13+
{
14+
title: 'Sample response (members visible)',
15+
sample: {
16+
group: {
17+
id: '74b6a013-6c3d-4de6-8f61-444b4cf6e909',
18+
name: 'Online players',
19+
description: 'Players that are currently online',
20+
rules: [
21+
{
22+
name: 'EQUALS',
23+
negate: false,
24+
field: 'props.online',
25+
castType: 'CHAR',
26+
operands: [
27+
'true'
28+
]
29+
}
30+
],
31+
ruleMode: '$and',
32+
membersVisible: true,
33+
updatedAt: '2024-10-01T23:09:18.000Z',
34+
count: 1,
35+
members: [
36+
{
37+
id: '714b38f5-1dee-4243-a54c-50fb1cfe2e4e',
38+
props: [
39+
{
40+
key: 'online',
41+
value: 'true'
42+
}
43+
],
44+
aliases: [],
45+
devBuild: false,
46+
createdAt: '2024-09-29T14:14:34.000Z',
47+
lastSeenAt: '2024-10-03T08:03:16.000Z',
48+
groups: [
49+
{
50+
id: '74b6a013-6c3d-4de6-8f61-444b4cf6e909',
51+
name: 'Online players'
52+
}
53+
]
54+
}
55+
]
56+
}
57+
}
58+
},
59+
{
60+
title: 'Sample response (members not visible)',
61+
sample: {
62+
group: {
63+
id: '74b6a013-6c3d-4de6-8f61-444b4cf6e909',
64+
name: 'Online players',
65+
description: 'Players that are currently online',
66+
rules: [
67+
{
68+
name: 'EQUALS',
69+
negate: false,
70+
field: 'props.online',
71+
castType: 'CHAR',
72+
operands: [
73+
'true'
74+
]
75+
}
76+
],
77+
ruleMode: '$and',
78+
membersVisible: false,
79+
updatedAt: '2024-10-01T23:09:18.000Z',
80+
count: 1
81+
}
82+
}
83+
}
84+
]
85+
}
86+
}
87+
88+
export default PlayerGroupAPIDocs

src/entities/api-key.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import Game from './game'
33
import User from './user'
44

55
export enum APIKeyScope {
6+
READ_PLAYER_GROUPS = 'read:playerGroups',
67
WRITE_CONTINUITY_REQUESTS = 'write:continuityRequests',
78
READ_GAME_FEEDBACK = 'read:gameFeedback',
89
WRITE_GAME_FEEDBACK = 'write:gameFeedback',

src/entities/player-group.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ export default class PlayerGroup {
4848
@Property()
4949
description: string
5050

51+
@Required()
52+
@Property({ default: false })
53+
membersVisible: boolean
54+
5155
@Required({
5256
validation: rulesValidation
5357
})
@@ -103,7 +107,15 @@ export default class PlayerGroup {
103107
description: this.description,
104108
rules: this.rules,
105109
ruleMode: this.ruleMode,
110+
membersVisible: this.membersVisible,
106111
updatedAt: this.updatedAt
107112
}
108113
}
114+
115+
async toJSONWithCount() {
116+
return {
117+
...this.toJSON(),
118+
count: await this.members.loadCount()
119+
}
120+
}
109121
}

src/migrations/.snapshot-gs_dev.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,17 @@
791791
"length": 255,
792792
"mappedType": "string"
793793
},
794+
"members_visible": {
795+
"name": "public",
796+
"type": "tinyint(1)",
797+
"unsigned": false,
798+
"autoincrement": false,
799+
"primary": false,
800+
"nullable": false,
801+
"length": 1,
802+
"default": "false",
803+
"mappedType": "boolean"
804+
},
794805
"rules": {
795806
"name": "rules",
796807
"type": "json",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Migration } from '@mikro-orm/migrations'
2+
3+
export class AddPlayerGroupMembersVisibleColumn extends Migration {
4+
5+
override async up(): Promise<void> {
6+
this.addSql('alter table `player_group` add `members_visible` tinyint(1) not null default false;')
7+
}
8+
9+
override async down(): Promise<void> {
10+
this.addSql('alter table `player_group` drop column `members_visible`;')
11+
}
12+
13+
}

src/migrations/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { UpdatePlayerAliasServiceColumn } from './20240916213402UpdatePlayerAlia
3030
import { AddPlayerAliasAnonymisedColumn } from './20240920121232AddPlayerAliasAnonymisedColumn'
3131
import { AddLeaderboardEntryPropsColumn } from './20240922222426AddLeaderboardEntryPropsColumn'
3232
import { CreateUserPinnedGroupsTable } from './20241001194252CreateUserPinnedGroupsTable'
33+
import { AddPlayerGroupMembersVisibleColumn } from './20241014202844AddPlayerGroupMembersVisibleColumn'
3334

3435
export default [
3536
{
@@ -159,5 +160,9 @@ export default [
159160
{
160161
name: 'CreateUserPinnedGroupsTable',
161162
class: CreateUserPinnedGroupsTable
163+
},
164+
{
165+
name: 'AddPlayerGroupMembersVisibleColumn',
166+
class: AddPlayerGroupMembersVisibleColumn
162167
}
163168
]
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import Policy from '../policy'
2+
import { PolicyDenial, PolicyResponse, Request } from 'koa-clay'
3+
import { APIKeyScope } from '../../entities/api-key'
4+
import PlayerGroup from '../../entities/player-group'
5+
6+
export default class PlayerGroupAPIPolicy extends Policy {
7+
async get(req: Request): Promise<PolicyResponse> {
8+
const { id } = req.params
9+
10+
const key = await this.getAPIKey()
11+
const group = await this.em.getRepository(PlayerGroup).findOne({
12+
id,
13+
game: key.game
14+
})
15+
16+
this.ctx.state.group = group
17+
if (!group) return new PolicyDenial({ message: 'Group not found' }, 404)
18+
19+
return await this.hasScope(APIKeyScope.READ_PLAYER_GROUPS)
20+
}
21+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { HasPermission, Request, Response, Docs } from 'koa-clay'
2+
import APIService from './api-service'
3+
import PlayerGroupAPIPolicy from '../../policies/api/player-group-api.policy'
4+
import PlayerGroup from '../../entities/player-group'
5+
import Player from '../../entities/player'
6+
import PlayerGroupAPIDocs from '../../docs/player-group-api.docs'
7+
8+
type PlayerGroupWithCountAndMembers = Pick<PlayerGroup, 'id' | 'name' | 'description' | 'rules' | 'ruleMode' | 'updatedAt'> & { count: number, members?: Player[] }
9+
10+
export default class PlayerGroupAPIService extends APIService {
11+
@HasPermission(PlayerGroupAPIPolicy, 'get')
12+
@Docs(PlayerGroupAPIDocs.get)
13+
async get(req: Request): Promise<Response> {
14+
const group: PlayerGroup = req.ctx.state.group
15+
16+
const groupWithCountAndMembers: PlayerGroupWithCountAndMembers = await group.toJSONWithCount()
17+
if (group.membersVisible) {
18+
groupWithCountAndMembers.members = await group.members.loadItems()
19+
}
20+
21+
return {
22+
status: 200,
23+
body: {
24+
group: groupWithCountAndMembers
25+
}
26+
}
27+
}
28+
}

src/services/player-group.service.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ import PlayerGroupPolicy from '../policies/player-group.policy'
99
import getUserFromToken from '../lib/auth/getUserFromToken'
1010
import UserPinnedGroup from '../entities/user-pinned-group'
1111

12-
type PlayerGroupWithCount = Pick<PlayerGroup, 'id' | 'name' | 'description' | 'rules' | 'ruleMode' | 'updatedAt'> & { count: number }
13-
1412
@Routes([
1513
{
1614
method: 'GET'
@@ -46,13 +44,6 @@ type PlayerGroupWithCount = Pick<PlayerGroup, 'id' | 'name' | 'description' | 'r
4644
}
4745
])
4846
export default class PlayerGroupService extends Service {
49-
private async groupWithCount(group: PlayerGroup): Promise<PlayerGroupWithCount> {
50-
return {
51-
...group.toJSON(),
52-
count: await group.members.loadCount()
53-
}
54-
}
55-
5647
@HasPermission(PlayerGroupPolicy, 'index')
5748
async index(req: Request): Promise<Response> {
5849
const em: EntityManager = req.ctx.em
@@ -61,7 +52,7 @@ export default class PlayerGroupService extends Service {
6152
return {
6253
status: 200,
6354
body: {
64-
groups: await Promise.all(groups.map(this.groupWithCount))
55+
groups: await Promise.all(groups.map((group) => group.toJSONWithCount()))
6556
}
6657
}
6758
}
@@ -79,14 +70,15 @@ export default class PlayerGroupService extends Service {
7970
@Validate({ body: [PlayerGroup] })
8071
@HasPermission(PlayerGroupPolicy, 'post')
8172
async post(req: Request): Promise<Response> {
82-
const { name, description, ruleMode, rules } = req.body
73+
const { name, description, ruleMode, rules, membersVisible } = req.body
8374
const em: EntityManager = req.ctx.em
8475

8576
const group = new PlayerGroup(req.ctx.state.game)
8677
group.name = name
8778
group.description = description
8879
group.ruleMode = ruleMode
8980
group.rules = this.buildRulesFromData(rules)
81+
group.membersVisible = membersVisible
9082
await group.checkMembership(em)
9183

9284
createGameActivity(em, {
@@ -103,22 +95,23 @@ export default class PlayerGroupService extends Service {
10395
return {
10496
status: 200,
10597
body: {
106-
group: await this.groupWithCount(group)
98+
group: await group.toJSONWithCount()
10799
}
108100
}
109101
}
110102

111103
@Validate({ body: [PlayerGroup] })
112104
@HasPermission(PlayerGroupPolicy, 'put')
113105
async put(req: Request): Promise<Response> {
114-
const { name, description, ruleMode, rules } = req.body
106+
const { name, description, ruleMode, rules, membersVisible } = req.body
115107
const em: EntityManager = req.ctx.em
116108

117109
const group: PlayerGroup = req.ctx.state.group
118110
group.name = name
119111
group.description = description
120112
group.ruleMode = ruleMode
121113
group.rules = this.buildRulesFromData(rules)
114+
group.membersVisible = membersVisible
122115
await group.checkMembership(em)
123116

124117
createGameActivity(em, {
@@ -135,7 +128,7 @@ export default class PlayerGroupService extends Service {
135128
return {
136129
status: 200,
137130
body: {
138-
group
131+
group: await group.toJSONWithCount()
139132
}
140133
}
141134
}
@@ -260,7 +253,7 @@ export default class PlayerGroupService extends Service {
260253
orderBy: { createdAt: 'desc' }
261254
})
262255

263-
const groups = await Promise.all(pinnedGroups.map(({ group }) => this.groupWithCount(group)))
256+
const groups = await Promise.all(pinnedGroups.map(({ group }) => group.toJSONWithCount()))
264257

265258
return {
266259
status: 200,

0 commit comments

Comments
 (0)