@babylonjs-toolkit/server 0.0.0-stage → 9.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CUSTOM.md +1690 -0
- package/DEPLOY.md +2012 -0
- package/README.md +1081 -2
- package/lib/AbuseLimits.d.ts +64 -0
- package/lib/AbuseLimits.js +121 -0
- package/lib/Authentication.d.ts +86 -0
- package/lib/Authentication.js +272 -0
- package/lib/BootSessionOwner.d.ts +86 -0
- package/lib/BootSessionOwner.js +116 -0
- package/lib/ColyseusLoggerAdapter.d.ts +32 -0
- package/lib/ColyseusLoggerAdapter.js +73 -0
- package/lib/DedicatedSceneRegistry.d.ts +52 -0
- package/lib/DedicatedSceneRegistry.js +74 -0
- package/lib/DedicatedSession.d.ts +152 -0
- package/lib/DedicatedSession.js +173 -0
- package/lib/DualTransport.d.ts +112 -0
- package/lib/DualTransport.js +197 -0
- package/lib/GameServer.d.ts +64 -0
- package/lib/GameServer.js +647 -0
- package/lib/GameServerConfig.d.ts +387 -0
- package/lib/GameServerConfig.js +1059 -0
- package/lib/GameServerLobbyRoom.d.ts +33 -0
- package/lib/GameServerLobbyRoom.js +56 -0
- package/lib/GameServerLogger.d.ts +111 -0
- package/lib/GameServerLogger.js +190 -0
- package/lib/GameServerMetrics.d.ts +158 -0
- package/lib/GameServerMetrics.js +276 -0
- package/lib/GameServerState.d.ts +164 -0
- package/lib/GameServerState.js +26 -0
- package/lib/HeadlessRuntime.d.ts +316 -0
- package/lib/HeadlessRuntime.js +380 -0
- package/lib/JoinGate.d.ts +34 -0
- package/lib/JoinGate.js +221 -0
- package/lib/NetworkAddresses.d.ts +33 -0
- package/lib/NetworkAddresses.js +90 -0
- package/lib/PlayerTokens.d.ts +17 -0
- package/lib/PlayerTokens.js +24 -0
- package/lib/RoomServerTransport.d.ts +73 -0
- package/lib/RoomServerTransport.js +90 -0
- package/lib/ServerEndpoints.d.ts +133 -0
- package/lib/ServerEndpoints.js +247 -0
- package/lib/ServerInit.d.ts +28 -0
- package/lib/ServerInit.js +113 -0
- package/lib/SessionRoom.d.ts +391 -0
- package/lib/SessionRoom.js +930 -0
- package/lib/TlsCertificates.d.ts +35 -0
- package/lib/TlsCertificates.js +130 -0
- package/lib/admin/AccountRoutes.d.ts +8 -0
- package/lib/admin/AccountRoutes.js +502 -0
- package/lib/admin/AdminAccounts.d.ts +329 -0
- package/lib/admin/AdminAccounts.js +607 -0
- package/lib/admin/AdminApiRouter.d.ts +202 -0
- package/lib/admin/AdminApiRouter.js +465 -0
- package/lib/admin/AdminApiTypes.d.ts +1782 -0
- package/lib/admin/AdminApiTypes.js +4 -0
- package/lib/admin/AdminAudit.d.ts +113 -0
- package/lib/admin/AdminAudit.js +266 -0
- package/lib/admin/AdminCli.d.ts +7 -0
- package/lib/admin/AdminCli.js +351 -0
- package/lib/admin/AdminClock.d.ts +51 -0
- package/lib/admin/AdminClock.js +10 -0
- package/lib/admin/AdminConsole.d.ts +39 -0
- package/lib/admin/AdminConsole.js +325 -0
- package/lib/admin/AdminConstants.d.ts +147 -0
- package/lib/admin/AdminConstants.js +167 -0
- package/lib/admin/AdminCrypto.d.ts +19 -0
- package/lib/admin/AdminCrypto.js +29 -0
- package/lib/admin/AdminExtensionsHost.d.ts +176 -0
- package/lib/admin/AdminExtensionsHost.js +307 -0
- package/lib/admin/AdminPasswords.d.ts +33 -0
- package/lib/admin/AdminPasswords.js +92 -0
- package/lib/admin/AdminPermissions.d.ts +28 -0
- package/lib/admin/AdminPermissions.js +74 -0
- package/lib/admin/AdminRecords.d.ts +196 -0
- package/lib/admin/AdminRecords.js +3 -0
- package/lib/admin/AdminRequestBodies.d.ts +47 -0
- package/lib/admin/AdminRequestBodies.js +84 -0
- package/lib/admin/AdminRequestGate.d.ts +38 -0
- package/lib/admin/AdminRequestGate.js +59 -0
- package/lib/admin/AdminResponses.d.ts +103 -0
- package/lib/admin/AdminResponses.js +164 -0
- package/lib/admin/AdminServices.d.ts +99 -0
- package/lib/admin/AdminServices.js +4 -0
- package/lib/admin/AdminStaticFiles.d.ts +29 -0
- package/lib/admin/AdminStaticFiles.js +97 -0
- package/lib/admin/AdminStore.d.ts +152 -0
- package/lib/admin/AdminStore.js +46 -0
- package/lib/admin/AlertChannels.d.ts +43 -0
- package/lib/admin/AlertChannels.js +120 -0
- package/lib/admin/AlertEngine.d.ts +231 -0
- package/lib/admin/AlertEngine.js +681 -0
- package/lib/admin/AlertRoutes.d.ts +8 -0
- package/lib/admin/AlertRoutes.js +180 -0
- package/lib/admin/ApiKeyRoutes.d.ts +8 -0
- package/lib/admin/ApiKeyRoutes.js +53 -0
- package/lib/admin/ApiKeys.d.ts +63 -0
- package/lib/admin/ApiKeys.js +167 -0
- package/lib/admin/AuditRoutes.d.ts +8 -0
- package/lib/admin/AuditRoutes.js +106 -0
- package/lib/admin/AuthToolRoutes.d.ts +13 -0
- package/lib/admin/AuthToolRoutes.js +164 -0
- package/lib/admin/BanList.d.ts +87 -0
- package/lib/admin/BanList.js +152 -0
- package/lib/admin/CborDecoder.d.ts +18 -0
- package/lib/admin/CborDecoder.js +201 -0
- package/lib/admin/ColyseusCloudClient.d.ts +313 -0
- package/lib/admin/ColyseusCloudClient.js +272 -0
- package/lib/admin/ColyseusCloudRoutes.d.ts +8 -0
- package/lib/admin/ColyseusCloudRoutes.js +91 -0
- package/lib/admin/ColyseusCloudService.d.ts +180 -0
- package/lib/admin/ColyseusCloudService.js +476 -0
- package/lib/admin/ColyseusCloudSettings.d.ts +16 -0
- package/lib/admin/ColyseusCloudSettings.js +29 -0
- package/lib/admin/ColyseusCloudViews.d.ts +105 -0
- package/lib/admin/ColyseusCloudViews.js +371 -0
- package/lib/admin/CommonPasswords.d.ts +6 -0
- package/lib/admin/CommonPasswords.js +19 -0
- package/lib/admin/ConfigOverrides.d.ts +174 -0
- package/lib/admin/ConfigOverrides.js +406 -0
- package/lib/admin/ConfigRoutes.d.ts +8 -0
- package/lib/admin/ConfigRoutes.js +91 -0
- package/lib/admin/ConsoleSessions.d.ts +112 -0
- package/lib/admin/ConsoleSessions.js +201 -0
- package/lib/admin/DedicatedRoutes.d.ts +21 -0
- package/lib/admin/DedicatedRoutes.js +254 -0
- package/lib/admin/ExtensionRoutes.d.ts +8 -0
- package/lib/admin/ExtensionRoutes.js +145 -0
- package/lib/admin/FileAdminStore.d.ts +157 -0
- package/lib/admin/FileAdminStore.js +349 -0
- package/lib/admin/FleetDirectory.d.ts +323 -0
- package/lib/admin/FleetDirectory.js +489 -0
- package/lib/admin/FleetLeader.d.ts +43 -0
- package/lib/admin/FleetLeader.js +89 -0
- package/lib/admin/FleetRoutes.d.ts +55 -0
- package/lib/admin/FleetRoutes.js +465 -0
- package/lib/admin/HealthWarnings.d.ts +10 -0
- package/lib/admin/HealthWarnings.js +95 -0
- package/lib/admin/LeaderboardRoutes.d.ts +8 -0
- package/lib/admin/LeaderboardRoutes.js +569 -0
- package/lib/admin/LogBuffer.d.ts +60 -0
- package/lib/admin/LogBuffer.js +92 -0
- package/lib/admin/LogRedaction.d.ts +15 -0
- package/lib/admin/LogRedaction.js +72 -0
- package/lib/admin/LogRoutes.d.ts +45 -0
- package/lib/admin/LogRoutes.js +307 -0
- package/lib/admin/MaintenanceMode.d.ts +41 -0
- package/lib/admin/MaintenanceMode.js +74 -0
- package/lib/admin/MaintenanceWindows.d.ts +91 -0
- package/lib/admin/MaintenanceWindows.js +207 -0
- package/lib/admin/MetricsRoutes.d.ts +8 -0
- package/lib/admin/MetricsRoutes.js +209 -0
- package/lib/admin/MetricsSampler.d.ts +124 -0
- package/lib/admin/MetricsSampler.js +211 -0
- package/lib/admin/OneTimeCodes.d.ts +60 -0
- package/lib/admin/OneTimeCodes.js +162 -0
- package/lib/admin/OpenIdConnect.d.ts +146 -0
- package/lib/admin/OpenIdConnect.js +335 -0
- package/lib/admin/Passkeys.d.ts +130 -0
- package/lib/admin/Passkeys.js +432 -0
- package/lib/admin/PlayerRoutes.d.ts +20 -0
- package/lib/admin/PlayerRoutes.js +676 -0
- package/lib/admin/RedisAdminStore.d.ts +114 -0
- package/lib/admin/RedisAdminStore.js +258 -0
- package/lib/admin/RolloutCoordinator.d.ts +99 -0
- package/lib/admin/RolloutCoordinator.js +321 -0
- package/lib/admin/ServerEvents.d.ts +57 -0
- package/lib/admin/ServerEvents.js +89 -0
- package/lib/admin/SessionRoutes.d.ts +113 -0
- package/lib/admin/SessionRoutes.js +386 -0
- package/lib/admin/UserRoutes.d.ts +8 -0
- package/lib/admin/UserRoutes.js +214 -0
- package/lib/cli.d.ts +2 -0
- package/lib/cli.js +221 -0
- package/lib/console/assets/index-CI5N7c-T.js +21 -0
- package/lib/console/assets/index-DPitvzLp.css +1 -0
- package/lib/console/favicon.svg +1 -0
- package/lib/console/index.html +16 -0
- package/lib/discord/DiscordOAuth.d.ts +57 -0
- package/lib/discord/DiscordOAuth.js +127 -0
- package/lib/discord/DiscordSessions.d.ts +79 -0
- package/lib/discord/DiscordSessions.js +135 -0
- package/lib/discord/DiscordSignInConstants.d.ts +54 -0
- package/lib/discord/DiscordSignInConstants.js +55 -0
- package/lib/discord/DiscordSignInRoutes.d.ts +51 -0
- package/lib/discord/DiscordSignInRoutes.js +359 -0
- package/lib/index.d.ts +24 -0
- package/lib/index.js +18 -0
- package/lib/leaderboards/FileLeaderboardRankStore.d.ts +293 -0
- package/lib/leaderboards/FileLeaderboardRankStore.js +808 -0
- package/lib/leaderboards/LeaderboardConstants.d.ts +63 -0
- package/lib/leaderboards/LeaderboardConstants.js +70 -0
- package/lib/leaderboards/LeaderboardError.d.ts +15 -0
- package/lib/leaderboards/LeaderboardError.js +20 -0
- package/lib/leaderboards/LeaderboardIdentity.d.ts +44 -0
- package/lib/leaderboards/LeaderboardIdentity.js +72 -0
- package/lib/leaderboards/LeaderboardPublicApi.d.ts +17 -0
- package/lib/leaderboards/LeaderboardPublicApi.js +389 -0
- package/lib/leaderboards/LeaderboardRankStore.d.ts +296 -0
- package/lib/leaderboards/LeaderboardRankStore.js +18 -0
- package/lib/leaderboards/LeaderboardRules.d.ts +122 -0
- package/lib/leaderboards/LeaderboardRules.js +472 -0
- package/lib/leaderboards/LeaderboardScheduler.d.ts +43 -0
- package/lib/leaderboards/LeaderboardScheduler.js +103 -0
- package/lib/leaderboards/LeaderboardService.d.ts +669 -0
- package/lib/leaderboards/LeaderboardService.js +1667 -0
- package/lib/leaderboards/LeaderboardTransfer.d.ts +35 -0
- package/lib/leaderboards/LeaderboardTransfer.js +580 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.d.ts +282 -0
- package/lib/leaderboards/RedisLeaderboardRankStore.js +691 -0
- package/package.json +64 -4
- package/templates/init/Caddyfile +6 -0
- package/templates/init/DEPLOY.md +233 -0
- package/templates/init/Dockerfile +12 -0
- package/templates/init/app.json +21 -0
- package/templates/init/aws/README.md +118 -0
- package/templates/init/aws/ecs-fargate-alb.yml +207 -0
- package/templates/init/aws/ecs-fargate-nlb-quic.yml +210 -0
- package/templates/init/babylon-game-server.config.mjs +158 -0
- package/templates/init/colyseus-cloud/README.md +133 -0
- package/templates/init/colyseus-cloud/babylon-game-server.config.mjs +36 -0
- package/templates/init/colyseus-cloud/ecosystem.config.cjs +35 -0
- package/templates/init/colyseus-cloud/gitignore +6 -0
- package/templates/init/colyseus-cloud/package.json +15 -0
- package/templates/init/digitalocean-app.yaml +30 -0
- package/templates/init/docker-compose.full.yml +45 -0
- package/templates/init/docker-compose.websocket.yml +41 -0
- package/templates/init/fly.toml +56 -0
- package/templates/init/heroku.yml +11 -0
- package/templates/init/kubernetes/deployment.yaml +71 -0
- package/templates/init/kubernetes/redis.yaml +73 -0
- package/templates/init/kubernetes/service.yaml +24 -0
- package/templates/init/monitoring/grafana-dashboard.json +539 -0
- package/templates/init/monitoring/prometheus-alerts.yml +51 -0
- package/templates/init/monitoring/prometheus-scrape.yml +17 -0
- package/templates/init/railway.json +13 -0
- package/templates/init/render.yaml +19 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
// The leaderboards/* admin routes (game-server-leaderboards T6, DR §6 admin table, D9, D17, D18, D21, D22): boards and
|
|
2
|
+
// seasons, entry moderation, players, rules, activity, status, backups and the server-trust submission. Every change is
|
|
3
|
+
// audited with its reason fields and new scores (never keys); reads are not audited.
|
|
4
|
+
import { AdminRequestError } from "./AdminApiRouter.js";
|
|
5
|
+
import { bodyObject, booleanField, numberField, optionalStringField, stringField } from "./AdminRequestBodies.js";
|
|
6
|
+
import { LEADERBOARD_ALL_VARIANT_KEY, LEADERBOARD_DEFAULT_PAGE_SIZE, LEADERBOARD_MAXIMUM_PAGE_SIZE } from "../leaderboards/LeaderboardConstants.js";
|
|
7
|
+
import { LeaderboardError } from "../leaderboards/LeaderboardError.js";
|
|
8
|
+
import { variantKeyOf, variantValuesOf } from "../leaderboards/LeaderboardRules.js";
|
|
9
|
+
import { exportLeaderboards, exportRankingCsv, importLeaderboards } from "../leaderboards/LeaderboardTransfer.js";
|
|
10
|
+
/** HTTP status of a successful answer. */
|
|
11
|
+
const HTTP_OK = 200;
|
|
12
|
+
/** HTTP status of an invalid request. */
|
|
13
|
+
const HTTP_BAD_REQUEST = 400;
|
|
14
|
+
/** The ok body of an answer with nothing else to say. */
|
|
15
|
+
const OK_BODY = { ok: true };
|
|
16
|
+
/** Longest moderation reason, in characters. */
|
|
17
|
+
const MAXIMUM_REASON_LENGTH = 500;
|
|
18
|
+
/** Activity records one read returns unless the query says otherwise. */
|
|
19
|
+
const DEFAULT_ACTIVITY_LIMIT = 50;
|
|
20
|
+
/** Most activity records one read returns. */
|
|
21
|
+
const MAXIMUM_ACTIVITY_LIMIT = 500;
|
|
22
|
+
/** The activity kinds the activity view can be filtered by. */
|
|
23
|
+
const ACTIVITY_KINDS = new Set(["accepted", "duplicate", "refused"]);
|
|
24
|
+
/** The download of each export format. */
|
|
25
|
+
const EXPORT_CONTENT_TYPES = { json: "application/json; charset=utf-8", csv: "text/csv; charset=utf-8" };
|
|
26
|
+
/** The file name of a JSON backup. */
|
|
27
|
+
const JSON_EXPORT_FILE_NAME = "leaderboards.json";
|
|
28
|
+
/**
|
|
29
|
+
* Registers every leaderboards/* route (DR §6 admin table).
|
|
30
|
+
* @param router The admin API router.
|
|
31
|
+
* @param services The shared services.
|
|
32
|
+
*/
|
|
33
|
+
export function registerLeaderboardRoutes(router, services) {
|
|
34
|
+
const leaderboards = services.leaderboards;
|
|
35
|
+
const routes = [
|
|
36
|
+
{ method: "GET", pattern: "leaderboards/status", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async () => ok(await leaderboards.status()) },
|
|
37
|
+
{ method: "GET", pattern: "leaderboards/boards", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async () => listBoards(leaderboards) },
|
|
38
|
+
{ method: "POST", pattern: "leaderboards/boards", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.boardCreate", handler: async (request) => createBoard(leaderboards, request) },
|
|
39
|
+
{ method: "GET", pattern: "leaderboards/boards/:boardId", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await leaderboards.describeBoard(request.params.boardId, seasonQueryOf(request.query))) },
|
|
40
|
+
{ method: "PUT", pattern: "leaderboards/boards/:boardId", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.boardUpdate", handler: async (request) => updateBoard(leaderboards, request) },
|
|
41
|
+
{ method: "POST", pattern: "leaderboards/boards/:boardId/duplicate", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.boardDuplicate", handler: async (request) => duplicateBoard(leaderboards, request) },
|
|
42
|
+
{ method: "DELETE", pattern: "leaderboards/boards/:boardId", permission: "leaderboards.manage", requiresStepUp: true, auditAction: "leaderboard.boardDelete", handler: async (request) => deleteBoard(leaderboards, request) },
|
|
43
|
+
{ method: "GET", pattern: "leaderboards/boards/:boardId/entries", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async (request) => readEntries(leaderboards, request) },
|
|
44
|
+
{ method: "GET", pattern: "leaderboards/boards/:boardId/removed", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async (request) => readRemoved(leaderboards, request) },
|
|
45
|
+
{ method: "POST", pattern: "leaderboards/boards/:boardId/entries", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.entryAdd", handler: async (request) => addEntry(leaderboards, request) },
|
|
46
|
+
{ method: "PUT", pattern: "leaderboards/boards/:boardId/entries/:playerId", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.entryCorrect", handler: async (request) => correctEntry(leaderboards, request) },
|
|
47
|
+
{ method: "POST", pattern: "leaderboards/boards/:boardId/entries/:playerId/remove", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.entryRemove", handler: async (request) => removeEntry(leaderboards, request) },
|
|
48
|
+
{ method: "POST", pattern: "leaderboards/boards/:boardId/entries/:playerId/restore", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.entryRestore", handler: async (request) => restoreEntry(leaderboards, request) },
|
|
49
|
+
{ method: "POST", pattern: "leaderboards/boards/:boardId/seasons/reset", permission: "leaderboards.manage", requiresStepUp: true, auditAction: "leaderboard.seasonReset", handler: async (request) => resetSeason(leaderboards, request) },
|
|
50
|
+
{ method: "DELETE", pattern: "leaderboards/boards/:boardId/seasons/:seasonId", permission: "leaderboards.manage", requiresStepUp: true, auditAction: "leaderboard.seasonDelete", handler: async (request) => deleteSeason(leaderboards, request) },
|
|
51
|
+
{ method: "GET", pattern: "leaderboards/players", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await leaderboards.searchPlayers(request.query.get("search") ?? "")) },
|
|
52
|
+
{ method: "GET", pattern: "leaderboards/players/:playerId", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async (request) => ok(await leaderboards.getPlayerDetail(request.params.playerId)) },
|
|
53
|
+
{ method: "PUT", pattern: "leaderboards/players/:playerId/name", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.playerRename", handler: async (request) => renamePlayer(leaderboards, request) },
|
|
54
|
+
{ method: "POST", pattern: "leaderboards/players/:playerId/hide", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.playerHide", handler: async (request) => hidePlayer(leaderboards, request) },
|
|
55
|
+
{ method: "POST", pattern: "leaderboards/players/:playerId/merge", permission: "leaderboards.manage", requiresStepUp: true, auditAction: "leaderboard.playerMerge", handler: async (request) => mergePlayer(leaderboards, request) },
|
|
56
|
+
{ method: "DELETE", pattern: "leaderboards/players/:playerId", permission: "leaderboards.manage", requiresStepUp: true, auditAction: "leaderboard.playerErase", handler: async (request) => erasePlayer(leaderboards, request) },
|
|
57
|
+
{ method: "GET", pattern: "leaderboards/rules", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async () => ok(await leaderboards.getRulesView()) },
|
|
58
|
+
{ method: "PUT", pattern: "leaderboards/rules", permission: "leaderboards.manage", requiresStepUp: false, auditAction: "leaderboard.rules", handler: async (request) => updateRules(leaderboards, request) },
|
|
59
|
+
{ method: "GET", pattern: "leaderboards/activity", permission: "leaderboards.view", requiresStepUp: false, auditAction: null, handler: async (request) => readActivity(leaderboards, request) },
|
|
60
|
+
{ method: "GET", pattern: "leaderboards/export", permission: "leaderboards.view", requiresStepUp: false, auditAction: "leaderboard.export", handler: async (request) => exportBoards(services, request) },
|
|
61
|
+
{
|
|
62
|
+
method: "POST",
|
|
63
|
+
pattern: "leaderboards/import",
|
|
64
|
+
permission: "leaderboards.manage",
|
|
65
|
+
requiresStepUp: (request) => !isDryRun(request),
|
|
66
|
+
auditAction: (request) => isDryRun(request) ? "leaderboard.importPreview" : "leaderboard.import",
|
|
67
|
+
handler: async (request) => importBoards(services, request)
|
|
68
|
+
},
|
|
69
|
+
{ method: "POST", pattern: "leaderboards/submit", permission: "leaderboards.submit", requiresStepUp: false, auditAction: null, handler: async (request) => submitAtServerTrust(leaderboards, request) }
|
|
70
|
+
];
|
|
71
|
+
for (const route of routes)
|
|
72
|
+
router.register({ ...route, handler: withLeaderboardErrors(route.handler) });
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether an import is a dry run: a missing flag is a dry run, so only an explicit false writes.
|
|
76
|
+
* @param request The request.
|
|
77
|
+
* @returns True unless the body sets dryRun to false.
|
|
78
|
+
*/
|
|
79
|
+
function isDryRun(request) {
|
|
80
|
+
return request.body?.dryRun !== false;
|
|
81
|
+
}
|
|
82
|
+
// ---- Boards and seasons ---------------------------------------------------------------------------------------------
|
|
83
|
+
/**
|
|
84
|
+
* GET leaderboards/boards: every board's summary, sorted by board id.
|
|
85
|
+
* @param leaderboards The leaderboard service.
|
|
86
|
+
* @returns The summaries.
|
|
87
|
+
*/
|
|
88
|
+
async function listBoards(leaderboards) {
|
|
89
|
+
const records = await leaderboards.listBoardRecords();
|
|
90
|
+
const summaries = [];
|
|
91
|
+
for (const stored of records)
|
|
92
|
+
summaries.push(await leaderboards.summarizeBoard(stored));
|
|
93
|
+
return ok(summaries);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* POST leaderboards/boards: the body is `{ definition }` (the definition itself is accepted too).
|
|
97
|
+
* @param leaderboards The leaderboard service.
|
|
98
|
+
* @param request The request.
|
|
99
|
+
* @returns The new board's summary.
|
|
100
|
+
*/
|
|
101
|
+
async function createBoard(leaderboards, request) {
|
|
102
|
+
const body = bodyObject(request.body);
|
|
103
|
+
const definition = ((body.definition != null) ? body.definition : body);
|
|
104
|
+
const summary = await leaderboards.createBoard(definition);
|
|
105
|
+
return { status: HTTP_OK, body: summary, audit: { target: summary.definition.boardId, parameters: { definition: summary.definition } } };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* PUT leaderboards/boards/:boardId `{ definition, revision, startNewSeason }`.
|
|
109
|
+
* @param leaderboards The leaderboard service.
|
|
110
|
+
* @param request The request.
|
|
111
|
+
* @returns The updated summary.
|
|
112
|
+
*/
|
|
113
|
+
async function updateBoard(leaderboards, request) {
|
|
114
|
+
const body = bodyObject(request.body);
|
|
115
|
+
const definition = bodyObject(body.definition);
|
|
116
|
+
const revision = numberField(body, "revision");
|
|
117
|
+
const startNewSeason = (body.startNewSeason == null) ? false : booleanField(body, "startNewSeason");
|
|
118
|
+
const boardId = request.params.boardId;
|
|
119
|
+
const summary = await leaderboards.updateBoard(boardId, definition, revision, startNewSeason);
|
|
120
|
+
return { status: HTTP_OK, body: summary, audit: { target: boardId, parameters: { definition: summary.definition, startNewSeason: startNewSeason } } };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* POST leaderboards/boards/:boardId/duplicate `{ newBoardId, displayName }`.
|
|
124
|
+
* @param leaderboards The leaderboard service.
|
|
125
|
+
* @param request The request.
|
|
126
|
+
* @returns The new board's summary.
|
|
127
|
+
*/
|
|
128
|
+
async function duplicateBoard(leaderboards, request) {
|
|
129
|
+
const body = bodyObject(request.body);
|
|
130
|
+
const newBoardId = stringField(body, "newBoardId");
|
|
131
|
+
const displayName = stringField(body, "displayName");
|
|
132
|
+
const boardId = request.params.boardId;
|
|
133
|
+
const summary = await leaderboards.duplicateBoard(boardId, newBoardId, displayName);
|
|
134
|
+
return { status: HTTP_OK, body: summary, audit: { target: boardId, parameters: { newBoardId: newBoardId, displayName: displayName } } };
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* DELETE leaderboards/boards/:boardId: the board and every ranking of every season (step-up).
|
|
138
|
+
* @param leaderboards The leaderboard service.
|
|
139
|
+
* @param request The request.
|
|
140
|
+
* @returns Ok.
|
|
141
|
+
*/
|
|
142
|
+
async function deleteBoard(leaderboards, request) {
|
|
143
|
+
const boardId = request.params.boardId;
|
|
144
|
+
await leaderboards.deleteBoard(boardId);
|
|
145
|
+
return { status: HTTP_OK, body: OK_BODY, audit: { target: boardId, parameters: {} } };
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* POST leaderboards/boards/:boardId/seasons/reset `{ reason }` (step-up).
|
|
149
|
+
* @param leaderboards The leaderboard service.
|
|
150
|
+
* @param request The request.
|
|
151
|
+
* @returns The board's summary with its new season.
|
|
152
|
+
*/
|
|
153
|
+
async function resetSeason(leaderboards, request) {
|
|
154
|
+
const reason = reasonField(bodyObject(request.body));
|
|
155
|
+
const boardId = request.params.boardId;
|
|
156
|
+
const summary = await leaderboards.resetSeason(boardId, "admin");
|
|
157
|
+
return { status: HTTP_OK, body: summary, audit: { target: boardId, parameters: { reason: reason, newSeasonId: summary.currentSeasonId } } };
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* DELETE leaderboards/boards/:boardId/seasons/:seasonId: an archived season's rankings (step-up).
|
|
161
|
+
* @param leaderboards The leaderboard service.
|
|
162
|
+
* @param request The request.
|
|
163
|
+
* @returns `{ deletedEntries }`.
|
|
164
|
+
*/
|
|
165
|
+
async function deleteSeason(leaderboards, request) {
|
|
166
|
+
const boardId = request.params.boardId;
|
|
167
|
+
const seasonId = seasonIdOf(request.params.seasonId, "seasonId");
|
|
168
|
+
const deletedEntries = await leaderboards.deleteSeason(boardId, seasonId);
|
|
169
|
+
return { status: HTTP_OK, body: { deletedEntries: deletedEntries }, audit: { target: boardId, parameters: { seasonId: seasonId, deletedEntries: deletedEntries } } };
|
|
170
|
+
}
|
|
171
|
+
// ---- Entries --------------------------------------------------------------------------------------------------------
|
|
172
|
+
/**
|
|
173
|
+
* GET leaderboards/boards/:boardId/entries (`?season=&variantKey=&offset=&limit=&search=`): a page of one ranking, or
|
|
174
|
+
* the entries matching a search.
|
|
175
|
+
* @param leaderboards The leaderboard service.
|
|
176
|
+
* @param request The request.
|
|
177
|
+
* @returns The page.
|
|
178
|
+
*/
|
|
179
|
+
async function readEntries(leaderboards, request) {
|
|
180
|
+
const boardId = request.params.boardId;
|
|
181
|
+
const ranking = await rankingOf(leaderboards, boardId, request.query);
|
|
182
|
+
const search = textQueryOf(request.query, "search");
|
|
183
|
+
if (search != null)
|
|
184
|
+
return ok(await leaderboards.searchEntries(boardId, ranking.variantKey, ranking.seasonId, search));
|
|
185
|
+
const offset = integerQueryOf(request.query, "offset", 0, 0, Number.MAX_SAFE_INTEGER);
|
|
186
|
+
const limit = integerQueryOf(request.query, "limit", LEADERBOARD_DEFAULT_PAGE_SIZE, 1, LEADERBOARD_MAXIMUM_PAGE_SIZE);
|
|
187
|
+
return ok(await leaderboards.readEntries(boardId, variantValuesOf(ranking.variantKey), ranking.seasonId, offset, limit, "admin"));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* GET leaderboards/boards/:boardId/removed (`?season=&variantKey=`).
|
|
191
|
+
* @param leaderboards The leaderboard service.
|
|
192
|
+
* @param request The request.
|
|
193
|
+
* @returns The removed entries.
|
|
194
|
+
*/
|
|
195
|
+
async function readRemoved(leaderboards, request) {
|
|
196
|
+
const boardId = request.params.boardId;
|
|
197
|
+
const ranking = await rankingOf(leaderboards, boardId, request.query);
|
|
198
|
+
return ok(await leaderboards.readRemoved(boardId, ranking.variantKey, ranking.seasonId));
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* POST leaderboards/boards/:boardId/entries `{ variant, playerId, displayName, score, reason }`: an entry by hand at
|
|
202
|
+
* server trust, for a new manual player when playerId is missing.
|
|
203
|
+
* @param leaderboards The leaderboard service.
|
|
204
|
+
* @param request The request.
|
|
205
|
+
* @returns The entry with its rank.
|
|
206
|
+
*/
|
|
207
|
+
async function addEntry(leaderboards, request) {
|
|
208
|
+
const body = bodyObject(request.body);
|
|
209
|
+
const variant = variantFieldOf(body);
|
|
210
|
+
const playerId = optionalStringField(body, "playerId");
|
|
211
|
+
const displayName = optionalStringField(body, "displayName") ?? "";
|
|
212
|
+
const score = numberField(body, "score");
|
|
213
|
+
const reason = reasonField(body);
|
|
214
|
+
const boardId = request.params.boardId;
|
|
215
|
+
const entry = await leaderboards.addEntry(boardId, variant, (playerId === "") ? null : playerId, displayName, score);
|
|
216
|
+
return { status: HTTP_OK, body: entry, audit: { target: boardId, parameters: { playerId: entry.playerId, variant: variant, score: score, reason: reason } } };
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* PUT leaderboards/boards/:boardId/entries/:playerId `{ seasonId, variantKey, score, reason }`.
|
|
220
|
+
* @param leaderboards The leaderboard service.
|
|
221
|
+
* @param request The request.
|
|
222
|
+
* @returns The corrected entry.
|
|
223
|
+
*/
|
|
224
|
+
async function correctEntry(leaderboards, request) {
|
|
225
|
+
const body = bodyObject(request.body);
|
|
226
|
+
const seasonId = seasonIdField(body);
|
|
227
|
+
const variantKey = stringField(body, "variantKey");
|
|
228
|
+
const score = numberField(body, "score");
|
|
229
|
+
const reason = reasonField(body);
|
|
230
|
+
const { boardId, playerId } = request.params;
|
|
231
|
+
const entry = await leaderboards.correctEntry(boardId, variantKey, seasonId, playerId, score);
|
|
232
|
+
return { status: HTTP_OK, body: entry, audit: { target: boardId, parameters: { playerId: playerId, seasonId: seasonId, variantKey: variantKey, score: score, reason: reason } } };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* POST leaderboards/boards/:boardId/entries/:playerId/remove `{ seasonId, variantKey, reason }`.
|
|
236
|
+
* @param leaderboards The leaderboard service.
|
|
237
|
+
* @param request The request.
|
|
238
|
+
* @returns Ok.
|
|
239
|
+
*/
|
|
240
|
+
async function removeEntry(leaderboards, request) {
|
|
241
|
+
const body = bodyObject(request.body);
|
|
242
|
+
const seasonId = seasonIdField(body);
|
|
243
|
+
const variantKey = stringField(body, "variantKey");
|
|
244
|
+
const reason = reasonField(body);
|
|
245
|
+
const { boardId, playerId } = request.params;
|
|
246
|
+
await leaderboards.removeEntry(boardId, variantKey, seasonId, playerId, reason, callerNameOf(request));
|
|
247
|
+
return { status: HTTP_OK, body: OK_BODY, audit: { target: boardId, parameters: { playerId: playerId, seasonId: seasonId, variantKey: variantKey, reason: reason } } };
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* POST leaderboards/boards/:boardId/entries/:playerId/restore `{ seasonId, variantKey }`.
|
|
251
|
+
* @param leaderboards The leaderboard service.
|
|
252
|
+
* @param request The request.
|
|
253
|
+
* @returns Ok.
|
|
254
|
+
*/
|
|
255
|
+
async function restoreEntry(leaderboards, request) {
|
|
256
|
+
const body = bodyObject(request.body);
|
|
257
|
+
const seasonId = seasonIdField(body);
|
|
258
|
+
const variantKey = stringField(body, "variantKey");
|
|
259
|
+
const { boardId, playerId } = request.params;
|
|
260
|
+
await leaderboards.restoreEntry(boardId, variantKey, seasonId, playerId);
|
|
261
|
+
return { status: HTTP_OK, body: OK_BODY, audit: { target: boardId, parameters: { playerId: playerId, seasonId: seasonId, variantKey: variantKey } } };
|
|
262
|
+
}
|
|
263
|
+
// ---- Players --------------------------------------------------------------------------------------------------------
|
|
264
|
+
/**
|
|
265
|
+
* PUT leaderboards/players/:playerId/name `{ displayName }`.
|
|
266
|
+
* @param leaderboards The leaderboard service.
|
|
267
|
+
* @param request The request.
|
|
268
|
+
* @returns The player.
|
|
269
|
+
*/
|
|
270
|
+
async function renamePlayer(leaderboards, request) {
|
|
271
|
+
const displayName = stringField(bodyObject(request.body), "displayName");
|
|
272
|
+
const playerId = request.params.playerId;
|
|
273
|
+
const player = await leaderboards.renamePlayer(playerId, displayName);
|
|
274
|
+
return { status: HTTP_OK, body: player, audit: { target: playerId, parameters: { displayName: player.displayName } } };
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* POST leaderboards/players/:playerId/hide `{ hidden }` (D18).
|
|
278
|
+
* @param leaderboards The leaderboard service.
|
|
279
|
+
* @param request The request.
|
|
280
|
+
* @returns The player.
|
|
281
|
+
*/
|
|
282
|
+
async function hidePlayer(leaderboards, request) {
|
|
283
|
+
const hidden = booleanField(bodyObject(request.body), "hidden");
|
|
284
|
+
const playerId = request.params.playerId;
|
|
285
|
+
const player = await leaderboards.hidePlayer(playerId, hidden, callerNameOf(request));
|
|
286
|
+
return { status: HTTP_OK, body: player, audit: { target: playerId, parameters: { hidden: hidden } } };
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* POST leaderboards/players/:playerId/merge `{ intoPlayerId }` (step-up, DR §15).
|
|
290
|
+
* @param leaderboards The leaderboard service.
|
|
291
|
+
* @param request The request.
|
|
292
|
+
* @returns The kept player's detail.
|
|
293
|
+
*/
|
|
294
|
+
async function mergePlayer(leaderboards, request) {
|
|
295
|
+
const intoPlayerId = stringField(bodyObject(request.body), "intoPlayerId");
|
|
296
|
+
const playerId = request.params.playerId;
|
|
297
|
+
const detail = await leaderboards.mergePlayers(playerId, intoPlayerId);
|
|
298
|
+
return { status: HTTP_OK, body: detail, audit: { target: playerId, parameters: { intoPlayerId: intoPlayerId } } };
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* DELETE leaderboards/players/:playerId: every entry and the record (step-up).
|
|
302
|
+
* @param leaderboards The leaderboard service.
|
|
303
|
+
* @param request The request.
|
|
304
|
+
* @returns `{ erasedEntries }`.
|
|
305
|
+
*/
|
|
306
|
+
async function erasePlayer(leaderboards, request) {
|
|
307
|
+
const playerId = request.params.playerId;
|
|
308
|
+
const erasedEntries = await leaderboards.erasePlayer(playerId);
|
|
309
|
+
return { status: HTTP_OK, body: { erasedEntries: erasedEntries }, audit: { target: playerId, parameters: { erasedEntries: erasedEntries } } };
|
|
310
|
+
}
|
|
311
|
+
// ---- Rules, activity, backups and server submissions ----------------------------------------------------------------
|
|
312
|
+
/**
|
|
313
|
+
* PUT leaderboards/rules `{ rules, revision }`.
|
|
314
|
+
* @param leaderboards The leaderboard service.
|
|
315
|
+
* @param request The request.
|
|
316
|
+
* @returns The new rules view.
|
|
317
|
+
*/
|
|
318
|
+
async function updateRules(leaderboards, request) {
|
|
319
|
+
const body = bodyObject(request.body);
|
|
320
|
+
const rules = bodyObject(body.rules);
|
|
321
|
+
const revision = numberField(body, "revision");
|
|
322
|
+
const view = await leaderboards.updateRules(rules, revision);
|
|
323
|
+
return { status: HTTP_OK, body: view, audit: { target: "rules", parameters: { rules: view.rules } } };
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* GET leaderboards/activity (`?kind=&boardId=&before=&limit=`): newest first; client addresses only for callers holding
|
|
327
|
+
* players.viewAddresses (D21).
|
|
328
|
+
* @param leaderboards The leaderboard service.
|
|
329
|
+
* @param request The request.
|
|
330
|
+
* @returns The records.
|
|
331
|
+
*/
|
|
332
|
+
async function readActivity(leaderboards, request) {
|
|
333
|
+
const kind = textQueryOf(request.query, "kind");
|
|
334
|
+
if (kind != null && !ACTIVITY_KINDS.has(kind))
|
|
335
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "kind must be accepted, duplicate or refused");
|
|
336
|
+
const boardId = textQueryOf(request.query, "boardId");
|
|
337
|
+
const before = (textQueryOf(request.query, "before") != null) ? integerQueryOf(request.query, "before", 0, 0, Number.MAX_SAFE_INTEGER) : null;
|
|
338
|
+
const limit = integerQueryOf(request.query, "limit", DEFAULT_ACTIVITY_LIMIT, 1, MAXIMUM_ACTIVITY_LIMIT);
|
|
339
|
+
const includeAddresses = request.caller?.permissions.has("players.viewAddresses") === true;
|
|
340
|
+
return ok(await leaderboards.readActivity(kind, boardId, before, limit, includeAddresses));
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* GET leaderboards/export (`?format=json|csv&boardId=&season=&variantKey=`): a JSON backup (all boards or one) or one
|
|
344
|
+
* ranking as CSV (boardId required; season and variantKey default to the current season and the first variant).
|
|
345
|
+
* @param services The shared services.
|
|
346
|
+
* @param request The request.
|
|
347
|
+
* @returns The download.
|
|
348
|
+
*/
|
|
349
|
+
async function exportBoards(services, request) {
|
|
350
|
+
const format = request.query.get("format") ?? "json";
|
|
351
|
+
if (format !== "json" && format !== "csv")
|
|
352
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "format must be json or csv");
|
|
353
|
+
const boardId = textQueryOf(request.query, "boardId");
|
|
354
|
+
if (format === "json") {
|
|
355
|
+
const seasonId = seasonQueryOf(request.query);
|
|
356
|
+
const file = await exportLeaderboards(services.leaderboards, boardId, seasonId, services.clock);
|
|
357
|
+
const audit = { target: boardId ?? "all", parameters: { format: format, boardId: boardId, seasonId: seasonId } };
|
|
358
|
+
return downloadOf(JSON.stringify(file, null, 2), "json", JSON_EXPORT_FILE_NAME, audit);
|
|
359
|
+
}
|
|
360
|
+
if (boardId == null)
|
|
361
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "a CSV export needs boardId");
|
|
362
|
+
const ranking = await rankingOf(services.leaderboards, boardId, request.query);
|
|
363
|
+
const csv = await exportRankingCsv(services.leaderboards, boardId, ranking.seasonId, ranking.variantKey);
|
|
364
|
+
const audit = { target: boardId, parameters: { format: format, boardId: boardId, seasonId: ranking.seasonId, variantKey: ranking.variantKey } };
|
|
365
|
+
return downloadOf(csv, "csv", boardId + "-season-" + ranking.seasonId + ".csv", audit);
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* POST leaderboards/import `LeaderboardImportRequest` (step-up unless a dry run). The audit names the policy and the
|
|
369
|
+
* result, never the file (it holds key hashes).
|
|
370
|
+
* @param services The shared services.
|
|
371
|
+
* @param request The request.
|
|
372
|
+
* @returns The import result (the preview of a dry run).
|
|
373
|
+
*/
|
|
374
|
+
async function importBoards(services, request) {
|
|
375
|
+
const body = bodyObject(request.body);
|
|
376
|
+
const importRequest = {
|
|
377
|
+
file: bodyObject(body.file),
|
|
378
|
+
conflictPolicy: stringField(body, "conflictPolicy"),
|
|
379
|
+
dryRun: isDryRun(request)
|
|
380
|
+
};
|
|
381
|
+
const result = await importLeaderboards(services.leaderboards, importRequest, services.clock);
|
|
382
|
+
return { status: HTTP_OK, body: result, audit: { target: "import", parameters: { conflictPolicy: importRequest.conflictPolicy, ...result } } };
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* POST leaderboards/submit: a submission at the server trust level (D9) for the player named by `body.playerId`; an
|
|
386
|
+
* unknown player with a displayName becomes a manual player.
|
|
387
|
+
* @param leaderboards The leaderboard service.
|
|
388
|
+
* @param request The request.
|
|
389
|
+
* @returns The per-score results.
|
|
390
|
+
*/
|
|
391
|
+
async function submitAtServerTrust(leaderboards, request) {
|
|
392
|
+
const body = bodyObject(request.body);
|
|
393
|
+
const caller = { userId: null, userDisplayName: null, userAvatar: null, isNameManaged: false, playerKey: null, clientAddress: request.clientAddress };
|
|
394
|
+
return ok(await leaderboards.submit(caller, body, "server"));
|
|
395
|
+
}
|
|
396
|
+
// ---- Helpers --------------------------------------------------------------------------------------------------------
|
|
397
|
+
/**
|
|
398
|
+
* Wraps a handler so a LeaderboardError answers with its status, sentence and reason (as the error detail).
|
|
399
|
+
* @param handler The handler.
|
|
400
|
+
* @returns The wrapped handler.
|
|
401
|
+
*/
|
|
402
|
+
function withLeaderboardErrors(handler) {
|
|
403
|
+
return async (request) => {
|
|
404
|
+
try {
|
|
405
|
+
return await handler(request);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
if (error instanceof LeaderboardError)
|
|
409
|
+
throw new AdminRequestError(error.status, error.message, error.reason);
|
|
410
|
+
throw error;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A 200 answer.
|
|
416
|
+
* @param body The JSON body.
|
|
417
|
+
* @returns The result.
|
|
418
|
+
*/
|
|
419
|
+
function ok(body) {
|
|
420
|
+
return { status: HTTP_OK, body: body };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* A download answer.
|
|
424
|
+
* @param text The file's text.
|
|
425
|
+
* @param format "json" or "csv".
|
|
426
|
+
* @param fileName The file name.
|
|
427
|
+
* @param audit The audit detail.
|
|
428
|
+
* @returns The result.
|
|
429
|
+
*/
|
|
430
|
+
function downloadOf(text, format, fileName, audit) {
|
|
431
|
+
const bytes = new TextEncoder().encode(text);
|
|
432
|
+
return { status: HTTP_OK, body: null, download: { bytes: bytes, contentType: EXPORT_CONTENT_TYPES[format], fileName: fileName }, audit: audit };
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* The ranking a read names: `?season=` (default the current season) and `?variantKey=` (default the season's first
|
|
436
|
+
* written variant, else the first value of each dimension).
|
|
437
|
+
* @param leaderboards The leaderboard service.
|
|
438
|
+
* @param boardId The board.
|
|
439
|
+
* @param query The query.
|
|
440
|
+
* @returns The season and variant key.
|
|
441
|
+
* @throws LeaderboardError 404 for an unknown board or a season it does not keep.
|
|
442
|
+
*/
|
|
443
|
+
async function rankingOf(leaderboards, boardId, query) {
|
|
444
|
+
const record = (await leaderboards.getBoardRecord(boardId)).value;
|
|
445
|
+
const seasonId = seasonQueryOf(query) ?? record.currentSeasonId;
|
|
446
|
+
if (!record.seasons.some((season) => season.seasonId === seasonId)) {
|
|
447
|
+
throw new LeaderboardError(404, "invalidRequest", "board " + boardId + " has no season " + seasonId);
|
|
448
|
+
}
|
|
449
|
+
const variantKey = textQueryOf(query, "variantKey");
|
|
450
|
+
if (variantKey != null)
|
|
451
|
+
return { seasonId: seasonId, variantKey: variantKey };
|
|
452
|
+
return { seasonId: seasonId, variantKey: await defaultVariantKeyOf(leaderboards, record, seasonId) };
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* The variant a read shows when none is named: the season's first written variant, else the first value of each
|
|
456
|
+
* dimension ("all" without dimensions).
|
|
457
|
+
* @param leaderboards The leaderboard service.
|
|
458
|
+
* @param record The board.
|
|
459
|
+
* @param seasonId The season.
|
|
460
|
+
* @returns The variant key.
|
|
461
|
+
*/
|
|
462
|
+
async function defaultVariantKeyOf(leaderboards, record, seasonId) {
|
|
463
|
+
const [firstWritten] = await leaderboards.stores.rankStore.listVariantKeys(record.definition.boardId, seasonId);
|
|
464
|
+
if (firstWritten != null)
|
|
465
|
+
return firstWritten;
|
|
466
|
+
const firstValues = {};
|
|
467
|
+
for (const dimension of record.definition.dimensions)
|
|
468
|
+
firstValues[dimension.name] = dimension.values[0];
|
|
469
|
+
return variantKeyOf(record.definition, firstValues).variantKey ?? LEADERBOARD_ALL_VARIANT_KEY;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* The `?season=` of a query.
|
|
473
|
+
* @param query The query.
|
|
474
|
+
* @returns The season, or null when absent.
|
|
475
|
+
* @throws AdminRequestError 400 when it is not a whole number from 1.
|
|
476
|
+
*/
|
|
477
|
+
function seasonQueryOf(query) {
|
|
478
|
+
const text = textQueryOf(query, "season");
|
|
479
|
+
return (text != null) ? seasonIdOf(text, "season") : null;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* A season id from text.
|
|
483
|
+
* @param text The text.
|
|
484
|
+
* @param name The field name for the error.
|
|
485
|
+
* @returns The season.
|
|
486
|
+
* @throws AdminRequestError 400 when it is not a whole number from 1.
|
|
487
|
+
*/
|
|
488
|
+
function seasonIdOf(text, name) {
|
|
489
|
+
const seasonId = Number(text);
|
|
490
|
+
if (!Number.isSafeInteger(seasonId) || seasonId < 1)
|
|
491
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, name + " must be a whole number from 1");
|
|
492
|
+
return seasonId;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* A non-empty query value.
|
|
496
|
+
* @param query The query.
|
|
497
|
+
* @param name The name.
|
|
498
|
+
* @returns The value, or null when absent or empty.
|
|
499
|
+
*/
|
|
500
|
+
function textQueryOf(query, name) {
|
|
501
|
+
const value = query.get(name);
|
|
502
|
+
return (value == null || value === "") ? null : value;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* A whole-number query value within limits.
|
|
506
|
+
* @param query The query.
|
|
507
|
+
* @param name The name.
|
|
508
|
+
* @param defaultValue The value when absent.
|
|
509
|
+
* @param minimum The least value.
|
|
510
|
+
* @param maximum The greatest value.
|
|
511
|
+
* @returns The value.
|
|
512
|
+
* @throws AdminRequestError 400 when it is not a whole number within the limits.
|
|
513
|
+
*/
|
|
514
|
+
function integerQueryOf(query, name, defaultValue, minimum, maximum) {
|
|
515
|
+
const text = textQueryOf(query, name);
|
|
516
|
+
if (text == null)
|
|
517
|
+
return defaultValue;
|
|
518
|
+
const value = Number(text);
|
|
519
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum)
|
|
520
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, name + " must be a whole number from " + minimum + " to " + maximum);
|
|
521
|
+
return value;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* The required `seasonId` of a body.
|
|
525
|
+
* @param body The body.
|
|
526
|
+
* @returns The season.
|
|
527
|
+
* @throws AdminRequestError 400 when it is not a whole number from 1.
|
|
528
|
+
*/
|
|
529
|
+
function seasonIdField(body) {
|
|
530
|
+
const seasonId = numberField(body, "seasonId");
|
|
531
|
+
if (!Number.isSafeInteger(seasonId) || seasonId < 1)
|
|
532
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "seasonId must be a whole number from 1");
|
|
533
|
+
return seasonId;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* The required, non-empty `reason` of a body, trimmed.
|
|
537
|
+
* @param body The body.
|
|
538
|
+
* @returns The reason.
|
|
539
|
+
* @throws AdminRequestError 400 when it is missing, empty or too long.
|
|
540
|
+
*/
|
|
541
|
+
function reasonField(body) {
|
|
542
|
+
const reason = stringField(body, "reason").trim();
|
|
543
|
+
if (reason === "" || reason.length > MAXIMUM_REASON_LENGTH)
|
|
544
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "reason must be 1 to " + MAXIMUM_REASON_LENGTH + " characters");
|
|
545
|
+
return reason;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* The optional `variant` of a body: an object of texts, or null.
|
|
549
|
+
* @param body The body.
|
|
550
|
+
* @returns The variant, or null when absent.
|
|
551
|
+
* @throws AdminRequestError 400 when it is not an object of texts.
|
|
552
|
+
*/
|
|
553
|
+
function variantFieldOf(body) {
|
|
554
|
+
const variant = body.variant;
|
|
555
|
+
if (variant == null)
|
|
556
|
+
return null;
|
|
557
|
+
const isTextObject = typeof variant === "object" && !Array.isArray(variant) && Object.values(variant).every((value) => typeof value === "string");
|
|
558
|
+
if (!isTextObject)
|
|
559
|
+
throw new AdminRequestError(HTTP_BAD_REQUEST, "variant must be an object of texts");
|
|
560
|
+
return variant;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* The name of the admin making a request, for removedBy fields.
|
|
564
|
+
* @param request The request.
|
|
565
|
+
* @returns The caller's name.
|
|
566
|
+
*/
|
|
567
|
+
function callerNameOf(request) {
|
|
568
|
+
return request.caller?.name ?? "unknown";
|
|
569
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { GameServerLogLevel } from "../GameServerConfig.js";
|
|
2
|
+
import type { LogSink } from "../GameServerLogger.js";
|
|
3
|
+
import type { LogEntry, LogEntryView } from "./AdminApiTypes.js";
|
|
4
|
+
/** What a log read keeps; every field left out matches everything. */
|
|
5
|
+
export interface LogFilter {
|
|
6
|
+
/** This level or a more severe one. */
|
|
7
|
+
level?: GameServerLogLevel;
|
|
8
|
+
/** Entries of this room. */
|
|
9
|
+
roomId?: string;
|
|
10
|
+
/** Entries of this session. */
|
|
11
|
+
sessionName?: string;
|
|
12
|
+
/** Entries of this player. */
|
|
13
|
+
playerId?: number;
|
|
14
|
+
/** Entries of this user. */
|
|
15
|
+
userId?: string;
|
|
16
|
+
/** Text in the message or the fields, ignoring case. */
|
|
17
|
+
text?: string;
|
|
18
|
+
}
|
|
19
|
+
/** The in-memory log buffer of this instance. */
|
|
20
|
+
export declare class LogBuffer {
|
|
21
|
+
/** The logger sink that captures into this buffer (add it with GameServerLogger.addSink). */
|
|
22
|
+
readonly sink: LogSink;
|
|
23
|
+
/** The entries, oldest first. */
|
|
24
|
+
private readonly entries;
|
|
25
|
+
/** How many entries are kept. */
|
|
26
|
+
private readonly capacity;
|
|
27
|
+
/** The sequence of the newest entry (0 before any). */
|
|
28
|
+
private newestSequence;
|
|
29
|
+
/**
|
|
30
|
+
* Creates an empty buffer.
|
|
31
|
+
* @param capacity How many entries are kept (admin.logBufferEntries); the oldest are dropped first.
|
|
32
|
+
*/
|
|
33
|
+
constructor(capacity: number);
|
|
34
|
+
/** The sequence of the newest entry, 0 before any. */
|
|
35
|
+
get latestSequence(): number;
|
|
36
|
+
/**
|
|
37
|
+
* Adds an entry, redacted (DR §6.8), with the next sequence and the current time.
|
|
38
|
+
* @param level The entry's level.
|
|
39
|
+
* @param message What happened.
|
|
40
|
+
* @param fields The entry's fields.
|
|
41
|
+
*/
|
|
42
|
+
capture(level: GameServerLogLevel, message: string, fields: Record<string, unknown>): void;
|
|
43
|
+
/**
|
|
44
|
+
* Reads matching entries. With a cursor, the oldest entries after it come first, so a live tail misses nothing;
|
|
45
|
+
* without one (0), the newest entries are returned, so a first look shows what just happened.
|
|
46
|
+
* @param filter What to keep.
|
|
47
|
+
* @param afterSequence Only entries with a larger sequence; 0 for the newest.
|
|
48
|
+
* @param limit The most entries returned.
|
|
49
|
+
* @returns The entries, oldest first.
|
|
50
|
+
*/
|
|
51
|
+
read(filter: LogFilter, afterSequence: number, limit: number): LogEntry[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A buffered entry as the console shows it.
|
|
55
|
+
* @param entry The entry.
|
|
56
|
+
* @param processId The instance it came from.
|
|
57
|
+
* @param includeAddresses False removes the client address fields (callers without players.viewAddresses).
|
|
58
|
+
* @returns The view.
|
|
59
|
+
*/
|
|
60
|
+
export declare function toLogEntryView(entry: LogEntry, processId: string, includeAddresses: boolean): LogEntryView;
|