diff --git a/frontend/__tests__/root/config.spec.ts b/frontend/__tests__/root/config.spec.ts index 90eb7ca8fa44..0f89fb6aa4d6 100644 --- a/frontend/__tests__/root/config.spec.ts +++ b/frontend/__tests__/root/config.spec.ts @@ -10,7 +10,7 @@ import { Config as ConfigType, CaretStyleSchema, } from "@monkeytype/schemas/configs"; -import * as FunboxValidation from "../../src/ts/test/funbox/funbox-validation"; +import * as FunboxValidation from "../../src/ts/config/funbox-validation"; import * as ConfigValidation from "../../src/ts/config/validation"; import * as ConfigEvent from "../../src/ts/observables/config-event"; import * as ApeConfig from "../../src/ts/ape/config"; diff --git a/frontend/__tests__/test/funbox/funbox-validation.spec.ts b/frontend/__tests__/test/funbox/funbox-validation.spec.ts index 1f033d3d6d4e..00c1e0825028 100644 --- a/frontend/__tests__/test/funbox/funbox-validation.spec.ts +++ b/frontend/__tests__/test/funbox/funbox-validation.spec.ts @@ -1,27 +1,18 @@ -import { describe, it, expect, afterEach, vi } from "vitest"; -import { canSetConfigWithCurrentFunboxes } from "../../../src/ts/test/funbox/funbox-validation"; +import { describe, it, expect } from "vitest"; +import { canSetConfigWithCurrentFunboxes } from "../../../src/ts/config/funbox-validation"; -import * as Notifications from "../../../src/ts/states/notifications"; import { FunboxName } from "@monkeytype/schemas/configs"; describe("funbox-validation", () => { describe("canSetConfigWithCurrentFunboxes", () => { - const addNotificationMock = vi.spyOn( - Notifications, - "showNoticeNotification", - ); - afterEach(() => { - addNotificationMock.mockClear(); - }); - const testCases = [ //checks for frontendForcedConfig { key: "mode", value: "zen", funbox: ["memory"], - error: "You can't set mode to zen with currently active funboxes.", + expected: false, }, - { key: "mode", value: "words", funbox: ["memory"] }, //ok + { key: "mode", value: "words", funbox: ["memory"], expected: true }, //checks for zen mode ...[ @@ -40,10 +31,15 @@ describe("funbox-validation", () => { key: "mode", value: "zen", funbox: [funbox], - error: "You can't set mode to zen with currently active funboxes.", + expected: false, })), - { key: "mode", value: "zen", funbox: ["mirror"] }, //ok - { key: "mode", value: "zen", funbox: ["space_balls"] }, //no frontendFunctions + { key: "mode", value: "zen", funbox: ["mirror"], expected: true }, + { + key: "mode", + value: "zen", + funbox: ["space_balls"], + expected: true, + }, //checks for words and custom ...["quote", "custom"].flatMap((value) => @@ -56,23 +52,22 @@ describe("funbox-validation", () => { key: "mode", value, funbox: [funbox], - error: `You can't set mode to ${value} with currently active funboxes.`, + expected: false, })), ), - { key: "mode", value: "quote", funbox: ["space_balls"] }, //no frontendFunctions + { + key: "mode", + value: "quote", + funbox: ["space_balls"], + expected: true, + }, ]; it.for(testCases)( `check $funbox with $key=$value`, - ({ key, value, funbox, error }) => { + ({ key, value, funbox, expected }) => { expect( canSetConfigWithCurrentFunboxes(key, value, funbox as FunboxName[]), - ).toBe(error === undefined); - - if (error !== undefined) { - expect(addNotificationMock).toHaveBeenCalledWith(error, { - durationMs: 5000, - }); - } + ).toBe(expected); }, ); }); diff --git a/frontend/src/terms-of-service.html b/frontend/src/terms-of-service.html index 8430fedaa118..0ae96cb1e318 100644 --- a/frontend/src/terms-of-service.html +++ b/frontend/src/terms-of-service.html @@ -211,26 +211,28 @@

Limitations

Users as a whole.

Privacy Policy

- If you use our Services, you must abide by our Privacy Policy. You - acknowledge that you have read our - - Privacy Policy - -  and understand that it sets forth how we collect, use, and store - your information. If you do not agree with our Privacy Statement, then - you must stop using the Services immediately. Any person, entity, or - service collecting data from the Services must comply with our Privacy - Statement. Misuse of any User's Personal Information is prohibited. If - you collect any Personal Information from a User, you agree that you - will only use the Personal Information you gather for the purpose for - which the User has authorized it. You agree that you will reasonably - secure any Personal Information you have gathered from the Services, and - you will respond promptly to complaints, removal requests, and 'do not - contact' requests from us or Users. +
+ If you use our Services, you must abide by our Privacy Policy. You + acknowledge that you have read our + + Privacy Policy + +  and understand that it sets forth how we collect, use, and store + your information. If you do not agree with our Privacy Statement, then + you must stop using the Services immediately. Any person, entity, or + service collecting data from the Services must comply with our Privacy + Statement. Misuse of any User's Personal Information is prohibited. If + you collect any Personal Information from a User, you agree that you + will only use the Personal Information you gather for the purpose for + which the User has authorized it. You agree that you will reasonably + secure any Personal Information you have gathered from the Services, + and you will respond promptly to complaints, removal requests, and 'do + not contact' requests from us or Users. +

Limitations on Automated Use

You shouldn't use bots or access our Services in malicious or prohibited diff --git a/frontend/src/ts/config/funbox-validation.ts b/frontend/src/ts/config/funbox-validation.ts new file mode 100644 index 000000000000..974a4876d3d2 --- /dev/null +++ b/frontend/src/ts/config/funbox-validation.ts @@ -0,0 +1,89 @@ +import { checkForcedConfig, getFunbox } from "@monkeytype/funbox"; +import { Config, ConfigValue, FunboxName } from "@monkeytype/schemas/configs"; + +export function canSetConfigWithCurrentFunboxes( + key: string, + value: ConfigValue, + funbox: FunboxName[] = [], +): boolean { + const funboxes = getFunbox(funbox); + if (key === "mode") { + let fb = getFunbox(funbox).filter( + (f) => + f.frontendForcedConfig?.["mode"] !== undefined && + !(f.frontendForcedConfig["mode"] as ConfigValue[]).includes(value), + ); + if (value === "zen") { + fb = fb.concat( + funboxes.filter((f) => { + const funcs = f.frontendFunctions ?? []; + const props = f.properties ?? []; + return ( + funcs.includes("getWord") || + funcs.includes("pullSection") || + funcs.includes("alterText") || + funcs.includes("withWords") || + props.includes("changesCapitalisation") || + props.includes("nospace") || + props.some((fp) => fp.startsWith("toPush:")) || + props.includes("changesWordsVisibility") || + props.includes("speaks") || + props.includes("changesLayout") || + props.includes("changesWordsFrequency") + ); + }), + ); + } + if (value === "quote" || value === "custom") { + fb = fb.concat( + funboxes.filter((f) => { + const funcs = f.frontendFunctions ?? []; + const props = f.properties ?? []; + return ( + funcs.includes("getWord") || + funcs.includes("pullSection") || + funcs.includes("withWords") || + props.includes("changesWordsFrequency") + ); + }), + ); + } + + if (fb.length > 0) { + return false; + } + } + if (!checkForcedConfig(key, value, funboxes).result) { + return false; + } + + return true; +} + +export type FunboxConfigError = { + key: string; + value: ConfigValue; +}; + +export function canSetFunboxWithConfig( + funbox: FunboxName, + config: Config, +): { ok: true } | { ok: false; errors: FunboxConfigError[] } { + const funboxToCheck = [...config.funbox, funbox]; + + const errors: FunboxConfigError[] = []; + for (const [configKey, configValue] of Object.entries(config)) { + if ( + !canSetConfigWithCurrentFunboxes(configKey, configValue, funboxToCheck) + ) { + errors.push({ + key: configKey, + value: configValue, + }); + } + } + if (errors.length > 0) { + return { ok: false, errors }; + } + return { ok: true }; +} diff --git a/frontend/src/ts/config/metadata.ts b/frontend/src/ts/config/metadata.ts index d3947ae9bec6..4a5f62f5c2a6 100644 --- a/frontend/src/ts/config/metadata.ts +++ b/frontend/src/ts/config/metadata.ts @@ -2,7 +2,7 @@ import { checkCompatibility } from "@monkeytype/funbox"; import * as DB from "../db"; import { showNoticeNotification } from "../states/notifications"; import { isAuthenticated } from "../firebase"; -import { canSetFunboxWithConfig } from "../test/funbox/funbox-validation"; +import { canSetFunboxWithConfig } from "./funbox-validation"; import { reloadAfter } from "../utils/misc"; import { isDevEnvironment } from "../utils/env"; import * as ConfigSchemas from "@monkeytype/schemas/configs"; @@ -307,9 +307,10 @@ export const configMetadata: ConfigMetadataObject = { } for (const funbox of value) { - if (!canSetFunboxWithConfig(funbox, currentConfig)) { + const check = canSetFunboxWithConfig(funbox, currentConfig); + if (!check.ok) { showNoticeNotification( - `${value}" cannot be enabled with the current config`, + `"${funbox}" cannot be enabled with the current config`, ); return true; } diff --git a/frontend/src/ts/config/setters.ts b/frontend/src/ts/config/setters.ts index 84fb44e08dd2..e409c043eabb 100644 --- a/frontend/src/ts/config/setters.ts +++ b/frontend/src/ts/config/setters.ts @@ -8,9 +8,10 @@ import { showNoticeNotification } from "../states/notifications"; import { canSetConfigWithCurrentFunboxes, canSetFunboxWithConfig, -} from "../test/funbox/funbox-validation"; +} from "./funbox-validation"; import * as TestState from "../test/test-state"; -import { typedKeys, triggerResize } from "../utils/misc"; +import { typedKeys, triggerResize, escapeHTML } from "../utils/misc"; +import { camelCaseToWords, capitalizeFirstLetter } from "../utils/strings"; import { Config, setConfigStore } from "./store"; import { FunboxName } from "@monkeytype/schemas/configs"; @@ -77,6 +78,16 @@ export function setConfig( } if (!canSetConfigWithCurrentFunboxes(key, value, Config.funbox)) { + if (key === "words" || key === "time") { + showNoticeNotification("Active funboxes do not support infinite tests"); + } else { + showNoticeNotification( + `You can't set ${camelCaseToWords( + key, + )} to ${String(value)} with currently active funboxes.`, + { durationMs: 5000 }, + ); + } console.warn( `Could not set config key "${key}" with value "${JSON.stringify( value, @@ -152,7 +163,18 @@ export function toggleFunbox(funbox: FunboxName, nosave?: boolean): boolean { return false; } - if (!canSetFunboxWithConfig(funbox, Config)) { + const funboxCheck = canSetFunboxWithConfig(funbox, Config); + if (!funboxCheck.ok) { + const errorStrings = funboxCheck.errors.map( + (e) => + `${capitalizeFirstLetter( + camelCaseToWords(e.key), + )} cannot be set to ${String(e.value)}.`, + ); + showNoticeNotification( + `You can't enable ${escapeHTML(funbox.replace(/_/g, " "))}:
${errorStrings.map((s) => escapeHTML(s)).join("
")}`, + { durationMs: 5000, useInnerHtml: true }, + ); return false; } diff --git a/frontend/src/ts/test/funbox/funbox-validation.tsx b/frontend/src/ts/test/funbox/funbox-validation.tsx deleted file mode 100644 index 4875b2874238..000000000000 --- a/frontend/src/ts/test/funbox/funbox-validation.tsx +++ /dev/null @@ -1,198 +0,0 @@ -import { FunboxMetadata, getFunbox } from "@monkeytype/funbox"; -import { Config, ConfigValue, FunboxName } from "@monkeytype/schemas/configs"; -import { intersect } from "@monkeytype/util/arrays"; - -import { showNoticeNotification } from "../../states/notifications"; -import { escapeHTML } from "../../utils/misc"; -import * as Strings from "../../utils/strings"; - -export function checkForcedConfig( - key: string, - value: ConfigValue, - funboxes: FunboxMetadata[], -): { - result: boolean; - forcedConfigs?: ConfigValue[]; -} { - if (funboxes.length === 0) { - return { result: true }; - } - - if (key === "words" || key === "time") { - if (value === 0) { - const fb = funboxes.filter((f) => - f.properties?.includes("noInfiniteDuration"), - ); - if (fb.length > 0) { - return { - result: false, - forcedConfigs: [key === "words" ? 10 : 15], - }; - } else { - return { result: true }; - } - } else { - return { result: true }; - } - } else { - const forcedConfigs: Record = {}; - // collect all forced configs - for (const fb of funboxes) { - if (fb.frontendForcedConfig) { - //push keys to forcedConfigs, if they don't exist. if they do, intersect the values - for (const key in fb.frontendForcedConfig) { - if (forcedConfigs[key] === undefined) { - forcedConfigs[key] = fb.frontendForcedConfig[key] as ConfigValue[]; - } else { - forcedConfigs[key] = intersect( - forcedConfigs[key], - fb.frontendForcedConfig[key] as ConfigValue[], - true, - ); - } - } - } - } - - //check if the key is in forcedConfigs, if it is check the value, if its not, return true - if (forcedConfigs[key] === undefined) { - return { result: true }; - } else { - if (forcedConfigs[key]?.length === 0) { - throw new Error("No intersection of forced configs"); - } - return { - result: (forcedConfigs[key] ?? []).includes(value), - forcedConfigs: forcedConfigs[key], - }; - } - } -} - -// function: canSetConfigWithCurrentFunboxes -// checks using checkFunboxForcedConfigs. if it returns true, return true -// if it returns false, show a notification and return false -export function canSetConfigWithCurrentFunboxes( - key: string, - value: ConfigValue, - funbox: FunboxName[] = [], - noNotification = false, -): boolean { - let errorCount = 0; - const funboxes = getFunbox(funbox); - if (key === "mode") { - let fb = getFunbox(funbox).filter( - (f) => - f.frontendForcedConfig?.["mode"] !== undefined && - !(f.frontendForcedConfig["mode"] as ConfigValue[]).includes(value), - ); - if (value === "zen") { - fb = fb.concat( - funboxes.filter((f) => { - const funcs = f.frontendFunctions ?? []; - const props = f.properties ?? []; - return ( - funcs.includes("getWord") || - funcs.includes("pullSection") || - funcs.includes("alterText") || - funcs.includes("withWords") || - props.includes("changesCapitalisation") || - props.includes("nospace") || - props.some((fp) => fp.startsWith("toPush:")) || - props.includes("changesWordsVisibility") || - props.includes("speaks") || - props.includes("changesLayout") || - props.includes("changesWordsFrequency") - ); - }), - ); - } - if (value === "quote" || value === "custom") { - fb = fb.concat( - funboxes.filter((f) => { - const funcs = f.frontendFunctions ?? []; - const props = f.properties ?? []; - return ( - funcs.includes("getWord") || - funcs.includes("pullSection") || - funcs.includes("withWords") || - props.includes("changesWordsFrequency") - ); - }), - ); - } - - if (fb.length > 0) { - errorCount += 1; - } - } - if (key === "words" || key === "time") { - if (!checkForcedConfig(key, value, funboxes).result) { - if (!noNotification) { - showNoticeNotification("Active funboxes do not support infinite tests"); - return false; - } else { - errorCount += 1; - } - } - } else if (!checkForcedConfig(key, value, funboxes).result) { - errorCount += 1; - } - - if (errorCount > 0) { - if (!noNotification) { - showNoticeNotification( - `You can't set ${Strings.camelCaseToWords( - key, - )} to ${value.toString()} with currently active funboxes.`, - { - durationMs: 5000, - }, - ); - } - return false; - } else { - return true; - } -} - -export function canSetFunboxWithConfig( - funbox: FunboxName, - config: Config, -): boolean { - let funboxToCheck = [...config.funbox, funbox]; - - const errors = []; - for (const [configKey, configValue] of Object.entries(config)) { - if ( - !canSetConfigWithCurrentFunboxes( - configKey, - configValue, - funboxToCheck, - true, - ) - ) { - errors.push({ - key: configKey, - value: configValue, - }); - } - } - if (errors.length > 0) { - const errorStrings = []; - for (const error of errors) { - errorStrings.push( - `${Strings.capitalizeFirstLetter( - Strings.camelCaseToWords(error.key), - )} cannot be set to ${error.value.toString()}.`, - ); - } - showNoticeNotification( - `You can't enable ${escapeHTML(funbox.replace(/_/g, " "))}:
${errorStrings.map((s) => escapeHTML(s)).join("
")}`, - { durationMs: 5000, useInnerHtml: true }, - ); - return false; - } else { - return true; - } -} diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index 716723ea3449..7d4d846fbcdf 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -13,7 +13,7 @@ import * as MemoryTimer from "./memory-funbox-timer"; import * as FunboxMemory from "./funbox-memory"; import { HighlightMode, FunboxName } from "@monkeytype/schemas/configs"; import { Mode } from "@monkeytype/schemas/shared"; -import { checkCompatibility } from "@monkeytype/funbox"; +import { checkCompatibility, checkForcedConfig } from "@monkeytype/funbox"; import { getAllFunboxes, getActiveFunboxes, @@ -22,7 +22,6 @@ import { isFunboxActiveWithProperty, getActiveFunboxesWithProperty, } from "./list"; -import { checkForcedConfig } from "./funbox-validation"; import { tryCatch } from "@monkeytype/util/trycatch"; import { qs, qsa } from "../../utils/dom"; import * as ConfigEvent from "../../observables/config-event"; diff --git a/packages/funbox/src/index.ts b/packages/funbox/src/index.ts index 2d937562b4a7..25b87ccd82da 100644 --- a/packages/funbox/src/index.ts +++ b/packages/funbox/src/index.ts @@ -1,10 +1,10 @@ import { FunboxName } from "@monkeytype/schemas/configs"; import { getList, getFunbox, getObject, getFunboxNames } from "./list"; import { FunboxMetadata, FunboxProperty } from "./types"; -import { checkCompatibility } from "./validation"; +import { checkCompatibility, checkForcedConfig } from "./validation"; export type { FunboxMetadata, FunboxProperty }; -export { checkCompatibility, getFunbox, getFunboxNames }; +export { checkCompatibility, checkForcedConfig, getFunbox, getFunboxNames }; export function getAllFunboxes(): FunboxMetadata[] { return getList(); diff --git a/packages/funbox/src/validation.ts b/packages/funbox/src/validation.ts index b7c7c98ea16a..0ad0335d372b 100644 --- a/packages/funbox/src/validation.ts +++ b/packages/funbox/src/validation.ts @@ -1,9 +1,74 @@ import { intersect } from "@monkeytype/util/arrays"; import { FunboxForcedConfig, FunboxMetadata } from "./types"; import { getFunbox } from "./list"; -import { FunboxName } from "@monkeytype/schemas/configs"; +import { ConfigValue, FunboxName } from "@monkeytype/schemas/configs"; import { safeNumber } from "@monkeytype/util/numbers"; +export function checkForcedConfig( + key: string, + value: ConfigValue, + funboxes: FunboxMetadata[], +): { + result: boolean; + forcedConfigs?: ConfigValue[]; +} { + if (funboxes.length === 0) { + return { result: true }; + } + + if (key === "words" || key === "time") { + if (value === 0) { + const fb = funboxes.filter((f) => + f.properties?.includes("noInfiniteDuration"), + ); + if (fb.length > 0) { + return { + result: false, + forcedConfigs: [key === "words" ? 10 : 15], + }; + } else { + return { result: true }; + } + } else { + return { result: true }; + } + } else { + const forcedConfigs: Record = {}; + // collect all forced configs + for (const fb of funboxes) { + if (fb.frontendForcedConfig) { + //push keys to forcedConfigs, if they don't exist. if they do, intersect the values + for (const forcedKey in fb.frontendForcedConfig) { + if (forcedConfigs[forcedKey] === undefined) { + forcedConfigs[forcedKey] = fb.frontendForcedConfig[ + forcedKey + ] as ConfigValue[]; + } else { + forcedConfigs[forcedKey] = intersect( + forcedConfigs[forcedKey], + fb.frontendForcedConfig[forcedKey] as ConfigValue[], + true, + ); + } + } + } + } + + //check if the key is in forcedConfigs, if it is check the value, if its not, return true + if (forcedConfigs[key] === undefined) { + return { result: true }; + } else { + if (forcedConfigs[key]?.length === 0) { + throw new Error("No intersection of forced configs"); + } + return { + result: (forcedConfigs[key] ?? []).includes(value), + forcedConfigs: forcedConfigs[key], + }; + } + } +} + export function checkCompatibility( funboxNames: FunboxName[], withFunbox?: FunboxName,