Add weekly xp leaderboards backend (#3511) Bruception

* Add weekly seasons

* Fix test

* Add week timestamp tests

* Add unit tests

* Fix weeks before calculation

* Update user.spec.ts

* Remove minXp maxXp reward config

* Record total time typed + last activity timestamp

* Season -> Weekly XP Leaderboard

* prettier

* Add config hints

* Update leaderboard.ts

* monkeytype

Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
Bruce Berrios 2022-11-28 08:10:02 -05:00 committed by GitHub
parent 550695c1ab
commit 87b89e0d57
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 642 additions and 17 deletions

View file

@ -0,0 +1,35 @@
import request from "supertest";
import app from "../../../src/app";
import * as Configuration from "../../../src/init/configuration";
const mockApp = request(app);
describe("leaderboards controller test", () => {
it("GET /leaderboards/xp/weekly", async () => {
const configSpy = jest
.spyOn(Configuration, "getCachedConfiguration")
.mockResolvedValue({
leaderboards: {
weeklyXp: {
enabled: true,
expirationTimeInDays: 15,
xpRewardBrackets: [],
},
},
} as any);
const response = await mockApp
.get("/leaderboards/xp/weekly")
.set({
Accept: "application/json",
})
.expect(200);
expect(response.body).toEqual({
message: "Weekly xp leaderboard retrieved",
data: [],
});
configSpy.mockRestore();
});
});

View file

@ -0,0 +1,27 @@
import * as WeeklyXpLeaderboard from "../../src/services/weekly-xp-leaderboard";
const weeklyXpLeaderboardConfig = {
enabled: true,
expirationTimeInDays: 15,
xpRewardBrackets: [],
};
describe("Weekly Xp Leaderboard", () => {
it("should properly consider config", () => {
const weeklyXpLeaderboard = WeeklyXpLeaderboard.get(
weeklyXpLeaderboardConfig
);
expect(weeklyXpLeaderboard).toBeInstanceOf(
WeeklyXpLeaderboard.WeeklyXpLeaderboard
);
weeklyXpLeaderboardConfig.enabled = false;
const weeklyXpLeaderboardNull = WeeklyXpLeaderboard.get(
weeklyXpLeaderboardConfig
);
expect(weeklyXpLeaderboardNull).toBeNull();
});
// TODO: Setup Redis mock and test the rest of this
});

View file

@ -21,8 +21,6 @@ const dailyLeaderboardsConfig = {
],
dailyLeaderboardCacheSize: 3,
topResultsToAnnounce: 3,
maxXpReward: 0,
minXpReward: 0,
xpRewardBrackets: [],
};

View file

@ -274,4 +274,110 @@ describe("Misc Utils", () => {
expect(misc.getOrdinalNumberString(input)).toEqual(output);
});
});
it("getStartOfWeekTimestamp", () => {
const testCases = [
{
input: 1662400184017, // Mon Sep 05 2022 17:49:44 GMT+0000
expected: 1662336000000, // Mon Sep 05 2022 00:00:00 GMT+0000
},
{
input: 1559771456000, // Wed Jun 05 2019 21:50:56 GMT+0000
expected: 1559520000000, // Mon Jun 03 2019 00:00:00 GMT+0000
},
{
input: 1465163456000, // Sun Jun 05 2016 21:50:56 GMT+0000
expected: 1464566400000, // Mon May 30 2016 00:00:00 GMT+0000
},
{
input: 1491515456000, // Thu Apr 06 2017 21:50:56 GMT+0000
expected: 1491177600000, // Mon Apr 03 2017 00:00:00 GMT+0000
},
{
input: 1462507200000, // Fri May 06 2016 04:00:00 GMT+0000
expected: 1462147200000, // Mon May 02 2016 00:00:00 GMT+0000
},
{
input: 1231218000000, // Tue Jan 06 2009 05:00:00 GMT+0000,
expected: 1231113600000, // Mon Jan 05 2009 00:00:00 GMT+0000
},
{
input: 1709420681000, // Sat Mar 02 2024 23:04:41 GMT+0000
expected: 1708905600000, // Mon Feb 26 2024 00:00:00 GMT+0000
},
];
testCases.forEach(({ input, expected }) => {
expect(misc.getStartOfWeekTimestamp(input)).toEqual(expected);
});
});
it("getCurrentWeekTimestamp", () => {
Date.now = jest.fn(() => 825289481000); // Sun Feb 25 1996 23:04:41 GMT+0000
const currentWeek = misc.getCurrentWeekTimestamp();
expect(currentWeek).toBe(824688000000); // Mon Feb 19 1996 00:00:00 GMT+0000
});
it("mapRange", () => {
const testCases = [
{
input: {
value: 123,
inMin: 0,
inMax: 200,
outMin: 0,
outMax: 1000,
clamp: false,
},
expected: 615,
},
{
input: {
value: 123,
inMin: 0,
inMax: 200,
outMin: 1000,
outMax: 0,
clamp: false,
},
expected: 385,
},
{
input: {
value: 10001,
inMin: 0,
inMax: 10000,
outMin: 0,
outMax: 1000,
clamp: false,
},
expected: 1000.1,
},
{
input: {
value: 10001,
inMin: 0,
inMax: 10000,
outMin: 0,
outMax: 1000,
clamp: true,
},
expected: 1000,
},
];
testCases.forEach(({ input, expected }) => {
expect(
misc.mapRange(
input.value,
input.inMin,
input.inMax,
input.outMin,
input.outMax,
input.clamp
)
).toEqual(expected);
});
});
});

View file

@ -1,11 +1,25 @@
let state = {};
let schema = {};
const buildLabel = (elementType, text) => {
const buildLabel = (elementType, text, hintText) => {
const labelElement = document.createElement("label");
labelElement.innerHTML = text;
labelElement.style.fontWeight = elementType === "group" ? "bold" : "lighter";
if (hintText) {
const hintElement = document.createElement("span");
hintElement.classList.add("tooltip");
hintElement.innerHTML = " ⓘ";
const hintTextElement = document.createElement("span");
hintTextElement.classList.add("tooltip-text");
hintTextElement.innerHTML = hintText;
hintElement.appendChild(hintTextElement);
labelElement.appendChild(hintElement);
}
return labelElement;
};
@ -132,10 +146,10 @@ const render = (state, schema) => {
const parent = document.createElement("div");
parent.classList.add("form-element");
const { type, label, fields, items } = schema;
const { type, label, hint, fields, items } = schema;
if (label) {
parent.appendChild(buildLabel(type, label));
parent.appendChild(buildLabel(type, label, hint));
}
parent.id = path;

View file

@ -182,3 +182,17 @@ input[type="checkbox"] {
transform: rotate(360deg);
}
}
.tooltip:hover .tooltip-text {
display: block;
}
.tooltip-text {
display: none;
color: var(--text-color);
background-color: var(--sub-alt-color);
position: absolute;
z-index: 1000;
padding: 10px;
border-radius: var(--roundness);
}

View file

@ -0,0 +1,19 @@
local redis_call = redis.call
local leaderboard_scores_key, leaderboard_results_key = KEYS[1], KEYS[2]
local leaderboard_expiration_time = ARGV[1]
local user_id = ARGV[2]
local xp_gained = tonumber(ARGV[3])
local user_data = ARGV[4]
redis_call('ZINCRBY', leaderboard_scores_key, xp_gained, user_id)
redis_call('HSET', leaderboard_results_key, user_id, user_data)
local number_of_results = redis_call('ZCARD', leaderboard_scores_key)
if (number_of_results == 1) then
redis_call('EXPIREAT', leaderboard_scores_key, leaderboard_expiration_time)
redis_call('EXPIREAT', leaderboard_results_key, leaderboard_expiration_time)
end
return redis_call('ZREVRANK', leaderboard_scores_key, user_id)

View file

@ -3,16 +3,22 @@ local leaderboard_scores_key, leaderboard_results_key = KEYS[1], KEYS[2]
local min_rank = tonumber(ARGV[1])
local max_rank = tonumber(ARGV[2])
local include_scores = ARGV[3]
local results = {}
local scores = {}
local scores_in_range = redis_call('ZRANGE', leaderboard_scores_key, min_rank, max_rank, 'REV')
for _, user_id in ipairs(scores_in_range) do
local result_data = redis_call('HGET', leaderboard_results_key, user_id)
if (include_scores == "true") then
scores[#scores + 1] = redis_call('ZSCORE', leaderboard_scores_key, user_id)
end
if (result_data ~= nil) then
results[#results + 1] = result_data
end
end
return results
return {results, scores}

View file

@ -1,9 +1,14 @@
import _ from "lodash";
import { getCurrentDayTimestamp, MILLISECONDS_IN_DAY } from "../../utils/misc";
import {
getCurrentDayTimestamp,
MILLISECONDS_IN_DAY,
getCurrentWeekTimestamp,
} from "../../utils/misc";
import { MonkeyResponse } from "../../utils/monkey-response";
import * as LeaderboardsDAL from "../../dal/leaderboards";
import MonkeyError from "../../utils/error";
import * as DailyLeaderboards from "../../utils/daily-leaderboards";
import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
export async function getLeaderboard(
req: MonkeyTypes.Request
@ -129,3 +134,58 @@ export async function getDailyLeaderboardRank(
return new MonkeyResponse("Daily leaderboard rank retrieved", rank);
}
function getWeeklyXpLeaderboardWithError(
req: MonkeyTypes.Request
): WeeklyXpLeaderboard.WeeklyXpLeaderboard {
const { weeksBefore } = req.query;
const normalizedWeeksBefore = parseInt(weeksBefore as string, 10);
const currentWeekTimestamp = getCurrentWeekTimestamp();
const weekBeforeTimestamp =
currentWeekTimestamp - normalizedWeeksBefore * MILLISECONDS_IN_DAY * 7;
const customTimestamp = _.isNil(weeksBefore) ? -1 : weekBeforeTimestamp;
const weeklyXpLeaderboard = WeeklyXpLeaderboard.get(
req.ctx.configuration.leaderboards.weeklyXp,
customTimestamp
);
if (!weeklyXpLeaderboard) {
throw new MonkeyError(404, "XP leaderboard for this week not found.");
}
return weeklyXpLeaderboard;
}
export async function getWeeklyXpLeaderboardResults(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { skip = 0, limit = 50 } = req.query;
const minRank = parseInt(skip as string, 10);
const maxRank = minRank + parseInt(limit as string, 10) - 1;
const weeklyXpLeaderboard = getWeeklyXpLeaderboardWithError(req);
const results = await weeklyXpLeaderboard.getResults(
minRank,
maxRank,
req.ctx.configuration.leaderboards.weeklyXp
);
return new MonkeyResponse("Weekly xp leaderboard retrieved", results);
}
export async function getWeeklyXpLeaderboardRank(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
const weeklyXpLeaderboard = getWeeklyXpLeaderboardWithError(req);
const rankEntry = await weeklyXpLeaderboard.getRank(
uid,
req.ctx.configuration.leaderboards.weeklyXp
);
return new MonkeyResponse("Weekly xp leaderboard rank retrieved", rankEntry);
}

View file

@ -36,6 +36,7 @@ import { getDailyLeaderboard } from "../../utils/daily-leaderboards";
import AutoRoleList from "../../constants/auto-roles";
import * as UserDAL from "../../dal/user";
import { buildMonkeyMail } from "../../utils/monkey-mail";
import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard";
try {
if (anticheatImplemented() === false) throw new Error("undefined");
@ -116,6 +117,7 @@ interface AddResultData {
tagPbs: any[];
insertedId: ObjectId;
dailyLeaderboardRank?: number;
weeklyXpLeaderboardRank?: number;
xp: number;
dailyXpBonus: boolean;
xpBreakdown: Record<string, number>;
@ -344,14 +346,15 @@ export async function addResult(
delete result.challenge;
}
let tt = 0;
let totalDurationTypedSeconds = 0;
let afk = result.afkDuration;
if (afk == undefined) {
afk = 0;
}
tt = result.testDuration + result.incompleteTestSeconds - afk;
updateTypingStats(uid, result.restartCount, tt);
PublicDAL.updateStats(result.restartCount, tt);
totalDurationTypedSeconds =
result.testDuration + result.incompleteTestSeconds - afk;
updateTypingStats(uid, result.restartCount, totalDurationTypedSeconds);
PublicDAL.updateStats(result.restartCount, totalDurationTypedSeconds);
const dailyLeaderboardsConfig = req.ctx.configuration.dailyLeaderboards;
const dailyLeaderboard = getDailyLeaderboard(
@ -370,10 +373,9 @@ export async function addResult(
!user.banned &&
(process.env.MODE === "dev" || (user.timeTyping ?? 0) > 7200);
if (dailyLeaderboard && validResultCriteria) {
//get the selected badge id
const badgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
if (dailyLeaderboard && validResultCriteria) {
incrementDailyLeaderboard(result.mode, result.mode2, result.language);
dailyLeaderboardRank = await dailyLeaderboard.addResult(
{
@ -386,7 +388,7 @@ export async function addResult(
uid,
discordAvatar: user.discordAvatar,
discordId: user.discordId,
badgeId,
badgeId: selectedBadgeId,
},
dailyLeaderboardsConfig
);
@ -402,6 +404,37 @@ export async function addResult(
streak
);
const weeklyXpLeaderboardConfig = req.ctx.configuration.leaderboards.weeklyXp;
let weeklyXpLeaderboardRank = -1;
const eligibleForWeeklyXpLeaderboard =
!user.banned &&
(process.env.MODE === "dev" || (user.timeTyping ?? 0) > 7200);
const weeklyXpLeaderboard = WeeklyXpLeaderboard.get(
weeklyXpLeaderboardConfig
);
if (
eligibleForWeeklyXpLeaderboard &&
xpGained.xp > 0 &&
weeklyXpLeaderboard
) {
weeklyXpLeaderboardRank = await weeklyXpLeaderboard.addResult(
weeklyXpLeaderboardConfig,
{
entry: {
uid,
name: user.name,
discordAvatar: user.discordAvatar,
discordId: user.discordId,
badgeId: selectedBadgeId,
lastActivityTimestamp: Date.now(),
},
xpGained: xpGained.xp,
timeTypedSeconds: totalDurationTypedSeconds,
}
);
}
if (result.bailedOut === false) delete result.bailedOut;
if (result.blindMode === false) delete result.blindMode;
if (result.lazyMode === false) delete result.lazyMode;
@ -443,6 +476,11 @@ export async function addResult(
if (dailyLeaderboardRank !== -1) {
data.dailyLeaderboardRank = dailyLeaderboardRank;
}
if (weeklyXpLeaderboardRank !== -1) {
data.weeklyXpLeaderboardRank = weeklyXpLeaderboardRank;
}
incrementResult(result);
return new MonkeyResponse("Result saved", data);

View file

@ -78,4 +78,40 @@ router.get(
asyncHandler(LeaderboardController.getDailyLeaderboardRank)
);
const BASE_XP_LEADERBOARD_VALIDATION_SCHEMA = {
skip: joi.number().min(0),
limit: joi.number().min(0).max(50),
};
const WEEKLY_XP_LEADERBOARD_VALIDATION_SCHEMA = {
...BASE_XP_LEADERBOARD_VALIDATION_SCHEMA,
weeksBefore: joi.number().min(1).max(1),
};
const requireWeeklyXpLeaderboardEnabled = validateConfiguration({
criteria: (configuration) => {
return configuration.leaderboards.weeklyXp.enabled;
},
invalidMessage: "Weekly XP leaderboards are not available at this time.",
});
router.get(
"/xp/weekly",
requireWeeklyXpLeaderboardEnabled,
authenticateRequest({ isPublic: true }),
withApeRateLimiter(RateLimit.leaderboardsGet),
validateRequest({
query: WEEKLY_XP_LEADERBOARD_VALIDATION_SCHEMA,
}),
asyncHandler(LeaderboardController.getWeeklyXpLeaderboardResults)
);
router.get(
"/xp/weekly/rank",
requireWeeklyXpLeaderboardEnabled,
authenticateRequest(),
withApeRateLimiter(RateLimit.leaderboardsGet),
asyncHandler(LeaderboardController.getWeeklyXpLeaderboardRank)
);
export default router;

View file

@ -75,11 +75,19 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
topResultsToAnnounce: 1, // This should never be 0. Setting to zero will announce all results.
xpRewardBrackets: [],
},
leaderboards: {
weeklyXp: {
enabled: false,
expirationTimeInDays: 0, // This should atleast be 15
xpRewardBrackets: [],
},
},
};
interface BaseSchema {
type: string;
label?: string;
hint?: string;
}
interface NumberSchema extends BaseSchema {
@ -394,6 +402,7 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = {
type: "number",
label: "Top Results To Announce",
min: 1,
hint: "This should atleast be 1. Setting to zero is very bad.",
},
xpRewardBrackets: {
type: "array",
@ -427,5 +436,57 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = {
},
},
},
leaderboards: {
type: "object",
label: "Leaderboards",
fields: {
weeklyXp: {
type: "object",
label: "Weekly XP",
fields: {
enabled: {
type: "boolean",
label: "Enabled",
},
expirationTimeInDays: {
type: "number",
label: "Expiration time in days",
min: 0,
hint: "This should atleast be 15, to allow for past week queries.",
},
xpRewardBrackets: {
type: "array",
label: "XP Reward Brackets",
items: {
type: "object",
label: "Bracket",
fields: {
minRank: {
type: "number",
label: "Min Rank",
min: 1,
},
maxRank: {
type: "number",
label: "Max Rank",
min: 1,
},
minReward: {
type: "number",
label: "Min Reward",
min: 0,
},
maxReward: {
type: "number",
label: "Max Reward",
min: 0,
},
},
},
},
},
},
},
},
},
};

View file

@ -0,0 +1,189 @@
import * as RedisClient from "../init/redis";
import { getCurrentWeekTimestamp } from "../utils/misc";
interface InternalWeeklyXpLeaderboardEntry {
uid: string;
name: string;
discordAvatar?: string;
discordId?: string;
badgeId?: number;
lastActivityTimestamp: number;
}
interface WeeklyXpLeaderboardEntry extends InternalWeeklyXpLeaderboardEntry {
totalXp: number;
rank: number;
count?: number;
timeTypedSeconds: number;
}
interface AddResultOpts {
entry: InternalWeeklyXpLeaderboardEntry;
xpGained: number;
timeTypedSeconds: number;
}
const weeklyXpLeaderboardLeaderboardNamespace =
"monkeytype:weekly-xp-leaderboard";
const scoresNamespace = `${weeklyXpLeaderboardLeaderboardNamespace}:scores`;
const resultsNamespace = `${weeklyXpLeaderboardLeaderboardNamespace}:results`;
export class WeeklyXpLeaderboard {
private weeklyXpLeaderboardResultsKeyName: string;
private weeklyXpLeaderboardScoresKeyName: string;
private customTime: number;
constructor(customTime = -1) {
this.weeklyXpLeaderboardResultsKeyName = resultsNamespace;
this.weeklyXpLeaderboardScoresKeyName = scoresNamespace;
this.customTime = customTime;
}
private getThisWeeksXpLeaderboardKeys(): {
currentWeekTimestamp: number;
weeklyXpLeaderboardScoresKey: string;
weeklyXpLeaderboardResultsKey: string;
} {
const currentWeekTimestamp =
this.customTime === -1 ? getCurrentWeekTimestamp() : this.customTime;
const weeklyXpLeaderboardScoresKey = `${this.weeklyXpLeaderboardScoresKeyName}:${currentWeekTimestamp}`;
const weeklyXpLeaderboardResultsKey = `${this.weeklyXpLeaderboardResultsKeyName}:${currentWeekTimestamp}`;
return {
currentWeekTimestamp,
weeklyXpLeaderboardScoresKey,
weeklyXpLeaderboardResultsKey,
};
}
public async addResult(
weeklyXpLeaderboardConfig: MonkeyTypes.Configuration["leaderboards"]["weeklyXp"],
opts: AddResultOpts
): Promise<number> {
const { entry, xpGained, timeTypedSeconds } = opts;
const connection = RedisClient.getConnection();
if (!connection || !weeklyXpLeaderboardConfig.enabled) {
return -1;
}
const {
currentWeekTimestamp,
weeklyXpLeaderboardScoresKey,
weeklyXpLeaderboardResultsKey,
} = this.getThisWeeksXpLeaderboardKeys();
const { expirationTimeInDays } = weeklyXpLeaderboardConfig;
const weeklyXpLeaderboardExpirationDurationInMilliseconds =
expirationTimeInDays * 24 * 60 * 60 * 1000;
const weeklyXpLeaderboardExpirationTimeInSeconds = Math.floor(
(currentWeekTimestamp +
weeklyXpLeaderboardExpirationDurationInMilliseconds) /
1000
);
const currentEntry = await connection.hget(
weeklyXpLeaderboardResultsKey,
entry.uid
);
const totalTimeTypedSeconds =
timeTypedSeconds +
((currentEntry && JSON.parse(currentEntry)?.timeTypedSeconds) || 0);
// @ts-ignore
const rank: number = await connection.addResultIncrement(
2,
weeklyXpLeaderboardScoresKey,
weeklyXpLeaderboardResultsKey,
weeklyXpLeaderboardExpirationTimeInSeconds,
entry.uid,
xpGained,
JSON.stringify({ ...entry, timeTypedSeconds: totalTimeTypedSeconds })
);
return rank + 1;
}
public async getResults(
minRank: number,
maxRank: number,
weeklyXpLeaderboardConfig: MonkeyTypes.Configuration["leaderboards"]["weeklyXp"]
): Promise<WeeklyXpLeaderboardEntry[]> {
const connection = RedisClient.getConnection();
if (!connection || !weeklyXpLeaderboardConfig.enabled) {
return [];
}
const { weeklyXpLeaderboardScoresKey, weeklyXpLeaderboardResultsKey } =
this.getThisWeeksXpLeaderboardKeys();
// @ts-ignore
const [results, scores]: string[][] = await connection.getResults(
2, // How many of the arguments are redis keys (https://redis.io/docs/manual/programmability/lua-api/)
weeklyXpLeaderboardScoresKey,
weeklyXpLeaderboardResultsKey,
minRank,
maxRank,
"true"
);
const resultsWithRanks: WeeklyXpLeaderboardEntry[] = results.map(
(resultJSON: string, index: number) => ({
...JSON.parse(resultJSON),
rank: minRank + index + 1,
totalXp: parseInt(scores[index], 10),
})
);
return resultsWithRanks;
}
public async getRank(
uid: string,
weeklyXpLeaderboardConfig: MonkeyTypes.Configuration["leaderboards"]["weeklyXp"]
): Promise<WeeklyXpLeaderboardEntry | null> {
const connection = RedisClient.getConnection();
if (!connection || !weeklyXpLeaderboardConfig.enabled) {
return null;
}
const { weeklyXpLeaderboardScoresKey, weeklyXpLeaderboardResultsKey } =
this.getThisWeeksXpLeaderboardKeys();
connection.set;
const [[, rank], [, totalXp], [, count], [, result]] = await connection
.multi()
.zrevrank(weeklyXpLeaderboardScoresKey, uid)
.zscore(weeklyXpLeaderboardScoresKey, uid)
.zcard(weeklyXpLeaderboardScoresKey)
.hget(weeklyXpLeaderboardResultsKey, uid)
.exec();
if (rank === null) {
return null;
}
return {
rank: rank + 1,
count: count ?? 0,
totalXp: parseInt(totalXp, 10),
...JSON.parse(result ?? "null"),
};
}
}
export function get(
weeklyXpLeaderboardConfig: MonkeyTypes.Configuration["leaderboards"]["weeklyXp"],
customTimestamp?: number
): WeeklyXpLeaderboard | null {
const { enabled } = weeklyXpLeaderboardConfig;
if (!enabled) {
return null;
}
return new WeeklyXpLeaderboard(customTimestamp);
}

View file

@ -80,6 +80,13 @@ declare namespace MonkeyTypes {
topResultsToAnnounce: number;
xpRewardBrackets: RewardBracket[];
};
leaderboards: {
weeklyXp: {
enabled: boolean;
expirationTimeInDays: number;
xpRewardBrackets: RewardBracket[];
};
};
}
interface RewardBracket {

View file

@ -108,12 +108,13 @@ export class DailyLeaderboard {
this.getTodaysLeaderboardKeys();
// @ts-ignore
const results: string[] = await connection.getResults(
const [results]: string[][] = await connection.getResults(
2,
leaderboardScoresKey,
leaderboardResultsKey,
minRank,
maxRank
maxRank,
"false"
);
const resultsWithRanks: DailyLeaderboardEntry[] = results.map(

View file

@ -204,3 +204,17 @@ export function mapRange(
return result;
}
export function getStartOfWeekTimestamp(timestamp: number): number {
const date = new Date(timestamp);
const monday = date.getDate() - (date.getDay() || 7) + 1;
date.setDate(monday);
return getStartOfDayTimestamp(date.getTime());
}
export function getCurrentWeekTimestamp(): number {
const currentTime = Date.now();
return getStartOfWeekTimestamp(currentTime);
}