From d7214895063605a015d889999ffcb106ceec37fb Mon Sep 17 00:00:00 2001 From: tsa96 Date: Tue, 6 Feb 2024 02:15:00 +0000 Subject: [PATCH 1/3] fix(formats): generate replay hashes in seed script --- libs/db/src/scripts/seed.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/libs/db/src/scripts/seed.ts b/libs/db/src/scripts/seed.ts index b83dad009..94a717ce9 100644 --- a/libs/db/src/scripts/seed.ts +++ b/libs/db/src/scripts/seed.ts @@ -602,6 +602,11 @@ prismaWrapper(async (prisma: PrismaClient) => { user: { connect: { id: userID } }, time, rank: rank++, + // Just any SHA1 hash is fine so long as unique, so game + // can use for unique compator on these + replayHash: createHash('sha1') + .update(Math.random().toString()) + .digest('hex'), stats: {}, // TODO: Add proper stats here when we actually do stats seriously leaderboard: { connect: { From d67db78b5cd01f433d2025ab9a9e416656b35bd7 Mon Sep 17 00:00:00 2001 From: tsa96 Date: Tue, 6 Feb 2024 03:56:20 +0000 Subject: [PATCH 2/3] feat(back): filterUserIDs query param on maps/{id}/leaderboard --- apps/backend-e2e/src/maps-2.e2e-spec.ts | 38 +++++++++++++++++-- .../src/app/dto/queries/map-queries.dto.ts | 4 ++ .../modules/runs/leaderboard-runs.service.ts | 30 ++++++++++----- .../src/types/queries/map-queries.model.ts | 1 + 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/apps/backend-e2e/src/maps-2.e2e-spec.ts b/apps/backend-e2e/src/maps-2.e2e-spec.ts index 31905ee31..6b941635e 100644 --- a/apps/backend-e2e/src/maps-2.e2e-spec.ts +++ b/apps/backend-e2e/src/maps-2.e2e-spec.ts @@ -1461,6 +1461,21 @@ describe('Maps Part 2', () => { token })); + it('should respond with filtered runs given using the filterUserID param', async () => { + const res = await req.get({ + url: `maps/${map.id}/leaderboard`, + query: { + gamemode: Gamemode.AHOP, + filterUserIDs: `${u1.id},${u3.id}` + }, + validatePaged: { type: MinimalLeaderboardRunDto, count: 2 }, + token + }); + + expect(res.body.data[0].userID).toBe(u1.id); + expect(res.body.data[1].userID).toBe(u3.id); + }); + // Test that permissions checks are getting called // Yes, u1 has runs on the map, but we don't actually test for that it('should 403 if the user does not have permission to access to the map', async () => { @@ -1495,7 +1510,7 @@ describe('Maps Part 2', () => { }); describe("GET - 'around' filter", () => { - let map, user7Token, runs; + let map, u7, u7Token, runs; beforeAll(async () => { map = await db.createMap(); @@ -1508,7 +1523,8 @@ describe('Maps Part 2', () => { }) ) ); - user7Token = auth.login(runs[6].user); + u7 = runs[6].user; + u7Token = auth.login(u7); }); afterAll(() => db.cleanup('leaderboardRun', 'pastRun', 'user', 'mMap')); @@ -1518,7 +1534,7 @@ describe('Maps Part 2', () => { url: `maps/${map.id}/leaderboard`, query: { gamemode: Gamemode.AHOP, filter: 'around', take: 8 }, status: 200, - token: user7Token, + token: u7Token, validatePaged: { type: MinimalLeaderboardRunDto, count: 9 } }); @@ -1534,6 +1550,22 @@ describe('Maps Part 2', () => { // 12. expect(rankIndex).toBe(12); }); + + it('should return a list of ranks around your rank filter by userID if given', async () => { + const res = await req.get({ + url: `maps/${map.id}/leaderboard`, + query: { + gamemode: Gamemode.AHOP, + filter: 'around', + filterUserIDs: u7.id + }, + status: 200, + token: u7Token, + validatePaged: { type: MinimalLeaderboardRunDto, count: 1 } + }); + + expect(res.body.data[0].userID).toBe(u7.id); + }); }); describe("GET - 'friends' filter", () => { diff --git a/apps/backend/src/app/dto/queries/map-queries.dto.ts b/apps/backend/src/app/dto/queries/map-queries.dto.ts index f2300d44c..2161a6b2b 100644 --- a/apps/backend/src/app/dto/queries/map-queries.dto.ts +++ b/apps/backend/src/app/dto/queries/map-queries.dto.ts @@ -36,6 +36,7 @@ import { EnumQueryProperty, ExpandQueryProperty, FilterQueryProperty, + IntCsvQueryProperty, IntQueryProperty, SingleExpandQueryProperty, SkipQueryProperty, @@ -266,6 +267,9 @@ export class MapLeaderboardGetQueryDto }) readonly filter?: MapRunsGetFilter; + @IntCsvQueryProperty({ description: 'List of users to limit results to' }) + readonly filterUserIDs?: number[]; + @BooleanQueryProperty({ description: 'Whether to order by date or not (false for reverse)' }) diff --git a/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts b/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts index 5903895b5..2a39c35c8 100644 --- a/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts +++ b/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts @@ -74,6 +74,10 @@ export class LeaderboardRunsService { style: query.style }; + if (query.filterUserIDs) { + where.userID = { in: query.filterUserIDs }; + } + const select = { ...this.minimalRunsSelect, stats: Boolean(query.expand) @@ -89,17 +93,23 @@ export class LeaderboardRunsService { // Potentially a faster way of doing this in one query in raw SQL, something // to investigate when we move to that/query builder. if (query.filter?.[0] === 'around') { - const userRun = await this.db.leaderboardRun.findUnique({ - where: { - userID_gamemode_style_mapID_trackType_trackNum: { - mapID, - userID: loggedInUserID, - gamemode: query.gamemode, - trackType: query.trackType, - trackNum: query.trackNum, - style: query.style - } + const whereAround: Prisma.LeaderboardRunWhereUniqueInput = { + userID_gamemode_style_mapID_trackType_trackNum: { + mapID, + userID: loggedInUserID, + gamemode: query.gamemode, + trackType: query.trackType, + trackNum: query.trackNum, + style: query.style } + }; + + // if (query.filterUserIDs) { + // where.userID = { in: query.filterUserIDs }; + // } + + const userRun = await this.db.leaderboardRun.findUnique({ + where: whereAround }); if (!userRun) diff --git a/libs/constants/src/types/queries/map-queries.model.ts b/libs/constants/src/types/queries/map-queries.model.ts index 201479578..a5a2a1c6b 100644 --- a/libs/constants/src/types/queries/map-queries.model.ts +++ b/libs/constants/src/types/queries/map-queries.model.ts @@ -122,6 +122,7 @@ export type MapLeaderboardGetQuery = PagedQuery & { style?: Style; // Default 0 expand?: MapRunsGetExpand; filter?: MapRunsGetFilter; + filterUserIDs?: number[]; orderByDate?: boolean; }; From f48f3508afbd7ede75db34d6573af2163305ac67 Mon Sep 17 00:00:00 2001 From: tsa96 Date: Thu, 18 Jan 2024 20:47:26 +0000 Subject: [PATCH 3/3] refactor(back): more appropriate status codes in leaderboard endpoints sorry ImATeapotException --- apps/backend-e2e/src/maps-2.e2e-spec.ts | 4 ++-- .../src/app/modules/maps/maps.controller.ts | 17 ++++++++++++++++- .../modules/runs/leaderboard-runs.service.ts | 16 ++++++++++------ 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/apps/backend-e2e/src/maps-2.e2e-spec.ts b/apps/backend-e2e/src/maps-2.e2e-spec.ts index 6b941635e..7174a3bed 100644 --- a/apps/backend-e2e/src/maps-2.e2e-spec.ts +++ b/apps/backend-e2e/src/maps-2.e2e-spec.ts @@ -1621,13 +1621,13 @@ describe('Maps Part 2', () => { expect(mockSteamIDs).toContain(BigInt(run.user.steamID)); }); - it('should 418 if the user has no Steam friends', async () => { + it('should 410 if the user has no Steam friends', async () => { jest.spyOn(steamService, 'getSteamFriends').mockResolvedValueOnce([]); return req.get({ url: `maps/${map.id}/leaderboard`, query: { gamemode: Gamemode.AHOP, filter: 'friends' }, - status: 418, + status: 410, token }); }); diff --git a/apps/backend/src/app/modules/maps/maps.controller.ts b/apps/backend/src/app/modules/maps/maps.controller.ts index 39a32486f..a02b3fb08 100644 --- a/apps/backend/src/app/modules/maps/maps.controller.ts +++ b/apps/backend/src/app/modules/maps/maps.controller.ts @@ -27,11 +27,13 @@ import { ApiConsumes, ApiCreatedResponse, ApiForbiddenResponse, + ApiGoneResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiParam, + ApiServiceUnavailableResponse, ApiTags } from '@nestjs/swagger'; import { @@ -649,6 +651,18 @@ export class MapsController { required: true }) @ApiOkResponse({ description: "The found leaderboard's runs" }) + @ApiNotFoundResponse({ description: "When the map doesn't exist" }) + @ApiGoneResponse({ + description: + "When the filtering by 'around', and the user doesn't have a PB" + }) + @ApiGoneResponse({ + description: + "When the filtering by 'friends', and the user doesn't have any Steam friends" + }) + @ApiServiceUnavailableResponse({ + description: "Steam fails to return the user's friends list (Tuesdays lol)" + }) getLeaderboards( @Param('mapID', ParseIntSafePipe) mapID: number, @LoggedInUser() { id, steamID }: UserJwtAccessPayload, @@ -666,7 +680,8 @@ export class MapsController { required: true }) @ApiOkResponse({ description: 'The found run' }) - @ApiNotFoundResponse({ description: 'Either map or run not found' }) + @ApiNotFoundResponse({ description: 'Map not found' }) + @ApiNotFoundResponse({ description: 'Run not found' }) getLeaderboardRun( @Param('mapID', ParseIntSafePipe) mapID: number, @LoggedInUser('id') userID: number, diff --git a/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts b/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts index 2a39c35c8..f4d7d7553 100644 --- a/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts +++ b/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts @@ -1,10 +1,11 @@ import { BadRequestException, forwardRef, - ImATeapotException, + GoneException, Inject, Injectable, - NotFoundException + NotFoundException, + ServiceUnavailableException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { runPath } from '@momentum/constants'; @@ -113,7 +114,7 @@ export class LeaderboardRunsService { }); if (!userRun) - throw new NotFoundException('User has no runs on this leaderboard'); + throw new GoneException('User has no runs on this leaderboard'); // Start at your rank, then backtrack by half of `take`, then 1 for your rank skip = Math.max(userRun.rank - Math.floor(take / 2) - 1, 0); @@ -122,11 +123,14 @@ export class LeaderboardRunsService { } else if (query.filter?.[0] === 'friends') { // Regular skip/take should work fine here. - const steamFriends = - await this.steamService.getSteamFriends(loggedInUserSteamID); + const steamFriends = await this.steamService + .getSteamFriends(loggedInUserSteamID) + .catch(() => { + throw new ServiceUnavailableException(); + }); if (steamFriends.length === 0) - throw new ImATeapotException('No friends detected :('); + throw new GoneException('No friends detected :('); // Doing this with a window function is gonna be fun... where.user = {