mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-09 12:56:07 +08:00
Added ability to opt out of leaderboards (#4016)
* added button to opt out of leaderboards * also not allowing daily leaderboards * also not allowing weekly lb * added lb opt out notice on the leaderboards * updated message * resetting lboptout state on account reset
This commit is contained in:
parent
52b731d607
commit
56d8c7c9ae
15 changed files with 205 additions and 10 deletions
|
|
@ -283,7 +283,8 @@ export async function addResult(
|
|||
result.mode === "time" &&
|
||||
result.wpm > 130 &&
|
||||
result.testDuration < 122 &&
|
||||
(user.verified === false || user.verified === undefined)
|
||||
(user.verified === false || user.verified === undefined) &&
|
||||
user.lbOptOut !== true
|
||||
) {
|
||||
if (!result.keySpacingStats || !result.keyDurationStats) {
|
||||
const status = MonkeyStatusCodes.MISSING_KEY_DATA;
|
||||
|
|
@ -389,7 +390,8 @@ export async function addResult(
|
|||
const validResultCriteria =
|
||||
(funbox === "none" || funbox === "plus_one" || funbox === "plus_two") &&
|
||||
!bailedOut &&
|
||||
!user.banned &&
|
||||
user.banned !== true &&
|
||||
user.lbOptOut !== true &&
|
||||
(process.env.MODE === "dev" || (user.timeTyping ?? 0) > 7200);
|
||||
|
||||
const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id;
|
||||
|
|
@ -438,7 +440,8 @@ export async function addResult(
|
|||
const weeklyXpLeaderboardConfig = req.ctx.configuration.leaderboards.weeklyXp;
|
||||
let weeklyXpLeaderboardRank = -1;
|
||||
const eligibleForWeeklyXpLeaderboard =
|
||||
!user.banned &&
|
||||
user.banned !== true &&
|
||||
user.lbOptOut !== true &&
|
||||
(process.env.MODE === "dev" || (user.timeTyping ?? 0) > 7200);
|
||||
|
||||
const weeklyXpLeaderboard = WeeklyXpLeaderboard.get(
|
||||
|
|
|
|||
|
|
@ -201,6 +201,21 @@ export async function clearPb(
|
|||
return new MonkeyResponse("User's PB cleared");
|
||||
}
|
||||
|
||||
export async function optOutOfLeaderboards(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
await UserDAL.optOutOfLeaderboards(uid);
|
||||
await purgeUserFromDailyLeaderboards(
|
||||
uid,
|
||||
req.ctx.configuration.dailyLeaderboards
|
||||
);
|
||||
Logger.logToDb("user_opted_out_of_leaderboards", "", uid);
|
||||
|
||||
return new MonkeyResponse("User opted out of leaderboards");
|
||||
}
|
||||
|
||||
export async function checkName(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
|
|
@ -606,6 +621,7 @@ export async function getProfile(
|
|||
discordAvatar,
|
||||
xp,
|
||||
streak,
|
||||
lbOptOut,
|
||||
} = user;
|
||||
|
||||
const validTimePbs = _.pick(personalBests?.time, "15", "30", "60", "120");
|
||||
|
|
@ -633,6 +649,7 @@ export async function getProfile(
|
|||
xp,
|
||||
streak: streak?.length ?? 0,
|
||||
maxStreak: streak?.maxLength ?? 0,
|
||||
lbOptOut,
|
||||
};
|
||||
|
||||
if (banned) {
|
||||
|
|
|
|||
|
|
@ -202,6 +202,15 @@ router.delete(
|
|||
asyncHandler(UserController.clearPb)
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/optOutOfLeaderboards",
|
||||
authenticateRequest({
|
||||
requireFreshToken: true,
|
||||
}),
|
||||
RateLimit.userOptOutOfLeaderboards,
|
||||
asyncHandler(UserController.optOutOfLeaderboards)
|
||||
);
|
||||
|
||||
const requireFilterPresetsEnabled = validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.results.filterPresets.enabled;
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export async function resetUser(uid: string): Promise<void> {
|
|||
$unset: {
|
||||
discordAvatar: "",
|
||||
discordId: "",
|
||||
lbOptOut: "",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
@ -136,6 +137,20 @@ export async function clearPb(uid: string): Promise<void> {
|
|||
);
|
||||
}
|
||||
|
||||
export async function optOutOfLeaderboards(uid: string): Promise<void> {
|
||||
await getUsersCollection().updateOne(
|
||||
{ uid },
|
||||
{
|
||||
$set: {
|
||||
lbOptOut: true,
|
||||
lbPersonalBests: {
|
||||
time: {},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateQuoteRatings(
|
||||
uid: string,
|
||||
quoteRatings: MonkeyTypes.UserQuoteRatings
|
||||
|
|
|
|||
|
|
@ -348,6 +348,13 @@ export const userClearPB = rateLimit({
|
|||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userOptOutOfLeaderboards = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 10 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userCustomFilterAdd = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
|
|
|
|||
1
backend/src/types/types.d.ts
vendored
1
backend/src/types/types.d.ts
vendored
|
|
@ -189,6 +189,7 @@ declare namespace MonkeyTypes {
|
|||
inbox?: MonkeyMail[];
|
||||
streak?: UserStreak;
|
||||
lastReultHashes?: string[];
|
||||
lbOptOut?: boolean;
|
||||
}
|
||||
|
||||
interface UserStreak {
|
||||
|
|
|
|||
|
|
@ -209,6 +209,9 @@
|
|||
align-self: normal;
|
||||
color: var(--text-color);
|
||||
grid-area: text;
|
||||
.red {
|
||||
color: var(--error-color);
|
||||
}
|
||||
}
|
||||
|
||||
.inputs {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ export default class Users {
|
|||
return await this.httpClient.patch(`${BASE_PATH}/reset`);
|
||||
}
|
||||
|
||||
async optOutOfLeaderboards(): Ape.EndpointData {
|
||||
return await this.httpClient.post(`${BASE_PATH}/optOutOfLeaderboards`);
|
||||
}
|
||||
|
||||
async updateName(name: string): Ape.EndpointData {
|
||||
return await this.httpClient.patch(`${BASE_PATH}/name`, {
|
||||
payload: { name },
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export function setSnapshot(
|
|||
): void {
|
||||
const originalBanned = dbSnapshot?.banned;
|
||||
const originalVerified = dbSnapshot?.verified;
|
||||
const lbOptOut = dbSnapshot?.lbOptOut;
|
||||
|
||||
//not allowing user to override these values i guess?
|
||||
try {
|
||||
|
|
@ -26,10 +27,14 @@ export function setSnapshot(
|
|||
try {
|
||||
delete newSnapshot?.verified;
|
||||
} catch {}
|
||||
try {
|
||||
delete newSnapshot?.lbOptOut;
|
||||
} catch {}
|
||||
dbSnapshot = newSnapshot;
|
||||
if (dbSnapshot) {
|
||||
dbSnapshot.banned = originalBanned;
|
||||
dbSnapshot.verified = originalVerified;
|
||||
dbSnapshot.lbOptOut = lbOptOut;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,6 +96,7 @@ export async function initSnapshot(): Promise<
|
|||
snap.name = userData.name;
|
||||
snap.personalBests = userData.personalBests;
|
||||
snap.banned = userData.banned;
|
||||
snap.lbOptOut = userData.lbOptOut;
|
||||
snap.verified = userData.verified;
|
||||
snap.discordId = userData.discordId;
|
||||
snap.discordAvatar = userData.discordAvatar;
|
||||
|
|
|
|||
|
|
@ -150,11 +150,20 @@ function updateFooter(lb: LbKey): void {
|
|||
return;
|
||||
}
|
||||
|
||||
$(`#leaderboardsWrapper table.${side} tfoot`).html(`
|
||||
if (DB.getSnapshot()?.lbOptOut === true) {
|
||||
$(`#leaderboardsWrapper table.${side} tfoot`).html(`
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;">You have opted out of the leaderboards</>
|
||||
</tr>
|
||||
`);
|
||||
return;
|
||||
} else {
|
||||
$(`#leaderboardsWrapper table.${side} tfoot`).html(`
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;">Not qualified</>
|
||||
</tr>
|
||||
`);
|
||||
}
|
||||
|
||||
let toppercent;
|
||||
if (currentTimeRange === "allTime" && currentRank[lb]) {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,8 @@ export async function update(
|
|||
|
||||
const banned = profile.banned === true;
|
||||
|
||||
const lbOptOut = profile.lbOptOut === true;
|
||||
|
||||
if (!details || !profile || !profile.name || !profile.addedAt) return;
|
||||
|
||||
details.find(".placeholderAvatar").removeClass("hidden");
|
||||
|
|
@ -75,6 +77,14 @@ export async function update(
|
|||
);
|
||||
}
|
||||
|
||||
if (lbOptOut) {
|
||||
details
|
||||
.find(".name")
|
||||
.append(
|
||||
`<div class="bannedIcon" aria-label="This account has opted out of leaderboards" data-balloon-pos="up"><i class="fas fa-crown"></i></div>`
|
||||
);
|
||||
}
|
||||
|
||||
updateNameFontSize(where);
|
||||
|
||||
const joinedText = "Joined " + format(profile.addedAt ?? 0, "dd MMM yyyy");
|
||||
|
|
|
|||
|
|
@ -828,6 +828,10 @@ export function showAccountSection(): void {
|
|||
refreshTagsSettingsSection();
|
||||
refreshPresetsSettingsSection();
|
||||
updateDiscordSection();
|
||||
|
||||
if (DB.getSnapshot()?.lbOptOut === true) {
|
||||
$(".pageSettings .section.optOutOfLeaderboards").remove();
|
||||
}
|
||||
}
|
||||
|
||||
export async function update(groupUpdate = true): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -734,7 +734,7 @@ list["resetAccount"] = new SimplePopup(
|
|||
Notifications.add("Resetting settings...", 0);
|
||||
UpdateConfig.reset();
|
||||
Loader.show();
|
||||
Notifications.add("Resetting account and stats...", 0);
|
||||
Notifications.add("Resetting account...", 0);
|
||||
const response = await Ape.users.reset();
|
||||
|
||||
if (response.status !== 200) {
|
||||
|
|
@ -772,6 +772,71 @@ list["resetAccount"] = new SimplePopup(
|
|||
}
|
||||
);
|
||||
|
||||
list["optOutOfLeaderboards"] = new SimplePopup(
|
||||
"optOutOfLeaderboards",
|
||||
"text",
|
||||
"Opt out of leaderboards",
|
||||
[
|
||||
{
|
||||
placeholder: "Password",
|
||||
type: "password",
|
||||
initVal: "",
|
||||
},
|
||||
],
|
||||
"Are you sure you want to opt out of leaderboards?",
|
||||
"Opt out",
|
||||
async (_thisPopup, password: string) => {
|
||||
try {
|
||||
const user = Auth?.currentUser;
|
||||
if (!user) return;
|
||||
if (user.providerData.find((p) => p?.providerId === "password")) {
|
||||
const credential = EmailAuthProvider.credential(
|
||||
user.email as string,
|
||||
password
|
||||
);
|
||||
await reauthenticateWithCredential(user, credential);
|
||||
} else {
|
||||
await reauthenticateWithPopup(user, AccountController.gmailProvider);
|
||||
}
|
||||
|
||||
Loader.show();
|
||||
const response = await Ape.users.optOutOfLeaderboards();
|
||||
|
||||
if (response.status !== 200) {
|
||||
Loader.hide();
|
||||
return Notifications.add(
|
||||
`Failed to opt out of leaderboards: ${response.message}`,
|
||||
-1
|
||||
);
|
||||
}
|
||||
Loader.hide();
|
||||
Notifications.add("Leaderboard opt out successful", 1);
|
||||
setTimeout(() => {
|
||||
location.reload();
|
||||
}, 3000);
|
||||
} catch (e) {
|
||||
const typedError = e as FirebaseError;
|
||||
Loader.hide();
|
||||
if (typedError.code === "auth/wrong-password") {
|
||||
Notifications.add("Incorrect password", -1);
|
||||
} else {
|
||||
Notifications.add("Something went wrong: " + e, -1);
|
||||
}
|
||||
}
|
||||
},
|
||||
(thisPopup) => {
|
||||
const user = Auth?.currentUser;
|
||||
if (!user) return;
|
||||
if (!user.providerData.find((p) => p?.providerId === "password")) {
|
||||
thisPopup.inputs = [];
|
||||
thisPopup.buttonText = "Reauthenticate to reset";
|
||||
}
|
||||
},
|
||||
(_thisPopup) => {
|
||||
//
|
||||
}
|
||||
);
|
||||
|
||||
list["clearTagPb"] = new SimplePopup(
|
||||
"clearTagPb",
|
||||
"text",
|
||||
|
|
@ -1376,6 +1441,14 @@ $(".pageSettings #resetAccount").on("click", () => {
|
|||
list["resetAccount"].show();
|
||||
});
|
||||
|
||||
$(".pageSettings #optOutOfLeaderboardsButton").on("click", () => {
|
||||
if (!ConnectionState.get()) {
|
||||
Notifications.add("You are offline", 0, 2);
|
||||
return;
|
||||
}
|
||||
list["optOutOfLeaderboards"].show();
|
||||
});
|
||||
|
||||
$("#popups").on("click", "#apeKeysPopup .generateApeKey", () => {
|
||||
if (!ConnectionState.get()) {
|
||||
Notifications.add("You are offline", 0, 2);
|
||||
|
|
|
|||
1
frontend/src/ts/types/types.d.ts
vendored
1
frontend/src/ts/types/types.d.ts
vendored
|
|
@ -565,6 +565,7 @@ declare namespace MonkeyTypes {
|
|||
inboxUnreadSize: number;
|
||||
streak: number;
|
||||
maxStreak: number;
|
||||
lbOptOut?: boolean;
|
||||
}
|
||||
|
||||
interface UserDetails {
|
||||
|
|
|
|||
|
|
@ -2760,8 +2760,9 @@
|
|||
<span>reset settings</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
Resets settings to the default (but doesn't touch your tags). Warning:
|
||||
you can't undo this action!
|
||||
Resets settings to the default (but doesn't touch your tags).
|
||||
<br />
|
||||
<span class="red">You can't undo this action!</span>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div
|
||||
|
|
@ -2781,7 +2782,9 @@
|
|||
</div>
|
||||
<div class="text">
|
||||
Resets all your personal bests (but doesn't delete any tests from your
|
||||
history). Warning: you can't undo this action!
|
||||
history).
|
||||
<br />
|
||||
<span class="red">You can't undo this action!</span>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div
|
||||
|
|
@ -2794,12 +2797,38 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section optOutOfLeaderboards needsAccount hidden">
|
||||
<div class="groupTitle">
|
||||
<i class="fas fa-crown"></i>
|
||||
<span>opt out of leaderboards</span>
|
||||
</div>
|
||||
<div class="text">
|
||||
Use this if you frequently trigger the anticheat (for example if using
|
||||
stenography) to opt out of leaderboards.
|
||||
<br />
|
||||
<span class="red">You can't undo this action!</span>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div
|
||||
class="button danger"
|
||||
id="optOutOfLeaderboardsButton"
|
||||
tabindex="0"
|
||||
onclick="this.blur();"
|
||||
>
|
||||
opt out
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section resetAccount needsAccount hidden">
|
||||
<div class="groupTitle">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
<span>reset account</span>
|
||||
</div>
|
||||
<div class="text">Completely resets your account to a blank state.</div>
|
||||
<div class="text">
|
||||
Completely resets your account to a blank state.
|
||||
<br />
|
||||
<span class="red">You can't undo this action!</span>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div
|
||||
class="button danger"
|
||||
|
|
@ -2816,7 +2845,11 @@
|
|||
<i class="fas fa-trash"></i>
|
||||
<span>delete account</span>
|
||||
</div>
|
||||
<div class="text">Deletes your account and all data connected to it.</div>
|
||||
<div class="text">
|
||||
Deletes your account and all data connected to it.
|
||||
<br />
|
||||
<span class="red">You can't undo this action!</span>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<div
|
||||
class="button danger"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue