Skip to content

Commit 01dee3f

Browse files
committed
feat: leaderboards remake, weekly xp leaderboards (@Miodec) (monkeytypegame#6250)
1 parent e7685c5 commit 01dee3f

32 files changed

+2214
-1427
lines changed

backend/__tests__/api/controllers/leaderboard.spec.ts

Lines changed: 158 additions & 103 deletions
Large diffs are not rendered by default.

backend/__tests__/api/controllers/user.spec.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ import { ObjectId } from "mongodb";
2424
import { PersonalBest } from "@monkeytype/contracts/schemas/shared";
2525
import { pb } from "../../dal/leaderboards.spec";
2626
import { mockAuthenticateWithApeKey } from "../../__testData__/auth";
27-
import { LeaderboardRank } from "@monkeytype/contracts/schemas/leaderboards";
2827
import { randomUUID } from "node:crypto";
2928
import _ from "lodash";
3029
import { MonkeyMail, UserStreak } from "@monkeytype/contracts/schemas/users";
3130
import { isFirebaseError } from "../../../src/utils/error";
31+
import { LeaderboardEntry } from "@monkeytype/contracts/schemas/leaderboards";
3232

3333
const mockApp = request(app);
3434
const configuration = Configuration.getCachedConfiguration();
@@ -2696,6 +2696,7 @@ describe("user controller test", () => {
26962696
const getUserByNameMock = vi.spyOn(UserDal, "getUserByName");
26972697
const checkIfUserIsPremiumMock = vi.spyOn(UserDal, "checkIfUserIsPremium");
26982698
const leaderboardGetRankMock = vi.spyOn(LeaderboardDal, "getRank");
2699+
const leaderboardGetCountMock = vi.spyOn(LeaderboardDal, "getCount");
26992700

27002701
const foundUser: Partial<UserDal.DBUser> = {
27012702
_id: new ObjectId(),
@@ -2747,15 +2748,17 @@ describe("user controller test", () => {
27472748
getUserByNameMock.mockReset();
27482749
checkIfUserIsPremiumMock.mockReset().mockResolvedValue(true);
27492750
leaderboardGetRankMock.mockReset();
2751+
leaderboardGetCountMock.mockReset();
27502752
await enableProfiles(true);
27512753
});
27522754

27532755
it("should get by name without authentication", async () => {
27542756
//GIVEN
27552757
getUserByNameMock.mockResolvedValue(foundUser as any);
27562758

2757-
const rank: LeaderboardRank = { count: 100, rank: 24 };
2759+
const rank = { rank: 24 } as LeaderboardEntry;
27582760
leaderboardGetRankMock.mockResolvedValue(rank);
2761+
leaderboardGetCountMock.mockResolvedValue(100);
27592762

27602763
//WHEN
27612764
const { body } = await mockApp.get("/users/bob/profile").expect(200);
@@ -2815,8 +2818,9 @@ describe("user controller test", () => {
28152818
banned: true,
28162819
} as any);
28172820

2818-
const rank: LeaderboardRank = { count: 100, rank: 24 };
2821+
const rank = { rank: 24 } as LeaderboardEntry;
28192822
leaderboardGetRankMock.mockResolvedValue(rank);
2823+
leaderboardGetCountMock.mockResolvedValue(100);
28202824

28212825
//WHEN
28222826
const { body } = await mockApp.get("/users/bob/profile").expect(200);
@@ -2865,8 +2869,9 @@ describe("user controller test", () => {
28652869
const uid = foundUser.uid;
28662870
getUserMock.mockResolvedValue(foundUser as any);
28672871

2868-
const rank: LeaderboardRank = { count: 100, rank: 24 };
2872+
const rank = { rank: 24 } as LeaderboardEntry;
28692873
leaderboardGetRankMock.mockResolvedValue(rank);
2874+
leaderboardGetCountMock.mockResolvedValue(100);
28702875

28712876
//WHEN
28722877
const { body } = await mockApp

backend/__tests__/dal/leaderboards.spec.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe("LeaderboardsDal", () => {
2828

2929
//WHEN
3030
await LeaderboardsDal.update("time", "15", "english");
31-
const result = await LeaderboardsDal.get("time", "15", "english", 0);
31+
const result = await LeaderboardsDal.get("time", "15", "english", 0, 50);
3232

3333
//THEN
3434
expect(result).toHaveLength(1);
@@ -50,7 +50,8 @@ describe("LeaderboardsDal", () => {
5050
"time",
5151
"15",
5252
"english",
53-
0
53+
0,
54+
50
5455
)) as DBLeaderboardEntry[];
5556

5657
//THEN
@@ -76,7 +77,8 @@ describe("LeaderboardsDal", () => {
7677
"time",
7778
"60",
7879
"english",
79-
0
80+
0,
81+
50
8082
)) as LeaderboardsDal.DBLeaderboardEntry[];
8183

8284
//THEN
@@ -102,7 +104,8 @@ describe("LeaderboardsDal", () => {
102104
"time",
103105
"60",
104106
"english",
105-
0
107+
0,
108+
50
106109
)) as DBLeaderboardEntry[];
107110

108111
//THEN
@@ -125,7 +128,8 @@ describe("LeaderboardsDal", () => {
125128
"time",
126129
"15",
127130
"english",
128-
0
131+
0,
132+
50
129133
)) as DBLeaderboardEntry[];
130134

131135
//THEN
@@ -187,7 +191,8 @@ describe("LeaderboardsDal", () => {
187191
"time",
188192
"15",
189193
"english",
190-
0
194+
0,
195+
50
191196
)) as DBLeaderboardEntry[];
192197

193198
//THEN
@@ -223,7 +228,8 @@ describe("LeaderboardsDal", () => {
223228
"time",
224229
"15",
225230
"english",
226-
0
231+
0,
232+
50
227233
)) as DBLeaderboardEntry[];
228234

229235
//THEN
@@ -255,7 +261,8 @@ describe("LeaderboardsDal", () => {
255261
"time",
256262
"15",
257263
"english",
258-
0
264+
0,
265+
50
259266
)) as DBLeaderboardEntry[];
260267

261268
//THEN

backend/redis-scripts/get-results.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ for _, user_id in ipairs(scores_in_range) do
2121
end
2222
end
2323

24-
return {results, scores}
24+
return {results, scores}

backend/src/api/controllers/leaderboard.ts

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
77
import {
88
GetDailyLeaderboardQuery,
99
GetDailyLeaderboardRankQuery,
10+
GetDailyLeaderboardResponse,
1011
GetLeaderboardDailyRankResponse,
1112
GetLeaderboardQuery,
13+
GetLeaderboardRankQuery,
1214
GetLeaderboardRankResponse,
1315
GetLeaderboardResponse as GetLeaderboardResponse,
1416
GetWeeklyXpLeaderboardQuery,
1517
GetWeeklyXpLeaderboardRankResponse,
1618
GetWeeklyXpLeaderboardResponse,
17-
LanguageAndModeQuery,
1819
} from "@monkeytype/contracts/leaderboards";
1920
import { Configuration } from "@monkeytype/contracts/schemas/configuration";
2021
import {
@@ -27,14 +28,14 @@ import { MonkeyRequest } from "../types";
2728
export async function getLeaderboard(
2829
req: MonkeyRequest<GetLeaderboardQuery>
2930
): Promise<GetLeaderboardResponse> {
30-
const { language, mode, mode2, skip = 0, limit = 50 } = req.query;
31+
const { language, mode, mode2, page, pageSize } = req.query;
3132

3233
const leaderboard = await LeaderboardsDAL.get(
3334
mode,
3435
mode2,
3536
language,
36-
skip,
37-
limit
37+
page,
38+
pageSize
3839
);
3940

4041
if (leaderboard === false) {
@@ -44,13 +45,18 @@ export async function getLeaderboard(
4445
);
4546
}
4647

48+
const count = await LeaderboardsDAL.getCount(mode, mode2, language);
4749
const normalizedLeaderboard = leaderboard.map((it) => _.omit(it, ["_id"]));
4850

49-
return new MonkeyResponse("Leaderboard retrieved", normalizedLeaderboard);
51+
return new MonkeyResponse("Leaderboard retrieved", {
52+
count,
53+
entries: normalizedLeaderboard,
54+
pageSize,
55+
});
5056
}
5157

5258
export async function getRankFromLeaderboard(
53-
req: MonkeyRequest<LanguageAndModeQuery>
59+
req: MonkeyRequest<GetLeaderboardRankQuery>
5460
): Promise<GetLeaderboardRankResponse> {
5561
const { language, mode, mode2 } = req.query;
5662
const { uid } = req.ctx.decodedToken;
@@ -91,25 +97,33 @@ function getDailyLeaderboardWithError(
9197

9298
export async function getDailyLeaderboard(
9399
req: MonkeyRequest<GetDailyLeaderboardQuery>
94-
): Promise<GetLeaderboardResponse> {
95-
const { skip = 0, limit = 50 } = req.query;
100+
): Promise<GetDailyLeaderboardResponse> {
101+
const { page, pageSize } = req.query;
96102

97103
const dailyLeaderboard = getDailyLeaderboardWithError(
98104
req.query,
99105
req.ctx.configuration.dailyLeaderboards
100106
);
101107

102-
const minRank = skip;
103-
const maxRank = minRank + limit - 1;
104-
105-
const topResults = await dailyLeaderboard.getResults(
106-
minRank,
107-
maxRank,
108+
const results = await dailyLeaderboard.getResults(
109+
page,
110+
pageSize,
108111
req.ctx.configuration.dailyLeaderboards,
109112
req.ctx.configuration.users.premium.enabled
110113
);
111114

112-
return new MonkeyResponse("Daily leaderboard retrieved", topResults);
115+
const minWpm = await dailyLeaderboard.getMinWpm(
116+
req.ctx.configuration.dailyLeaderboards
117+
);
118+
119+
const count = await dailyLeaderboard.getCount();
120+
121+
return new MonkeyResponse("Daily leaderboard retrieved", {
122+
entries: results,
123+
minWpm,
124+
count,
125+
pageSize,
126+
});
113127
}
114128

115129
export async function getDailyLeaderboardRank(
@@ -131,8 +145,8 @@ export async function getDailyLeaderboardRank(
131145
}
132146

133147
function getWeeklyXpLeaderboardWithError(
134-
{ weeksBefore }: GetWeeklyXpLeaderboardQuery,
135-
config: Configuration["leaderboards"]["weeklyXp"]
148+
config: Configuration["leaderboards"]["weeklyXp"],
149+
weeksBefore?: number
136150
): WeeklyXpLeaderboard.WeeklyXpLeaderboard {
137151
const customTimestamp =
138152
weeksBefore === undefined
@@ -150,22 +164,26 @@ function getWeeklyXpLeaderboardWithError(
150164
export async function getWeeklyXpLeaderboardResults(
151165
req: MonkeyRequest<GetWeeklyXpLeaderboardQuery>
152166
): Promise<GetWeeklyXpLeaderboardResponse> {
153-
const { skip = 0, limit = 50 } = req.query;
154-
155-
const minRank = skip;
156-
const maxRank = minRank + limit - 1;
167+
const { page, pageSize, weeksBefore } = req.query;
157168

158169
const weeklyXpLeaderboard = getWeeklyXpLeaderboardWithError(
159-
req.query,
160-
req.ctx.configuration.leaderboards.weeklyXp
170+
req.ctx.configuration.leaderboards.weeklyXp,
171+
weeksBefore
161172
);
162173
const results = await weeklyXpLeaderboard.getResults(
163-
minRank,
164-
maxRank,
165-
req.ctx.configuration.leaderboards.weeklyXp
174+
page,
175+
pageSize,
176+
req.ctx.configuration.leaderboards.weeklyXp,
177+
req.ctx.configuration.users.premium.enabled
166178
);
167179

168-
return new MonkeyResponse("Weekly xp leaderboard retrieved", results);
180+
const count = await weeklyXpLeaderboard.getCount();
181+
182+
return new MonkeyResponse("Weekly xp leaderboard retrieved", {
183+
entries: results,
184+
count,
185+
pageSize,
186+
});
169187
}
170188

171189
export async function getWeeklyXpLeaderboardRank(
@@ -174,7 +192,6 @@ export async function getWeeklyXpLeaderboardRank(
174192
const { uid } = req.ctx.decodedToken;
175193

176194
const weeklyXpLeaderboard = getWeeklyXpLeaderboardWithError(
177-
{},
178195
req.ctx.configuration.leaderboards.weeklyXp
179196
);
180197
const rankEntry = await weeklyXpLeaderboard.getRank(

backend/src/api/controllers/result.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,7 @@ export async function addResult(
593593
discordId: user.discordId,
594594
badgeId: selectedBadgeId,
595595
lastActivityTimestamp: Date.now(),
596+
isPremium,
596597
},
597598
xpGained: xpGained.xp,
598599
timeTypedSeconds: totalDurationTypedSeconds,

backend/src/api/controllers/user.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,27 +1071,39 @@ async function getAllTimeLbs(uid: string): Promise<AllTimeLbs> {
10711071
uid
10721072
);
10731073

1074+
const allTime15EnglishCount = await LeaderboardsDAL.getCount(
1075+
"time",
1076+
"15",
1077+
"english"
1078+
);
1079+
10741080
const allTime60English = await LeaderboardsDAL.getRank(
10751081
"time",
10761082
"60",
10771083
"english",
10781084
uid
10791085
);
10801086

1087+
const allTime60EnglishCount = await LeaderboardsDAL.getCount(
1088+
"time",
1089+
"60",
1090+
"english"
1091+
);
1092+
10811093
const english15 =
1082-
allTime15English === false
1094+
allTime15English === false || allTime15English === null
10831095
? undefined
10841096
: {
10851097
rank: allTime15English.rank,
1086-
count: allTime15English.count,
1098+
count: allTime15EnglishCount,
10871099
};
10881100

10891101
const english60 =
1090-
allTime60English === false
1102+
allTime60English === false || allTime60English === null
10911103
? undefined
10921104
: {
10931105
rank: allTime60English.rank,
1094-
count: allTime60English.count,
1106+
count: allTime60EnglishCount,
10951107
};
10961108

10971109
return {

0 commit comments

Comments
 (0)