Upgrade to ESLint 9

Following the upgrade in Client and Server; you know the drill.

(ESLint 9 requires minimum Node version 18, hence the GitHub Actions
version bump.)
This commit is contained in:
Guangcong Luo 2025-02-25 21:05:33 -08:00
parent de9746faba
commit a56f1f160f
25 changed files with 1716 additions and 1767 deletions

View File

@ -1,278 +0,0 @@
{
"parserOptions": {
"ecmaVersion": "es2021",
"sourceType": "module",
"ecmaFeatures": {
"globalReturn": true
}
},
"ignorePatterns": ["node_modules/", ".dist/", "config/", "src/test/"],
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"rules": {
"no-console": "off",
// bad code, modern (new code patterns we don't like because they're less readable or performant)
"no-restricted-globals": ["error", "Proxy", "Reflect", "WeakSet"],
// bad code, deprecated (deprecated/bad patterns that should be written a different way)
"eqeqeq": "error",
"func-names": "off", // has minor advantages but way too verbose, hurting readability
"guard-for-in": "off", // guarding is a deprecated pattern, we just use no-extend-native instead
"init-declarations": "off", // TypeScript lets us delay initialization safely
"no-caller": "error",
"no-eval": "error",
"no-extend-native": "error",
"no-implied-eval": "error",
"no-inner-declarations": ["error", "functions"],
"no-iterator": "error",
"no-labels": ["error", {"allowLoop": true, "allowSwitch": true}],
"no-multi-str": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-proto": "error",
"no-restricted-syntax": ["error", "WithStatement"],
"no-sparse-arrays": "error",
"no-var": "error",
"no-with": "error",
// probably bugs (code with no reason to exist, probably typos)
"array-callback-return": "error",
"block-scoped-var": "error", // not actually used; precluded by no-var
"callback-return": [2, ["callback", "cb", "done"]],
"consistent-this": "off", // we use arrow functions instead
"constructor-super": "error",
"default-case": "off", // hopefully TypeScript will let us skip `default` for things that are exhaustive
"no-case-declarations": "off", // meh, we have no-shadow
"no-duplicate-case": "error",
"no-empty": ["error", {"allowEmptyCatch": true}],
"no-extra-bind": "error",
"no-extra-label": "error",
"no-fallthrough": "error",
"no-label-var": "error",
"no-new-require": "error",
"no-new": "error",
"no-redeclare": "off", // Useful with type namespaces
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow-restricted-names": "error",
"no-shadow": "error",
"no-template-curly-in-string": "error",
"no-throw-literal": "error",
"no-undef": ["error", {"typeof": true}],
"no-unmodified-loop-condition": "error",
"no-unused-expressions": "error",
"no-unsafe-finally": "error",
"no-unused-labels": "error",
"no-use-before-define": ["error", {"functions": false, "classes": false, "variables": false}],
"use-isnan": "error",
"valid-typeof": "error",
// style choices
"no-constant-condition": ["error", {"checkLoops": false}],
"no-lonely-if": "off",
"radix": ["error", "as-needed"],
// naming style
"camelcase": "off", // mostly only so we can import `child_process`
"id-length": "off",
"id-match": "off",
"new-cap": ["error", {"newIsCap": true, "capIsNew": false}],
"no-underscore-dangle": "off",
// syntax style (local syntactical, usually autofixable formatting decisions)
"arrow-parens": "off",
"arrow-body-style": "error",
"brace-style": ["error", "1tbs", {"allowSingleLine": true}],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "ignore"
}],
"comma-style": ["error", "last"],
"curly": ["error", "multi-line", "consistent"],
"dot-notation": "off",
"max-len": ["error", {"code": 120, "ignoreUrls": true}],
"new-parens": "error",
"no-array-constructor": "error",
"no-div-regex": "error",
"no-duplicate-imports": "error",
"no-extra-parens": "off",
"no-floating-decimal": "error",
"no-mixed-requires": "error",
"no-multi-spaces": "error",
"no-new-object": "error",
"no-octal-escape": "error",
"no-return-assign": ["error", "except-parens"],
"no-undef-init": "off",
"no-unneeded-ternary": "error",
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-concat": "off",
"no-useless-rename": "error",
"object-shorthand": ["error", "methods"],
"one-var": "off",
"operator-assignment": "off",
"prefer-arrow-callback": "off",
"prefer-const": ["error", {"destructuring": "all"}],
"quote-props": "off",
"quotes": "off",
"semi": ["error", "always"],
"sort-vars": "off",
"vars-on-top": "off",
"wrap-iife": ["error", "inside"],
"wrap-regex": "off",
"yoda": ["error", "never", { "exceptRange": true }],
// whitespace
"array-bracket-spacing": ["error", "never"],
"arrow-spacing": ["error", {"before": true, "after": true}],
"block-spacing": ["error", "always"],
"comma-spacing": ["error", {"before": false, "after": true}],
"computed-property-spacing": ["error", "never"],
"dot-location": ["error", "property"],
"eol-last": ["error", "always"],
"func-call-spacing": "error",
"function-paren-newline": ["error", "consistent"],
"indent": [2, "tab"],
"key-spacing": "error",
"keyword-spacing": ["error", {"before": true, "after": true}],
"lines-around-comment": "off",
"no-mixed-spaces-and-tabs": "error",
"no-multiple-empty-lines": ["error", {"max": 2, "maxEOF": 1}],
"no-trailing-spaces": ["error", {"ignoreComments": false}],
"object-curly-spacing": ["error", "never"],
"operator-linebreak": ["error", "after", { "overrides": { "?": "before", ":": "before" } }],
"padded-blocks": ["error", "never"],
"padding-line-between-statements": "off",
"rest-spread-spacing": ["error", "never"],
"semi-spacing": ["error", {"before": false, "after": true}],
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", {"anonymous": "always", "named": "never"}],
"spaced-comment": ["error", "always", {"exceptions": ["*"]}],
"space-in-parens": ["error", "never"],
"space-infix-ops": "error",
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
"template-curly-spacing": ["error", "never"]
},
"overrides": [
{
"files": ["**/*.ts", "**/*.tsx", "**/*.test.ts"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 11,
"sourceType": "module",
"tsconfigRootDir": ".",
"project": ["./tsconfig.json"]
},
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"plugins": [
"import"
],
"rules": {
// TODO revisit
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/member-ordering": "off",
// "@typescript-eslint/no-extraneous-class": "error",
// "@typescript-eslint/no-type-alias": "error",
"@typescript-eslint/no-namespace": "off",
"new-parens": "off", // used for the `new class {...}` pattern
"no-prototype-builtins": "off",
// typescript-eslint defaults too strict
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/unbound-method": ["error", {"ignoreStatic": true}],
// disable additional typescript-eslint 3.0 defaults
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-return": "off",
// probably bugs
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/no-dupe-class-members": "error",
"@typescript-eslint/no-empty-interface": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
"@typescript-eslint/return-await": ["error", "in-try-catch"],
"import/no-extraneous-dependencies": "error",
"no-dupe-class-members": "off",
"no-unused-expressions": ["error", {"allowTernary": true}], // ternary is used to convert callbacks to Promises
// naming style
"@typescript-eslint/camelcase": "off",
// syntax style (local syntactical, usually autofixable formatting decisions)
"@typescript-eslint/adjacent-overload-signatures": "error",
// "@typescript-eslint/array-type": "error",
"@typescript-eslint/consistent-type-assertions": ["error", {"assertionStyle": "as"}],
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/explicit-member-accessibility": ["error", {"accessibility": "no-public"}],
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/no-this-alias": "error",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/parameter-properties": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-namespace-keyword": "error",
"prefer-object-spread": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unified-signatures": "error",
"quotes": "off",
"semi": "off",
"@typescript-eslint/semi": ["error", "always"],
// whitespace
"@typescript-eslint/type-annotation-spacing": "error",
"spaced-comment": ["error", "always", {"exceptions": ["*", "/"]}],
// overriding base
"@typescript-eslint/indent": ["error", "tab", {"flatTernaryExpressions": true}],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["error", {"functions": false, "classes": false, "variables": false}],
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
// types
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/prefer-string-starts-ends-with": "off",
// "@typescript-eslint/switch-exhaustiveness-check": "error",
// types - probably bugs
"@typescript-eslint/no-floating-promises": [
"error", {"ignoreVoid": true, "ignoreIIFE": true}
],
"@typescript-eslint/no-for-in-array": "error",
"@typescript-eslint/no-misused-promises": "error",
"@typescript-eslint/no-throw-literal": "error",
// syntax style (local syntactical, usually autofixable formatting decisions)
"@typescript-eslint/no-unnecessary-qualifier": "off",
"@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/no-unnecessary-type-assertion": "error",
"@typescript-eslint/prefer-regexp-exec": "error"
}
}
]
}

View File

@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
node-version: [16.x, 20.x]
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v2

View File

@ -54,7 +54,7 @@ exports.colorpath = null;
/** @type {string | null} */
exports.coilpath = null;
/** @type {string | null} Password, whether to debug error stacks in request or not*/
/** @type {string | null} Password, whether to debug error stacks in request or not */
exports.devmode = null;
// absolute path to your PS instance. can use the checked-out client that the client clones in.
@ -106,9 +106,9 @@ exports.challengekeyid = 4;
/**
* DBs.
*/
/** @type {typeof exports.mysql | undefined}*/
/** @type {typeof exports.mysql | undefined} */
exports.replaysdb = undefined;
/** @type {typeof exports.mysql | undefined}*/
/** @type {typeof exports.mysql | undefined} */
exports.ladderdb = undefined;
/**

View File

@ -1,7 +1,7 @@
module.exports = {
apps: [{
name: "loginserver",
script: "./.dist/src",
exec_mode: "cluster",
}],
apps: [{
name: "loginserver",
script: "./.dist/src",
exec_mode: "cluster",
}],
};

435
eslint-ps-standard.mjs Normal file
View File

@ -0,0 +1,435 @@
/**
* Pokemon Showdown standard style
*
* This is Showdown's shared ESLint configuration. Each project overrides
* at least a little of it here and there, but these are the rules we use
* unless there's a good reason otherwise.
*/
// @ts-check
import eslint from '@eslint/js';
import globals from 'globals';
import tseslint from 'typescript-eslint';
import stylistic from '@stylistic/eslint-plugin';
/** @typedef {import('typescript-eslint').Config} ConfigFile */
/** @typedef {Awaited<ConfigFile>[number]} Config */
/** @typedef {NonNullable<Config['rules']>} Rules */
export { eslint, globals, tseslint, stylistic };
/** @type {Config} */
export const plugin = {
plugins: {
'@stylistic': stylistic,
'@typescript-eslint': tseslint.plugin,
},
};
/** @type {typeof tseslint.config} */
export const configure = (...args) => [
plugin,
...tseslint.config(...args),
];
/** @type {NonNullable<Config['rules']>} */
export const defaultRules = {
...stylistic.configs.customize({
braceStyle: '1tbs',
indent: 'tab',
semi: true,
jsx: true,
// ...
}).rules,
// TODO rules to revisit
// =====================
// nice to have but we mostly know && || precedence so not urgent to fix
"@stylistic/no-mixed-operators": "off",
// test only (should never be committed, but useful when testing)
// ==============================================================
// do we want unused args/destructures to start with _? unsure
"no-unused-vars": ["warn", {
args: "all",
argsIgnorePattern: ".",
caughtErrors: "all",
destructuredArrayIgnorePattern: ".",
ignoreRestSiblings: true,
}],
// "no-unused-vars": ["warn", {
// args: "all",
// argsIgnorePattern: "^_",
// caughtErrors: "all",
// destructuredArrayIgnorePattern: "^_",
// ignoreRestSiblings: true
// }],
"@stylistic/max-len": ["warn", {
"code": 120, "tabWidth": 0,
// DO NOT EDIT DIRECTLY: see bottom of file for source
"ignorePattern": "^\\s*(?:\\/\\/ \\s*)?(?:(?:export )?(?:let |const |readonly )?[a-zA-Z0-9_$.]+(?: \\+?=>? )|[a-zA-Z0-9$]+: \\[?|(?:return |throw )?(?:new )?(?:[a-zA-Z0-9$.]+\\()?)?(?:Utils\\.html|(?:this\\.)?(?:room\\.)?tr|\\$\\()?['\"`/]",
}],
"prefer-const": ["warn", { "destructuring": "all" }],
// PS code (code specific to PS)
// =============================
"@stylistic/new-parens": "off", // used for the `new class {...}` pattern
"no-prototype-builtins": "off",
// defaults too strict
// ===================
"no-empty": ["error", { "allowEmptyCatch": true }],
"no-case-declarations": "off",
// probably bugs
// =============
"array-callback-return": "error",
"no-constructor-return": "error",
"no-dupe-class-members": "error",
"no-extend-native": "error",
"no-extra-bind": "warn",
"no-extra-label": "warn",
"no-eval": "error",
"no-implied-eval": "error",
"no-inner-declarations": ["error", "functions"],
"no-iterator": "error",
"no-fallthrough": ["error", { allowEmptyCase: true, reportUnusedFallthroughComment: true }],
"no-promise-executor-return": ["error", { allowVoid: true }],
"no-return-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "error",
"no-template-curly-in-string": "error",
"no-throw-literal": "warn",
"no-unmodified-loop-condition": "error",
// best way to read first key of object
// "no-unreachable-loop": "error",
// ternary is used to convert callbacks to Promises
// tagged templates are used for the SQL library
"no-unused-expressions": ["error", { allowTernary: true, allowTaggedTemplates: true, enforceForJSX: true }],
"no-useless-call": "error",
// "no-useless-assignment": "error",
"require-atomic-updates": "error",
// syntax style (local syntactical, usually autofixable formatting decisions)
// ===========================================================================
"@stylistic/member-delimiter-style": ["error", {
multiline: { delimiter: "comma", requireLast: true },
singleline: { delimiter: "comma", requireLast: false },
overrides: { interface: {
multiline: { delimiter: "semi", requireLast: true },
singleline: { delimiter: "semi", requireLast: false },
} },
}],
"default-case-last": "error",
"eqeqeq": ["error", "always", { null: "ignore" }],
"no-array-constructor": "error",
"no-duplicate-imports": "error",
"no-implicit-coercion": ["error", { allow: ["!!", "+"] }],
"no-multi-str": "error",
"no-object-constructor": "error",
"no-proto": "error",
"no-unneeded-ternary": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-var": "error",
"object-shorthand": ["error", "always"],
"operator-assignment": ["error", "always"],
"prefer-arrow-callback": "error",
"prefer-exponentiation-operator": "error",
"prefer-numeric-literals": "error",
"prefer-object-has-own": "error",
"prefer-object-spread": "error",
"prefer-promise-reject-errors": "error",
"prefer-regex-literals": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"radix": ["error", "as-needed"],
// syntax style, overriding base
// =============================
"@stylistic/quotes": "off",
"@stylistic/quote-props": "off",
"@stylistic/function-call-spacing": "error",
"@stylistic/arrow-parens": ["error", "as-needed"],
"@stylistic/comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "never",
"importAttributes": "always-multiline",
"dynamicImports": "always-multiline",
"enums": "always-multiline",
"generics": "always-multiline",
"tuples": "always-multiline",
}],
"@stylistic/jsx-wrap-multilines": "off",
"@stylistic/jsx-closing-bracket-location": ["error", "line-aligned"],
// "@stylistic/jsx-closing-tag-location": ["error", "line-aligned"],
"@stylistic/jsx-closing-tag-location": "off",
"@stylistic/jsx-one-expression-per-line": "off",
"@stylistic/jsx-max-props-per-line": "off",
"@stylistic/jsx-function-call-newline": "off",
"no-restricted-syntax": ["error",
{ selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" },
],
// whitespace
// ==========
"@stylistic/block-spacing": "error",
"@stylistic/operator-linebreak": ["error", "after"],
"@stylistic/max-statements-per-line": ["error", { max: 3, ignoredNodes: ['BreakStatement'] }],
"@stylistic/lines-between-class-members": "off",
"@stylistic/multiline-ternary": "off",
"@stylistic/object-curly-spacing": ["error", "always"],
"@stylistic/indent": ["error", "tab", { "flatTernaryExpressions": true }],
};
/** @type {NonNullable<Config['rules']>} */
export const defaultRulesTS = {
...defaultRules,
// TODO: revisit
// we should do this someday but it'd have to be a gradual manual process
// "@typescript-eslint/explicit-module-boundary-types": "off",
// like above but slightly harder, so do that one first
// "@typescript-eslint/explicit-function-return-type": "off",
// probably we should settle on a standard someday
// "@typescript-eslint/member-ordering": "off",
// "@typescript-eslint/no-extraneous-class": "error",
// maybe we should consider this
"@typescript-eslint/consistent-indexed-object-style": "off",
// typescript-eslint specific
// ==========================
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": defaultRules["no-unused-vars"],
"no-shadow": "off",
"@typescript-eslint/no-shadow": defaultRules["no-shadow"],
"no-dupe-class-members": "off",
"@typescript-eslint/no-dupe-class-members": defaultRules["no-dupe-class-members"],
"no-unused-expressions": "off",
"@typescript-eslint/no-unused-expressions": defaultRules["no-unused-expressions"],
// defaults too strict
// ===================
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
// probably bugs
// =============
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-extra-non-null-assertion": "error",
"@typescript-eslint/no-misused-new": "error",
// no way to get it to be less strict unfortunately
// "@typescript-eslint/no-misused-spread": "error",
"@typescript-eslint/no-non-null-asserted-optional-chain": "error",
// naming style
// ============
"@typescript-eslint/naming-convention": ["error", {
"selector": ["class", "interface", "typeAlias"],
"format": ["PascalCase"],
}],
// syntax style (local syntactical, usually autofixable formatting decisions)
// ===========================================================================
"@typescript-eslint/no-namespace": ["error", { allowDeclarations: true }],
"@typescript-eslint/prefer-namespace-keyword": "error",
"@typescript-eslint/adjacent-overload-signatures": "error",
"@typescript-eslint/array-type": "error",
"@typescript-eslint/consistent-type-assertions": ["error", { "assertionStyle": "as" }],
"@typescript-eslint/consistent-type-definitions": "off",
"@typescript-eslint/consistent-type-imports": ["error", { fixStyle: "inline-type-imports" }],
"@typescript-eslint/explicit-member-accessibility": ["error", { "accessibility": "no-public" }],
"@typescript-eslint/parameter-properties": "error",
// `source` and `target` are frequently used as variables that may point to `this`
// or to another `Pokemon` object, depending on how the given method is invoked
"@typescript-eslint/no-this-alias": ["error", { "allowedNames": ["source", "target"] }],
// unfortunately this has lots of false positives without strict array/object property access
// "@typescript-eslint/no-unnecessary-boolean-literal-compare": "error",
"@typescript-eslint/prefer-as-const": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-function-type": "error",
"@typescript-eslint/prefer-return-this-type": "error",
"@typescript-eslint/triple-slash-reference": "error",
"@typescript-eslint/unified-signatures": "error",
};
/** @type {NonNullable<Config['rules']>} */
export const defaultRulesTSChecked = {
...defaultRulesTS,
// style
// =====
"@typescript-eslint/no-unnecessary-type-arguments": "error",
"@typescript-eslint/restrict-plus-operands": ["error", {
allowBoolean: false, allowNullish: false, allowNumberAndString: false, allowRegExp: false,
}],
"@typescript-eslint/restrict-template-expressions": ["error", {
allow: [{ name: ['Error', 'URL', 'URLSearchParams'], from: 'lib' }],
allowBoolean: false, allowNever: false, allowNullish: false, allowRegExp: false,
}],
// we use `any`
// ============
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-argument": "off",
// yes-types syntax style, overriding base
// =======================================
"@typescript-eslint/prefer-includes": "error",
"@typescript-eslint/prefer-nullish-coalescing": "off",
"@typescript-eslint/dot-notation": "off",
"@typescript-eslint/no-confusing-non-null-assertion": "off",
};
/** @type {NonNullable<Config['rules']>} */
export const defaultRulesES3 = {
...defaultRules,
// required in ES3
// ================
"no-var": "off",
"object-shorthand": ["error", "never"],
"prefer-arrow-callback": "off",
"prefer-exponentiation-operator": "off",
"prefer-object-has-own": "off",
"prefer-object-spread": "off",
"prefer-rest-params": "off",
"prefer-spread": "off",
"radix": "off",
"@stylistic/comma-dangle": "error",
"no-unused-vars": ["warn", {
args: "all",
argsIgnorePattern: ".",
caughtErrors: "all",
caughtErrorsIgnorePattern: "^e(rr)?$",
destructuredArrayIgnorePattern: ".",
ignoreRestSiblings: true,
}],
"no-restricted-syntax": ["error",
{ selector: "TaggedTemplateExpression", message: "Hard to compile down to ES3" },
{ selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" },
],
// with no block scoping, coming up with original variable names is too hard
"no-redeclare": "off",
// treat var as let
// unfortunately doesn't actually let me redeclare
// "block-scoped-var": "error",
"no-caller": "error",
"no-invalid-this": "error",
"no-new-wrappers": "error",
// Map/Set can be polyfilled but it's nontrivial and it's easier just to use bare objects
"no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map"],
"unicode-bom": "error",
};
/**
* Actually very different from defaultRulesES3, because we don't have to
* worry about syntax that's easy to transpile to ES3 (which is basically
* all syntax).
* @type {NonNullable<Config['rules']>}
*/
export const defaultRulesES3TSChecked = {
...defaultRulesTSChecked,
"radix": "off",
"no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map"],
"no-restricted-syntax": ["error", "TaggedTemplateExpression", "YieldExpression", "AwaitExpression", "BigIntLiteral"],
};
/**
* @param {Config[]} configs
* @returns {Config}
*/
function extractPlugin(configs) {
return configs.find(config => !config.rules) ||
(() => { throw new Error('No plugin found'); })();
}
/**
* @param {Config[]} configs
* @returns {Rules}
*/
function extractRules(configs) {
const rules = {};
for (const config of configs.filter(c => c.rules)) {
Object.assign(rules, config.rules);
}
return rules;
}
const tseslintPlugin = extractPlugin(tseslint.configs.stylisticTypeChecked);
/** @type {{[k: string]: Config[]}} */
export const configs = {
js: [{
rules: {
...eslint.configs.recommended.rules,
...defaultRules,
},
}],
ts: [tseslintPlugin, {
rules: {
...eslint.configs.recommended.rules,
...extractRules(tseslint.configs.recommendedTypeChecked),
...extractRules(tseslint.configs.stylisticTypeChecked),
...defaultRulesTSChecked,
},
}],
es3: [{
rules: {
...eslint.configs.recommended.rules,
...defaultRulesES3,
},
}],
es3ts: [tseslintPlugin, {
rules: {
...eslint.configs.recommended.rules,
...extractRules(tseslint.configs.recommendedTypeChecked),
...extractRules(tseslint.configs.stylisticTypeChecked),
...defaultRulesES3TSChecked,
},
}],
};
/*
SOURCE FOR IGNOREPATTERN (compile with https://regexfree.k55.io/ )
# indentation
^\s*
# possibly commented out
(\/\/\ \s*)?
(
# define a variable, append to a variable, or define a single-arg arrow function
(export\ )? (let\ |const\ |readonly\ )? [a-zA-Z0-9_$.]+ (\ \+?=>?\ )
|
# define a property (oversize arrays are only allowed in properties)
[a-zA-Z0-9$]+:\ \[?
|
# optionally return or throw
(return\ |throw\ )?
# call a function or constructor
(new\ )?([a-zA-Z0-9$.]+\()?
)?
(
Utils\.html
|
(this\.)?(room\.)?tr
|
\$\(
)?
# start of string or regex
['"`\/]
*/

45
eslint.config.mjs Normal file
View File

@ -0,0 +1,45 @@
// @ts-check
import { configs, configure, globals } from './eslint-ps-standard.mjs';
export default configure([
{
name: "JavaScript",
files: [
'*.mjs', // look mom I'm linting myself!
'**/*.js',
],
extends: [configs.js],
languageOptions: {
globals: {
...globals.builtin,
...globals.node,
...globals.mocha,
},
},
},
{
name: "TypeScript",
files: [
"**/*.ts",
],
extends: [configs.ts],
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
"@typescript-eslint/no-require-imports": "off",
// used for documentation
"@typescript-eslint/no-redundant-type-constituents": "off",
},
},
{
name: "TypeScript tests",
rules: {
"@typescript-eslint/restrict-template-expressions": "off",
},
},
]);

2121
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
"license": "AGPL-3.0",
"main": "./.dist/src/index.js",
"scripts": {
"lint": "eslint . --cache --ext .js,.ts",
"lint": "eslint --cache --max-warnings 0",
"build": "npx tsc",
"run": "npx tsc && node .dist/src/",
"start": "npx tsc && npx pm2 start config/pm2.js",
@ -22,17 +22,17 @@
"testcontainers": "^9.1.1"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^4.0.1",
"@types/bcrypt": "^5.0.0",
"@types/mocha": "^5.2.6",
"@types/node": "^22.7.8",
"@types/pg": "^8.10.3",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"eslint": "^8.56.0",
"eslint-plugin-import": "^2.29.1",
"eslint": "^9.21.0",
"globals": "^16.0.0",
"mocha": "^6.0.2",
"nodemailer": "^6.6.5",
"typescript": "^5.3.3"
"typescript": "^5.7.3",
"typescript-eslint": "^8.25.0"
},
"private": true
}

View File

@ -4,20 +4,20 @@
* By Mia
* @author mia-pi-git
*/
import {promises as fs, readFileSync, watchFile} from 'fs';
import { promises as fs, readFileSync, watchFile } from 'fs';
import * as pathModule from 'path';
import * as crypto from 'crypto';
import * as url from 'url';
import {Config} from './config-loader';
import {Ladder, LadderEntry} from './ladder';
import {Replays} from './replays';
import {ActionError, QueryHandler, Server} from './server';
import {Session} from './user';
import { Config } from './config-loader';
import { Ladder, type LadderEntry } from './ladder';
import { Replays } from './replays';
import { ActionError, type QueryHandler, Server } from './server';
import { Session } from './user';
import {
toID, updateserver, bash, time, escapeHTML, signAsync, TimeSorter,
} from './utils';
import * as tables from './tables';
import {SQL} from './database';
import { SQL } from './database';
import IPTools from './ip-tools';
export interface Suspect {
@ -80,22 +80,22 @@ const redundantFetch = async (targetUrl: string, data: RequestInit, attempts = 0
}
};
export const smogonFetch = async (targetUrl: string, method: string, data: {[k: string]: any}) => {
export const smogonFetch = async (targetUrl: string, method: string, data: { [k: string]: any }) => {
const bodyText = JSON.stringify(data);
const hash = await signAsync("RSA-SHA1", bodyText, Config.privatekey);
return redundantFetch(`https://www.smogon.com/${targetUrl}`, {
method,
body: new URLSearchParams({data: bodyText, hash}),
body: new URLSearchParams({ data: bodyText, hash }),
});
};
export function checkSuspectVerified(
rating: LadderEntry,
suspect: Suspect,
suspect: Suspect
) {
let reqsMet = 0;
let reqCount = 0;
const userData: Partial<{elo: number, gxe: number, coil: number}> = {};
const userData: Partial<{ elo: number, gxe: number, coil: number }> = {};
const reqKeys = ['elo', 'coil', 'gxe'] as const;
for (const k of reqKeys) {
if (!suspect[k]) continue;
@ -103,14 +103,14 @@ export function checkSuspectVerified(
switch (k) {
case 'coil':
const N = rating.w + rating.l + rating.t;
const coilNum = Math.round(40.0 * rating.gxe * Math.pow(2.0, -coil[suspect.formatid] / N));
const coilNum = Math.round(40.0 * rating.gxe * (2.0 ** (-coil[suspect.formatid] / N)));
if (coilNum >= suspect.coil!) {
reqsMet++;
}
userData.coil = coilNum;
break;
case 'elo': case 'gxe':
if (suspect[k] && rating[k] >= suspect[k]!) {
if (suspect[k] && rating[k] >= suspect[k]) {
reqsMet++;
}
userData[k] = rating[k];
@ -127,7 +127,7 @@ export function checkSuspectVerified(
userid: rating.userid,
format: suspect.formatid,
reqs: {
required: {elo: suspect.elo, gxe: suspect.gxe, coil: suspect.coil},
required: { elo: suspect.elo, gxe: suspect.gxe, coil: suspect.coil },
actual: userData,
},
suspectStartDate: suspect.start_date,
@ -137,10 +137,10 @@ export function checkSuspectVerified(
return false;
}
export const actions: {[k: string]: QueryHandler} = {
export const actions: { [k: string]: QueryHandler } = {
async register(params) {
this.verifyCrossDomainRequest();
const {username, password, cpassword, captcha} = params;
const { username, password, cpassword, captcha } = params;
if (!username) {
throw new ActionError(`You must specify a username.`);
}
@ -182,7 +182,7 @@ export const actions: {[k: string]: QueryHandler} = {
return {
assertion,
actionsuccess: !assertion.startsWith(';'),
curuser: {loggedin: true, username, userid},
curuser: { loggedin: true, username, userid },
};
},
@ -191,10 +191,10 @@ export const actions: {[k: string]: QueryHandler} = {
this.request.method !== "POST" || !params.userid ||
params.userid !== this.user.id || this.user.id === 'guest'
) {
return {actionsuccess: false};
return { actionsuccess: false };
}
await this.session.logout(true);
return {actionsuccess: true};
return { actionsuccess: true };
},
async login(params) {
@ -212,7 +212,7 @@ export const actions: {[k: string]: QueryHandler} = {
}
const challengekeyid = parseInt(params.challengekeyid!) || -1;
const actionsuccess = await this.session.login(params.name, params.pass);
if (!actionsuccess) return {actionsuccess, assertion: false};
if (!actionsuccess) return { actionsuccess, assertion: false };
const challenge = params.challstr || params.challenge || "";
const assertion = await this.session.getAssertion(
userid, challengekeyid, null, challenge, challengeprefix
@ -221,7 +221,7 @@ export const actions: {[k: string]: QueryHandler} = {
return {
actionsuccess: true,
assertion,
curuser: {loggedin: true, username: params.name, userid},
curuser: { loggedin: true, username: params.name, userid },
};
},
@ -231,7 +231,7 @@ export const actions: {[k: string]: QueryHandler} = {
const date = parseInt(params.date!);
const usercount = parseInt(params.users! || params.usercount!);
if (isNaN(date) || isNaN(usercount)) {
return {actionsuccess: false};
return { actionsuccess: false };
}
await tables.userstats.replace({
@ -239,14 +239,14 @@ export const actions: {[k: string]: QueryHandler} = {
});
if (server.id === Config.mainserver) {
await tables.userstatshistory.insert({date, usercount});
await tables.userstatshistory.insert({ date, usercount });
}
return {actionsuccess: true};
return { actionsuccess: true };
},
async upkeep(params) {
const challengeprefix = this.verifyCrossDomainRequest();
const res = {assertion: '', username: '', loggedin: false};
const res = { assertion: '', username: '', loggedin: false };
const curuser = this.user;
let userid = '';
if (curuser.id !== 'guest') {
@ -277,7 +277,7 @@ export const actions: {[k: string]: QueryHandler} = {
const server = await this.getServer(true);
if (!server) {
// legacy error
return {errorip: this.getIp()};
return { errorip: this.getIp() };
}
// the server must send all the required values
@ -317,7 +317,7 @@ export const actions: {[k: string]: QueryHandler} = {
});
this.setPrefix(''); // No need for prefix since only usable by server.
return {replayid: out};
return { replayid: out };
},
prepreplay() {
@ -335,9 +335,9 @@ export const actions: {[k: string]: QueryHandler} = {
const cssfile = pathModule.join(process.env.CSS_DIR || Config.cssdir, `/${server['id']}.css`);
try {
await fs.unlink(cssfile);
return {actionsuccess: true};
} catch (err) {
return {actionsuccess: false};
return { actionsuccess: true };
} catch {
return { actionsuccess: false };
}
},
@ -369,7 +369,7 @@ export const actions: {[k: string]: QueryHandler} = {
throw new ActionError('Your new password must be at least 5 characters long.');
}
const actionsuccess = await this.session.changePassword(this.user.id, params.password);
return {actionsuccess};
return { actionsuccess };
},
async changeusername(params) {
@ -390,7 +390,7 @@ export const actions: {[k: string]: QueryHandler} = {
username: params.username,
});
await this.session.setSid();
return {actionsuccess};
return { actionsuccess };
},
async getassertion(params) {
@ -412,7 +412,7 @@ export const actions: {[k: string]: QueryHandler} = {
const server = await this.getServer(true);
if (server?.id !== Config.mainserver) {
// legacy error
return {errorip: this.getIp()};
return { errorip: this.getIp() };
}
const formatid = toID(params.format);
@ -422,7 +422,7 @@ export const actions: {[k: string]: QueryHandler} = {
if (!Ladder.isValidPlayer(params.p1)) return 0;
if (!Ladder.isValidPlayer(params.p2)) return 0;
const out: {[k: string]: any} = {};
const out: { [k: string]: any } = {};
const [p1rating, p2rating] = await ladder.addMatch(params.p1!, params.p2!, parseFloat(params.score));
const suspect = await tables.suspects.get(formatid);
@ -446,7 +446,7 @@ export const actions: {[k: string]: QueryHandler} = {
const user = Ladder.isValidPlayer(params.user);
if (!user) throw new ActionError("Invalid username.");
const ratings = await Ladder.getAllRatings(user) as (LadderEntry & {suspect?: boolean})[];
const ratings = await Ladder.getAllRatings(user) as (LadderEntry & { suspect?: boolean })[];
for (const rating of ratings) {
const suspect = await tables.suspects.get(rating.formatid);
if (suspect) {
@ -460,7 +460,7 @@ export const actions: {[k: string]: QueryHandler} = {
const server = await this.getServer(true);
if (server?.id !== Config.mainserver) {
// legacy error
return {errorip: "This ladder is not for your server. You should turn off Config.remoteladder."};
return { errorip: "This ladder is not for your server. You should turn off Config.remoteladder." };
}
if (!toID(params.format)) throw new ActionError("Specify a format.");
const ladder = new Ladder(params.format!);
@ -485,7 +485,7 @@ export const actions: {[k: string]: QueryHandler} = {
if (stderr) throw new ActionError(`Compilation failed:\n${stderr}`);
[, , stderr] = await bash('npx pm2 reload loginserver');
if (stderr) throw new ActionError(stderr);
return {updated: update, success: true};
return { updated: update, success: true };
},
async rebuildclient(params) {
@ -509,7 +509,7 @@ export const actions: {[k: string]: QueryHandler} = {
`sudo -u www-data node build${params.full ? ' full' : ''}`, Config.clientpath
);
if (update[0]) throw new ActionError(`Compilation failed:\n${update.join(',')}`);
return {updated: update, success: true};
return { updated: update, success: true };
},
async updatenamecolor(params) {
@ -537,8 +537,8 @@ export const actions: {[k: string]: QueryHandler} = {
try {
const content = await fs.readFile(Config.colorpath, 'utf-8');
Object.assign(colors, JSON.parse(content));
} catch (e) {
throw new ActionError(`Could not read color file (${e})`);
} catch (err) {
throw new ActionError(`Could not read color file (${err as any})`);
}
let entry = '';
if (!('source' in params)) {
@ -561,7 +561,7 @@ export const actions: {[k: string]: QueryHandler} = {
userid, actorid: by, date: time(), ip: this.getIp(), entry,
});
return {success: true};
return { success: true };
},
async updatecoil(params) {
@ -571,7 +571,7 @@ export const actions: {[k: string]: QueryHandler} = {
if (!formatid) {
throw new ActionError('No format was specified.');
}
const source = parseFloat(params.coil_b + "");
const source = parseFloat(`${params.coil_b!}`);
if ('coil_b' in params && (isNaN(source) || !source || source < 1)) {
throw new ActionError('No B value was specified.');
}
@ -589,7 +589,7 @@ export const actions: {[k: string]: QueryHandler} = {
}
await fs.writeFile(Config.coilpath, JSON.stringify(coil));
return {success: true};
return { success: true };
},
async setstanding(params) {
@ -626,7 +626,7 @@ export const actions: {[k: string]: QueryHandler} = {
ip: this.getIp(),
entry: `Standing changed to ${standing} (${Config.standings[standing]}): ${params.reason}`,
});
return {success: true};
return { success: true };
},
async ipstanding(params) {
@ -651,8 +651,8 @@ export const actions: {[k: string]: QueryHandler} = {
throw new ActionError("Invalid standing.");
}
const matches = await tables.users.selectAll(['userid'])`WHERE ip = ${ip}`;
for (const {userid} of matches) {
await tables.users.update(userid, {banstate: standing});
for (const { userid } of matches) {
await tables.users.update(userid, { banstate: standing });
await tables.usermodlog.insert({
actorid: actor,
userid,
@ -661,7 +661,7 @@ export const actions: {[k: string]: QueryHandler} = {
entry: `Standing changed to ${standing} (${Config.standings[standing]}): ${params.reason}`,
});
}
return {success: matches.length};
return { success: matches.length };
},
async ipmatches(params) {
@ -717,16 +717,16 @@ export const actions: {[k: string]: QueryHandler} = {
if (existing) {
if (Date.now() - existing.time > OAUTH_TOKEN_TIME) { // 2w
await tables.oauthTokens.delete(existing.id);
return {success: false};
return { success: false };
} else {
return {success: existing.id};
return { success: existing.id };
}
}
const id = crypto.randomBytes(16).toString('hex');
await tables.oauthTokens.insert({
id, owner: this.user.id, client: clientInfo.id, time: Date.now(),
});
return {success: id, expires: Date.now() + OAUTH_TOKEN_TIME};
return { success: id, expires: Date.now() + OAUTH_TOKEN_TIME };
},
async 'oauth/api/refreshtoken'(params) {
@ -738,14 +738,14 @@ export const actions: {[k: string]: QueryHandler} = {
}
const tokenEntry = await tables.oauthTokens.get(token);
if (!tokenEntry) {
return {success: false};
return { success: false };
}
const id = crypto.randomBytes(16).toString('hex');
await tables.oauthTokens.insert({
id, owner: tokenEntry.owner, client: clientInfo.id, time: Date.now(),
});
await tables.oauthTokens.delete(tokenEntry.id);
return {success: id, expires: Date.now() + OAUTH_TOKEN_TIME};
return { success: id, expires: Date.now() + OAUTH_TOKEN_TIME };
},
// validate assertion & get token if it's valid
@ -762,11 +762,11 @@ export const actions: {[k: string]: QueryHandler} = {
}
const tokenEntry = await tables.oauthTokens.get(token);
if (!tokenEntry || tokenEntry.id !== token) {
return {success: false};
return { success: false };
}
if ((Date.now() - tokenEntry.time) > OAUTH_TOKEN_TIME) { // 2w
await tables.oauthTokens.delete(tokenEntry.id);
return {success: false};
return { success: false };
}
this.user.login(tokenEntry.owner);
return this.session.getAssertion(
@ -791,7 +791,7 @@ export const actions: {[k: string]: QueryHandler} = {
for (const token of tokens) {
const client = await tables.oauthClients.get(token.client);
if (!client) throw new Error("Tokens exist for nonexistent application");
applications.push({title: client.client_title, url: client.origin_url});
applications.push({ title: client.client_title, url: client.origin_url });
}
return {
username: this.user.id,
@ -815,13 +815,13 @@ export const actions: {[k: string]: QueryHandler} = {
throw new ActionError("That application doesn't have access granted to your account.");
}
await tables.oauthTokens.deleteAll()`WHERE client = ${client.id} and owner = ${this.user.id}`;
return {success: true};
return { success: true };
},
async getteams(params) {
this.verifyCrossDomainRequest();
if (!this.user.loggedIn || this.user.id === 'guest') {
return {teams: []}; // don't wanna nag people with popups if they aren't logged in
return { teams: [] }; // don't wanna nag people with popups if they aren't logged in
}
let teams = [];
try {
@ -844,13 +844,13 @@ export const actions: {[k: string]: QueryHandler} = {
// and fetch the team later
t.team = mons.join(',');
}
return {teams};
return { teams };
},
async getteam(params) {
if (!this.user.loggedIn || this.user.id === 'guest') {
throw new ActionError("Access denied");
}
let {teamid} = params;
let { teamid } = params;
teamid = toID(teamid);
if (!teamid) {
throw new ActionError("Invalid team ID");
@ -860,7 +860,7 @@ export const actions: {[k: string]: QueryHandler} = {
SQL`ownerid, team, private as privacy`
)`WHERE teamid = ${teamid}`;
if (!data || data.ownerid !== this.user.id) {
return {team: null};
return { team: null };
}
return data;
} catch (e) {
@ -887,7 +887,7 @@ export const actions: {[k: string]: QueryHandler} = {
const page = Number(params.page || '1');
const before = Number(params.before) || undefined;
if (isNaN(page) || page !== Math.trunc(page) || page <= 0) {
throw new ActionError(`Invalid page number: ${params.page}`);
throw new ActionError(`Invalid page number: ${params.page!}`);
}
if (params.page && before) {
throw new ActionError(`Cannot set both "page" and "before", please choose one method of pagination`);
@ -901,7 +901,7 @@ export const actions: {[k: string]: QueryHandler} = {
}
const search = {
usernames: usernames,
usernames,
format: toID(params.format),
page,
before,
@ -924,7 +924,7 @@ export const actions: {[k: string]: QueryHandler} = {
const page = Number(params.page || '1');
const before = Number(params.before) || undefined;
if (isNaN(page) || page !== Math.trunc(page) || page <= 0) {
throw new ActionError(`Invalid page number: ${params.page}`);
throw new ActionError(`Invalid page number: ${params.page!}`);
}
if (params.page && before) {
throw new ActionError(`Cannot set both "page" and "before", please choose one method of pagination`);
@ -937,13 +937,13 @@ export const actions: {[k: string]: QueryHandler} = {
this.response.setHeader('Content-Type', 'application/json');
try {
return JSON.stringify(await Replays.fullSearch(params.contains));
} catch (e) {
} catch {
throw new ActionError(`Could not search (timeout?)`);
}
}
const search = {
usernames: usernames,
usernames,
format: toID(params.format),
page,
before,
@ -970,7 +970,7 @@ export const actions: {[k: string]: QueryHandler} = {
const page = Number(params.page || '1');
const before = Number(params.before) || null;
if (isNaN(page) || page !== Math.trunc(page) || page <= 0) {
throw new ActionError(`Invalid page number: ${params.page}`);
throw new ActionError(`Invalid page number: ${params.page!}`);
}
if (params.page && before) {
throw new ActionError(`Cannot set both "page" and "before", please choose one method of pagination`);
@ -1023,7 +1023,7 @@ export const actions: {[k: string]: QueryHandler} = {
});
break;
}
return {password: pw};
return { password: pw };
},
// sent by ps server
@ -1066,7 +1066,7 @@ export const actions: {[k: string]: QueryHandler} = {
times.merge(await Config.getuserips(userid));
}
return {ips: times.toJSON()};
return { ips: times.toJSON() };
},
async 'suspects/add'(params) {
@ -1095,7 +1095,7 @@ export const actions: {[k: string]: QueryHandler} = {
try {
await smogonFetch("tools/api/suspect-create", "POST", {
url: params.url,
date: start + "",
date: `${start}`,
reqs,
format: id,
});
@ -1118,7 +1118,7 @@ export const actions: {[k: string]: QueryHandler} = {
coil: reqs.coil || null,
});
}
return {success: true};
return { success: true };
},
async 'suspects/edit'(params) {
if (this.getIp() !== Config.restartip) {
@ -1154,7 +1154,7 @@ export const actions: {[k: string]: QueryHandler} = {
format: id,
reqs,
});
return {success: true};
return { success: true };
},
async 'suspects/end'(params) {
if (this.getIp() !== Config.restartip) {
@ -1169,7 +1169,7 @@ export const actions: {[k: string]: QueryHandler} = {
formatid: id,
time: suspect.start_date,
});
return {success: true};
return { success: true };
},
async 'suspects/verify'(params) {
if (this.getIp() !== Config.restartip) {

View File

@ -6,6 +6,7 @@
import * as fs from 'fs';
import * as path from 'path';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore no typedef file
import * as defaults from '../config/config-example';
@ -14,22 +15,22 @@ export type Configuration = typeof defaults;
export function load(invalidate = false): Configuration {
const configPath = path.resolve(__dirname + "/../../", (process.argv[2] || process.env.CONFIG_PATH || ""));
if (invalidate) delete require.cache[configPath];
let config = {...defaults};
let config = { ...defaults };
try {
config = {...config, ...require(configPath)};
config = { ...config, ...require(configPath) };
} catch (err: any) {
if (err.code !== 'MODULE_NOT_FOUND') throw err; // Should never happen
if (process.env.IS_TEST) return config; // should not need this for tests
console.log("No config specified in process.argv or process.env - loading default settings...");
return {...config};
return { ...config };
}
return {...config};
return { ...config };
}
export const Config: Configuration = load();
if (Config.watchconfig) {
fs.watchFile(require.resolve('../../config/config'), () => {
Object.assign(Config, {...load(true)});
Object.assign(Config, { ...load(true) });
});
}

View File

@ -10,8 +10,7 @@ import * as mysql from 'mysql2';
import * as pg from 'pg';
export type BasicSQLValue = string | number | null;
// eslint-disable-next-line
export type SQLRow = {[k: string]: BasicSQLValue};
export type SQLRow = { [k: string]: BasicSQLValue };
export type SQLValue = BasicSQLValue | SQLStatement | PartialOrSQL<SQLRow> | BasicSQLValue[] | undefined;
export class SQLStatement {
@ -113,7 +112,7 @@ export function SQL(strings: TemplateStringsArray, ...values: SQLValue[]) {
return new SQLStatement(strings, values);
}
export interface ResultRow {[k: string]: BasicSQLValue}
export interface ResultRow { [k: string]: BasicSQLValue }
export const connectedDatabases: Database[] = [];
@ -242,7 +241,7 @@ export class DatabaseTable<Row, DB extends Database> {
eval<T>():
(strings: TemplateStringsArray, ...rest: SQLValue[]) => Promise<T | undefined> {
return (strings, ...rest) =>
this.queryOne<{result: T}>(
this.queryOne<{ result: T }>(
)`SELECT ${new SQLStatement(strings, rest)} AS result FROM "${this.name}" LIMIT 1`
.then(row => row?.result);
}
@ -298,10 +297,10 @@ export class DatabaseTable<Row, DB extends Database> {
export class MySQLDatabase extends Database<mysql.Pool, mysql.OkPacket> {
override type = 'mysql' as const;
constructor(config: mysql.PoolOptions & {prefix?: string}) {
constructor(config: mysql.PoolOptions & { prefix?: string }) {
const prefix = config.prefix || "";
if (config.prefix) {
config = {...config};
config = { ...config };
delete config.prefix;
}
super(mysql.createPool(config), prefix);
@ -312,7 +311,7 @@ export class MySQLDatabase extends Database<mysql.Pool, mysql.OkPacket> {
for (let i = 0; i < query.values.length; i++) {
const value = query.values[i];
if (query.sql[i + 1].startsWith('`') || query.sql[i + 1].startsWith('"')) {
sql = sql.slice(0, -1) + this.escapeId('' + value) + query.sql[i + 1].slice(1);
sql = sql.slice(0, -1) + this.escapeId(`${value as any}`) + query.sql[i + 1].slice(1);
} else {
sql += '?' + query.sql[i + 1];
values.push(value);
@ -324,7 +323,7 @@ export class MySQLDatabase extends Database<mysql.Pool, mysql.OkPacket> {
return new Promise((resolve, reject) => {
this.connection.query(query, values, (e, results: any) => {
if (e) {
return reject(new Error(`${e.message} (${query}) (${values}) [${e.code}]`));
return reject(new Error(`${e.message} (${query}) (${values as any}) [${e.code}]`));
}
if (Array.isArray(results)) {
for (const row of results) {
@ -345,7 +344,7 @@ export class MySQLDatabase extends Database<mysql.Pool, mysql.OkPacket> {
}
}
export class PGDatabase extends Database<pg.Pool, {affectedRows: number | null}> {
export class PGDatabase extends Database<pg.Pool, { affectedRows: number | null }> {
override type = 'pg' as const;
constructor(config: pg.PoolConfig) {
super(new pg.Pool(config));
@ -357,7 +356,7 @@ export class PGDatabase extends Database<pg.Pool, {affectedRows: number | null}>
for (let i = 0; i < query.values.length; i++) {
const value = query.values[i];
if (query.sql[i + 1].startsWith('`') || query.sql[i + 1].startsWith('"')) {
sql = sql.slice(0, -1) + this.escapeId('' + value) + query.sql[i + 1].slice(1);
sql = sql.slice(0, -1) + this.escapeId(`${value as any}`) + query.sql[i + 1].slice(1);
} else {
paramCount++;
sql += `$${paramCount}` + query.sql[i + 1];
@ -370,7 +369,7 @@ export class PGDatabase extends Database<pg.Pool, {affectedRows: number | null}>
return this.connection.query(query, values).then(res => res.rows);
}
override _queryExec(query: string, values: BasicSQLValue[]) {
return this.connection.query<never>(query, values).then(res => ({affectedRows: res.rowCount}));
return this.connection.query<never>(query, values).then(res => ({ affectedRows: res.rowCount }));
}
override escapeId(id: string) {
// @ts-expect-error @types/pg really needs to be updated

View File

@ -1,10 +1,10 @@
/**
* Initialization.
*/
import {Server} from './server';
import { Server } from './server';
export const server = new Server();
import {connectedDatabases} from './database';
import { connectedDatabases } from './database';
console.log(`Server listening on ${server.port}`);

View File

@ -3,16 +3,15 @@
*/
export const IPTools = new class {
privateRelayIPs: {minIP: number; maxIP: number}[] = [];
// eslint-disable-next-line max-len
privateRelayIPs: { minIP: number, maxIP: number }[] = [];
readonly ipRegex = /^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$/;
getCidrRange(cidr: string): {minIP: number; maxIP: number} | null {
getCidrRange(cidr: string): { minIP: number, maxIP: number } | null {
if (!cidr) return null;
const index = cidr.indexOf('/');
if (index <= 0) {
const ip = IPTools.ipToNumber(cidr);
if (ip === null) return null;
return {minIP: ip, maxIP: ip};
return { minIP: ip, maxIP: ip };
}
const low = IPTools.ipToNumber(cidr.slice(0, index));
const bits = this.parseExactInt(cidr.slice(index + 1));
@ -20,7 +19,7 @@ export const IPTools = new class {
// does << with signed int32s.
if (low === null || !bits || bits < 2 || bits > 32) return null;
const high = low + (1 << (32 - bits)) - 1;
return {minIP: low, maxIP: high};
return { minIP: low, maxIP: high };
}
parseExactInt(str: string): number {
if (!/^-?(0|[1-9][0-9]*)$/.test(str)) return NaN;

View File

@ -5,8 +5,8 @@
* @author Zarel, mia-pi-git
*/
import {toID, time} from './utils';
import {ladder} from './tables';
import { toID, time } from './utils';
import { ladder } from './tables';
export interface LadderEntry {
entryid: number;
@ -269,7 +269,7 @@ export class Ladder {
const exp = ((1500 - glicko.rating) / 400 / Math.sqrt(1 + 0.0000100724 * (glicko.rd * glicko.rd + 130 * 130)));
rating.gxe = Number((
100 / (1 + Math.pow(10, exp))
100 / (1 + (10 ** exp))
).toFixed(1));
// if ($newM) {
@ -286,7 +286,7 @@ export class Ladder {
// }
// }
if (offset) {
rating.rpdata += '##' + offset;
rating.rpdata += `##${offset}`;
}
if (newM) {
@ -302,7 +302,7 @@ export class Ladder {
} else if (elo > 1300) {
K = 40;
}
const E = 1 / (1 + Math.pow(10, (newMelo - elo) / 400));
const E = 1 / (1 + (10 ** ((newMelo - elo) / 400)));
elo += K * (newM.score - E);
if (elo < 1000) elo = 1000;
@ -389,7 +389,7 @@ export class GlickoPlayer {
if (m.length === 0) {
const RD = Math.sqrt((this.rd * this.rd) + (this.c * this.c));
return {R: this.rating, RD};
return { R: this.rating, RD };
}
let A = 0.0;
@ -416,7 +416,7 @@ export class GlickoPlayer {
RD = this.RDmin;
}
return {R, RD};
return { R, RD };
}
g(RD: number) {
@ -424,6 +424,6 @@ export class GlickoPlayer {
}
E(R: number, rJ: number, rdJ: number) {
return 1.0 / (1.0 + Math.pow(10.0, -this.g(rdJ) * (R - rJ) / 400.0));
return 1.0 / (1.0 + (10.0 ** (-this.g(rdJ) * (R - rJ) / 400.0)));
}
}

View File

@ -5,12 +5,11 @@
* Ported to TypeScript by Annika and Mia.
* Ported to Postgres by Zarel.
*/
import {toID, time} from './utils';
import {replayPlayers, replays} from './tables';
import {SQL} from './database';
import { toID, time } from './utils';
import { replayPlayers, replays } from './tables';
import { SQL } from './database';
// must be a type and not an interface to qualify as an SQLRow
// eslint-disable-next-line
export type ReplayRow = {
id: string,
format: string,
@ -120,14 +119,14 @@ export const Replays = new class {
const replayData = await replays.get(id);
if (!replayData) return null;
await replays.update(replayData.id, {views: SQL`views + 1`});
await replays.update(replayData.id, { views: SQL`views + 1` });
return this.toReplay(replayData);
}
async edit(replay: Replay) {
const replayData = this.toReplayRow(replay);
await replays.update(replay.id, {private: replayData.private, password: replayData.password});
await replays.update(replay.id, { private: replayData.private, password: replayData.password });
}
generatePassword(length = 31) {
@ -222,7 +221,7 @@ export const Replays = new class {
ORDER BY uploadtime DESC LIMIT 50;`.then(this.toReplays);
}
recent(args?: {before?: number}) {
recent(args?: { before?: number }) {
if (args?.before) {
return replays.selectAll(
SQL`uploadtime, id, format, players, rating`

View File

@ -8,11 +8,11 @@ import * as https from 'https';
import * as child from 'child_process';
import * as dns from 'dns';
import * as fs from 'fs';
import {toID, md5} from './utils';
import {Config} from './config-loader';
import {actions} from './actions';
import {User, Session} from './user';
import {URLSearchParams} from 'url';
import { toID, md5 } from './utils';
import { Config } from './config-loader';
import { actions } from './actions';
import { type User, Session } from './user';
import { URLSearchParams } from 'url';
import IPTools from './ip-tools';
/**
@ -72,7 +72,7 @@ export interface RegisteredServer {
export type QueryHandler = (
this: ActionContext, params: ActionRequest
) => {[k: string]: any} | string | Promise<{[k: string]: any} | string>;
) => { [k: string]: any } | string | Promise<{ [k: string]: any } | string>;
export class ActionContext {
readonly request: http.IncomingMessage;
@ -108,12 +108,12 @@ export class ActionContext {
this.user = await this.session.getUser();
const result = await handler.call(this, body);
if (result === null) return {code: 404};
if (result === null) return { code: 404 };
return result;
} catch (e: any) {
if (e?.name?.endsWith('ActionError')) {
return {actionerror: e.message};
return { actionerror: e.message };
}
for (const k of ['pass', 'password']) delete body[k];
@ -131,7 +131,7 @@ export class ActionContext {
return body;
}
static sanitizeBody(body: any): ActionRequest {
if (typeof body === 'string') return {act: body};
if (typeof body === 'string') return { act: body };
if (typeof body !== 'object') throw new ActionError("Body must be an object or string", 400);
if (!('act' in body)) body.act = ''; // we'll let the action handler throw the error
for (const k in body) {
@ -140,7 +140,7 @@ export class ActionContext {
return body as ActionRequest;
}
static async getBody(req: http.IncomingMessage): Promise<ActionRequest | ActionRequest[]> {
let result: {[k: string]: any} = this.parseURLRequest(req);
let result: { [k: string]: any } = this.parseURLRequest(req);
let json;
const bodyData = await this.getRequestBody(req);
@ -162,7 +162,7 @@ export class ActionContext {
try {
const jsonResult = JSON.parse(json);
if (Array.isArray(jsonResult)) {
return jsonResult.map(body => this.sanitizeBody({...result, ...body}));
return jsonResult.map(body => this.sanitizeBody({ ...result, ...body }));
} else {
result = Object.assign(result, jsonResult);
}
@ -223,7 +223,7 @@ export class ActionContext {
if (this._ip) return this._ip;
let ip = this.request.socket.remoteAddress || "";
if (this.isTrustedProxy(ip)) {
const ips = ((this.request.headers['x-forwarded-for'] || '') + "").split(',').reverse();
const ips = `${this.request.headers['x-forwarded-for'] as any || ''}`.split(',').reverse();
for (let proxy of ips) {
proxy = proxy.trim();
if (!this.isTrustedProxy(proxy)) {
@ -260,7 +260,7 @@ export class ActionContext {
}
export const SimServers = new class SimServersT {
servers: {[k: string]: RegisteredServer} = this.loadServers();
servers: { [k: string]: RegisteredServer } = this.loadServers();
hostCache = new Map<string, string>();
constructor() {
fs.watchFile(Config.serverlist, (curr, prev) => {
@ -307,7 +307,7 @@ export const SimServers = new class SimServersT {
}
return server;
}
loadServers(path = Config.serverlist): {[k: string]: RegisteredServer} {
loadServers(path = Config.serverlist): { [k: string]: RegisteredServer } {
if (!path) return {};
try {
const stdout = child.execFileSync(
@ -344,8 +344,8 @@ export class Server {
return console.log(`${source} crashed`, error, details);
}
try {
const {crashlogger} = require(Config.pspath);
crashlogger(error, source, {...details, date: new Date().toISOString()}, Config.crashguardemail);
const { crashlogger } = require(Config.pspath);
crashlogger(error, source, { ...details, date: new Date().toISOString() }, Config.crashguardemail);
} catch (e) {
// don't have data/pokemon-showdown built? something else went wrong? oh well
console.log('CRASH', error);
@ -388,7 +388,7 @@ export class Server {
if (e.httpStatus) {
res.writeHead(e.httpStatus).end('Error: ' + e.message);
} else {
res.writeHead(200).end(this.stringify({actionerror: e.message}));
res.writeHead(200).end(this.stringify({ actionerror: e.message }));
}
} else {
Server.crashlog(e);

View File

@ -1,12 +1,12 @@
/**
* Login server database tables
*/
import {MySQLDatabase, PGDatabase} from './database';
import {Config} from './config-loader';
import { MySQLDatabase, PGDatabase } from './database';
import { Config } from './config-loader';
import type {LadderEntry} from './ladder';
import type {ReplayRow} from './replays';
import type {Suspect} from './actions';
import type { LadderEntry } from './ladder';
import type { ReplayRow } from './replays';
import type { Suspect } from './actions';
// direct access
export const psdb = new MySQLDatabase(Config.mysql);
@ -15,133 +15,133 @@ export const replaysDB = Config.replaysdb ? new PGDatabase(Config.replaysdb) : p
export const ladderDB = Config.ladderdb ? new MySQLDatabase(Config.ladderdb!) : psdb;
export const users = psdb.getTable<{
userid: string;
usernum: number;
username: string;
nonce: string | null;
passwordhash: string | null;
email: string | null;
registertime: number;
userid: string,
usernum: number,
username: string,
nonce: string | null,
passwordhash: string | null,
email: string | null,
registertime: number,
/**
* 0 = unregistered (should never be in db)
* 1 = regular user
* 2 = admin
* 3...6 = PS-specific ranks (voice, driver, mod, leader)
*/
group: number;
banstate: number;
ip: string;
avatar: number;
logintime: number;
loginip: string | null;
group: number,
banstate: number,
ip: string,
avatar: number,
logintime: number,
loginip: string | null,
}>('users', 'userid');
export const ladder = ladderDB.getTable<
LadderEntry
LadderEntry
>('ladder', 'entryid');
export const replayPrep = replaysDB.getTable<{
id: string;
format: string;
players: string;
id: string,
format: string,
players: string,
/**
* 0 = public
* 1 = private (with password)
* 2 = private (no password; used for punishment logging)
* 3 = NOT USED; only used in the full replay table
*/
private: 0 | 1 | 2;
loghash: string;
inputlog: string;
rating: number;
uploadtime: number;
private: 0 | 1 | 2,
loghash: string,
inputlog: string,
rating: number,
uploadtime: number,
}>('replayprep', 'id');
export const replays = replaysDB.getTable<
ReplayRow
ReplayRow
>('replays', 'id');
export const replayPlayers = replaysDB.getTable<{
playerid: string;
formatid: string;
id: string;
rating: number | null;
uploadtime: number;
private: ReplayRow['private'];
password: string | null;
format: string;
playerid: string,
formatid: string,
id: string,
rating: number | null,
uploadtime: number,
private: ReplayRow['private'],
password: string | null,
format: string,
/** comma-delimited player names */
players: string;
players: string,
}>('replayplayers');
export const sessions = psdb.getTable<{
session: number;
sid: string;
userid: string;
time: number;
timeout: number;
ip: string;
session: number,
sid: string,
userid: string,
time: number,
timeout: number,
ip: string,
}>('sessions', 'session');
export const userstats = psdb.getTable<{
id: number;
serverid: string;
usercount: number;
date: number;
id: number,
serverid: string,
usercount: number,
date: number,
}>('userstats', 'id');
export const loginthrottle = psdb.getTable<{
ip: string;
count: number;
time: number;
lastuserid: string;
ip: string,
count: number,
time: number,
lastuserid: string,
}>('loginthrottle', 'ip');
export const loginattempts = psdb.getTable<{
count: number;
time: number;
userid: string;
count: number,
time: number,
userid: string,
}>('loginattempts', 'userid');
export const usermodlog = psdb.getTable<{
entryid: number;
userid: string;
actorid: string;
date: number;
ip: string;
entry: string;
entryid: number,
userid: string,
actorid: string,
date: number,
ip: string,
entry: string,
}>('usermodlog', 'entryid');
export const userstatshistory = psdb.getTable<{
id: number;
date: number;
usercount: number;
programid: 'showdown' | 'po';
id: number,
date: number,
usercount: number,
programid: 'showdown' | 'po',
}>('userstatshistory', 'id');
// oauth stuff
export const oauthClients = psdb.getTable<{
owner: string; // ps username
client_title: string;
id: string; // hex hash
origin_url: string;
owner: string, // ps username
client_title: string,
id: string, // hex hash
origin_url: string,
}>('oauth_clients', 'id');
export const oauthTokens = psdb.getTable<{
owner: string;
client: string; // id of client
id: string;
time: number;
owner: string,
client: string, // id of client
id: string,
time: number,
}>('oauth_tokens', 'id');
export const teams = pgdb.getTable<{
teamid: string;
ownerid: string;
team: string;
format: string;
title: string;
private: number;
teamid: string,
ownerid: string,
team: string,
format: string,
title: string,
private: number,
}>('teams', 'teamid');
export const suspects = psdb.getTable<Suspect>("suspects", 'formatid');

View File

@ -3,15 +3,15 @@
* By Mia.
* @author mia-pi-git
*/
import {strict as assert} from 'assert';
import {Ladder} from '../ladder';
import {toID} from '../utils';
import { strict as assert } from 'assert';
import { Ladder } from '../ladder';
import { toID } from '../utils';
import * as utils from './test-utils';
import * as tables from '../tables';
const token = '42354y6dhgfdsretr';
describe('Loginserver actions', () => {
const server = utils.addServer({
/* const server = */ utils.addServer({
id: 'showdown',
name: 'Etheria',
port: 8000,
@ -20,7 +20,7 @@ describe('Loginserver actions', () => {
});
it('Should properly log userstats and userstats history', async () => {
const {result} = await utils.testDispatcher({
const { result } = await utils.testDispatcher({
act: 'updateuserstats',
users: '20',
date: `${Date.now()}`,
@ -36,7 +36,7 @@ describe('Loginserver actions', () => {
// erase the user so the test runs uncorrupted
await tables.users.delete('catra').catch(() => null);
const {result} = await utils.testDispatcher({
const { result } = await utils.testDispatcher({
act: 'register',
username: 'Catra',
password: 'applesauce',
@ -51,7 +51,7 @@ describe('Loginserver actions', () => {
});
it('Should log in a user', async () => {
const {result} = await utils.testDispatcher({
const { result } = await utils.testDispatcher({
act: 'login',
name: 'catra',
pass: 'applesauce',
@ -63,7 +63,7 @@ describe('Loginserver actions', () => {
});
it("should change the user's password", async () => {
const {result} = await utils.testDispatcher({
const { result } = await utils.testDispatcher({
act: 'changepassword',
username: 'Catra',
oldpassword: 'applesauce',
@ -112,7 +112,7 @@ describe('Loginserver actions', () => {
// clear their ratings entirely
await tables.ladder.deleteOne()`userid = ${id} AND formatid = ${'gen1randombattle'}`;
}
const {result} = await utils.testDispatcher({
const { result } = await utils.testDispatcher({
act: 'ladderupdate',
serverid: 'showdown',
servertoken: token,
@ -133,7 +133,7 @@ describe('Loginserver actions', () => {
await tables.ladder.deleteAll()`WHERE userid = ${toID(player)} AND formatid = ${ladder.formatid}`;
}
const [p1r, _p2r] = await ladder.addMatch(p1, p2, 1);
const {result} = await utils.testDispatcher({
const { result } = await utils.testDispatcher({
act: 'mmr',
format: 'gen5randombattle',
user: 'shera',

View File

@ -3,9 +3,9 @@
* By Mia.
* @author mia-pi-git
*/
import {strict as assert} from 'assert';
import {Config} from '../config-loader';
import {ActionContext, ActionError, SimServers} from '../server';
import { strict as assert } from 'assert';
import { Config } from '../config-loader';
import { ActionContext, ActionError, SimServers } from '../server';
import * as path from 'path';
import * as utils from './test-utils';
@ -33,6 +33,7 @@ describe('Dispatcher features', () => {
assert(cur);
assert(server.id === cur.id);
// invalidate the servertoken, we shouldn't find the server now
// eslint-disable-next-line require-atomic-updates
context.body.servertoken = '';
const result = await context.getServer(true).catch(e => e);
assert(result instanceof ActionError);
@ -44,7 +45,7 @@ describe('Dispatcher features', () => {
];
context.request.headers['origin'] = 'https://etheria.psim.us/';
let prefix = context.verifyCrossDomainRequest();
assert(prefix === 'server_', 'Wrong challengeprefix: ' + prefix);
assert(prefix === 'server_', `Wrong challengeprefix: ${prefix}`);
assert(context.response.hasHeader('Access-Control-Allow-Origin'), 'missing CORS header');
context.response.removeHeader('Access-Control-Allow-Origin');
@ -52,9 +53,9 @@ describe('Dispatcher features', () => {
context.setPrefix('');
prefix = context.verifyCrossDomainRequest();
assert(prefix === '', 'has improper challengeprefix: ' + prefix);
assert(prefix === '', `has improper challengeprefix: ${prefix}`);
const header = context.response.hasHeader('Access-Control-Allow-Origin');
assert(!header, 'has CORS header where it should not: ' + header);
assert(!header, `has CORS header where it should not: ${header}`);
});
it('Should support requesting /api/[action]', async () => {
const req = context.request;
@ -81,7 +82,7 @@ describe('Dispatcher features', () => {
id: 'showdown',
server: 'sim.psim.us',
port: 8000,
owner: 'mia'
owner: 'mia',
},
}, servers);
});

View File

@ -1,7 +1,7 @@
/**
* Test setup.
*/
import {start} from './mysql';
import { start } from './mysql';
import * as fs from 'fs';
import * as path from 'path';
before(async () => {

View File

@ -2,7 +2,7 @@
import * as tc from 'testcontainers';
import * as path from 'path';
import {Config} from '../config-loader';
import { Config } from '../config-loader';
/* HACK
Similar hack to postgresql.
@ -28,18 +28,29 @@ class MySQLReadyHack extends RegExp {
}
export class StartedMysqlContainer {
private container: tc.StartedTestContainer;
connectionInfo: {
address: {
host: string,
port: number,
},
user: string,
database: string,
};
constructor(
private container: tc.StartedTestContainer,
public connectionInfo = {
container: tc.StartedTestContainer,
connectionInfo?: StartedMysqlContainer['connectionInfo']
) {
this.container = container;
this.connectionInfo = connectionInfo ?? {
address: {
host: container.getHost(),
port: container.getMappedPort(3306),
},
user: 'test',
database: 'test',
}
) {
(Config.mysql as any) = connectionInfo;
};
(Config.mysql as any) = this.connectionInfo;
}
stop() {
@ -47,7 +58,7 @@ export class StartedMysqlContainer {
}
}
export type StartupFile = {src: string; dst: string} | string;
export type StartupFile = { src: string, dst: string } | string;
export interface StartOptions {
version?: number;
@ -74,7 +85,7 @@ export async function start(options: StartOptions = {}) {
const startupFiles = options.startupFiles ?? [];
const container = await new tc.GenericContainer(`mysql:${version}`)
.withExposedPorts(3306)
.withEnvironment({MYSQL_ALLOW_EMPTY_PASSWORD: "yes", MYSQL_DATABASE: "xenforo"})
.withEnvironment({ MYSQL_ALLOW_EMPTY_PASSWORD: "yes", MYSQL_DATABASE: "xenforo" })
.withWaitStrategy(tc.Wait.forLogMessage(new MySQLReadyHack))
.withCopyFilesToContainer(startupFiles.map(toTcCopyFile))
.start();

View File

@ -2,11 +2,11 @@
* Tests for replays.ts.
* @author Annika
*/
import {Replays, ReplayRow} from '../replays';
import {replayPrep, replays} from '../tables';
import {strict as assert} from 'assert';
import * as utils from './test-utils';
import {md5, stripNonAscii} from '../utils';
import { Replays } from '../replays';
import { replays } from '../tables';
import { strict as assert } from 'assert';
// import * as utils from './test-utils';
// import {md5, stripNonAscii} from '../utils';
(describe.skip)('Replay database manipulation', () => {
// it('should properly prepare replays', async () => {
@ -172,23 +172,23 @@ import {md5, stripNonAscii} from '../utils';
});
it('should support searching for replays by privacy', async () => {
const results = await search({isPrivate: true});
const results = await search({ isPrivate: true });
assert.deepEqual(results, ['searchtest1', 'searchtest2', 'searchtest3']);
});
it('should support searching for replays by format', async () => {
const results = await search({format: 'gen8ou'});
const results = await search({ format: 'gen8ou' });
assert.deepEqual(results, ['searchtest3', 'searchtest4']);
});
it('should support searching for replays by username', async () => {
const oneName = await search({username: 'somerandomreg'});
const oneName = await search({ username: 'somerandomreg' });
assert.deepEqual(oneName, ['searchtest1', 'searchtest2', 'searchtest3', 'searchtest4']);
const twoNames = await search({username: 'somerandomreg', username2: 'annikaskywalker'});
const twoNames = await search({ username: 'somerandomreg', username2: 'annikaskywalker' });
assert.deepEqual(twoNames, ['searchtest1']);
const reversed = await search({username: 'annikaskywalker', username2: 'somerandomreg'});
const reversed = await search({ username: 'annikaskywalker', username2: 'somerandomreg' });
assert.deepEqual(twoNames, reversed);
});
@ -200,10 +200,10 @@ import {md5, stripNonAscii} from '../utils';
});
it('should support different orderings', async () => {
const rating = await search({format: 'gen8anythinggoes', byRating: true});
const rating = await search({ format: 'gen8anythinggoes', byRating: true });
assert.deepEqual(rating, ['searchtest6', 'searchtest5']);
const uploadtime = await search({format: 'gen8anythinggoes'});
const uploadtime = await search({ format: 'gen8anythinggoes' });
assert.deepEqual(uploadtime, ['searchtest5', 'searchtest6']);
});

View File

@ -6,13 +6,13 @@
*/
import * as net from 'net';
import {IncomingMessage, ServerResponse} from 'http';
import {ActionContext, RegisteredServer, SimServers, ActionRequest} from '../server';
import { IncomingMessage, ServerResponse } from 'http';
import { ActionContext, type RegisteredServer, SimServers, type ActionRequest } from '../server';
import * as crypto from 'crypto';
import {strict as assert} from 'assert';
import {md5} from '../utils';
import { strict as assert } from 'assert';
import { md5 } from '../utils';
/** Removing this as it does not work, but could be useful for future reference.
/* Removing this as it does not work, but could be useful for future reference.
const commands = [
'docker run --name api-test -p 3308:3306 -e MYSQL_ROOT_PASSWORD=testpw -d mysql:latest',
];
@ -22,17 +22,17 @@ const config = {
user: 'root',
host: '127.0.0.1',
port: 3308,
};*/
}; */
export function makeDispatcher(body: ActionRequest, url?: string) {
const socket = new net.Socket();
const req = new IncomingMessage(socket);
if (body && !url) {
const params = Object.entries(body)
.filter(k => k[0] !== 'act')
.map(([k, v]) => `${k}=${v}`)
.join('&');
}
// if (body && !url) {
// const params = Object.entries(body)
// .filter(k => k[0] !== 'act')
// .map(([k, v]) => `${k}=${v!}`)
// .join('&');
// }
if (url) req.url = url;
return new ActionContext(req, new ServerResponse(req), body);
}
@ -57,7 +57,7 @@ export async function testDispatcher(
assert(false, e.message);
}
// we return context in case we need to do more
return {result, context};
return { result, context };
}
export async function randomBytes(size = 128) {

View File

@ -8,12 +8,12 @@
*/
import * as bcrypt from 'bcrypt';
import {Config} from './config-loader';
import { Config } from './config-loader';
import * as crypto from 'crypto';
import * as gal from 'google-auth-library';
import {SQL} from './database';
import {ActionError, ActionContext} from './server';
import {toID, time, signAsync} from './utils';
import { SQL } from './database';
import { ActionError, type ActionContext } from './server';
import { toID, time, signAsync } from './utils';
import {
ladder, loginthrottle, loginattempts, sessions, users, usermodlog,
} from './tables';
@ -100,13 +100,13 @@ export class Session {
this.context.setHeader(
"Set-Cookie",
`sid=${encodeURIComponent(`,,${this.sidhash}`)}; ` +
`Max-Age=0; Domain=${Config.routes.root}; Path=/; Secure; SameSite=None`
`Max-Age=0; Domain=${Config.routes.root}; Path=/; Secure; SameSite=None`
);
} else {
this.context.setHeader(
"Set-Cookie",
`sid=;` +
`Max-Age=0; Domain=${Config.routes.root}; Path=/; Secure; SameSite=None`
`Max-Age=0; Domain=${Config.routes.root}; Path=/; Secure; SameSite=None`
);
}
}
@ -126,7 +126,7 @@ export class Session {
async getRecentRegistrationCount(period: number) {
const ip = this.context.getIp();
const timestamp = time() - period;
const result = await users.selectOne<{regcount: number}>(
const result = await users.selectOne<{ regcount: number }>(
SQL`COUNT(*) AS regcount`
)`WHERE \`ip\` = ${ip} AND \`registertime\` > ${timestamp}`;
return result?.['regcount'] || 0;
@ -203,7 +203,7 @@ export class Session {
}
let userType = '';
const userData = user.loggedIn ? await users.get(user.id, SQL`banstate, registertime, logintime`) : null;
const {banstate, registertime, logintime} = userData || {
const { banstate, registertime, logintime } = userData || {
banstate: 0, registertime: 0, logintime: 0,
};
const server = await this.context.getServer();
@ -238,15 +238,15 @@ export class Session {
const ladders = await ladder.selectOne(['formatid'])`WHERE userid = ${userid} AND w != 0`;
if (ladders) {
userType = '4';
void users.update(userid, {banstate: -10});
void users.update(userid, { banstate: -10 });
}
}
}
}
if (!logintime || time() - logintime > LOGINTIME_INTERVAL) {
await users.update(userid, {logintime: time(), loginip: ip});
await users.update(userid, { logintime: time(), loginip: ip });
}
data = userid + ',' + userType + ',' + time() + ',' + serverHost;
data = `${userid},${userType},${time()},${serverHost}`;
} else {
if (userid.length < 1 || !/[a-z]/.test(userid)) {
return ';;Your username must contain at least one letter.';
@ -264,7 +264,7 @@ export class Session {
// Unregistered username.
userType = '1';
if (forceUsertype) userType = forceUsertype;
data = userid + ',' + userType + ',' + time() + ',' + serverHost;
data = `${userid},${userType},${time()},${serverHost}`;
}
}
let splitChallenge: string[] = [];
@ -341,7 +341,7 @@ export class Session {
const userData = await users.get(userid);
if (!userData) return false;
const entry = 'Password changed from: ' + userData.passwordhash;
const entry = `Password changed from: ${userData.passwordhash!}`;
await usermodlog.insert({
userid, actorid: userid, date: time(), ip: this.context.getIp(), entry,
});
@ -358,7 +358,7 @@ export class Session {
async passwordVerify(name: string, pass: string) {
const ip = this.context.getIp();
const userid = toID(name);
let attempts = (await loginattempts.get(userid)) as {time: number, count: number};
let attempts = (await loginattempts.get(userid)) as { time: number, count: number };
if (attempts) {
const shouldBeBlocked = (
// too many attempts
@ -371,7 +371,7 @@ export class Session {
);
if (shouldBeBlocked) {
attempts.count++;
await loginattempts.update(userid, {time: time(), count: attempts.count});
await loginattempts.update(userid, { time: time(), count: attempts.count });
throw new ActionError(
`Too many unrecognized login attempts have been made against this account. Please try again later.`
);
@ -384,7 +384,7 @@ export class Session {
}
let throttleTable = await loginthrottle.get(
ip, ['count', 'time']
) as {count: number; time: number} || null;
) as { count: number, time: number } || null;
if (throttleTable) {
if (throttleTable.count > 500) {
throttleTable.count++;
@ -412,11 +412,11 @@ export class Session {
const payload = ticket.getPayload()!; // dunno why this would happen.
if (payload.email?.toLowerCase() !== userData.email.slice(0, -1).toLowerCase()) {
throw new ActionError(
`Wrong Google account for this Showdown account (${payload.email} doesn't match this account)`
`Wrong Google account for this Showdown account (${payload.email!} doesn't match this account)`
);
}
return true;
} catch (e) {
} catch {
// throw new ActionError(`OAuth error: ${e}`);
return false;
}
@ -442,7 +442,7 @@ export class Session {
count: attempts.count, time: time(),
});
} else {
await loginattempts.insert({userid, count: 1, time: time()});
await loginattempts.insert({ userid, count: 1, time: time() });
}
return false;
}
@ -486,7 +486,7 @@ export class Session {
let sid = '';
let session = 0;
const scsplit = scookie.split(',').filter(Boolean);
let cookieName;
let cookieName = '';
if (scsplit.length === 3) {
cookieName = scsplit[0];
session = parseInt(scsplit[1]);
@ -513,7 +513,7 @@ export class Session {
// okay, legit session ID - you're logged in now.
const user = new User();
user.login(cookieName as string);
user.login(cookieName);
user.group = (await users.get(user.id))?.group || 0;
this.sidhash = sid;
@ -523,7 +523,7 @@ export class Session {
static sanitizeHash(pass: string) {
// https://youtu.be/rnzMkJocw6Q?t=9
// (php uses $2y, js uses $2b)
if (!pass.startsWith('$2b')) {
if (pass.startsWith('$2y')) {
pass = `$2b${pass.slice(3)}`;
}
return pass;

View File

@ -8,7 +8,7 @@ export function toID(text: any): string {
text = text.userid;
}
if (typeof text !== 'string' && typeof text !== 'number') return '';
return ('' + text).toLowerCase().replace(/[^a-z0-9]+/g, '');
return `${text}`.toLowerCase().replace(/[^a-z0-9]+/g, '');
}
export function time() {
@ -97,7 +97,7 @@ export function signAsync(algo: string, data: string, key: string) {
export function escapeHTML(str: string | number) {
if (str === null || str === undefined) return '';
return ('' + str)
return `${str}`
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
@ -106,7 +106,7 @@ export function escapeHTML(str: string | number) {
}
export class TimeSorter {
private data: Record<string, {min: number, max: number, count: number}> = {};
private data: Record<string, { min: number, max: number, count: number }> = {};
add(key: string, timestamp: number) {
if (this.data[key]) {
if (this.data[key].min > timestamp) {
@ -117,7 +117,7 @@ export class TimeSorter {
}
this.data[key].count++;
} else {
this.data[key] = {min: timestamp, max: timestamp, count: 1};
this.data[key] = { min: timestamp, max: timestamp, count: 1 };
}
}
toJSON(): TimeSorter['data'] {