Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Leaderboard fixes for Panzer #902

Merged
merged 3 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 37 additions & 5 deletions apps/backend-e2e/src/maps-2.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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();
Expand All @@ -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'));
Expand All @@ -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 }
});

Expand All @@ -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", () => {
Expand Down Expand Up @@ -1589,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
});
});
Expand Down
4 changes: 4 additions & 0 deletions apps/backend/src/app/dto/queries/map-queries.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
EnumQueryProperty,
ExpandQueryProperty,
FilterQueryProperty,
IntCsvQueryProperty,
IntQueryProperty,
SingleExpandQueryProperty,
SkipQueryProperty,
Expand Down Expand Up @@ -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)'
})
Expand Down
17 changes: 16 additions & 1 deletion apps/backend/src/app/modules/maps/maps.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ import {
ApiConsumes,
ApiCreatedResponse,
ApiForbiddenResponse,
ApiGoneResponse,
ApiNoContentResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiOperation,
ApiParam,
ApiServiceUnavailableResponse,
ApiTags
} from '@nestjs/swagger';
import {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
46 changes: 30 additions & 16 deletions apps/backend/src/app/modules/runs/leaderboard-runs.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -74,6 +75,10 @@ export class LeaderboardRunsService {
style: query.style
};

if (query.filterUserIDs) {
where.userID = { in: query.filterUserIDs };
}

const select = {
...this.minimalRunsSelect,
stats: Boolean(query.expand)
Expand All @@ -89,21 +94,27 @@ 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)
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);
Expand All @@ -112,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 = {
Expand Down
1 change: 1 addition & 0 deletions libs/constants/src/types/queries/map-queries.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ export type MapLeaderboardGetQuery = PagedQuery & {
style?: Style; // Default 0
expand?: MapRunsGetExpand;
filter?: MapRunsGetFilter;
filterUserIDs?: number[];
orderByDate?: boolean;
};

Expand Down
5 changes: 5 additions & 0 deletions libs/db/src/scripts/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading