Merge branch 'master' into newads

This commit is contained in:
Miodec 2023-03-24 14:15:20 +01:00
commit ecd97ebc51
16 changed files with 173 additions and 24 deletions

View file

@ -0,0 +1,5 @@
import { MonkeyResponse } from "../../utils/monkey-response";
export async function test(): Promise<MonkeyResponse> {
return new MonkeyResponse("OK");
}

View file

@ -0,0 +1,18 @@
// import joi from "joi";
import { Router } from "express";
import { authenticateRequest } from "../../middlewares/auth";
import { asyncHandler, checkIfUserIsAdmin } from "../../middlewares/api-utils";
import * as AdminController from "../controllers/admin";
const router = Router();
router.get(
"/",
authenticateRequest({
noCache: true,
}),
checkIfUserIsAdmin(),
asyncHandler(AdminController.test)
);
export default router;

View file

@ -8,6 +8,7 @@ import configs from "./configs";
import results from "./results";
import presets from "./presets";
import apeKeys from "./ape-keys";
import admin from "./admin";
import configuration from "./configuration";
import { version } from "../../version";
import leaderboards from "./leaderboards";
@ -37,6 +38,7 @@ const API_ROUTE_MAP = {
"/leaderboards": leaderboards,
"/quotes": quotes,
"/ape-keys": apeKeys,
"/admin": admin,
};
function addApiRoutes(app: Application): void {

View file

@ -0,0 +1,10 @@
import * as db from "../init/db";
export async function isAdmin(uid: string): Promise<boolean> {
const doc = await db.collection("admin-uids").findOne({ uid });
if (doc) {
return true;
} else {
return false;
}
}

View file

@ -4,6 +4,7 @@ import MonkeyError from "../utils/error";
import { Response, NextFunction, RequestHandler } from "express";
import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response";
import { getUser } from "../dal/user";
import { isAdmin } from "../dal/admin-uids";
interface ValidationOptions<T> {
criteria: (data: T) => boolean;
@ -40,6 +41,31 @@ function validateConfiguration(
};
}
/**
* Check if the user is an admin before handling request.
* Note that this middleware must be used after authentication in the middleware stack.
*/
function checkIfUserIsAdmin(): RequestHandler {
return async (
req: MonkeyTypes.Request,
_res: Response,
next: NextFunction
) => {
try {
const { uid } = req.ctx.decodedToken;
const admin = await isAdmin(uid);
if (!admin) {
throw new MonkeyError(403, "You don't have permission to do this.");
}
} catch (error) {
next(error);
}
next();
};
}
/**
* Check user permissions before handling request.
* Note that this middleware must be used after authentication in the middleware stack.
@ -158,6 +184,7 @@ function useInProduction(middlewares: RequestHandler[]): RequestHandler[] {
export {
validateConfiguration,
checkUserPermissions,
checkIfUserIsAdmin,
asyncHandler,
validateRequest,
useInProduction,

View file

@ -17,6 +17,7 @@ interface RequestAuthenticationOptions {
isPublic?: boolean;
acceptApeKeys?: boolean;
requireFreshToken?: boolean;
noCache?: boolean;
}
const DEFAULT_OPTIONS: RequestAuthenticationOptions = {
@ -149,7 +150,10 @@ async function authenticateWithBearerToken(
options: RequestAuthenticationOptions
): Promise<MonkeyTypes.DecodedToken> {
try {
const decodedToken = await verifyIdToken(token, options.requireFreshToken);
const decodedToken = await verifyIdToken(
token,
options.requireFreshToken || options.noCache
);
if (options.requireFreshToken) {
const now = Date.now();

View file

@ -231,6 +231,7 @@ export async function setup(challengeName: string): Promise<boolean> {
UpdateConfig.setMode("words", true);
UpdateConfig.setDifficulty("normal", true);
} else if (challenge.type === "customText") {
CustomText.setPopupTextareaState(challenge.parameters[0] as string);
CustomText.setText((challenge.parameters[0] as string).split(" "));
CustomText.setIsTimeRandom(false);
CustomText.setIsWordRandom(challenge.parameters[1] as boolean);
@ -249,6 +250,7 @@ export async function setup(challengeName: string): Promise<boolean> {
let text = scriptdata.trim();
text = text.replace(/[\n\r\t ]/gm, " ");
text = text.replace(/ +/gm, " ");
CustomText.setPopupTextareaState(text);
CustomText.setText(text.split(" "));
CustomText.setIsWordRandom(false);
CustomText.setTime(-1);

View file

@ -6,6 +6,7 @@ import { getHTMLById } from "../controllers/badge-controller";
import { throttle } from "throttle-debounce";
import * as EditProfilePopup from "../popups/edit-profile-popup";
import * as ActivePage from "../states/active-page";
import { formatDistanceToNowStrict } from "date-fns/esm";
type ProfileViewPaths = "profile" | "account";
@ -93,6 +94,8 @@ export async function update(
const balloonText = `${diffDays} day${diffDays != 1 ? "s" : ""} ago`;
details.find(".joined").text(joinedText).attr("aria-label", balloonText);
let hoverText = "";
if (profile.streak && profile?.streak > 1) {
details
.find(".streak")
@ -100,17 +103,42 @@ export async function update(
`Current streak: ${profile.streak} ${
profile.streak === 1 ? "day" : "days"
}`
)
.attr(
"aria-label",
`Longest streak: ${profile.maxStreak} ${
profile.maxStreak === 1 ? "day" : "days"
}`
);
hoverText = `Longest streak: ${profile.maxStreak} ${
profile.maxStreak === 1 ? "day" : "days"
}`;
} else {
details.find(".streak").text("").attr("aria-label", "");
details.find(".streak").text("");
hoverText = "";
}
if (where === "account") {
const results = DB.getSnapshot()?.results;
const lastResult = results?.[0];
const dayInMilis = 1000 * 60 * 60 * 24;
const timeDif = formatDistanceToNowStrict(
Misc.getCurrentDayTimestamp() + dayInMilis
);
if (lastResult) {
//check if the last result is from today
const isToday = Misc.isToday(lastResult.timestamp);
if (isToday) {
hoverText += `\nClaimed today: yes`;
hoverText += `\nCome back in: ${timeDif}`;
} else {
hoverText += `\nClaimed today: no`;
hoverText += `\nStreak lost in: ${timeDif}`;
}
}
}
details
.find(".streak")
.attr("aria-label", hoverText)
.attr("data-balloon-break", "");
const typingStatsEl = details.find(".typingStats");
typingStatsEl
.find(".started .value")

View file

@ -798,9 +798,17 @@ function fillContent(): void {
Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10));
if (!Config.startGraphsAtZero) {
accountHistoryScaleOptions["wpm"].min = Math.floor(minWpmChartVal);
const minWpmChartValFloor = Math.floor(minWpmChartVal);
accountHistoryScaleOptions["wpm"].min = minWpmChartValFloor;
accountHistoryScaleOptions["pb"].min = minWpmChartValFloor;
accountHistoryScaleOptions["wpmAvgTen"].min = minWpmChartValFloor;
accountHistoryScaleOptions["wpmAvgHundred"].min = minWpmChartValFloor;
} else {
accountHistoryScaleOptions["wpm"].min = 0;
accountHistoryScaleOptions["pb"].min = 0;
accountHistoryScaleOptions["wpmAvgTen"].min = 0;
accountHistoryScaleOptions["wpmAvgHundred"].min = 0;
}
if (!chartData || chartData.length == 0) {

View file

@ -93,6 +93,7 @@ export function init(missed: boolean, slow: boolean): boolean {
const numbers = before.numbers === null ? Config.numbers : before.numbers;
UpdateConfig.setMode("custom", true);
CustomText.setPopupTextareaState(newCustomText.join(CustomText.delimiter));
CustomText.setText(newCustomText);
CustomText.setIsWordRandom(true);
CustomText.setIsTimeRandom(false);

View file

@ -823,9 +823,6 @@ export async function update(
$("#words").empty();
ChartController.result.resize();
if (Config.alwaysShowWordsHistory && Config.burstHeatmap) {
TestUI.applyBurstHeatmap();
}
$("#result").trigger("focus");
window.scrollTo({ top: 0 });
$("#testModesNotice").addClass("hidden");
@ -838,7 +835,7 @@ export async function update(
125
);
if (Config.alwaysShowWordsHistory && !GlarsesMode.get()) {
TestUI.toggleResultWords();
TestUI.toggleResultWords(true);
}
Keymap.hide();
AdController.updateTestPageAds(true);

View file

@ -881,13 +881,17 @@ export async function init(): Promise<void> {
}
if (Config.tapeMode !== "off" && !language.leftToRight) {
Notifications.add("This language does not support tape mode.", 0);
Notifications.add("This language does not support tape mode.", 0, {
important: true,
});
UpdateConfig.setTapeMode("off");
}
if (Config.lazyMode === true && language.noLazyMode) {
rememberLazyMode = true;
Notifications.add("This language does not support lazy mode.", 0);
Notifications.add("This language does not support lazy mode.", 0, {
important: true,
});
UpdateConfig.setLazyMode(false, true);
} else if (rememberLazyMode === true && !language.noLazyMode) {
UpdateConfig.setLazyMode(true, true);
@ -1078,7 +1082,8 @@ export async function init(): Promise<void> {
`No ${Config.language
.replace(/_\d*k$/g, "")
.replace(/_/g, " ")} quotes found`,
0
0,
{ important: true }
);
if (Auth?.currentUser) {
QuoteSubmitPopup.show(false);
@ -1095,7 +1100,7 @@ export async function init(): Promise<void> {
);
if (targetQuote === undefined) {
rq = <MonkeyTypes.Quote>quotesCollection.groups[0][0];
Notifications.add("Quote Id Does Not Exist", 0);
Notifications.add("Quote Id Does Not Exist", 0, { important: true });
} else {
rq = targetQuote;
}
@ -1105,7 +1110,7 @@ export async function init(): Promise<void> {
);
if (randomQuote === null) {
Notifications.add("No favorite quotes found", 0);
Notifications.add("No favorite quotes found", 0, { important: true });
UpdateConfig.setQuoteLength(-1);
restart();
return;
@ -1115,7 +1120,9 @@ export async function init(): Promise<void> {
} else {
const randomQuote = QuotesController.getRandomQuote();
if (randomQuote === null) {
Notifications.add("No quotes found for selected quote length", 0);
Notifications.add("No quotes found for selected quote length", 0, {
important: true,
});
TestUI.setTestRestarting(false);
return;
}
@ -1312,6 +1319,7 @@ export async function retrySavingResult(): Promise<void> {
0,
{
duration: 5,
important: true,
}
);
@ -1668,17 +1676,22 @@ export async function finish(difficultyFailed = false): Promise<void> {
CustomText.setCustomTextLongProgress(customTextName, newProgress);
Notifications.add("Long custom text progress saved", 1, {
duration: 5,
important: true,
});
let newText = CustomText.getCustomText(customTextName, true);
newText = newText.slice(newProgress);
CustomText.setPopupTextareaState(newText.join(CustomText.delimiter));
CustomText.setText(newText);
} else {
// They finished the test
CustomText.setCustomTextLongProgress(customTextName, 0);
CustomText.setText(CustomText.getCustomText(customTextName, true));
const text = CustomText.getCustomText(customTextName, true);
CustomText.setPopupTextareaState(text.join(CustomText.delimiter));
CustomText.setText(text);
Notifications.add("Long custom text completed", 1, {
duration: 5,
important: true,
});
}
}
@ -1762,6 +1775,7 @@ async function saveResult(
Notifications.add("Result not saved: disabled by user", -1, {
duration: 3,
customTitle: "Notice",
important: true,
});
AccountButton.loading(false);
return;
@ -1771,6 +1785,7 @@ async function saveResult(
Notifications.add("Result not saved: offline", -1, {
duration: 2,
customTitle: "Notice",
important: true,
});
AccountButton.loading(false);
retrySaving.canRetry = true;
@ -1892,7 +1907,7 @@ async function saveResult(
$("#retrySavingResultButton").addClass("hidden");
if (isRetrying) {
Notifications.add("Result saved", 1);
Notifications.add("Result saved", 1, { important: true });
}
}

View file

@ -985,7 +985,7 @@ async function loadWordsHistory(): Promise<boolean> {
return true;
}
export function toggleResultWords(): void {
export function toggleResultWords(noAnimation = false): void {
if (resultVisible) {
if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) {
//show
@ -1001,7 +1001,7 @@ export function toggleResultWords(): void {
$("#resultWordsHistory")
.removeClass("hidden")
.css("display", "none")
.slideDown(250, () => {
.slideDown(noAnimation ? 0 : 250, () => {
if (Config.burstHeatmap) {
applyBurstHeatmap();
}
@ -1014,7 +1014,7 @@ export function toggleResultWords(): void {
$("#resultWordsHistory")
.removeClass("hidden")
.css("display", "none")
.slideDown(250);
.slideDown(noAnimation ? 0 : 250);
}
} else {
//hide

View file

@ -1522,3 +1522,26 @@ export async function checkIfLanguageSupportsZipf(
if (lang.orderedByFrequency === false) return "no";
return "unknown";
}
export function getStartOfDayTimestamp(timestamp: number): number {
return timestamp - (timestamp % 86400000);
}
export function getCurrentDayTimestamp(): number {
const currentTime = Date.now();
return getStartOfDayTimestamp(currentTime);
}
export function isYesterday(timestamp: number): boolean {
const yesterday = getStartOfDayTimestamp(Date.now() - 86400000);
const date = getStartOfDayTimestamp(timestamp);
return yesterday === date;
}
export function isToday(timestamp: number): boolean {
const today = getStartOfDayTimestamp(Date.now());
const date = getStartOfDayTimestamp(timestamp);
return today === date;
}

View file

@ -124,6 +124,9 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
if (de[2]) {
const customTextSettings = de[2];
CustomText.setPopupTextareaState(
customTextSettings["text"].join(customTextSettings["delimiter"])
);
CustomText.setText(customTextSettings["text"]);
CustomText.setIsTimeRandom(customTextSettings["isTimeRandom"]);
CustomText.setIsWordRandom(customTextSettings["isWordRandom"]);

View file

@ -38812,6 +38812,12 @@
"source": "The World Was Wide Enough, Hamilton",
"length": 81,
"id": 6814
},
{
"text": "You are a very special person. There is only one like you in the whole world. There's never been anyone exactly like you before, and there will never be again. Only you. And people can like you exactly as you are.",
"source": "Fred Rogers",
"length": 213,
"id": 6815
}
]
}