diff --git a/apps/backend-e2e/src/maps-2.e2e-spec.ts b/apps/backend-e2e/src/maps-2.e2e-spec.ts
index 152889173..b66ceead1 100644
--- a/apps/backend-e2e/src/maps-2.e2e-spec.ts
+++ b/apps/backend-e2e/src/maps-2.e2e-spec.ts
@@ -28,7 +28,7 @@ import {
import {
ActivityType,
AdminActivityType,
- CombinedMapStatuses,
+ MapStatuses,
Gamemode,
MapCreditType,
MapStatus,
@@ -1141,7 +1141,7 @@ describe('Maps Part 2', () => {
});
for (const status of Enum.values(MapStatus)) {
- const shouldPass = CombinedMapStatuses.IN_SUBMISSION.includes(status);
+ const shouldPass = MapStatuses.IN_SUBMISSION.includes(status);
const expectedStatus = shouldPass ? 200 : 403;
it(`should ${expectedStatus} if the map is not in the ${MapStatus[status]} state`, async () => {
@@ -1178,7 +1178,9 @@ describe('Maps Part 2', () => {
beforeAll(async () => {
[[u1, token], u2, u3, map] = await Promise.all([
- db.createAndLoginUser(),
+ db.createAndLoginUser({
+ data: { steamID: BigInt(Number.MAX_SAFE_INTEGER) * 2n }
+ }),
db.createUser(),
db.createUser(),
db.createMapWithFullLeaderboards() // Creates a bunch of ahop leaderboards
@@ -1288,12 +1290,27 @@ describe('Maps Part 2', () => {
token
}));
- it('should respond with filtered runs given using the filterUserID param', async () => {
+ it('should respond with filtered runs given using the userIDs param', async () => {
+ const res = await req.get({
+ url: `maps/${map.id}/leaderboard`,
+ query: {
+ gamemode: Gamemode.AHOP,
+ userIDs: `${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);
+ });
+
+ it('should respond with filtered runs given using the steamIDs param', async () => {
const res = await req.get({
url: `maps/${map.id}/leaderboard`,
query: {
gamemode: Gamemode.AHOP,
- filterUserIDs: `${u1.id},${u3.id}`
+ steamIDs: `${u1.steamID},${u3.steamID}`
},
validatePaged: { type: MinimalLeaderboardRunDto, count: 2 },
token
@@ -1377,7 +1394,7 @@ describe('Maps Part 2', () => {
query: {
gamemode: Gamemode.AHOP,
filter: 'around',
- filterUserIDs: u7.id
+ userIDs: u7.id
},
status: 200,
token: u7Token,
diff --git a/apps/backend-e2e/src/maps.e2e-spec.ts b/apps/backend-e2e/src/maps.e2e-spec.ts
index 5ac9d7d3f..ff500e359 100644
--- a/apps/backend-e2e/src/maps.e2e-spec.ts
+++ b/apps/backend-e2e/src/maps.e2e-spec.ts
@@ -8,7 +8,7 @@ import crypto from 'node:crypto';
import {
ActivityType,
Ban,
- CombinedMapStatuses,
+ MapStatuses,
Gamemode as GM,
Gamemode,
MapCreditType,
@@ -984,7 +984,7 @@ describe('Maps', () => {
await db.createMap({
submitter: { connect: { id: user.id } },
- status: CombinedMapStatuses.IN_SUBMISSION[1]
+ status: MapStatuses.IN_SUBMISSION[1]
});
await req.postAttach({
@@ -1012,7 +1012,7 @@ describe('Maps', () => {
});
describe('Permission checks', () => {
- for (const status of CombinedMapStatuses.IN_SUBMISSION) {
+ for (const status of MapStatuses.IN_SUBMISSION) {
it(`should 403 if the user already has a map in ${MapStatus[status]} and is not a MAPPER`, async () => {
await db.createMap({
submitter: { connect: { id: user.id } },
@@ -2529,7 +2529,7 @@ describe('Maps', () => {
Promise.all([db.cleanup('mMap'), fileStore.deleteDirectory('maplist')])
);
- for (const status of CombinedMapStatuses.IN_SUBMISSION) {
+ for (const status of MapStatuses.IN_SUBMISSION) {
it(`should allow the submitter to change most data during ${MapStatus[status]}`, async () => {
const map = await db.createMap({ ...createMapData, status });
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 d668b3188..280648a5d 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 {
IntQueryProperty,
SingleExpandQueryProperty,
SkipQueryProperty,
+ StringCsvQueryProperty,
StringQueryProperty,
TakeQueryProperty
} from '../decorators';
@@ -228,8 +229,13 @@ export class MapLeaderboardGetQueryDto
})
readonly filter?: MapRunsGetFilter;
- @IntCsvQueryProperty({ description: 'List of users to limit results to' })
- readonly filterUserIDs?: number[];
+ @IntCsvQueryProperty({ description: 'List of user IDs to limit results to' })
+ readonly userIDs?: number[];
+
+ @StringCsvQueryProperty({
+ description: 'List of user Steam IDs to limit results to'
+ })
+ readonly steamIDs?: string[];
@BooleanQueryProperty({
description: 'Whether to order by date or not (false for reverse)'
diff --git a/apps/backend/src/app/dto/queries/user-queries.dto.ts b/apps/backend/src/app/dto/queries/user-queries.dto.ts
index c7bec83dc..c0f7e69ce 100644
--- a/apps/backend/src/app/dto/queries/user-queries.dto.ts
+++ b/apps/backend/src/app/dto/queries/user-queries.dto.ts
@@ -18,7 +18,8 @@ import {
EnumQueryProperty,
ExpandQueryProperty,
IntCsvQueryProperty,
- IntQueryProperty
+ IntQueryProperty,
+ StringCsvQueryProperty
} from '../decorators';
import { IsBigInt } from '../../validators';
import { PagedQueryDto } from './pagination.dto';
@@ -64,12 +65,10 @@ export class UsersGetAllQueryDto
@IsOptional()
readonly steamID?: string;
- @IntCsvQueryProperty({
+ @StringCsvQueryProperty({
description: 'Filter by CSV list of Steam Community IDs',
- example: '123135674,7987347263,98312287631',
- bigint: true
+ example: '123135674,7987347263,98312287631'
})
- @IsBigInt({ each: true })
@IsOptional()
readonly steamIDs?: string[];
diff --git a/apps/backend/src/app/modules/maps/map-credits.service.ts b/apps/backend/src/app/modules/maps/map-credits.service.ts
index 66166bfd5..8054ff77c 100644
--- a/apps/backend/src/app/modules/maps/map-credits.service.ts
+++ b/apps/backend/src/app/modules/maps/map-credits.service.ts
@@ -10,7 +10,7 @@ import { Prisma } from '@prisma/client';
import * as Bitflags from '@momentum/bitflags';
import {
ActivityType,
- CombinedMapStatuses,
+ MapStatuses,
CombinedRoles,
MapCreditsGetExpand,
MapCreditType
@@ -114,7 +114,7 @@ export class MapCreditsService {
throw new ForbiddenException('User is not the submitter of this map');
}
- if (!CombinedMapStatuses.IN_SUBMISSION.includes(map.status)) {
+ if (!MapStatuses.IN_SUBMISSION.includes(map.status)) {
throw new ForbiddenException('Cannot change map in its current state');
}
}
diff --git a/apps/backend/src/app/modules/maps/map-image.service.ts b/apps/backend/src/app/modules/maps/map-image.service.ts
index 7d148d06c..c1decf31a 100644
--- a/apps/backend/src/app/modules/maps/map-image.service.ts
+++ b/apps/backend/src/app/modules/maps/map-image.service.ts
@@ -9,7 +9,7 @@ import {
} from '@nestjs/common';
import {
AdminActivityType,
- CombinedMapStatuses,
+ MapStatuses,
CombinedRoles,
imgLargePath,
imgMediumPath,
@@ -66,9 +66,7 @@ export class MapImageService {
const isMod = Bitflags.has(user.roles, CombinedRoles.MOD_OR_ADMIN);
const isSubmitter = map.submitterID === userID;
- const isInSubmission = CombinedMapStatuses.IN_SUBMISSION.includes(
- map.status
- );
+ const isInSubmission = MapStatuses.IN_SUBMISSION.includes(map.status);
if (!isMod) {
if (!isSubmitter)
diff --git a/apps/backend/src/app/modules/maps/maps.controller.ts b/apps/backend/src/app/modules/maps/maps.controller.ts
index 6914a9e55..003b7b37c 100644
--- a/apps/backend/src/app/modules/maps/maps.controller.ts
+++ b/apps/backend/src/app/modules/maps/maps.controller.ts
@@ -590,8 +590,13 @@ export class MapsController {
description:
"When the filtering by 'friends', and the user doesn't have any Steam friends"
})
+ @ApiConflictResponse({
+ description:
+ "When filtering by 'friends', the user's friend list is private"
+ })
@ApiServiceUnavailableResponse({
- description: "Steam fails to return the user's friends list (Tuesdays lol)"
+ description:
+ "When filtering by 'friends', and Steam fails to return the user's friends list (Tuesdays lol)"
})
getLeaderboards(
@Param('mapID', ParseIntSafePipe) mapID: number,
diff --git a/apps/backend/src/app/modules/maps/maps.service.ts b/apps/backend/src/app/modules/maps/maps.service.ts
index 40ce3a6b4..255e882db 100644
--- a/apps/backend/src/app/modules/maps/maps.service.ts
+++ b/apps/backend/src/app/modules/maps/maps.service.ts
@@ -21,7 +21,7 @@ import {
AdminActivityType,
Ban,
bspPath,
- CombinedMapStatuses,
+ MapStatuses,
CombinedRoles,
FlatMapList,
LeaderboardType,
@@ -204,8 +204,8 @@ export class MapsService {
if (Bitflags.has(CombinedRoles.MOD_OR_ADMIN, roles)) {
where.status = {
in: filter
- ? intersection(filter, CombinedMapStatuses.IN_SUBMISSION)
- : CombinedMapStatuses.IN_SUBMISSION
+ ? intersection(filter, MapStatuses.IN_SUBMISSION)
+ : MapStatuses.IN_SUBMISSION
};
} else if (Bitflags.has(Role.REVIEWER, roles)) {
if (filter?.length > 0) {
@@ -639,7 +639,7 @@ export class MapsService {
throw new ForbiddenException('User is banned from map submission');
}
- if (!CombinedMapStatuses.IN_SUBMISSION.includes(map.status)) {
+ if (!MapStatuses.IN_SUBMISSION.includes(map.status)) {
throw new ForbiddenException('Map does not allow editing');
}
@@ -759,7 +759,7 @@ export class MapsService {
if (
!Bitflags.has(user.roles, CombinedRoles.MAPPER_AND_ABOVE) &&
user.submittedMaps.some((map) =>
- CombinedMapStatuses.IN_SUBMISSION.includes(map.status)
+ MapStatuses.IN_SUBMISSION.includes(map.status)
)
) {
throw new ForbiddenException(
@@ -1022,7 +1022,7 @@ export class MapsService {
if (userID !== map.submitterID)
throw new ForbiddenException('User is not the map submitter');
- if (!CombinedMapStatuses.IN_SUBMISSION.includes(map.status))
+ if (!MapStatuses.IN_SUBMISSION.includes(map.status))
throw new ForbiddenException('Map can only be edited during submission');
// Force the submitter to keep their suggestions in sync with their zones.
@@ -1246,7 +1246,7 @@ export class MapsService {
const numInSubmissionStatuses = intersection(
[newStatus, oldStatus],
- CombinedMapStatuses.IN_SUBMISSION
+ MapStatuses.IN_SUBMISSION
).length;
// If going from submission -> submission just update submission list,
@@ -1615,7 +1615,7 @@ export class MapsService {
);
await this.mapListService.updateMapList(
- CombinedMapStatuses.IN_SUBMISSION.includes(map.status)
+ MapStatuses.IN_SUBMISSION.includes(map.status)
? FlatMapList.SUBMISSION
: FlatMapList.APPROVED
);
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 64ff4a734..3e4152cfb 100644
--- a/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts
+++ b/apps/backend/src/app/modules/runs/leaderboard-runs.service.ts
@@ -5,7 +5,6 @@ import {
Inject,
Injectable,
NotFoundException,
- ServiceUnavailableException,
UnauthorizedException
} from '@nestjs/common';
import { Prisma } from '@prisma/client';
@@ -76,8 +75,12 @@ export class LeaderboardRunsService {
style: query.style
};
- if (query.filterUserIDs) {
- where.userID = { in: query.filterUserIDs };
+ if (query.userIDs) {
+ where.userID = { in: query.userIDs };
+ }
+
+ if (query.steamIDs) {
+ where.user = { steamID: { in: query.steamIDs.map(BigInt) } };
}
const select = {
@@ -107,10 +110,6 @@ export class LeaderboardRunsService {
}
};
- // if (query.filterUserIDs) {
- // where.userID = { in: query.filterUserIDs };
- // }
-
const userRun = await this.db.leaderboardRun.findUnique({
where: whereAround
});
@@ -125,16 +124,15 @@ export class LeaderboardRunsService {
} else if (query.filter?.[0] === 'friends') {
// Regular skip/take should work fine here.
- const steamFriends = await this.steamService
- .getSteamFriends(loggedInUserSteamID)
- .catch(() => {
- throw new ServiceUnavailableException();
- });
+ // Fetch Steam friends, leave errors uncaught, this function will throw
+ // an appropriate response.
+ const steamFriends =
+ await this.steamService.getSteamFriends(loggedInUserSteamID);
if (steamFriends.length === 0)
throw new GoneException('No friends detected :(');
- // Doing this with a window function is gonna be fun...
+ // Overrides filterSteamIDs if exists
where.user = {
steamID: { in: steamFriends.map((item) => BigInt(item.steamid)) }
};
diff --git a/apps/backend/src/app/modules/steam/steam.service.ts b/apps/backend/src/app/modules/steam/steam.service.ts
index 1f092016c..87cd1a0d1 100644
--- a/apps/backend/src/app/modules/steam/steam.service.ts
+++ b/apps/backend/src/app/modules/steam/steam.service.ts
@@ -1,7 +1,9 @@
import { HttpService } from '@nestjs/axios';
import {
+ ConflictException,
Injectable,
InternalServerErrorException,
+ Logger,
ServiceUnavailableException,
UnauthorizedException
} from '@nestjs/common';
@@ -9,6 +11,7 @@ import { ConfigService } from '@nestjs/config';
import { catchError, lastValueFrom, map } from 'rxjs';
import * as AppTicket from 'steam-appticket';
import { SteamFriendData, SteamUserSummaryData } from './steam.interface';
+import { AxiosError } from 'axios';
@Injectable()
export class SteamService {
@@ -25,6 +28,8 @@ export class SteamService {
private readonly steamApiKey: string;
private readonly steamTicketsSecretKey: string;
+ private readonly logger = new Logger('Steam Service');
+
/**
* Handler for ISteamUser/GetPlayerSummaries/v2/
*/
@@ -57,9 +62,11 @@ export class SteamService {
/**
* Handler for ISteamUser/GetFriendList/v1/
+ * Throws several different different errors for different failures, which can
+ * be left uncaught if you want to throw an error response if this fails.
*/
async getSteamFriends(steamID: bigint): Promise
- By - @if (authors[0].id) { - {{ authors[0].alias }} - } @else { - {{ authors[0].alias }} - } - - @if (authors.length > 1) {, + {{ authors.length - 1 | plural: 'author' }}} -
- @if (isSubmission) { - @if (!isSubmitterAnAuthor(map)) { -- Submitted by - {{ map.submitter.alias }} -
- } ++ By + @if (authors[0]?.id) { + {{ authors[0].alias }} + } @else { + {{ authors[0].alias }} } - @if (isAdminPage || isSubmission) { -
- @if (map.status !== MapStatus.DISABLED) { - Current Status: - {{ MapStatusName.get(map.status) }} + + @if (authors.length > 1) {, + @if (authors[1]?.id) { + {{ authors[1].alias }} } @else { - Current Status: - {{ MapStatusName.get(map.status) }} - @if (!map.currentVersion?.bspHash) { - Files deleted! - } + {{ authors[1].alias }} } + } + + @if (authors.length > 2) {, + {{ authors.length - 1 | plural: 'author' }}} +
+ @if (isSubmission) { + @if (!isSubmitterAnAuthor(map)) { ++ Submitted by + {{ map.submitter.alias }}
} -Added {{ map.createdAt | timeAgo }}
-+ @if (map.status !== MapStatus.DISABLED) { + Current Status: + {{ MapStatusName.get(map.status) }} + } @else { + Current Status: + {{ MapStatusName.get(map.status) }} + @if (!map.currentVersion?.bspHash) { + Files deleted! + } + } +
+ } + +Added {{ map.createdAt | timeAgo }}
Update Map
diff --git a/apps/frontend/src/app/pages/maps/map-edit/map-edit.component.ts b/apps/frontend/src/app/pages/maps/map-edit/map-edit.component.ts index 8f1dcdbe7..5deab89b8 100644 --- a/apps/frontend/src/app/pages/maps/map-edit/map-edit.component.ts +++ b/apps/frontend/src/app/pages/maps/map-edit/map-edit.component.ts @@ -16,7 +16,7 @@ import { } from '@angular/forms'; import { CdkDrag, CdkDropList } from '@angular/cdk/drag-drop'; import { - CombinedMapStatuses, + MapStatuses, DateString, LeaderboardType, MAP_IMAGE_HEIGHT, @@ -258,9 +258,7 @@ export class MapEditComponent implements OnInit, ConfirmDeactivate { this.isMod = this.localUserService.isMod; this.isSubmitter = map.submitterID === this.localUserService.user.value?.id; - this.inSubmission = CombinedMapStatuses.IN_SUBMISSION.includes( - map.status - ); + this.inSubmission = MapStatuses.IN_SUBMISSION.includes(map.status); if ( !this.isAdmin && diff --git a/apps/frontend/src/app/pages/maps/map-info/map-info.component.ts b/apps/frontend/src/app/pages/maps/map-info/map-info.component.ts index 4fc8cfb5c..d4821c1d4 100644 --- a/apps/frontend/src/app/pages/maps/map-info/map-info.component.ts +++ b/apps/frontend/src/app/pages/maps/map-info/map-info.component.ts @@ -2,7 +2,7 @@ import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { Component, DestroyRef, OnInit } from '@angular/core'; import { switchMap, tap } from 'rxjs/operators'; import { - CombinedMapStatuses, + MapStatuses, DateString, MapCreditNames, MapCreditType, @@ -160,11 +160,11 @@ export class MapInfoComponent implements OnInit { this.prefix = prefix; this.credits = new GroupedMapCredits( this.map.credits ?? [], - CombinedMapStatuses.IN_SUBMISSION.includes(this.map.status) + MapStatuses.IN_SUBMISSION.includes(this.map.status) ? (this.map.submission?.placeholders ?? []) : [] ); - this.inSubmission = CombinedMapStatuses.IN_SUBMISSION.includes(map.status); + this.inSubmission = MapStatuses.IN_SUBMISSION.includes(map.status); // Show Review section first if in review, otherwise leaderboards (and the // tab view won't be visible anyway). this.currentSection = this.inSubmission diff --git a/apps/frontend/src/app/pages/maps/submission-form/map-submission-form.component.html b/apps/frontend/src/app/pages/maps/submission-form/map-submission-form.component.html index 0ccfb5a49..0c803047c 100644 --- a/apps/frontend/src/app/pages/maps/submission-form/map-submission-form.component.html +++ b/apps/frontend/src/app/pages/maps/submission-form/map-submission-form.component.html @@ -15,7 +15,7 @@