mirror of
https://github.com/smogon/pokemon-showdown-loginserver.git
synced 2026-04-25 08:04:15 -05:00
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:
parent
de9746faba
commit
a56f1f160f
278
.eslintrc.json
278
.eslintrc.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x, 20.x]
|
||||
node-version: [18.x, 20.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
435
eslint-ps-standard.mjs
Normal 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
45
eslint.config.mjs
Normal 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
2121
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
144
src/actions.ts
144
src/actions.ts
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) });
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
156
src/tables.ts
156
src/tables.ts
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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']);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
44
src/user.ts
44
src/user.ts
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
|
|
@ -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'] {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user