Reward daily leaderboards and refactor mail API (#3508) Bruception

* Reward daily leaderboards and refactor mail API

* Reorder

* Change mail template
This commit is contained in:
Bruce Berrios 2022-09-04 17:58:46 -04:00 committed by GitHub
parent 6f22eaa1fc
commit 20a5cac5e6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 270 additions and 63 deletions

View file

@ -501,19 +501,102 @@ describe("UserDal", () => {
"TestID",
[
{
getTemplate: (user) => ({
subject: `Hello ${user.name}!`,
}),
subject: `Hello!`,
} as any,
],
0
{
enabled: true,
maxMail: 100,
}
);
const inbox = await UserDAL.getInbox("TestID");
expect(inbox).toStrictEqual([
{
subject: "Hello test name!",
subject: "Hello!",
},
]);
});
it("addToInbox discards mail if inbox is full", async () => {
await UserDAL.addUser("test name", "test email", "TestID");
const config = {
enabled: true,
maxMail: 1,
};
await UserDAL.addToInbox(
"TestID",
[
{
subject: "Hello 1!",
} as any,
],
config
);
await UserDAL.addToInbox(
"TestID",
[
{
subject: "Hello 2!",
} as any,
],
config
);
const inbox = await UserDAL.getInbox("TestID");
expect(inbox).toStrictEqual([
{
subject: "Hello 2!",
},
]);
});
it("addToInboxBulk should add mail to multiple users", async () => {
await UserDAL.addUser("test name", "test email", "TestID");
await UserDAL.addUser("test name 2", "test email 2", "TestID2");
await UserDAL.addToInboxBulk(
[
{
uid: "TestID",
mail: [
{
subject: `Hello!`,
} as any,
],
},
{
uid: "TestID2",
mail: [
{
subject: `Hello 2!`,
} as any,
],
},
],
{
enabled: true,
maxMail: 100,
}
);
const inbox = await UserDAL.getInbox("TestID");
const inbox2 = await UserDAL.getInbox("TestID2");
expect(inbox).toStrictEqual([
{
subject: "Hello!",
},
]);
expect(inbox2).toStrictEqual([
{
subject: "Hello 2!",
},
]);
});

View file

@ -21,7 +21,8 @@ const dailyLeaderboardsConfig = {
],
dailyLeaderboardCacheSize: 3,
topResultsToAnnounce: 3,
xpReward: 0,
maxXpReward: 0,
minXpReward: 0,
};
describe("Daily Leaderboards", () => {

View file

@ -205,4 +205,73 @@ describe("Misc Utils", () => {
expect(misc.sanitizeString(input)).toEqual(expected);
});
});
it("getOrdinalNumberString", () => {
const testCases = [
{
input: 0,
output: "0th",
},
{
input: 1,
output: "1st",
},
{
input: 2,
output: "2nd",
},
{
input: 3,
output: "3rd",
},
{
input: 4,
output: "4th",
},
{
input: 10,
output: "10th",
},
{
input: 11,
output: "11th",
},
{
input: 12,
output: "12th",
},
{
input: 13,
output: "13th",
},
{
input: 100,
output: "100th",
},
{
input: 101,
output: "101st",
},
{
input: 102,
output: "102nd",
},
{
input: 103,
output: "103rd",
},
{
input: 104,
output: "104th",
},
{
input: 93589423,
output: "93589423rd",
},
];
testCases.forEach(({ input, output }) => {
expect(misc.getOrdinalNumberString(input)).toEqual(output);
});
});
});

View file

@ -6,7 +6,6 @@ describe("Monkey Mail", () => {
subject: "",
body: "",
timestamp: Date.now(),
getTemplate: (): any => ({}),
};
const mail = buildMonkeyMail(mailConfig) as any;
@ -17,6 +16,5 @@ describe("Monkey Mail", () => {
expect(mail.timestamp).toBeDefined();
expect(mail.read).toBe(false);
expect(mail.rewards).toEqual([]);
expect(mail.getTemplate).toBeDefined();
});
});

View file

@ -256,16 +256,10 @@ export async function addResult(
);
if (didUserGetBanned) {
const mail = buildMonkeyMail({
getTemplate: () => ({
subject: "Banned",
body: "Your account has been automatically banned for triggering the anticheat system. If you believe this is a mistake, please contact support.",
}),
subject: "Banned",
body: "Your account has been automatically banned for triggering the anticheat system. If you believe this is a mistake, please contact support.",
});
UserDAL.addToInbox(
uid,
[mail],
req.ctx.configuration.users.inbox.maxMail
);
UserDAL.addToInbox(uid, [mail], req.ctx.configuration.users.inbox);
}
}
const status = MonkeyStatusCodes.BOT_DETECTED;

View file

@ -67,7 +67,8 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
// GOTCHA! MUST ATLEAST BE 1, LRUCache module will make process crash and die
dailyLeaderboardCacheSize: 1,
topResultsToAnnounce: 1, // This should never be 0. Setting to zero will announce all results.
xpReward: 0,
minXpReward: 0,
maxXpReward: 0,
},
};
@ -367,9 +368,14 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = {
label: "Top Results To Announce",
min: 1,
},
xpReward: {
minXpReward: {
type: "number",
label: "XP Reward",
label: "Minimum XP Reward",
min: 0,
},
maxXpReward: {
type: "number",
label: "Maximum XP Reward",
min: 0,
},
},

View file

@ -7,7 +7,6 @@ import MonkeyError from "../utils/error";
import { Collection, ObjectId, WithId, Long, UpdateFilter } from "mongodb";
import Logger from "../utils/logger";
import { flattenObjectDeep } from "../utils/misc";
import { MonkeyMailWithTemplate } from "../utils/monkey-mail";
const SECONDS_PER_HOUR = 3600;
@ -745,37 +744,60 @@ export async function getInbox(
return user.inbox ?? [];
}
export async function addToInbox(
uid: string,
mail: MonkeyMailWithTemplate[],
maxInboxSize: number
interface AddToInboxBulkEntry {
uid: string;
mail: MonkeyTypes.MonkeyMail[];
}
export async function addToInboxBulk(
entries: AddToInboxBulkEntry[],
inboxConfig: MonkeyTypes.Configuration["users"]["inbox"]
): Promise<void> {
const user = await getUser(uid, "add to inbox");
const { enabled, maxMail } = inboxConfig;
const inbox = user.inbox ?? [];
for (let i = 0; i < inbox.length + mail.length - maxInboxSize; i++) {
inbox.pop();
if (!enabled) {
return;
}
const evaluatedMail: MonkeyTypes.MonkeyMail[] = mail.map((mail) => {
return _.omit(
{
...mail,
...(mail.getTemplate && mail.getTemplate(user)),
const bulk = getUsersCollection().initializeUnorderedBulkOp();
entries.forEach((entry) => {
bulk.find({ uid: entry.uid }).updateOne({
$push: {
inbox: {
$each: entry.mail,
$position: 0, // Prepends to the inbox
$slice: maxMail, // Keeps inbox size to maxInboxSize, maxMail the oldest
},
},
"getTemplate"
);
});
});
inbox.unshift(...evaluatedMail);
const newInbox = inbox.sort((a, b) => b.timestamp - a.timestamp);
await bulk.execute();
}
export async function addToInbox(
uid: string,
mail: MonkeyTypes.MonkeyMail[],
inboxConfig: MonkeyTypes.Configuration["users"]["inbox"]
): Promise<void> {
const { enabled, maxMail } = inboxConfig;
if (!enabled) {
return;
}
await getUsersCollection().updateOne(
{ uid },
{
$set: {
inbox: newInbox,
uid,
},
{
$push: {
inbox: {
$each: mail,
$position: 0, // Prepends to the inbox
$slice: maxMail, // Keeps inbox size to maxMail, discarding the oldest
},
},
}
);

View file

@ -1,9 +1,13 @@
import { CronJob } from "cron";
import { getCurrentDayTimestamp } from "../utils/misc";
import {
getCurrentDayTimestamp,
getOrdinalNumberString,
mapRange,
} from "../utils/misc";
import { getCachedConfiguration } from "../init/configuration";
import { DailyLeaderboard } from "../utils/daily-leaderboards";
import { announceDailyLeaderboardTopResults } from "../tasks/george";
import { addToInbox } from "../dal/user";
import { addToInboxBulk } from "../dal/user";
import { buildMonkeyMail } from "../utils/monkey-mail";
const CRON_SCHEDULE = "1 0 * * *"; // At 00:01.
@ -37,38 +41,52 @@ async function announceDailyLeaderboard(
yesterday
);
const topResults = await dailyLeaderboard.getResults(
const allResults = await dailyLeaderboard.getResults(
0,
dailyLeaderboardsConfig.topResultsToAnnounce - 1,
-1,
dailyLeaderboardsConfig
);
if (topResults.length === 0) {
if (allResults.length === 0) {
return;
}
if (inboxConfig.enabled) {
const { xpReward } = dailyLeaderboardsConfig;
const { maxXpReward, minXpReward, maxResults } = dailyLeaderboardsConfig;
const inboxPromises = topResults.map(async (entry) => {
const mail = buildMonkeyMail({
const mailEntries = allResults.map((entry) => {
const rank = entry.rank ?? maxResults;
const placementString = getOrdinalNumberString(rank);
const xpReward = Math.floor(
mapRange(rank, 1, maxResults, maxXpReward, minXpReward)
);
const rewardMail = buildMonkeyMail({
subject: "Daily leaderboard placement",
body: `Congratulations ${entry.name} on placing ${placementString} in the ${language} ${mode} ${mode2} daily leaderboard!`,
rewards: [
{
type: "xp",
item: xpReward,
},
],
getTemplate: (user) => ({
subject: `${xpReward} XP for top placement in the daily leaderboard!`,
body: `Congratulations ${user.name} on placing top ${entry.rank} in the ${language} ${mode} ${mode2} daily leaderboard! Claim your ${xpReward} xp!`,
}),
});
return await addToInbox(entry.uid, [mail], inboxConfig.maxMail);
return {
uid: entry.uid,
mail: [rewardMail],
};
});
await Promise.allSettled(inboxPromises);
await addToInboxBulk(mailEntries, inboxConfig);
}
const topResults = allResults.slice(
0,
dailyLeaderboardsConfig.topResultsToAnnounce
);
const leaderboardId = `${mode} ${mode2} ${language}`;
await announceDailyLeaderboardTopResults(
leaderboardId,

View file

@ -72,7 +72,8 @@ declare namespace MonkeyTypes {
validModeRules: ValidModeRule[];
dailyLeaderboardCacheSize: number;
topResultsToAnnounce: number;
xpReward: number;
maxXpReward: number;
minXpReward: number;
};
}

View file

@ -155,3 +155,22 @@ export function sanitizeString(str: string | undefined): string | undefined {
.trim()
.replace(/\s{3,}/g, " ");
}
const suffixes = ["th", "st", "nd", "rd"];
export function getOrdinalNumberString(number: number): string {
const lastTwo = number % 100;
const suffix =
suffixes[(lastTwo - 20) % 10] || suffixes[lastTwo] || suffixes[0];
return `${number}${suffix}`;
}
export function mapRange(
value: number,
inMin: number,
inMax: number,
outMin: number,
outMax: number
): number {
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
}

View file

@ -1,13 +1,10 @@
import { v4 } from "uuid";
type MonkeyMailOptions = Partial<Omit<MonkeyMailWithTemplate, "id" | "read">>;
export interface MonkeyMailWithTemplate extends MonkeyTypes.MonkeyMail {
getTemplate?: (user: MonkeyTypes.User) => MonkeyMailOptions;
}
type MonkeyMailOptions = Partial<Omit<MonkeyTypes.MonkeyMail, "id" | "read">>;
export function buildMonkeyMail(
options: MonkeyMailOptions
): MonkeyMailWithTemplate {
): MonkeyTypes.MonkeyMail {
return {
id: v4(),
subject: options.subject || "",
@ -15,6 +12,5 @@ export function buildMonkeyMail(
timestamp: options.timestamp || Date.now(),
read: false,
rewards: options.rewards || [],
getTemplate: options.getTemplate,
};
}