diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3742d1739..9cbe09859 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -9,7 +9,7 @@ assignees: '' **Describe the bug** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 67a209718..ad5fcc6b6 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -9,7 +9,7 @@ assignees: '' **Is your feature request related to a problem? Please describe.** diff --git a/.github/contributing.md b/.github/contributing.md index 55a2189db..0ed19b877 100644 --- a/.github/contributing.md +++ b/.github/contributing.md @@ -34,7 +34,7 @@ Before creating bug reports, please check [this list](#before-submitting-a-bug-r #### How to submit a bug report -Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you have determined that it is a bug, create an issue and provide following information by filling in [the template](https://github.com/idinium96/tf2autobot/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). +Bugs are tracked as [GitHub issues](https://guides.github.com/features/issues/). After you have determined that it is a bug, create an issue and provide following information by filling in [the template](https://github.com/TF2Autobot/tf2autobot/blob/master/.github/ISSUE_TEMPLATE/bug_report.md). #### Before submitting a bug report @@ -70,7 +70,7 @@ Just like [bug reports](#reporting-bugs), feature requests are tracked as [GitHu When contributing to this repository, please first discuss the change you wish to make via issue, or any other method with the owners or contributors of this repository before making a change. -All pull requests should be made to [the development branch](https://github.com/idinium96/tf2autobot/tree/development). When a new release is made the development branch will be merged with the master branch. +All pull requests should be made to [the development branch](https://github.com/TF2Autobot/tf2autobot/tree/development). When a new release is made the development branch will be merged with the master branch. Please make sure that you follow the [style guides](#styleguides). @@ -99,7 +99,7 @@ The body of the commit message should be one or more paragraphs, explaining things in more detail. Please word-wrap to keep columns to 72 characters or less. -Fixes: https://github.com/Nicklason/tf2autobot/issues/1337 +Fixes: https://github.com/TF2Autobot/tf2autobot/issues/1337 Refs: https://eslint.org/docs/rules/space-in-parens.html ``` diff --git a/LICENSE b/LICENSE index b6763773a..745086185 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 IdiNium/Nicklas Marc Pedersen +Copyright (c) 2020 - 2021 IdiNium/Nicklas Marc Pedersen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/package-lock.json b/package-lock.json index 81b6ea9dc..feafc3dd5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -97,13 +97,13 @@ } }, "@babel/helper-create-class-features-plugin": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.13.tgz", - "integrity": "sha512-Vs/e9wv7rakKYeywsmEBSRC9KtmE7Px+YBlESekLeJOF0zbGUicGfXSNi3o+tfXSNS48U/7K9mIOOCR79Cl3+Q==", + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.16.tgz", + "integrity": "sha512-KbSEj8l9zYkMVHpQqM3wJNxS1d9h3U9vm/uE5tpjMbaj3lTp+0noe3KPsV5dSD9jxKnf9jO9Ip9FX5PKNZCKow==", "dev": true, "requires": { "@babel/helper-function-name": "^7.12.13", - "@babel/helper-member-expression-to-functions": "^7.12.13", + "@babel/helper-member-expression-to-functions": "^7.12.16", "@babel/helper-optimise-call-expression": "^7.12.13", "@babel/helper-replace-supers": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13" @@ -119,9 +119,9 @@ } }, "@babel/generator": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.13.tgz", - "integrity": "sha512-9qQ8Fgo8HaSvHEt6A5+BATP7XktD/AdAnObUeTRz5/e2y3kbrxZgz32qUJJsdmwUvBJzF4AeV21nGTNwv05Mpw==", + "version": "7.12.15", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.15.tgz", + "integrity": "sha512-6F2xHxBiFXWNSGb7vyCUTBF8RCLY66rS0zEPcP8t/nQyXjha5EuK4z7H5o7fWG8B4M7y6mqVWq1J+1PuwRhecQ==", "dev": true, "requires": { "@babel/types": "^7.12.13", @@ -150,9 +150,9 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.13.tgz", - "integrity": "sha512-B+7nN0gIL8FZ8SvMcF+EPyB21KnCcZHQZFczCxbiNGV/O0rsrSBlWGLzmtBJ3GMjSVMIm4lpFhR+VdVBuIsUcQ==", + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.16.tgz", + "integrity": "sha512-zYoZC1uvebBFmj1wFAlXwt35JLEgecefATtKp20xalwEK8vHAixLBXTGxNrVGEmTT+gzOThUgr8UEdgtalc1BQ==", "dev": true, "requires": { "@babel/types": "^7.12.13" @@ -206,9 +206,9 @@ } }, "@babel/parser": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.13.tgz", - "integrity": "sha512-z7n7ybOUzaRc3wwqLpAX8UFIXsrVXUJhtNGBwAnLz6d1KUapqyq7ad2La8gZ6CXhHmGAIL32cop8Tst4/PNWLw==", + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.16.tgz", + "integrity": "sha512-c/+u9cqV6F0+4Hpq01jnJO+GLp2DdT63ppz9Xa+6cHaajM9VFzK/iDXiKK65YtpeVwu+ctfS6iqlMqRgQRzeCw==", "dev": true }, "@babel/template": { @@ -373,9 +373,9 @@ "dev": true }, "@babel/helper-validator-option": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz", - "integrity": "sha512-TBFCyj939mFSdeX7U7DDj32WtzYY7fDcalgq8v3fBZMNOJQNn7nOYzMaUCiPxPYfCup69mtIpqlKgMZLvQ8Xhw==", + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.16.tgz", + "integrity": "sha512-uCgsDBPUQDvzr11ePPo4TVEocxj8RXjUVSC/Y8N1YpVAI/XDdUwGJu78xmlGhTxj2ntaWM7n9LQdRtyhOzT2YQ==", "dev": true }, "@babel/helpers": { @@ -532,12 +532,12 @@ } }, "@babel/plugin-transform-typescript": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.13.tgz", - "integrity": "sha512-z1VWskPJxK9tfxoYvePWvzSJC+4pxXr8ArmRm5ofqgi+mwpKg6lvtomkIngBYMJVnKhsFYVysCQLDn//v2RHcg==", + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.16.tgz", + "integrity": "sha512-88hep+B6dtDOiEqtRzwHp2TYO+CN8nbAV3eh5OpBGPsedug9J6y1JwLKzXRIGGQZDC8NlpxpQMIIxcfIW96Wgw==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.13", + "@babel/helper-create-class-features-plugin": "^7.12.16", "@babel/helper-plugin-utils": "^7.12.13", "@babel/plugin-syntax-typescript": "^7.12.13" }, @@ -551,14 +551,14 @@ } }, "@babel/preset-typescript": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.13.tgz", - "integrity": "sha512-gYry7CeXwD2wtw5qHzrtzKaShEhOfTmKb4i0ZxeYBcBosN5VuAudsNbjX7Oj5EAfQ3K4s4HsVMQRRcqGsPvs2A==", + "version": "7.12.16", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.16.tgz", + "integrity": "sha512-IrYNrpDSuQfNHeqh7gsJsO35xTGyAyGkI1VxOpBEADFtxCqZ77a1RHbJqM3YJhroj7qMkNMkNtcw0lqeZUrzow==", "dev": true, "requires": { "@babel/helper-plugin-utils": "^7.12.13", - "@babel/helper-validator-option": "^7.12.11", - "@babel/plugin-transform-typescript": "^7.12.13" + "@babel/helper-validator-option": "^7.12.16", + "@babel/plugin-transform-typescript": "^7.12.16" }, "dependencies": { "@babel/helper-plugin-utils": { @@ -1782,9 +1782,9 @@ "dev": true }, "@types/cheerio": { - "version": "0.22.23", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.23.tgz", - "integrity": "sha512-QfHLujVMlGqcS/ePSf3Oe5hK3H8wi/yN2JYuxSB1U10VvW1fO3K8C+mURQesFYS1Hn7lspOsTT75SKq/XtydQg==", + "version": "0.22.24", + "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.24.tgz", + "integrity": "sha512-iKXt/cwltGvN06Dd6zwQG1U35edPwId9lmcSeYfcxSNvvNg4vysnFB+iBQNjj06tSVV7MBj0GWMQ7dwb4Z+p8Q==", "dev": true, "requires": { "@types/node": "*" @@ -1797,9 +1797,9 @@ "dev": true }, "@types/graceful-fs": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.4.tgz", - "integrity": "sha512-mWA/4zFQhfvOA8zWkXobwJvBD7vzcxgrOQ0J5CH1votGqdq9m7+FwtGaqyCZqC3NyyBkc9z4m+iry4LlqcMWJg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", "dev": true, "requires": { "@types/node": "*" @@ -2870,9 +2870,9 @@ } }, "bptf-listings-2": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/bptf-listings-2/-/bptf-listings-2-1.1.6.tgz", - "integrity": "sha512-r0AVY0j9uWj3nYjp8cQpvSpBP4ehjD+ct4QDHuwcUWjzXrx2q93EsELsmk4G7ER1wILBNcyz+N2cF+k2bpJNWg==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/bptf-listings-2/-/bptf-listings-2-1.1.11.tgz", + "integrity": "sha512-4CyuT4r/+6E+CeIYsfkfmTVTIbb8XrrnpY8Cvyyo6OXnZV09D42al8a5YsmX3Zfe4GNUTONuWCgGPZU63WpR1A==", "requires": { "async": "^3.2.0", "events": "^3.2.0", @@ -3026,12 +3026,12 @@ } }, "call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", "requires": { "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" + "get-intrinsic": "^1.0.2" } }, "callback-queue": { @@ -4201,12 +4201,12 @@ } }, "eslint": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.19.0.tgz", - "integrity": "sha512-CGlMgJY56JZ9ZSYhJuhow61lMPPjUzWmChFya71Z/jilVos7mR/jPgaEfVGgMBY5DshbKdG8Ezb8FDCHcoMEMg==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.20.0.tgz", + "integrity": "sha512-qGi0CTcOGP2OtCQBgWZlQjcTuP0XkIpYFj25XtRTQSHC+umNnp7UMshr2G8SLsRFYDdAPFeHOsiteadmMH02Yw==", "dev": true, "requires": { - "@babel/code-frame": "^7.0.0", + "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.3.0", "ajv": "^6.10.0", "chalk": "^4.0.0", @@ -4218,7 +4218,7 @@ "eslint-utils": "^2.1.0", "eslint-visitor-keys": "^2.0.0", "espree": "^7.3.1", - "esquery": "^1.2.0", + "esquery": "^1.4.0", "esutils": "^2.0.2", "file-entry-cache": "^6.0.0", "functional-red-black-tree": "^1.0.1", @@ -4245,13 +4245,43 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "@babel/code-frame": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", + "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", "dev": true, "requires": { - "color-convert": "^2.0.1" + "@babel/highlight": "^7.10.4" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==", + "dev": true + }, + "@babel/highlight": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.12.13.tgz", + "integrity": "sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.12.11", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + } } }, "chalk": { @@ -4262,6 +4292,26 @@ "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, "color-convert": { @@ -4404,15 +4454,6 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4569,9 +4610,9 @@ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -5087,9 +5128,9 @@ "dev": true }, "get-intrinsic": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz", - "integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", "requires": { "function-bind": "^1.1.1", "has": "^1.0.3", @@ -5248,9 +5289,9 @@ } }, "graceful-fs": { - "version": "4.2.5", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.5.tgz", - "integrity": "sha512-kBBSQbz2K0Nyn+31j/w36fUfxkBW9/gfwRWdUY1ULReH3iokVJgddZAFcD1D0xlgTmFxJCbUkUclAlc6/IDJkw==" + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, "growly": { "version": "1.3.0", @@ -5786,6 +5827,11 @@ "is-path-inside": "^3.0.1" } }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + }, "is-npm": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", @@ -5853,32 +5899,36 @@ } }, "is-typed-array": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.3.tgz", - "integrity": "sha512-BSYUBOK/HJibQ30wWkWold5txYwMUXQct9YHAQJr8fSwvZoiglcqB0pd7vEN23+Tsi9IUEjztdOSzl4qLVYGTQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.4.tgz", + "integrity": "sha512-ILaRgn4zaSrVNXNGtON6iFNotXW3hAPF3+0fB1usg2jFlWqo5fEDdmJkz0zBfoi7Dgskr8Khi2xZ8cXqZEfXNA==", "requires": { - "available-typed-arrays": "^1.0.0", - "es-abstract": "^1.17.4", + "available-typed-arrays": "^1.0.2", + "call-bind": "^1.0.0", + "es-abstract": "^1.18.0-next.1", "foreach": "^2.0.5", "has-symbols": "^1.0.1" }, "dependencies": { "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "version": "1.18.0-next.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", + "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", "requires": { + "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2", "has": "^1.0.3", "has-symbols": "^1.0.1", "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", + "object-inspect": "^1.9.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.3", + "string.prototype.trimstart": "^1.0.3" } }, "es-to-primitive": { @@ -5897,22 +5947,23 @@ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" }, "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", "requires": { + "call-bind": "^1.0.2", "has-symbols": "^1.0.1" } }, "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" }, "object.assign": { "version": "4.1.2", @@ -5924,6 +5975,24 @@ "has-symbols": "^1.0.1", "object-keys": "^1.1.1" } + }, + "string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } } } }, @@ -10711,9 +10780,9 @@ }, "dependencies": { "ajv": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.0.4.tgz", - "integrity": "sha512-xzzzaqgEQfmuhbhAoqjJ8T/1okb6gAzXn/eQRNpAN1AEUoHJTNF9xCDRTtf/s3SKldtZfa+RJeTs+BQq+eZ/sw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-7.1.0.tgz", + "integrity": "sha512-svS9uILze/cXbH0z2myCK2Brqprx/+JJYK5pHicT/GQiBfzzhUVAIT6MwqJg8y4xV/zoGsUeuPuwtoiKSGE15g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -10808,9 +10877,9 @@ } }, "tf2-schema-2": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/tf2-schema-2/-/tf2-schema-2-1.2.3.tgz", - "integrity": "sha512-HA0BHnb/ZP/pZt/6thEtpOR54vWt65dpT1N7LvKm5dFqVLUSpUqXABhxyl5tcgwXgByZD8jCgMQ9OtSCL4VOUA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tf2-schema-2/-/tf2-schema-2-1.2.4.tgz", + "integrity": "sha512-1U/dGRbhLMI7DfZKcQJRVDe8xZbROQJDicR/jWvNjV4p9dXbC4DAXIiAkY5ehYBA5p8cDkjcuDUSxlE+H3mPhQ==", "requires": { "async": "^3.2.0", "events": "^3.2.0", @@ -10836,9 +10905,9 @@ } }, "tf2-sku-2": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tf2-sku-2/-/tf2-sku-2-1.0.1.tgz", - "integrity": "sha512-iIFuv4XM/qtYlpWKVSidf6Roxo8Lj+5oT1reWI9DPHFinTZIyq1Axhx1NCSw9niQG/Qp3MkaQCF+XwXq+g/G8w==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tf2-sku-2/-/tf2-sku-2-1.0.3.tgz", + "integrity": "sha512-1uM++EN+Bu1ululVrEgjq1aHGYL/VZTBzrIin0Em0ob9kn0dWs5UNDiG3OrcyiT/0AbPARQSNdOLyMmdHlBJNQ==", "requires": { "defaults": "^1.0.3", "object-prettify": "^1.0.0" @@ -10963,9 +11032,9 @@ "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" }, "ts-jest": { - "version": "26.5.0", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.0.tgz", - "integrity": "sha512-Ya4IQgvIFNa2Mgq52KaO8yBw2W8tWp61Ecl66VjF0f5JaV8u50nGoptHVILOPGoI7SDnShmEqnYQEmyHdQ+56g==", + "version": "26.5.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.1.tgz", + "integrity": "sha512-G7Rmo3OJMvlqE79amJX8VJKDiRcd7/r61wh9fnvvG8cAjhA9edklGw/dCxRSQmfZ/z8NDums5srSVgwZos1qfg==", "dev": true, "requires": { "@types/jest": "26.x", @@ -11060,9 +11129,9 @@ } }, "typescript": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz", - "integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.5.tgz", + "integrity": "sha512-6OSu9PTIzmn9TCDiovULTnET6BgXtDYL4Gg4szY+cGsc3JP1dQL8qvE8kShTRx1NIw4Q9IBHlwODjkjWEtMUyA==", "dev": true }, "undefsafe": { @@ -11512,12 +11581,13 @@ "dev": true }, "which-typed-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.2.tgz", - "integrity": "sha512-KT6okrd1tE6JdZAy3o2VhMoYPh3+J6EMZLyrxBQsZflI1QCZIxMrIYLkosd8Twf+YfknVIHmYQPgJt238p8dnQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.4.tgz", + "integrity": "sha512-49E0SpUe90cjpoc7BOJwyPHRqSAd12c10Qm2amdEZrJPCY2NDxaW01zHITrem+rnETY3dwrbH3UUrUwagfCYDA==", "requires": { "available-typed-arrays": "^1.0.2", - "es-abstract": "^1.17.5", + "call-bind": "^1.0.0", + "es-abstract": "^1.18.0-next.1", "foreach": "^2.0.5", "function-bind": "^1.1.1", "has-symbols": "^1.0.1", @@ -11525,21 +11595,24 @@ }, "dependencies": { "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "version": "1.18.0-next.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.2.tgz", + "integrity": "sha512-Ih4ZMFHEtZupnUh6497zEL4y2+w8+1ljnCyaTa+adcoafI1GOvMwFlDjBLfWR7y9VLfrjRJe9ocuHY1PSR9jjw==", "requires": { + "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2", "has": "^1.0.3", "has-symbols": "^1.0.1", "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.1", "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", + "object-inspect": "^1.9.0", "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.3", + "string.prototype.trimstart": "^1.0.3" } }, "es-to-primitive": { @@ -11558,22 +11631,23 @@ "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", + "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" }, "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.2.tgz", + "integrity": "sha512-axvdhb5pdhEVThqJzYXwMlVuZwC+FF2DpcOhTS+y/8jVq4trxyPgfcwIxIKiyeuLlSQYKkmUaPQJ8ZE4yNKXDg==", "requires": { + "call-bind": "^1.0.2", "has-symbols": "^1.0.1" } }, "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", + "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" }, "object.assign": { "version": "4.1.2", @@ -11585,6 +11659,24 @@ "has-symbols": "^1.0.1", "object-keys": "^1.1.1" } + }, + "string.prototype.trimend": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", + "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", + "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + } } } }, diff --git a/package.json b/package.json index 8d8de28fa..01a66016a 100644 --- a/package.json +++ b/package.json @@ -15,19 +15,19 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/idinium96/tf2autobot.git" + "url": "git+https://github.com/TF2Autobot/tf2autobot.git" }, "author": "IdiNium/Nicklas Marc Pedersen", "license": "MIT", "bugs": { - "url": "https://github.com/idinium96/tf2autobot/issues" + "url": "https://github.com/TF2Autobot/tf2autobot/issues" }, - "homepage": "https://github.com/idinium96/tf2autobot#readme", + "homepage": "https://github.com/TF2Autobot/tf2autobot#readme", "dependencies": { "async": "^3.2.0", "bluebird": "^3.7.2", "bluebird-global": "^1.0.1", - "bptf-listings-2": "^1.1.6", + "bptf-listings-2": "^1.1.11", "bptf-login-2": "^1.0.0", "callback-queue": "^3.0.0", "change-case": "^4.1.2", @@ -37,7 +37,7 @@ "dot-prop": "^6.0.1", "dotenv": "^8.2.0", "eslint-config-prettier": "^7.2.0", - "graceful-fs": "^4.2.5", + "graceful-fs": "^4.2.6", "isobject": "^4.0.0", "js-levenshtein": "^1.1.6", "jsonlint": "^1.6.3", @@ -58,8 +58,8 @@ "steamid": "^1.1.3", "tf2": "^3.0.2", "tf2-currencies": "^1.2.4", - "tf2-schema-2": "^1.2.3", - "tf2-sku-2": "^1.0.1", + "tf2-schema-2": "^1.2.4", + "tf2-sku-2": "^1.0.3", "url": "^0.11.0", "valid-url": "^1.0.9", "websocket-extensions": "^0.1.4", @@ -68,12 +68,12 @@ "xmlhttprequest-ts": "^1.0.1" }, "devDependencies": { - "@babel/preset-typescript": "^7.12.13", + "@babel/preset-typescript": "^7.12.16", "@types/async": "^3.2.5", "@types/bluebird-global": "^3.5.12", - "@types/cheerio": "^0.22.23", + "@types/cheerio": "^0.22.24", "@types/death": "^1.1.1", - "@types/graceful-fs": "^4.1.4", + "@types/graceful-fs": "^4.1.5", "@types/jest": "^26.0.20", "@types/pluralize": "0.0.29", "@types/request": "^2.48.5", @@ -83,7 +83,7 @@ "@types/valid-url": "^1.0.3", "@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/parser": "^4.15.0", - "eslint": "^7.19.0", + "eslint": "^7.20.0", "eslint-plugin-jest": "^24.1.3", "eslint-plugin-prettier": "^3.3.1", "eslint-plugin-tsdoc": "^0.2.11", @@ -91,8 +91,8 @@ "nodemon": "^2.0.7", "prettier": "^2.2.1", "socket.io-mock": "^1.3.1", - "ts-jest": "^26.5.0", - "typescript": "^4.1.3" + "ts-jest": "^26.5.1", + "typescript": "^4.1.5" }, "private": true, "_moduleAliases": { diff --git a/src/app.ts b/src/app.ts index b18cf6bc8..1e596e777 100644 --- a/src/app.ts +++ b/src/app.ts @@ -38,7 +38,8 @@ init(paths, options); if (process.env.pm_id === undefined) { log.warn( - "You are not running the bot with PM2! If the bot crashes it won't start again. Get a VPS and run your bot with PM2: https://github.com/idinium96/tf2autobot/wiki/Getting-a-VPS" + "You are not running the bot with PM2! If the bot crashes it won't start again." + + ' Get a VPS and run your bot with PM2: https://github.com/TF2Autobot/tf2autobot/wiki/Getting-a-VPS' ); } @@ -104,7 +105,8 @@ ON_DEATH({ uncaughtException: true })((signalOrErr, origin) => { if (botReady) { log.error( - 'Refer to Wiki here: https://github.com/idinium96/tf2autobot/wiki/Common-Errors OR Create an issue here: https://github.com/idinium96/tf2autobot/issues/new?assignees=&labels=bug&template=bug_report.md&title=' + 'Refer to Wiki here: https://github.com/TF2Autobot/tf2autobot/wiki/Common-Errors OR ' + + 'Create an issue here: https://github.com/idinium96/TF2Autobot/issues/new?assignees=&labels=bug&template=bug_report.md&title=' ); } } else { diff --git a/src/classes/Bot.ts b/src/classes/Bot.ts index a6701a36e..ad1da0ebe 100644 --- a/src/classes/Bot.ts +++ b/src/classes/Bot.ts @@ -561,7 +561,7 @@ export default class Bot { log.debug('Setting Steam API Key to schema'); this.botManager.setAPIKeyForSchema = this.manager.apiKey; - this.manager.pollInterval = 1000; + this.manager.pollInterval = 30 * 1000; this.setReady = true; this.handler.onReady(); this.manager.doPoll(); diff --git a/src/classes/Carts/AdminCart.ts b/src/classes/Carts/AdminCart.ts index 17f8eb198..115afcd4e 100644 --- a/src/classes/Carts/AdminCart.ts +++ b/src/classes/Carts/AdminCart.ts @@ -27,31 +27,32 @@ export default class AdminCart extends Cart { let amount = this.getOurCount(sku); const ourAssetids = ourInventory.findBySKU(sku, true); + const ourAssetidsCount = ourAssetids.length; - if (amount > ourAssetids.length) { - amount = ourAssetids.length; + if (amount > ourAssetidsCount) { + amount = ourAssetidsCount; // Remove the item from the cart this.removeOurItem(sku); - if (ourAssetids.length === 0) { + if (ourAssetidsCount === 0) { alteredMessages.push( "I don't have any " + pluralize(this.bot.schema.getName(SKU.fromString(sku), false)) ); } else { alteredMessages.push( 'I only have ' + - pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetids.length, true) + pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetidsCount, true) ); // Add the max amount to the offer - this.addOurItem(sku, ourAssetids.length); + this.addOurItem(sku, ourAssetidsCount); } } let missing = amount; let isSkipped = false; - for (let i = 0; i < ourAssetids.length; i++) { + for (let i = 0; i < ourAssetidsCount; i++) { if ( this.bot.options.miscSettings.skipItemsInTrade.enable && this.bot.trades.isInTrade(ourAssetids[i]) @@ -132,13 +133,14 @@ export default class AdminCart extends Cart { let amount = this.getTheirCount(sku); const theirAssetids = theirInventory.findBySKU(sku, true); + const theirAssetidsCount = theirAssetids.length; - if (amount > theirAssetids.length) { - amount = theirAssetids.length; + if (amount > theirAssetidsCount) { + amount = theirAssetidsCount; // Remove the item from the cart this.removeTheirItem(sku); - if (theirAssetids.length === 0) { + if (theirAssetidsCount === 0) { alteredMessages.push( "you don't have any " + pluralize(this.bot.schema.getName(SKU.fromString(sku), false)) ); @@ -147,7 +149,7 @@ export default class AdminCart extends Cart { 'you only have ' + pluralize( this.bot.schema.getName(SKU.fromString(sku), false), - theirAssetids.length, + theirAssetidsCount, true ) ); @@ -155,7 +157,7 @@ export default class AdminCart extends Cart { // Add the max amount to the offer substract current added amount this.addTheirItem( sku, - this.their[sku] ? theirAssetids.length - this.their[sku] : theirAssetids.length + this.their[sku] ? theirAssetidsCount - this.their[sku] : theirAssetidsCount ); } } diff --git a/src/classes/Carts/Cart.ts b/src/classes/Carts/Cart.ts index 6d73abfd2..1c1c0f09d 100644 --- a/src/classes/Carts/Cart.ts +++ b/src/classes/Carts/Cart.ts @@ -213,28 +213,28 @@ export default abstract class Cart { summarize(isDonating: boolean, isBuyingPremium: boolean): string { const ourSummary = this.summarizeOur(); + const ourSummaryCount = ourSummary.length; let ourSummaryString: string; - if (ourSummary.length > 1) { + if (ourSummaryCount > 1) { ourSummaryString = - ourSummary.slice(0, ourSummary.length - 1).join(', ') + ' and ' + ourSummary[ourSummary.length - 1]; - } else if (ourSummary.length === 0) { + ourSummary.slice(0, ourSummaryCount - 1).join(', ') + ' and ' + ourSummary[ourSummaryCount - 1]; + } else if (ourSummaryCount === 0) { ourSummaryString = 'nothing'; } else { ourSummaryString = ourSummary.join(', '); } const theirSummary = this.summarizeTheir(); + const theirSummaryCount = theirSummary.length; let theirSummaryString: string; - if (theirSummary.length > 1) { + if (theirSummaryCount > 1) { theirSummaryString = - theirSummary.slice(0, theirSummary.length - 1).join(', ') + - ' and ' + - theirSummary[theirSummary.length - 1]; - } else if (theirSummary.length === 0) { + theirSummary.slice(0, theirSummaryCount - 1).join(', ') + ' and ' + theirSummary[theirSummaryCount - 1]; + } else if (theirSummaryCount === 0) { theirSummaryString = 'nothing'; } else { theirSummaryString = theirSummary.join(', '); diff --git a/src/classes/Carts/DonateCart.ts b/src/classes/Carts/DonateCart.ts index 688958e8b..88150cfa0 100644 --- a/src/classes/Carts/DonateCart.ts +++ b/src/classes/Carts/DonateCart.ts @@ -31,31 +31,32 @@ export default class DonateCart extends Cart { let amount = this.getOurCount(sku); const ourAssetids = ourInventory.findBySKU(sku, true); + const ourAssetidsCount = ourAssetids.length; - if (amount > ourAssetids.length) { - amount = ourAssetids.length; + if (amount > ourAssetidsCount) { + amount = ourAssetidsCount; // Remove the item from the cart this.removeOurItem(sku); - if (ourAssetids.length === 0) { + if (ourAssetidsCount === 0) { alteredMessages.push( "I don't have any " + pluralize(this.bot.schema.getName(SKU.fromString(sku), false)) ); } else { alteredMessages.push( 'I only have ' + - pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetids.length, true) + pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetidsCount, true) ); // Add the max amount to the offer - this.addOurItem(sku, ourAssetids.length); + this.addOurItem(sku, ourAssetidsCount); } } let missing = amount; let isSkipped = false; - for (let i = 0; i < ourAssetids.length; i++) { + for (let i = 0; i < ourAssetidsCount; i++) { if ( this.bot.options.miscSettings.skipItemsInTrade.enable && this.bot.trades.isInTrade(ourAssetids[i]) diff --git a/src/classes/Carts/PremiumCart.ts b/src/classes/Carts/PremiumCart.ts index 0f4887278..eba269d5a 100644 --- a/src/classes/Carts/PremiumCart.ts +++ b/src/classes/Carts/PremiumCart.ts @@ -31,31 +31,32 @@ export default class PremiumCart extends Cart { let amount = this.getOurCount(sku); const ourAssetids = ourInventory.findBySKU(sku, true); + const ourAssetidsCount = ourAssetids.length; - if (amount > ourAssetids.length) { - amount = ourAssetids.length; + if (amount > ourAssetidsCount) { + amount = ourAssetidsCount; // Remove the item from the cart this.removeOurItem(sku); - if (ourAssetids.length === 0) { + if (ourAssetidsCount === 0) { alteredMessages.push( "I don't have any " + pluralize(this.bot.schema.getName(SKU.fromString(sku), false)) ); } else { alteredMessages.push( 'I only have ' + - pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetids.length, true) + pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetidsCount, true) ); // Add the max amount to the offer - this.addOurItem(sku, ourAssetids.length); + this.addOurItem(sku, ourAssetidsCount); } } let missing = amount; let isSkipped = false; - for (let i = 0; i < ourAssetids.length; i++) { + for (let i = 0; i < ourAssetidsCount; i++) { if ( this.bot.options.miscSettings.skipItemsInTrade.enable && this.bot.trades.isInTrade(ourAssetids[i]) diff --git a/src/classes/Carts/UserCart.ts b/src/classes/Carts/UserCart.ts index ce3e01904..601401855 100644 --- a/src/classes/Carts/UserCart.ts +++ b/src/classes/Carts/UserCart.ts @@ -62,7 +62,9 @@ export default class UserCart extends Cart { log.debug(`Got result from dupe checks on ${assetidsToCheck.join(', ')}`, { result: result }); - for (let i = 0; i < result.length; i++) { + const resultCount = result.length; + + for (let i = 0; i < resultCount; i++) { if (result[i] === true) { // Found duped item return Promise.reject('offer contains duped items'); @@ -171,6 +173,7 @@ export default class UserCart extends Cart { } const skus = Object.keys(currencyValues); + const skusCount = skus.length; let remaining = price.toValue(useKeys ? keyPrice.metal : undefined); @@ -190,7 +193,7 @@ export default class UserCart extends Cart { amount = buyerCurrencies[key]; } - if (index === skus.length - 1) { + if (index === skusCount - 1) { // If we are at the end of the list and have a postive remaining amount, // then we need to loop the other way and pick the value that will make the remaining 0 or negative @@ -254,19 +257,21 @@ export default class UserCart extends Cart { log.debug('Picked too much value, removing...'); // Removes unnecessary items - for (let i = 0; i < skus.length; i++) { - if (pickedCurrencies[skus[i]] === undefined) { + for (let i = 0; i < skusCount; i++) { + const sku = skus[i]; + + if (pickedCurrencies[sku] === undefined) { continue; } - let amount = Math.floor(Math.abs(remaining) / currencyValues[skus[i]]); - if (pickedCurrencies[skus[i]] < amount) { - amount = pickedCurrencies[skus[i]]; + let amount = Math.floor(Math.abs(remaining) / currencyValues[sku]); + if (pickedCurrencies[sku] < amount) { + amount = pickedCurrencies[sku]; } if (amount >= 1) { - remaining += amount * currencyValues[skus[i]]; - pickedCurrencies[skus[i]] -= amount; + remaining += amount * currencyValues[sku]; + pickedCurrencies[sku] -= amount; } log.debug('Iteration', { @@ -381,20 +386,21 @@ export default class UserCart extends Cart { let amount = this.getOurCount(sku); const ourAssetids = ourInventory.findBySKU(sku, true); + const ourAssetidsCount = ourAssetids.length; - if (amount > ourAssetids.length) { - amount = ourAssetids.length; + if (amount > ourAssetidsCount) { + amount = ourAssetidsCount; // Remove the item from the cart this.removeOurItem(sku, Infinity); - if (ourAssetids.length === 0) { + if (ourAssetidsCount === 0) { alteredMessage = "I don't have any " + pluralize(this.bot.schema.getName(SKU.fromString(sku), false)); } else { alteredMessage = 'I only have ' + - pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetids.length, true); + pluralize(this.bot.schema.getName(SKU.fromString(sku), false), ourAssetidsCount, true); // Add the max amount to the cart this.addOurItem(sku, amount); @@ -463,19 +469,20 @@ export default class UserCart extends Cart { let amount = this.getTheirCount(sku); const theirAssetids = theirInventory.findBySKU(sku, true); + const theirAssetidsCount = theirAssetids.length; - if (amount > theirAssetids.length) { + if (amount > theirAssetidsCount) { // Remove the item from the cart this.removeTheirItem(sku, Infinity); - if (theirAssetids.length === 0) { + if (theirAssetidsCount === 0) { alteredMessage = "you don't have any " + pluralize(this.bot.schema.getName(SKU.fromString(sku), false)); } else { - amount = theirAssetids.length; + amount = theirAssetidsCount; alteredMessage = 'you only have ' + - pluralize(this.bot.schema.getName(SKU.fromString(sku), false), theirAssetids.length, true); + pluralize(this.bot.schema.getName(SKU.fromString(sku), false), theirAssetidsCount, true); // Add the max amount to the cart this.addTheirItem(sku, amount); @@ -599,25 +606,28 @@ export default class UserCart extends Cart { const amount = this.our[sku]; const assetids = ourInventory.findBySKU(sku, true); + const ourAssetidsCount = assetids.length; this.ourItemsCount += amount; let missing = amount; let isSkipped = false; - for (let i = 0; i < assetids.length; i++) { - if (opt.miscSettings.skipItemsInTrade.enable && this.bot.trades.isInTrade(assetids[i])) { + for (let i = 0; i < ourAssetidsCount; i++) { + const assetid = assetids[i]; + + if (opt.miscSettings.skipItemsInTrade.enable && this.bot.trades.isInTrade(assetid)) { isSkipped = true; continue; } const isAdded = offer.addMyItem({ appid: 440, contextid: '2', - assetid: assetids[i] + assetid: assetid }); if (isAdded) { // The item was added to the offer - whichAssetIds.our.push(assetids[i]); + whichAssetIds.our.push(assetid); missing--; if (missing === 0) { // We added all the items @@ -682,20 +692,24 @@ export default class UserCart extends Cart { assetids = getAssetidsWithFullUses(inventoryDict.their[sku]); } - for (let i = 0; i < assetids.length; i++) { + const theirAssetidsCount = assetids.length; + + for (let i = 0; i < theirAssetidsCount; i++) { + const assetid = assetids[i]; + const isAdded = offer.addTheirItem({ appid: 440, contextid: '2', - assetid: assetids[i] + assetid: assetid }); if (isAdded) { missing--; - whichAssetIds.their.push(assetids[i]); + whichAssetIds.their.push(assetid); if (addToDupeCheckList) { - assetidsToCheck.push(assetids[i]); + assetidsToCheck.push(assetid); } if (missing === 0) { @@ -778,6 +792,8 @@ export default class UserCart extends Cart { } } }); + } else if (item.isFullUses !== undefined) { + getHighValue[whichIs].items[sku] = { isFull: item.isFullUses }; } }); } @@ -844,17 +860,21 @@ export default class UserCart extends Cart { if (change / value >= 1) { const whose = isBuyer ? 'their' : 'our'; - for (let i = 0; i < currencies[sku].length; i++) { + const currenciesCount = currencies[sku].length; + + for (let i = 0; i < currenciesCount; i++) { + const assetid = currencies[sku][i]; + if ( !isBuyer && opt.miscSettings.skipItemsInTrade.enable && - this.bot.trades.isInTrade(currencies[sku][i]) + this.bot.trades.isInTrade(assetid) ) { isSkipped = true; continue; } const isAdded = offer[isBuyer ? 'addTheirItem' : 'addMyItem']({ - assetid: currencies[sku][i], + assetid: assetid, appid: 440, contextid: '2', amount: 1 @@ -917,17 +937,17 @@ export default class UserCart extends Cart { let isSkipped = false; - for (let i = 0; i < buyerCurrenciesWithAssetids[sku].length; i++) { - if ( - isBuyer && - opt.miscSettings.skipItemsInTrade.enable && - this.bot.trades.isInTrade(buyerCurrenciesWithAssetids[sku][i]) - ) { + const buyerCurrenciesWithAssetidsCount = buyerCurrenciesWithAssetids[sku].length; + + for (let i = 0; i < buyerCurrenciesWithAssetidsCount; i++) { + const assetid = buyerCurrenciesWithAssetids[sku][i]; + + if (isBuyer && opt.miscSettings.skipItemsInTrade.enable && this.bot.trades.isInTrade(assetid)) { isSkipped = true; continue; } const isAdded = offer[isBuyer ? 'addMyItem' : 'addTheirItem']({ - assetid: buyerCurrenciesWithAssetids[sku][i], + assetid: assetid, appid: 440, contextid: '2', amount: 1 diff --git a/src/classes/Commands/Commands.ts b/src/classes/Commands/Commands.ts index ee00c5328..6ef13dcde 100644 --- a/src/classes/Commands/Commands.ts +++ b/src/classes/Commands/Commands.ts @@ -4,7 +4,8 @@ import pluralize from 'pluralize'; import Currencies from 'tf2-currencies'; import dayjs from 'dayjs'; -import * as c from './functions/export'; +import * as c from './sub-classes/export'; +import { removeLinkProtocol, getItemFromParams, getItemAndAmount } from './functions/utils'; import Bot from '../Bot'; import CommandParser from '../CommandParser'; @@ -15,9 +16,8 @@ import UserCart from '../Carts/UserCart'; import DonateCart from '../Carts/DonateCart'; import PremiumCart from '../Carts/PremiumCart'; import CartQueue from '../Carts/CartQueue'; - +import Pricer from '../Pricer'; import { fixItem } from '../../lib/items'; -import Pricer, { GetPriceFn, GetSalesFn, RequestCheckFn } from '../Pricer'; type Instant = 'buy' | 'b' | 'sell' | 's'; type CraftUncraft = 'craftweapon' | 'uncraftweapon'; @@ -31,16 +31,34 @@ type ForceAction = 'faccept' | 'fdecline'; export default class Commands { private isDonating = false; - private getSales: GetSalesFn; + private help: c.HelpCommands; + + private manager: c.ManagerCommands; + + private message: c.MessageCommand; + + private misc: c.MiscCommands; + + private opt: c.OptionsCommand; + + private pManager: c.PricelistManager; - private requestCheck: RequestCheckFn; + private request: c.RequestCommands; - private getPrice: GetPriceFn; + private review: c.ReviewCommands; - constructor(private readonly bot: Bot, private priceSource: Pricer) { - this.getSales = this.priceSource.getSales.bind(this.priceSource); - this.requestCheck = this.priceSource.requestCheck.bind(this.priceSource); - this.getPrice = this.priceSource.requestCheck.bind(this.priceSource); + private status: c.StatusCommands; + + constructor(private readonly bot: Bot, private readonly pricer: Pricer) { + this.help = new c.HelpCommands(bot); + this.manager = new c.ManagerCommands(bot); + this.message = new c.MessageCommand(bot); + this.misc = new c.MiscCommands(bot); + this.opt = new c.OptionsCommand(bot); + this.pManager = new c.PricelistManager(bot); + this.request = new c.RequestCommands(bot, pricer); + this.review = new c.ReviewCommands(bot); + this.status = new c.StatusCommands(bot); } private get cartQueue(): CartQueue { @@ -54,6 +72,14 @@ export default class Commands { }; } + useStatsCommand(steamID: SteamID): void { + this.status.statsCommand(steamID); + } + + useUpdateOptionsCommand(steamID: SteamID | null, message: string): void { + this.opt.updateOptionsCommand(steamID, message); + } + processMessage(steamID: SteamID, message: string): void { const command = CommandParser.getCommand(message.toLowerCase()); const isAdmin = this.bot.isAdmin(steamID); @@ -120,9 +146,9 @@ export default class Commands { }; if (command === 'help') { - c.help.helpCommand(steamID, this.bot); + this.help.helpCommand(steamID); } else if (command === 'how2trade') { - c.help.howToTradeCommand(steamID, this.bot); + this.help.howToTradeCommand(steamID); } else if (['price', 'pc'].includes(command)) { this.priceCommand(steamID, message); } else if (['buy', 'b', 'sell', 's'].includes(command)) { @@ -142,83 +168,85 @@ export default class Commands { } else if (command === 'queue') { this.queueCommand(steamID); } else if (['time', 'uptime', 'pure', 'rate', 'owner', 'discord', 'stock'].includes(command)) { - c.misc.miscCommand(steamID, this.bot, command as Misc); + this.misc.miscCommand(steamID, command as Misc); } else if (command === 'paints' && isAdmin) { - c.misc.paintsCommand(steamID, this.bot); + this.misc.paintsCommand(steamID); } else if (command === 'more') { - c.help.moreCommand(steamID, this.bot); + this.help.moreCommand(steamID); } else if (command === 'autokeys') { - c.manager.autokeysCommand(steamID, this.bot); + this.manager.autokeysCommand(steamID); } else if (command === 'message') { - c.messageCommand(steamID, message, this.bot); + this.message.message(steamID, message); } else if (['craftweapon', 'uncraftweapon'].includes(command)) { - c.misc.weaponCommand(steamID, this.bot, command as CraftUncraft); + this.misc.weaponCommand(steamID, command as CraftUncraft); } else if (command === 'sales' && isAdmin) { - void c.request.getSalesCommand(steamID, message, this.bot, this.getSales); + void this.request.getSalesCommand(steamID, message); } else if (['deposit', 'd'].includes(command) && isAdmin) { this.depositCommand(steamID, message); } else if (['withdraw', 'w'].includes(command) && isAdmin) { this.withdrawCommand(steamID, message); } else if (command === 'add' && isAdmin) { - c.pricelist.addCommand(steamID, message, this.bot); + this.pManager.addCommand(steamID, message); } else if (command === 'update' && isAdmin) { - void c.pricelist.updateCommand(steamID, message, this.bot); + void this.pManager.updateCommand(steamID, message); } else if (command === 'remove' && isAdmin) { - void c.pricelist.removeCommand(steamID, message, this.bot); + void this.pManager.removeCommand(steamID, message); } else if (command === 'get' && isAdmin) { - c.pricelist.getCommand(steamID, message, this.bot); + this.pManager.getCommand(steamID, message); } else if (command === 'autoadd' && isAdmin) { - void c.pricelist.autoAddCommand(steamID, message, this.bot); + void this.pManager.autoAddCommand(steamID, message); } else if (command === 'stopautoadd' && isAdmin) { - c.pricelist.stopAutoAddCommand(); + this.pManager.stopAutoAddCommand(); } else if (command === 'shuffle' && isAdmin) { - void c.pricelist.shuffleCommand(steamID, this.bot); + void this.pManager.shuffleCommand(steamID); } else if (['expand', 'delete', 'use'].includes(command) && isAdmin) { - c.manager.TF2GCCommand(steamID, message, this.bot, command as TF2GC); + this.manager.TF2GCCommand(steamID, message, command as TF2GC); } else if (['name', 'avatar'].includes(command) && isAdmin) { - c.manager.nameAvatarCommand(steamID, message, this.bot, command as NameAvatar); + this.manager.nameAvatarCommand(steamID, message, command as NameAvatar); } else if (['block', 'unblock'].includes(command) && isAdmin) { - c.manager.blockUnblockCommand(steamID, message, this.bot, command as BlockUnblock); + this.manager.blockUnblockCommand(steamID, message, command as BlockUnblock); } else if (command === 'clearfriends' && isAdmin) { - void c.manager.clearFriendsCommand(steamID, this.bot); + void this.manager.clearFriendsCommand(steamID); } else if (command === 'stop' && isAdmin) { - c.manager.stopCommand(steamID, this.bot); + this.manager.stopCommand(steamID); } else if (command === 'restart' && isAdmin) { - c.manager.restartCommand(steamID, this.bot); + this.manager.restartCommand(steamID); } else if (command === 'updaterepo' && isAdmin) { - c.manager.updaterepoCommand(steamID, this.bot, message); + this.manager.updaterepoCommand(steamID, message); } else if (command === 'refreshautokeys' && isAdmin) { - c.manager.refreshAutokeysCommand(steamID, this.bot); + this.manager.refreshAutokeysCommand(steamID); } else if (command === 'refreshlist' && isAdmin) { - c.manager.refreshListingsCommand(steamID, this.bot); + this.manager.refreshListingsCommand(steamID); } else if (command === 'stats' && isAdmin) { - c.botStatus.statsCommand(steamID, this.bot); + this.status.statsCommand(steamID); } else if (command === 'statsdw' && isAdmin) { - c.botStatus.statsDWCommand(steamID, this.bot); + this.status.statsDWCommand(steamID); } else if (command === 'inventory' && isAdmin) { - c.botStatus.inventoryCommand(steamID, this.bot); + this.status.inventoryCommand(steamID); } else if (command === 'version' && isAdmin) { - c.botStatus.versionCommand(steamID, this.bot); + this.status.versionCommand(steamID); } else if (command === 'trades' && isAdmin) { - c.review.tradesCommand(steamID, this.bot); + this.review.tradesCommand(steamID); } else if (command === 'trade' && isAdmin) { - c.review.tradeCommand(steamID, message, this.bot); + this.review.tradeCommand(steamID, message); } else if (['accepttrade', 'accept', 'declinetrade', 'decline'].includes(command) && isAdmin) { - void c.review.actionOnTradeCommand(steamID, message, this.bot, command as ActionOnTrade); + void this.review.actionOnTradeCommand(steamID, message, command as ActionOnTrade); } else if (['faccept', 'fdecline'].includes(command) && isAdmin) { - void c.review.forceAction(steamID, message, this.bot, command as ForceAction); + void this.review.forceAction(steamID, message, command as ForceAction); + } else if (command === 'offerinfo' && isAdmin) { + this.review.offerInfo(steamID, message); } else if (command === 'pricecheck' && isAdmin) { - c.request.pricecheckCommand(steamID, message, this.bot, this.requestCheck); + this.request.pricecheckCommand(steamID, message); } else if (command === 'pricecheckall' && isAdmin) { - void c.request.pricecheckAllCommand(steamID, this.bot, this.requestCheck); + void this.request.pricecheckAllCommand(steamID); } else if (command === 'check' && isAdmin) { - void c.request.checkCommand(steamID, message, this.bot, this.getPrice); + void this.request.checkCommand(steamID, message); } else if (command === 'find' && isAdmin) { - void c.pricelist.findCommand(steamID, message, this.bot); + void this.pManager.findCommand(steamID, message); } else if (command === 'options' && isAdmin) { - void c.options.optionsCommand(steamID, this.bot, message); + void this.opt.optionsCommand(steamID, message); } else if (command === 'config' && isAdmin) { - c.options.updateOptionsCommand(steamID, message, this.bot); + this.opt.updateOptionsCommand(steamID, message); } else if (command === 'donatebptf' && isAdmin) { this.donateBPTFCommand(steamID, message); } else if (command === 'donatenow' && isAdmin) { @@ -251,7 +279,7 @@ export default class Commands { } } - const info = c.utils.getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot); + const info = getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot); if (info === null) { return; } @@ -338,7 +366,7 @@ export default class Commands { } } - const info = c.utils.getItemAndAmount( + const info = getItemAndAmount( steamID, CommandParser.removeCommand(message), this.bot, @@ -383,7 +411,7 @@ export default class Commands { } } - const info = c.utils.getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot, 'buycart'); + const info = getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot, 'buycart'); if (info === null) { return; @@ -459,7 +487,7 @@ export default class Commands { } } - const info = c.utils.getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot, 'sellcart'); + const info = getItemAndAmount(steamID, CommandParser.removeCommand(message), this.bot, 'sellcart'); if (info === null) { return; } @@ -727,11 +755,11 @@ export default class Commands { ); } - const params = CommandParser.parseParams(CommandParser.removeCommand(c.utils.removeLinkProtocol(message))); + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); if (params.sku === undefined) { - const item = c.utils.getItemFromParams(steamID, params, this.bot); + const item = getItemFromParams(steamID, params, this.bot); if (item === null) { - return this.bot.sendMessage(steamID, `āŒ Item not found.`); + return; } params.sku = SKU.fromObject(item); @@ -776,11 +804,11 @@ export default class Commands { ); } - const params = CommandParser.parseParams(CommandParser.removeCommand(c.utils.removeLinkProtocol(message))); + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); if (params.sku === undefined) { - const item = c.utils.getItemFromParams(steamID, params, this.bot); + const item = getItemFromParams(steamID, params, this.bot); if (item === null) { - return this.bot.sendMessage(steamID, `āŒ Item not found.`); + return; } params.sku = SKU.fromObject(item); @@ -852,11 +880,11 @@ export default class Commands { ); } - const params = CommandParser.parseParams(CommandParser.removeCommand(c.utils.removeLinkProtocol(message))); + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); if (params.sku === undefined) { - const item = c.utils.getItemFromParams(steamID, params, this.bot); + const item = getItemFromParams(steamID, params, this.bot); if (item === null) { - return this.bot.sendMessage(steamID, `āŒ Item not found.`); + return; } params.sku = SKU.fromObject(item); @@ -977,7 +1005,7 @@ export default class Commands { ); } - const params = CommandParser.parseParams(CommandParser.removeCommand(c.utils.removeLinkProtocol(message))); + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); if ( params.months === undefined || typeof params.months !== 'number' || diff --git a/src/classes/Commands/functions/export.ts b/src/classes/Commands/functions/export.ts deleted file mode 100644 index 4932889aa..000000000 --- a/src/classes/Commands/functions/export.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as botStatus from './status'; -import * as help from './help'; -import messageCommand from './message'; -import * as misc from './misc'; -import * as pricelist from './pricelistManager'; -import * as review from './review'; -import * as utils from './utils'; -import * as options from './options'; -import * as manager from './manager'; -import * as request from './request'; - -export { botStatus, help, messageCommand, misc, pricelist, review, utils, options, manager, request }; diff --git a/src/classes/Commands/functions/help.ts b/src/classes/Commands/functions/help.ts deleted file mode 100644 index f36594749..000000000 --- a/src/classes/Commands/functions/help.ts +++ /dev/null @@ -1,132 +0,0 @@ -import SteamID from 'steamid'; -import Bot from '../../Bot'; - -export function helpCommand(steamID: SteamID, bot: Bot): void { - const isAdmin = bot.isAdmin(steamID); - - bot.sendMessage( - steamID, - `šŸ“Œ Note šŸ“Œ${ - isAdmin - ? '\nā€¢ a = Directly add "a"' + - '\nā€¢ [a] = Optionally add "a"' + - '\nā€¢ (a|b) = Directly input "a" OR "b"' + - '\nā€¢ = Replace "a" with relevant content' + - '\n\nDo not include characters <>, ( | ) nor [ ] when typing it. For more info, please refer to the wiki: https://github.com/idinium96/tf2autobot/wiki/What-is-the-pricelist%3F#table-of-contents' - : `\nDo not include characters <> nor [ ] - <> means required and [] means optional.` - }\n\nšŸ“œ Here's a list of my commands:\n- ${ - isAdmin - ? [ - '!deposit (sku|name|defindex)=&amount= - Deposit items', - '!withdraw (sku|name|defindex)=&amount= - Withdraw items\n\nāœØ=== Pricelist manager ===āœØ', - '!add (sku|name|defindex)=&[Listing-parameters] - Add a pricelist entry āž•', - '!autoadd [Listing-parameters] - Perform automatic adding items to the pricelist based on items that are currently available in your bot inventory (about 2 seconds every item) šŸ¤–', - '!stopautoadd - Stop automatic add items operation šŸ›‘', - '!update (sku|name|defindex|item)=&[Listing-parameters] - Update a pricelist entry', - '!remove (sku|name|defindex|item)= - Remove a pricelist entry āž–', - '!shuffle - Shuffle pricelist entries.', - '!get (sku|name|defindex|item)= - Get raw information about a pricelist entry\n\nāœØ=== Bot manager ===āœØ', - "!expand craftable=(true|false) - Use Backpack Expanders to increase the bot's inventory limit", - '!use (sku|assetid)= - Use an item (such as Gift-Stuffed Stocking 2020 - sku: 5923;6;untradable)', - "!delete (sku|assetid)= - Delete an item from the bot's inventory (SKU input only) šŸš®", - '!message - Send a message to a specific user šŸ’¬', - '!block - Block a specific user', - '!unblock - Unblock a specific user', - '!clearfriends - Clear friendlist (will keep admins and friendsToKeep) šŸ‘‹', - '!stop - Stop the bot šŸ”“', - '!restart - Restart the bot šŸ”„', - '!updaterepo - Update your bot to the latest version (only if cloned and running with PM2)', - "!refreshautokeys - Refresh the bot's autokeys settings.", - '!refreshlist - Refresh sell listings šŸ”„', - "!name - Change the bot's name", - "!avatar - Change the bot's avatar\n\nāœØ=== Bot status ===āœØ", - '!stats - Get statistics for accepted trades šŸ“Š', - '!statsdw - Send statistics to Discord Webhook šŸ“Š', - "!inventory - Get the bot's current inventory spaces šŸŽ’", - '!version - Get the TF2Autobot version that the bot is running\n\nāœØ=== Manual review ===āœØ', - '!trades - Get a list of trade offers pending for manual review šŸ”', - '!trade - Get information about a trade', - '!accept [Your Message] - Manually accept an active offer āœ…šŸ”', - '!decline [Your Message] - Manually decline an active offer āŒšŸ”', - '!faccept [Your Message] - Force accept any failed to accept offer āœ…šŸ”‚', - '!fdecline [Your Message] - Force decline any failed to decline offer āŒšŸ”‚\n\nāœØ=== Request ===āœØ', - '!check (sku|name|defindex)= - Request the current price for an item from Prices.TF', - '!pricecheck (sku|name|defindex|item)= - Request an item to be price checked by Prices.TF', - "!pricecheckall - Request all items in the bot's pricelist to be price checked by Prices.TF\n\nāœØ=== Misc ===āœØ", - "!autokeys - Get info on the bot's current autokeys settings šŸ”‘", - "!time - Show the owner's current time šŸ•„", - '!uptime - Show the bot uptime šŸ”Œ', - "!pure - Get the bot's current pure stock šŸ’°", - "!rate - Get the bot's current key rates šŸ”‘", - '!stock - Get a list of items that the bot owns', - "!craftweapon - Get a list of the bot's craftable weapon stock šŸ”«", - "!uncraftweapon - Get a list of the bot's uncraftable weapon stock šŸ”«", - '!paints - Get a list of paints partial sku šŸŽØ', - '!sales (sku|name|defindex)= - Get the sales history for an item šŸ”', - '!find - Get the list of filtered items detail based on the parameters šŸ”', - '!options [OptionsKey] - Get options.json content (current bot option settings) šŸ”§', - '!config =[&OtherOptions] - Update the current options (example: !config game.customName=Selling Tools!) šŸ”§', - '!donatebptf (sku|name|defindex)=&amount= - Donate to backpack.tf (https://backpack.tf/donate) šŸ’°', - '!premium months= - Purchase backpack.tf premium using keys (https://backpack.tf/premium/subscribe) šŸ‘‘' - ].join('\n- ') - : [ - '!help - Get a list of commands', - '!how2trade - Guide on how to trade with the bot', - '!price [amount] - Get the price and stock of an item šŸ’²šŸ“¦\n\nāœØ=== Instant item trade ===āœØ', - '!buy [amount] - Instantly buy an item šŸ’²', - '!sell [amount] - Instantly sell an item šŸ’²\n\nāœØ=== Multiple items trade ===āœØ', - '!buycart [amount] - Add an item you want to buy to your cart šŸ›’', - '!sellcart [amount] - Add an item you want to sell to your cart šŸ›’', - '!cart - View your cart šŸ›’', - '!clearcart - Clear your cart āŽšŸ›’', - '!checkout - Have the bot send an offer with the items in your cart āœ…šŸ›’\n\nāœØ=== Trade actions ===āœØ', - '!cancel - Cancel the trade offer āŒ', - '!queue - Check your position in the queue\n\nāœØ=== Contact Owner ===āœØ', - "!owner - Get the owner's Steam profile and Backpack.tf links", - '!message - Send a message to the owner of the bot šŸ’¬', - "!discord - Get a link to join TF2Autobot and/or the owner's discord server\n\nāœØ=== Other Commands ===āœØ", - '!more - Show the advanced commands list' - ].join('\n- ') - }` - ); -} - -export function moreCommand(steamID: SteamID, bot: Bot): void { - const opt = bot.options.commands.more; - - if (!opt.enable) { - if (!bot.isAdmin(steamID)) { - const custom = opt.customReply.disabled; - return bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); - } - } - - bot.sendMessage( - steamID, - `Advanced commands list:\n- ${[ - "!autokeys - Get info on the bot's current autokeys settings šŸ”‘", - "!time - Show the owner's current time šŸ•„", - '!uptime - Show the bot uptime šŸ”Œ', - "!pure - Get the bot's current pure stock šŸ’°", - "!rate - Get the bot's current key rates šŸ”‘", - '!stock - Get a list of items that the bot owns', - "!craftweapon - Get a list of the bot's craftable weapon stock šŸ”«", - "!uncraftweapon - Get a list of the bot's uncraftable weapon stock šŸ”«" - ].join('\n- ')}` - ); -} - -export function howToTradeCommand(steamID: SteamID, bot: Bot): void { - const custom = bot.options.commands.how2trade.customReply.reply; - - bot.sendMessage( - steamID, - custom - ? custom - : '/quote You can either send me an offer yourself, or use one of my commands to request a trade. ' + - 'Say you want to buy a Team Captain, just type "!buy Team Captain", if want to buy more, ' + - 'just add the [amount] - "!buy 2 Team Captain". Type "!help" for all the commands.' + - '\nYou can also buy or sell multiple items by using the "!buycart [amount] " or ' + - '"!sellcart [amount] " commands.' - ); -} diff --git a/src/classes/Commands/functions/manager.ts b/src/classes/Commands/functions/manager.ts deleted file mode 100644 index bafd1d855..000000000 --- a/src/classes/Commands/functions/manager.ts +++ /dev/null @@ -1,640 +0,0 @@ -import SteamID from 'steamid'; -import SKU from 'tf2-sku-2'; -import pluralize from 'pluralize'; -import Currencies from 'tf2-currencies'; -import validUrl from 'valid-url'; -import child from 'child_process'; -import fs from 'graceful-fs'; -import sleepasync from 'sleep-async'; -import path from 'path'; -import { EPersonaState } from 'steam-user'; -import { utils } from './export'; -import Bot from '../../Bot'; -import CommandParser from '../../CommandParser'; -import log from '../../../lib/logger'; -import { pure } from '../../../lib/tools/export'; - -// Bot manager commands - -type TF2GC = 'expand' | 'use' | 'delete'; - -export function TF2GCCommand(steamID: SteamID, message: string, bot: Bot, command: TF2GC): void { - const params = CommandParser.parseParams(CommandParser.removeCommand(message)); - - if (command === 'expand') { - // Expand command - if (typeof params.craftable !== 'boolean') { - return bot.sendMessage(steamID, 'āš ļø Missing `craftable=true|false`'); - } - - const item = SKU.fromString('5050;6'); - if (params.craftable === false) { - item.craftable = false; - } - - const assetids = bot.inventoryManager.getInventory.findBySKU(SKU.fromObject(item), false); - if (assetids.length === 0) { - // No backpack expanders - return bot.sendMessage( - steamID, - `āŒ I couldn't find any ${!item.craftable ? 'Non-Craftable' : ''} Backpack Expander` - ); - } - - bot.tf2gc.useItem(assetids[0], err => { - if (err) { - log.warn('Error trying to expand inventory: ', err); - return bot.sendMessage(steamID, `āŒ Failed to expand inventory: ${err.message}`); - } - - bot.sendMessage(steamID, `āœ… Used ${!item.craftable ? 'Non-Craftable' : ''} Backpack Expander!`); - }); - } else { - // For use and delete commands - if (params.sku !== undefined && !utils.testSKU(params.sku as string)) { - return bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); - } - - if (params.assetid !== undefined && params.sku === undefined) { - const targetedAssetId = params.assetid as string; - const sku = bot.inventoryManager.getInventory.findByAssetid(targetedAssetId); - - if (params.i_am_sure !== 'yes_i_am') { - return bot.sendMessage( - steamID, - `āš ļø Are you sure that you want to ${command} ${ - sku === null - ? `the item with asset ID ${targetedAssetId}` - : `${bot.schema.getName(SKU.fromString(sku), false)}` - }?` + - `\n- This process is irreversible and will ${command} the item from your bot's backpack!` + - `\n- If you are sure, try again with i_am_sure=yes_i_am as a parameter` - ); - } - - return bot.tf2gc[command === 'use' ? 'useItem' : 'deleteItem'](targetedAssetId, err => { - const theItem = - sku === null - ? targetedAssetId - : `${bot.schema.getName(SKU.fromString(sku), false)} (${targetedAssetId})`; - - if (err) { - log.warn(`Error trying to ${command} ${theItem}: `, err); - return bot.sendMessage(steamID, `āŒ Failed to ${command} ${theItem}: ${err.message}`); - } - - bot.sendMessage(steamID, `āœ… ${command === 'use' ? 'Used' : 'Deleted'} ${theItem}!`); - }); - } - - if (params.name !== undefined || params.item !== undefined) { - return bot.sendMessage( - steamID, - command === 'use' - ? 'āš ļø Please only use sku property.' + - '\n\nBelow are some common items to use:\n ā€¢ ' + - [ - 'Gift-Stuffed Stocking 2013: 5718;6;untradable', - 'Gift-Stuffed Stocking 2017: 5886;6;untradable', - 'Gift-Stuffed Stocking 2018: 5900;6;untradable', - 'Gift-Stuffed Stocking 2019: 5910;6;untradable', - 'Gift-Stuffed Stocking 2020: 5923;6;untradable' - ].join('\nā€¢ ') - : 'āš ļø Please only use sku property.' + - '\n\nBelow are some common items to delete:\n ā€¢ ' + - [ - 'Smissmas Sweater: 16391;15;untradable;w1;pk391', - 'Soul Gargoyle: 5826;6;uncraftable;untradable', - 'Noise Maker - TF Birthday: 536;6;untradable', - 'Bronze Dueling Badge: 242;6;untradable', - 'Silver Dueling Badge: 243;6;untradable', - 'Gold Dueling Badge: 244;6;untradable', - 'Platinum Dueling Badge: 245;6;untradable', - 'Mercenary: 166;6;untradable', - 'Soldier of Fortune: 165;6;untradable', - 'Grizzled Veteran: 164;6;untradable', - 'Primeval Warrior: 170;6;untradable', - 'Professor Speks: 343;6;untradable', - 'Mann Co. Cap: 261;6;untradable', - 'Mann Co. Online Cap: 994;6;untradable', - 'Proof of Purchase: 471;6;untradable', - 'Mildly Disturbing Halloween Mask: 115;6;untradable', - 'Seal Mask: 582;6;untradable', - 'Pyrovision Goggles: 743;6;untradable', - 'Giftapult: 5083;6;untradable', - 'Spirit Of Giving: 655;11;untradable', - 'Party Hat: 537;6;untradable', - 'Name Tag: 5020;6;untradable', - 'Description Tag: 5044;6;untradable', - 'Ghastly Gibus: 584;6;untradable', - 'Ghastlier Gibus: 279;6;untradable', - 'Power Up Canteen: 489;6;untradable', - 'Bombinomicon: 583;6;untradable', - 'Skull Island Topper: 941;6;untradable', - 'Spellbook Page: 8935;6;untradable', - 'Gun Mettle Campaign Coin: 5809;6;untradable', - 'MONOCULUS!: 581;6;untradable' - ].join('\nā€¢ ') - ); - } - - if (params.sku === undefined) { - return bot.sendMessage(steamID, `āš ļø Missing sku property. Example: "!${command} sku=5923;6;untradable"`); - } - - const targetedSKU = params.sku as string; - const [uncraft, untrade] = [targetedSKU.includes(';uncraftable'), targetedSKU.includes(';untradable')]; - - const item = SKU.fromString(targetedSKU.replace(';uncraftable', '').replace(';untradable', '')); - - if (uncraft) { - item.craftable = !uncraft; - } - if (untrade) { - item.tradable = !untrade; - } - - const assetids = bot.inventoryManager.getInventory.findBySKU(SKU.fromObject(item), false); - const name = bot.schema.getName(item, false); - - if (assetids.length === 0) { - // Item not found - return bot.sendMessage(steamID, `āŒ I couldn't find any ${pluralize(name, 0)}`); - } - - let assetid: string; - if (params.assetid !== undefined) { - const targetedAssetId = params.assetid as string; - - if (assetids.includes(targetedAssetId)) { - assetid = targetedAssetId; - } else { - return bot.sendMessage( - steamID, - `āŒ Looks like an assetid ${targetedAssetId} did not match any assetids associated with ${name}` + - ` in my inventory. Try using the sku to use a random assetid.` - ); - } - } else { - assetid = assetids[0]; - } - - if (params.i_am_sure !== 'yes_i_am') { - return bot.sendMessage( - steamID, - `/pre āš ļø Are you sure that you want to ${command} ${name}?` + - `\n- This process is irreversible and will ${command} the item from your bot's backpack!` + - `\n- If you are sure, try again with i_am_sure=yes_i_am as a parameter` - ); - } - - bot.tf2gc[command === 'use' ? 'useItem' : 'deleteItem'](assetid, err => { - if (err) { - log.warn(`Error trying to ${command} ${name}: `, err); - return bot.sendMessage(steamID, `āŒ Failed to ${command} ${name} (${assetid}): ${err.message}`); - } - - bot.sendMessage(steamID, `āœ… ${command === 'use' ? 'Used' : 'Deleted'} ${name} (${assetid})!`); - }); - } -} - -type NameAvatar = 'name' | 'avatar'; - -export function nameAvatarCommand(steamID: SteamID, message: string, bot: Bot, command: NameAvatar): void { - const example = - 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/f5/f57685d33224e32436f366d1acb4a1769bdfa60f_full.jpg'; - const input = CommandParser.removeCommand(message); - - if (!input || input === `!${command}`) { - return bot.sendMessage( - steamID, - `āŒ You forgot to add ${command === 'name' ? 'a name' : 'an image url'}. Example: "!${ - command === 'name' ? 'name IdiNium' : `avatar ${example}` - } "` - ); - } - - if (command === 'name') { - bot.community.editProfile( - { - name: input - }, - err => { - if (err) { - log.warn('Error while changing name: ', err); - return bot.sendMessage(steamID, `āŒ Error while changing name: ${err.message}`); - } - - bot.sendMessage(steamID, 'āœ… Successfully changed name.'); - } - ); - } else { - if (!validUrl.isUri(input)) { - return bot.sendMessage(steamID, `āŒ Your url is not valid. Example: "!avatar ${example}"`); - } - - bot.community.uploadAvatar(input, err => { - if (err) { - log.warn('Error while uploading new avatar: ', err); - return bot.sendMessage(steamID, `āŒ Error while uploading a new avatar: ${err.message}`); - } - - bot.sendMessage(steamID, 'āœ… Successfully uploaded a new avatar.'); - }); - } -} - -type BlockUnblock = 'block' | 'unblock'; - -export function blockUnblockCommand(steamID: SteamID, message: string, bot: Bot, command: BlockUnblock): void { - const steamid = CommandParser.removeCommand(message); - - if (!steamid || steamid === `!${command}`) { - return bot.sendMessage( - steamID, - `āŒ You forgot to add their SteamID64. Example: "!${command} 76561198798404909"` - ); - } - - const targetSteamID64 = new SteamID(steamid); - if (!targetSteamID64.isValid()) { - return bot.sendMessage(steamID, `āŒ SteamID is not valid. Example: "!${command} 76561198798404909"`); - } - - bot.client[command === 'block' ? 'blockUser' : 'unblockUser'](targetSteamID64, err => { - if (err) { - log.warn(`Failed to ${command} user ${targetSteamID64.getSteamID64()}: `, err); - return bot.sendMessage( - steamID, - `āŒ Failed to ${command} user ${targetSteamID64.getSteamID64()}: ${err.message}` - ); - } - bot.sendMessage( - steamID, - `āœ… Successfully ${command === 'block' ? 'blocked' : 'unblocked'} user ${targetSteamID64.getSteamID64()}` - ); - }); -} - -export async function clearFriendsCommand(steamID: SteamID, bot: Bot): Promise { - const friendsToRemove = bot.friends.getFriends.filter(steamid => !bot.handler.friendsToKeep.includes(steamid)); - - for (const steamid of friendsToRemove) { - bot.sendMessage( - steamid, - bot.options.customMessage.clearFriends - ? bot.options.customMessage.clearFriends - : `/quote Hey ${ - bot.friends.getFriend(steamid).player_name - }! My owner has performed friend list clearance. Please feel free to add me again if you want to trade at a later time!` - ); - - bot.client.removeFriend(steamid); - - // Prevent Steam from detecting the bot as spamming - await sleepasync().Promise.sleep(2 * 1000); - } - - bot.sendMessage(steamID, `āœ… Friendlist clearance success! Removed ${friendsToRemove.length} friends.`); -} - -export function stopCommand(steamID: SteamID, bot: Bot): void { - bot.sendMessage(steamID, 'āŒ› Stopping...'); - - bot.botManager.stopProcess().catch(err => { - log.warn('Error occurred while trying to stop: ', err); - bot.sendMessage(steamID, `āŒ An error occurred while trying to stop: ${(err as Error).message}`); - }); -} - -export function restartCommand(steamID: SteamID, bot: Bot): void { - bot.sendMessage(steamID, 'āŒ› Restarting...'); - - bot.botManager - .restartProcess() - .then(restarting => { - if (!restarting) { - bot.sendMessage( - steamID, - 'āŒ You are not running the bot with PM2! Get a VPS and run ' + - 'your bot with PM2: https://github.com/idinium96/tf2autobot/wiki/Getting-a-VPS' - ); - } - }) - .catch(err => { - log.warn('Error occurred while trying to restart: ', err); - bot.sendMessage(steamID, `āŒ An error occurred while trying to restart: ${(err as Error).message}`); - }); -} - -export function updaterepoCommand(steamID: SteamID, bot: Bot, message: string): void { - if (!fs.existsSync(path.resolve(__dirname, '..', '..', '..', '..', '.git'))) { - return bot.sendMessage(steamID, 'āŒ You did not clone the bot from Github.'); - } - - if (process.env.pm_id === undefined) { - return bot.sendMessage( - steamID, - `āŒ You're not running the bot with PM2!` + - `\n\nNavigate to your bot folder and run ` + - `[git reset HEAD --hard && git checkout master && git pull && npm install && npm run build] ` + - `and then restart your bot.` - ); - } - - const params = CommandParser.parseParams(CommandParser.removeCommand(message)); - if (params.i_am_sure !== 'yes_i_am') { - bot.sendMessage( - steamID, - `Currently running TF2Autobot@v${process.env.BOT_VERSION}. Checking for a new version...` - ); - - bot.checkForUpdates - .then(({ hasNewVersion, latestVersion }) => { - if (!hasNewVersion) { - return bot.sendMessage(steamID, 'You are running the latest version of TF2Autobot!'); - } else if (bot.lastNotifiedVersion === latestVersion) { - return bot.sendMessage( - steamID, - `āš ļø Update available! Current: v${process.env.BOT_VERSION}, Latest: v${latestVersion}.` + - '\nSend "!updaterepo i_am_sure=yes_i_am" to update your repo now!' + - `\n\nRelease note: https://github.com/idinium96/tf2autobot/releases` - ); - } - }) - .catch(err => bot.sendMessage(steamID, `āŒ Failed to check for updates: ${JSON.stringify(err)}`)); - } else { - bot.sendMessage(steamID, 'āŒ› Updating...'); - // Make the bot snooze on Steam, that way people will know it is not running - bot.client.setPersona(EPersonaState.Snooze); - - // Set isUpdating status, so any command will not be processed - bot.handler.isUpdatingStatus = true; - - // Stop polling offers - bot.manager.pollInterval = -1; - - // Callback hell šŸ˜ˆ - - // git reset HEAD --hard - child.exec('git reset HEAD --hard', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { - // ignore err - - // git checkout master - child.exec('git checkout master', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { - // ignore err - - bot.sendMessage(steamID, 'āŒ› Pulling changes...'); - - // git pull - child.exec('git pull', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { - // ignore err - - void promiseDelay(3 * 1000); - - bot.sendMessage(steamID, 'āŒ› Installing packages...'); - - // npm install - child.exec('npm install', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { - // ignore err - - // 10 seconds delay, because idk why this always cause some problem - void promiseDelay(10 * 1000); - - bot.sendMessage(steamID, 'āŒ› Compiling TypeScript codes into JavaScript...'); - - // tsc -p . - child.exec('npm run build', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { - // ignore err - - // 5 seconds delay? - void promiseDelay(5 * 1000); - - bot.sendMessage(steamID, 'āŒ› Restarting...'); - - child.exec( - 'pm2 restart ecosystem.json', - { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, - () => { - // ignore err - } - ); - }); - }); - }); - }); - }); - } -} - -function promiseDelay(ms: number): Promise { - return new Promise(resolve => setTimeout(() => resolve(), ms)); -} - -export function autokeysCommand(steamID: SteamID, bot: Bot): void { - const opt = bot.options.commands.autokeys; - if (!opt.enable) { - if (!bot.isAdmin(steamID)) { - const custom = opt.customReply.disabled; - return bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); - } - } - - bot.sendMessage(steamID, '/pre ' + generateAutokeysReply(steamID, bot)); -} - -function generateAutokeysReply(steamID: SteamID, bot: Bot): string { - const pureNow = pure.currPure(bot); - const currKey = pureNow.key; - const currRef = pureNow.refTotalInScrap; - - const keyPrices = bot.pricelist.getKeyPrices; - - const autokeys = bot.handler.autokeys; - const userPure = autokeys.userPure; - const status = autokeys.getOverallStatus; - - const keyBlMin = ` X`; - const keyAbMax = ` X`; - const keyAtBet = ` X`; - const keyAtMin = ` X`; - const keyAtMax = ` X`; - const keysLine = `Keys ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶`; - const refBlMin = ` X`; - const refAbMax = ` X`; - const refAtBet = ` X`; - const refAtMin = ` X`; - const refAtMax = ` X`; - const refsLine = `Refs ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶`; - const xAxisRef = ` min max`; - const keysPosition = - currKey < userPure.minKeys - ? keyBlMin - : currKey > userPure.maxKeys - ? keyAbMax - : currKey > userPure.minKeys && currKey < userPure.maxKeys - ? keyAtBet - : currKey === userPure.minKeys - ? keyAtMin - : currKey === userPure.maxKeys - ? keyAtMax - : ''; - const refsPosition = - currRef < userPure.minRefs - ? refBlMin - : currRef > userPure.maxRefs - ? refAbMax - : currRef > userPure.minRefs && currRef < userPure.maxRefs - ? refAtBet - : currRef === userPure.minRefs - ? refAtMin - : currRef === userPure.maxRefs - ? refAtMax - : ''; - const summary = `\nā€¢ ${userPure.minKeys} ā‰¤ ${pluralize('key', currKey)}(${currKey}) ā‰¤ ${ - userPure.maxKeys - }\nā€¢ ${Currencies.toRefined(userPure.minRefs)} < ${pluralize( - 'ref', - Currencies.toRefined(currRef) - )}(${Currencies.toRefined(currRef)}) < ${Currencies.toRefined(userPure.maxRefs)}`; - - let reply = - (bot.isAdmin(steamID) ? 'Your ' : 'My ') + - `current Autokeys settings:\n${summary}\n\nDiagram:\n${keysPosition}\n${keysLine}\n${refsPosition}\n${refsLine}\n${xAxisRef}\n`; - reply += `\n Key prices: ${keyPrices.buy.toString()}/${keyPrices.sell.toString()} (${ - keyPrices.src === 'manual' ? 'manual' : 'prices.tf' - })`; - - const scrapAdjustmentEnabled = autokeys.isEnableScrapAdjustment; - const scrapAdjustmentValue = autokeys.scrapAdjustmentValue; - const keyBankingEnabled = autokeys.isKeyBankingEnabled; - - reply += `\nScrap Adjustment: ${scrapAdjustmentEnabled ? 'Enabled āœ…' : 'Disabled āŒ'}`; - reply += `\n Auto-banking: ${keyBankingEnabled ? 'Enabled āœ…' : 'Disabled āŒ'}`; - reply += `\n Autokeys status: ${ - autokeys.getActiveStatus - ? status.isBankingKeys - ? 'Banking' + (scrapAdjustmentEnabled ? ' (default price)' : '') - : status.isBuyingKeys - ? 'Buying for ' + - Currencies.toRefined( - keyPrices.buy.toValue() + (scrapAdjustmentEnabled ? scrapAdjustmentValue : 0) - ).toString() + - ' ref' + - (scrapAdjustmentEnabled ? ` (+${scrapAdjustmentValue} scrap)` : '') - : 'Selling for ' + - Currencies.toRefined( - keyPrices.sell.toValue() - (scrapAdjustmentEnabled ? scrapAdjustmentValue : 0) - ).toString() + - ' ref' + - (scrapAdjustmentEnabled ? ` (-${scrapAdjustmentValue} scrap)` : '') - : 'Not active' - }`; - /* - * X - * Keys ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶ - * X - * Refs ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶ - * min max - */ - - return reply; -} - -export function refreshAutokeysCommand(steamID: SteamID, bot: Bot): void { - if (bot.handler.autokeys.isEnabled === false) { - return bot.sendMessage(steamID, `This feature is disabled.`); - } - - bot.handler.autokeys.refresh(); - bot.sendMessage(steamID, 'āœ… Successfully refreshed Autokeys.'); -} - -export function refreshListingsCommand(steamID: SteamID, bot: Bot): void { - const listingsSKUs: string[] = []; - bot.listingManager.getListings(async err => { - if (err) { - return bot.sendMessage( - steamID, - 'āŒ Unable to refresh listings, please try again later: ' + JSON.stringify(err) - ); - } - bot.listingManager.listings.forEach(listing => { - let listingSKU = listing.getSKU(); - if (listing.intent === 1) { - if (bot.options.normalize.painted.our && /;[p][0-9]+/.test(listingSKU)) { - listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); - } - - if (bot.options.normalize.festivized.our && listingSKU.includes(';festive')) { - listingSKU = listingSKU.replace(';festive', ''); - } - - if (bot.options.normalize.strangeAsSecondQuality.our && listingSKU.includes(';strange')) { - listingSKU = listingSKU.replace(';strange', ''); - } - } else { - if (/;[p][0-9]+/.test(listingSKU)) { - listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); - } - } - - listingsSKUs.push(listingSKU); - }); - - // Remove duplicate elements - const newlistingsSKUs: string[] = []; - listingsSKUs.forEach(sku => { - if (!newlistingsSKUs.includes(sku)) { - newlistingsSKUs.push(sku); - } - }); - - const inventory = bot.inventoryManager; - const pricelist = bot.pricelist.getPrices.filter(entry => { - // First find out if lising for this item from bptf already exist. - const isExist = newlistingsSKUs.find(sku => entry.sku === sku); - - if (!isExist) { - // undefined - listing does not exist but item is in the pricelist - - // Get amountCanBuy and amountCanSell (already cover intent and so on) - const amountCanBuy = inventory.amountCanTrade(entry.sku, true); - const amountCanSell = inventory.amountCanTrade(entry.sku, false); - - if ( - (amountCanBuy > 0 && inventory.isCanAffordToBuy(entry.buy, inventory.getInventory)) || - amountCanSell > 0 - ) { - // if can amountCanBuy is more than 0 and isCanAffordToBuy is true OR amountCanSell is more than 0 - // return this entry - return true; - } - - // Else ignore - return false; - } - - // Else if listing already exist on backpack.tf, ignore - return false; - }); - - if (pricelist.length > 0) { - log.debug( - 'Checking listings for ' + - pluralize('item', pricelist.length, true) + - ` [${pricelist.map(entry => entry.sku).join(', ')}] ...` - ); - - bot.sendMessage(steamID, 'Refreshing listings for ' + pluralize('item', pricelist.length, true) + '...'); - - await bot.listings.recursiveCheckPricelist(pricelist, true); - - log.debug('Done checking ' + pluralize('item', pricelist.length, true)); - bot.sendMessage(steamID, 'āœ… Done refreshing ' + pluralize('item', pricelist.length, true)); - } else { - bot.sendMessage(steamID, 'āŒ Nothing to refresh.'); - } - }); -} diff --git a/src/classes/Commands/functions/message.ts b/src/classes/Commands/functions/message.ts deleted file mode 100644 index f17c6c88a..000000000 --- a/src/classes/Commands/functions/message.ts +++ /dev/null @@ -1,139 +0,0 @@ -import SteamID from 'steamid'; -import Bot from '../../Bot'; -import CommandParser from '../../CommandParser'; -import { generateLinks, timeNow } from '../../../lib/tools/export'; -import { sendPartnerMessage, sendAdminMessage } from '../../../lib/DiscordWebhook/export'; - -export default function message(steamID: SteamID, message: string, bot: Bot): void { - const isAdmin = bot.isAdmin(steamID); - const custom = bot.options.commands.message.customReply; - if (!bot.options.commands.enable) { - if (isAdmin) { - bot.sendMessage( - steamID, - 'āŒ The message command is disabled. Enable it by sending `!config commands.message.enable=true`.' - ); - } else { - bot.sendMessage(steamID, custom.disabled ? custom.disabled : 'āŒ The owner has disabled messages.'); - } - return; - } - - const senderDetails = bot.friends.getFriend(steamID); - const optDW = bot.options.discordWebhook.messages; - - if (isAdmin) { - const steamIdAndMessage = CommandParser.removeCommand(message); - const parts = steamIdAndMessage.split(' '); - let recipientSteamID: SteamID; - - try { - recipientSteamID = new SteamID(parts[0]); - } catch (err) { - return bot.sendMessage( - steamID, - 'āŒ Your syntax is wrong or the SteamID is incorrectly formatted. Here\'s an example: "!message 76561198120070906 Hi"' + - "\n\nHow to get the targeted user's SteamID?" + - '\n1. Go to his/her profile page.' + - '\n2. Go to https://steamrep.com/' + - '\n3. View this gif: https://user-images.githubusercontent.com/47635037/96715154-be80b580-13d5-11eb-9bd5-39613f600f6d.gif' - ); - } - - const steamIDString = recipientSteamID.getSteamID64(); - - if (!recipientSteamID.isValid()) { - return bot.sendMessage( - steamID, - `āŒ "${steamIDString}" is not a valid SteamID.` + - "\n\nHow to get the targeted user's SteamID?" + - '\n1. Go to his/her profile page.' + - '\n2. Go to https://steamrep.com/' + - '\n3. View this gif: https://user-images.githubusercontent.com/47635037/96715154-be80b580-13d5-11eb-9bd5-39613f600f6d.gif' - ); - } else if (!bot.friends.isFriend(recipientSteamID)) { - return bot.sendMessage(steamID, `āŒ I am not friends with the user.`); - } - - const recipientDetails = bot.friends.getFriend(recipientSteamID); - const reply = steamIdAndMessage.substr(steamIDString.length); - - // Send message to recipient - bot.sendMessage( - recipientSteamID, - custom.fromOwner - ? custom.fromOwner.replace(/%reply%/g, reply) - : `/quote šŸ’¬ Message from the owner: ${reply}` + - '\n\nā” Hint: You can use the !message command to respond to the owner of this bot.' + - '\nExample: !message Hi Thanks!' - ); - - // Send a notification to the admin with message contents & details - if (optDW.enable && optDW.url !== '') { - sendAdminMessage( - recipientSteamID.toString(), - reply, - recipientDetails, - generateLinks(steamID.toString()), - timeNow(bot.options).time, - bot - ); - } else { - const customInitializer = bot.options.steamChat.customInitializer.message.toOtherAdmins; - bot.messageAdmins( - `${ - customInitializer ? customInitializer : '/quote' - } šŸ’¬ Message sent to #${recipientSteamID.toString()} (${recipientDetails.player_name}): "${reply}". `, - [] - ); - } - - bot.sendMessage(steamID, custom.success ? custom.success : 'āœ… Your message has been sent.'); - - // Send message to all other admins that an admin replied - return bot.messageAdmins( - `${ - senderDetails ? `${senderDetails.player_name} (${steamID.toString()})` : steamID.toString() - } sent a message to ${ - recipientDetails - ? recipientDetails.player_name + ` (${recipientSteamID.toString()})` - : recipientSteamID.toString() - } with "${reply}".`, - [steamID] - ); - } else { - const admins = bot.getAdmins; - if (!admins || admins.length === 0) { - // Just default to same message as if it was disabled - return bot.sendMessage(steamID, custom.disabled ? custom.disabled : 'āŒ The owner has disabled messages.'); - } - - const msg = message.substr(message.toLowerCase().indexOf('message') + 8); - if (!msg) { - return bot.sendMessage( - steamID, - custom.wrongSyntax - ? custom.wrongSyntax - : 'āŒ Please include a message. Here\'s an example: "!message Hi"' - ); - } - - const links = generateLinks(steamID.toString()); - if (optDW.enable && optDW.url !== '') { - sendPartnerMessage(steamID.toString(), msg, senderDetails, links, timeNow(bot.options).time, bot); - } else { - const customInitializer = bot.options.steamChat.customInitializer.message.onReceive; - bot.messageAdmins( - `${ - customInitializer ? customInitializer : '/quote' - } šŸ’¬ You've got a message from #${steamID.toString()} (${senderDetails.player_name}):` + - `"${msg}". ` + - `\nSteam: ${links.steam}` + - `\nBackpack.tf: ${links.bptf}` + - `\nSteamREP: ${links.steamrep}`, - [] - ); - } - bot.sendMessage(steamID, custom.success ? custom.success : 'āœ… Your message has been sent.'); - } -} diff --git a/src/classes/Commands/functions/misc.ts b/src/classes/Commands/functions/misc.ts deleted file mode 100644 index d77b6098d..000000000 --- a/src/classes/Commands/functions/misc.ts +++ /dev/null @@ -1,242 +0,0 @@ -import SteamID from 'steamid'; -import SKU from 'tf2-sku-2'; -import pluralize from 'pluralize'; -import Bot from '../../Bot'; -import { Discord, Stock } from '../../Options'; -import { pure, timeNow, uptime } from '../../../lib/tools/export'; - -type Misc = 'time' | 'uptime' | 'pure' | 'rate' | 'owner' | 'discord' | 'stock'; - -export function miscCommand(steamID: SteamID, bot: Bot, command: Misc): void { - const opt = bot.options.commands[command]; - if (!opt.enable) { - if (!bot.isAdmin(steamID)) { - const custom = opt.customReply.disabled; - return bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); - } - } - - const custom = opt.customReply.reply; - if (command === 'time') { - const timeWithEmojis = timeNow(bot.options); - bot.sendMessage( - steamID, - custom - ? custom - .replace(/%emoji%/g, timeWithEmojis.emoji) - .replace(/%time%/g, timeWithEmojis.time) - .replace(/%note%/g, timeWithEmojis.note) - : `It is currently the following time in my owner's timezone: ${timeWithEmojis.emoji} ${ - timeWithEmojis.time + (timeWithEmojis.note !== '' ? `.\n\n${timeWithEmojis.note}.` : '.') - }` - ); - } else if (command === 'uptime') { - const botUptime = uptime(); - bot.sendMessage(steamID, custom ? custom.replace(/%uptime%/g, botUptime) : botUptime); - } else if (command === 'pure') { - const pureStock = pure.stock(bot); - bot.sendMessage( - steamID, - custom - ? custom.replace(/%pure%/g, pureStock.join(' and ')) - : `šŸ’° I have ${pureStock.join(' and ')} in my inventory.` - ); - } else if (command === 'rate') { - const key = bot.pricelist.getKeyPrices; - const keyRate = key.sell.toString(); - const source = key.src === 'manual' ? 'manual' : 'https://api.prices.tf/items/5021;6?src=bptf'; - - bot.sendMessage( - steamID, - custom - ? custom - .replace(/%keyRate%/g, keyRate) - .replace(/%keyPrices%/g, `${key.buy.metal} / ${key.sell.toString()}`) - .replace(/%source%/g, source) - : 'I value šŸ”‘ Mann Co. Supply Crate Keys at ' + - keyRate + - '. This means that one key is the same as ' + - keyRate + - ', and ' + - keyRate + - ' is the same as one key.' + - `\n\nKey rate source: ${source}` - ); - } else if (command === 'owner') { - const firstAdmin = bot.getAdmins[0]; - const steamURL = `https://steamcommunity.com/profiles/${firstAdmin.toString()}`; - const bptfURL = `https://backpack.tf/profiles/${firstAdmin.toString()}`; - - bot.sendMessage( - steamID, - custom - ? custom - .replace(/%steamurl%/g, steamURL) - .replace(/%bptfurl%/g, bptfURL) - .replace(/%steamid%/, firstAdmin.toString()) - : `ā€¢ Steam: ${steamURL}\nā€¢ Backpack.tf: ${bptfURL}` - ); - } else if (command === 'discord') { - const inviteURL = (opt as Discord).inviteURL; - let reply: string; - if (custom) { - reply = - 'TF2Autobot Discord Server: https://discord.gg/D2GNnp7tv8\n\n' + - custom.replace(/%discordurl%/g, inviteURL); - } else { - if (inviteURL) { - reply = `TF2Autobot Discord Server: https://discord.gg/D2GNnp7tv8\nOwner's Discord Server: ${inviteURL}`; - // - } else reply = 'TF2Autobot Discord Server: https://discord.gg/D2GNnp7tv8'; - } - bot.sendMessage(steamID, reply); - } else { - const inventory = bot.inventoryManager.getInventory; - const dict = inventory.getItems; - const items: { amount: number; name: string }[] = []; - - for (const sku in dict) { - if (!Object.prototype.hasOwnProperty.call(dict, sku)) { - continue; - } - - if (['5021;6', '5002;6', '5001;6', '5000;6'].includes(sku)) { - continue; - } - - items.push({ - name: bot.schema.getName(SKU.fromString(sku), false), - amount: dict[sku].length - }); - } - - items.sort((a, b) => { - if (a.amount === b.amount) { - if (a.name < b.name) { - return -1; - } else if (a.name > b.name) { - return 1; - } else { - return 0; - } - } - return b.amount - a.amount; - }); - - const pure = [ - { - name: 'Mann Co. Supply Crate Key', - amount: inventory.getAmount('5021;6') - }, - { - name: 'Refined Metal', - amount: inventory.getAmount('5002;6') - }, - { - name: 'Reclaimed Metal', - amount: inventory.getAmount('5001;6') - }, - { - name: 'Scrap Metal', - amount: inventory.getAmount('5000;6') - } - ]; - - const parsed = pure.concat(items); - - const stock: string[] = []; - let left = 0; - - const max = (opt as Stock).maximumItems; - - for (let i = 0; i < parsed.length; i++) { - if (stock.length > max) { - left += parsed[i].amount; - } else { - stock.push(`${parsed[i].name}: ${parsed[i].amount}`); - } - } - - const custom = opt.customReply.reply; - let reply = custom - ? custom.replace(/%stocklist%/g, stock.join(', \n')) - : `/pre šŸ“œ Here's a list of all the items that I have in my inventory:\n${stock.join(', \n')}`; - - if (left > 0) { - reply += `,\nand ${left} other ${pluralize('item', left)}`; - } - - bot.sendMessage(steamID, reply); - } -} - -type CraftUncraft = 'craftweapon' | 'uncraftweapon'; - -export function weaponCommand(steamID: SteamID, bot: Bot, type: CraftUncraft): void { - const opt = bot.options.commands[type]; - if (!opt.enable) { - if (!bot.isAdmin(steamID)) { - const custom = opt.customReply.disabled; - return bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); - } - } - - const weaponStock = getWeaponsStock(bot, type === 'craftweapon' ? bot.craftWeapons : bot.uncraftWeapons); - - let reply: string; - if (weaponStock.length > 0) { - const custom = opt.customReply.have; - reply = custom - ? custom.replace(/%list%/g, weaponStock.join(', \n')) - : `šŸ“ƒ Here's a list of all ${ - type === 'craftweapon' ? 'craft' : 'uncraft' - } weapons stock in my inventory:\n\n` + weaponStock.join(', \n'); - } else { - const custom = opt.customReply.dontHave; - reply = custom - ? custom - : `āŒ I don't have any ${type === 'craftweapon' ? 'craftable' : 'uncraftable'} weapons in my inventory.`; - } - - bot.sendMessage(steamID, reply); -} - -function getWeaponsStock(bot: Bot, type: string[]) { - const items: { amount: number; name: string }[] = []; - - type.forEach(sku => { - const amount = bot.inventoryManager.getInventory.getAmount(sku); - if (amount > 0) { - items.push({ - name: bot.schema.getName(SKU.fromString(sku), false), - amount: amount - }); - } - }); - - items.sort((a, b) => { - if (a.amount === b.amount) { - if (a.name < b.name) { - return -1; - } else if (a.name > b.name) { - return 1; - } else { - return 0; - } - } - return b.amount - a.amount; - }); - - const stock: string[] = []; - - if (items.length > 0) { - for (let i = 0; i < items.length; i++) { - stock.push(`${items[i].name}: ${items[i].amount}`); - } - } - return stock; -} - -export function paintsCommand(steamID: SteamID, bot: Bot): void { - bot.sendMessage(steamID, '/code ' + JSON.stringify(bot.paints, null, 4)); -} diff --git a/src/classes/Commands/functions/options.ts b/src/classes/Commands/functions/options.ts deleted file mode 100644 index 50a3d08f4..000000000 --- a/src/classes/Commands/functions/options.ts +++ /dev/null @@ -1,269 +0,0 @@ -import SteamID from 'steamid'; -import { promises as fsp } from 'fs'; -import sleepasync from 'sleep-async'; -import Bot from '../../Bot'; -import CommandParser from '../../CommandParser'; -import { getOptionsPath, JsonOptions, removeCliOptions } from '../../Options'; -import validator from '../../../lib/validator'; -import log from '../../../lib/logger'; -import { deepMerge } from '../../../lib/tools/deep-merge'; - -export async function optionsCommand(steamID: SteamID, bot: Bot, message: string): Promise { - const liveOptions = deepMerge({}, bot.options) as JsonOptions; - // remove any CLI stuff - removeCliOptions(liveOptions); - - const key = CommandParser.removeCommand(message); - - if (!key || key === '!options') { - bot.sendMessage( - steamID, - `/code ${JSON.stringify( - { - miscSettings: liveOptions.miscSettings, - sendAlert: liveOptions.sendAlert, - pricelist: liveOptions.pricelist, - bypass: liveOptions.bypass, - tradeSummary: liveOptions.tradeSummary - }, - null, - 4 - )}` - ); - - await sleepasync().Promise.sleep(1 * 1000); - - bot.sendMessage( - steamID, - `/code ${JSON.stringify( - { - steamChat: liveOptions.steamChat, - highValue: liveOptions.highValue, - normalize: liveOptions.normalize, - details: liveOptions.details, - statistics: liveOptions.statistics, - autokeys: liveOptions.autokeys, - crafting: liveOptions.crafting - }, - null, - 4 - )}` - ); - - await sleepasync().Promise.sleep(1 * 1000); - - bot.sendMessage( - steamID, - `/code ${JSON.stringify( - { - offerReceived: liveOptions.offerReceived, - manualReview: liveOptions.manualReview - }, - null, - 4 - )}` - ); - - await sleepasync().Promise.sleep(1 * 1000); - - bot.sendMessage( - steamID, - `/code ${JSON.stringify( - { - discordWebhook: liveOptions.discordWebhook, - customMessage: liveOptions.customMessage - }, - null, - 4 - )}` - ); - - await sleepasync().Promise.sleep(1 * 1000); - - bot.sendMessage(steamID, `/code ${JSON.stringify({ commands: liveOptions.commands }, null, 4)}`); - - await sleepasync().Promise.sleep(1 * 1000); - - bot.sendMessage(steamID, `/code ${JSON.stringify({ detailsExtra: liveOptions.detailsExtra }, null, 4)}`); - - bot.sendMessage( - steamID, - `\n\nYou can also get only the part the you want by sending "!options [OptionsParentKey]"` - ); - } else { - const optionsKeys = Object.keys(liveOptions); - - if (!optionsKeys.includes(key)) { - return bot.sendMessage( - steamID, - `āŒ "${key}" parent key does not exist in options.` + - `\n\nValid parent keys:\nā€¢ ` + - [ - 'miscSettings', - 'sendAlert', - 'pricelist', - 'bypass', - 'tradeSummary', - 'steamChat', - 'highValue', - 'normalize', - 'details', - 'statistics', - 'autokeys', - 'crafting', - 'offerReceived', - 'manualReview', - 'discordWebhook', - 'customMessage', - 'commands', - 'detailsExtra' - ].join('\nā€¢ ') - ); - } - - const show = {}; - - show[key] = liveOptions[key as OptionsKeys]; - - bot.sendMessage(steamID, `/code ${JSON.stringify(show, null, 4)}`); - } -} - -type OptionsKeys = - | 'miscSettings' - | 'sendAlert' - | 'pricelist' - | 'bypass' - | 'tradeSummary' - | 'steamChat' - | 'highValue' - | 'normalize' - | 'details' - | 'statistics' - | 'autokeys' - | 'crafting' - | 'offerReceived' - | 'manualReview' - | 'discordWebhook' - | 'customMessage' - | 'commands' - | 'detailsExtra'; - -export function updateOptionsCommand(steamID: SteamID, message: string, bot: Bot): void { - const opt = bot.options; - const params = CommandParser.parseParams(CommandParser.removeCommand(message)) as unknown; - - const optionsPath = getOptionsPath(opt.steamAccountName); - const saveOptions = deepMerge({}, opt) as JsonOptions; - removeCliOptions(saveOptions); - - if (Object.keys(params).length === 0) { - const msg = 'āš ļø Missing properties to update.'; - if (steamID) { - bot.sendMessage(steamID, msg); - } else { - log.warn(msg); - } - - return; - } - - const knownParams = params as JsonOptions; - - if (knownParams.discordWebhook?.ownerID !== undefined) { - // Stringify numbers - knownParams.discordWebhook.ownerID = String(knownParams.discordWebhook.ownerID); - } - - if (knownParams.discordWebhook?.embedColor !== undefined) { - // Stringify numbers - knownParams.discordWebhook.embedColor = String(knownParams.discordWebhook.embedColor); - } - - const result: JsonOptions = deepMerge(saveOptions, knownParams); - - const errors = validator(result, 'options'); - if (errors !== null) { - const msg = 'āŒ Error updating options: ' + errors.join(', '); - if (steamID) { - bot.sendMessage(steamID, msg); - } else { - log.error(msg); - } - - return; - } - - fsp.writeFile(optionsPath, JSON.stringify(saveOptions, null, 4), { encoding: 'utf8' }) - .then(() => { - deepMerge(opt, saveOptions); - const msg = 'āœ… Updated options!'; - - if (knownParams.miscSettings?.game?.playOnlyTF2 === true) { - bot.client.gamesPlayed([]); - bot.client.gamesPlayed(440); - } - - if (typeof knownParams.miscSettings?.game?.customName === 'string') { - bot.client.gamesPlayed([]); - bot.client.gamesPlayed( - ( - knownParams.miscSettings?.game?.playOnlyTF2 !== undefined - ? knownParams.miscSettings.game.playOnlyTF2 - : opt.miscSettings.game.playOnlyTF2 - ) - ? 440 - : [knownParams.miscSettings.game.customName, 440] - ); - } - - if (knownParams.miscSettings?.autobump?.enable === true) { - bot.listings.setupAutorelist(); - bot.handler.disableAutoRefreshListings(); - } else if (knownParams.miscSettings?.autobump?.enable === false) { - bot.listings.disableAutorelistOption(); - bot.handler.enableAutoRefreshListings(); - } - - if (knownParams.statistics?.sendStats?.enable === true) { - bot.handler.sendStats(); - } else if (knownParams.statistics?.sendStats?.enable === false) { - bot.handler.disableSendStats(); - } - - if (knownParams.statistics?.sendStats?.time !== undefined) { - bot.handler.sendStats(); - } - - if (typeof knownParams.highValue !== undefined) { - void bot.inventoryManager.getInventory.fetch(); - } - - if (typeof knownParams.normalize === 'object') { - void bot.inventoryManager.getInventory.fetch(); - } - - if (typeof knownParams.autokeys === 'object') { - if (knownParams.autokeys.enable !== undefined && !knownParams.autokeys.enable) { - bot.handler.autokeys.disable(bot.pricelist.getKeyPrices); - } - bot.handler.autokeys.check(); - } - - if (steamID) { - return bot.sendMessage(steamID, msg); - } else { - return log.info(msg); - } - }) - .catch(err => { - const msg = `āŒ Error saving options file to disk: ${JSON.stringify(err)}`; - if (steamID) { - bot.sendMessage(steamID, msg); - } else { - log.error(msg); - } - - return; - }); -} diff --git a/src/classes/Commands/functions/pricelistManager.ts b/src/classes/Commands/functions/pricelistManager.ts deleted file mode 100644 index f2ebf5e85..000000000 --- a/src/classes/Commands/functions/pricelistManager.ts +++ /dev/null @@ -1,1275 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ - -import SteamID from 'steamid'; -import SKU from 'tf2-sku-2'; -import Currencies from 'tf2-currencies'; -import pluralize from 'pluralize'; -import dayjs from 'dayjs'; -import sleepasync from 'sleep-async'; -import { removeLinkProtocol, testSKU, getItemFromParams } from './utils'; -import Bot from '../../Bot'; -import CommandParser from '../../CommandParser'; -import { Entry, EntryData, PricelistChangedSource } from '../../Pricelist'; -import validator from '../../../lib/validator'; -import log from '../../../lib/logger'; - -// Pricelist manager - -export function addCommand(steamID: SteamID, message: string, bot: Bot): void { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - - if (params.enabled === undefined) { - params.enabled = true; - } - - if (params.max === undefined) { - params.max = 1; - } - - if (params.min === undefined) { - params.min = 0; - } - - if (params.intent === undefined) { - params.intent = 2; - } else if (typeof params.intent === 'string') { - const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); - - if (intent !== -1) { - params.intent = intent; - } - } - - if (typeof params.buy === 'object') { - params.buy.keys = params.buy.keys || 0; - params.buy.metal = params.buy.metal || 0; - - if (params.autoprice === undefined) { - params.autoprice = false; - } - } - if (typeof params.sell === 'object') { - params.sell.keys = params.sell.keys || 0; - params.sell.metal = params.sell.metal || 0; - - if (params.autoprice === undefined) { - params.autoprice = false; - } - } - - const isPremium = bot.handler.getBotInfo.premium; - if (params.promoted !== undefined) { - if (!isPremium) { - return bot.sendMessage( - steamID, - `āŒ This account is not Backpack.tf Premium. You can't use "promoted" parameter.` - ); - } - - if (typeof params.promoted === 'boolean') { - if (params.promoted === true) { - params.promoted = 1; - } else { - params.promoted = 0; - } - } else { - if (typeof params.promoted !== 'number') { - return bot.sendMessage(steamID, 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)'); - } else if (params.promoted < 0 || params.promoted > 1) { - return bot.sendMessage(steamID, 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)'); - } - } - } else { - params['promoted'] = 0; - } - - if (typeof params.note === 'object') { - params.note.buy = params.note.buy || null; - params.note.sell = params.note.sell || null; - } - - if (params.note === undefined) { - // If note parameter is not defined, set both note.buy and note.sell to null. - params['note'] = { buy: null, sell: null }; - } - - if (params.group && typeof params.group !== 'string') { - // if group parameter is defined, convert anything to string - params.group = (params.group as string).toString(); - } - - if (typeof params.group === undefined) { - // If group parameter is not defined, set it to null. - params['group'] = 'all'; - } - - if (params.autoprice === undefined) { - params.autoprice = true; - } - - if (params.sku !== undefined && !testSKU(params.sku as string)) { - return bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); - } - - if (params.sku === undefined) { - const item = getItemFromParams(steamID, params, bot); - - if (item === null) { - return bot.sendMessage(steamID, `āŒ Item not found.`); - } - - params.sku = SKU.fromObject(item); - } - - bot.pricelist - .addPrice(params as EntryData, true, PricelistChangedSource.Command) - .then(entry => { - bot.sendMessage(steamID, `āœ… Added "${entry.name}"` + generateAddedReply(bot, isPremium, entry)); - }) - .catch(err => { - bot.sendMessage(steamID, `āŒ Failed to add the item to the pricelist: ${(err as Error).message}`); - }); -} - -let stopAutoAdd = false; - -export function stopAutoAddCommand(): void { - stopAutoAdd = true; -} - -export async function autoAddCommand(steamID: SteamID, message: string, bot: Bot): Promise { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - - if (params.sku !== undefined || params.name !== undefined || params.defindex !== undefined) { - return bot.sendMessage( - steamID, - `āŒ Please only define item listing settings parameters, more info:` + - ` https://github.com/TF2Autobot/tf2autobot/wiki/What-is-the-pricelist%3F#i2---item-listing-settings-parameters.` - ); - } - - if (!params) { - bot.sendMessage(steamID, `ā³ Adding all items with default settings...`); - } - - if (params.enabled === undefined) { - params.enabled = true; - } - - if (params.max === undefined) { - params.max = 1; - } - - if (params.min === undefined) { - params.min = 0; - } - - if (params.intent === undefined) { - params.intent = 2; - } else if (typeof params.intent === 'string') { - const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); - - if (intent !== -1) { - params.intent = intent; - } - } - - if (typeof params.buy === 'object') { - params.buy.keys = params.buy.keys || 0; - params.buy.metal = params.buy.metal || 0; - - if (params.autoprice === undefined) { - params.autoprice = false; - } - } - if (typeof params.sell === 'object') { - params.sell.keys = params.sell.keys || 0; - params.sell.metal = params.sell.metal || 0; - - if (params.autoprice === undefined) { - params.autoprice = false; - } - } - - if ( - (params.sell !== undefined && params.buy === undefined) || - (params.sell === undefined && params.buy !== undefined) - ) { - return bot.sendMessage(steamID, `āŒ Please set both buy and sell prices.`); - } - - const isPremium = bot.handler.getBotInfo.premium; - - if (params.promoted !== undefined) { - if (!isPremium) { - return bot.sendMessage( - steamID, - `āŒ This account is not Backpack.tf Premium. You can't use "promoted" paramter.` - ); - } - - if (typeof params.promoted === 'boolean') { - if (params.promoted === true) { - params.promoted = 1; - } else { - params.promoted = 0; - } - } else { - if (typeof params.promoted !== 'number') { - return bot.sendMessage(steamID, 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)'); - } - - if (params.promoted < 0 || params.promoted > 1) { - return bot.sendMessage(steamID, 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)'); - } - } - } else { - params['promoted'] = 0; - } - - if (typeof params.note === 'object') { - params.note.buy = params.note.buy || null; - params.note.sell = params.note.sell || null; - } - - if (params.note === undefined) { - // If note parameter is not defined, set both note.buy and note.sell to null. - params['note'] = { buy: null, sell: null }; - } - - if (params.group && typeof params.group !== 'string') { - // if group parameter is defined, convert anything to string - params.group = (params.group as string).toString(); - } - - if (typeof params.group === undefined) { - // If group parameter is not defined, set it to null. - params['group'] = 'all'; - } - - if (params.autoprice === undefined) { - params.autoprice = true; - } - - const pricelist = bot.pricelist.getPrices; - const dict = bot.inventoryManager.getInventory.getItems; - - const pureAndWeapons = ['5021;6', '5000;6', '5001;6', '5002;6'].concat(bot.craftWeapons.concat(bot.uncraftWeapons)); - - for (const sku in dict) { - if (!Object.prototype.hasOwnProperty.call(dict, sku)) { - continue; - } - - if (pureAndWeapons.some(pureOrWeaponsSKU => pureOrWeaponsSKU === sku)) { - delete dict[sku]; - } - } - - const total = Object.keys(dict).length; - - const totalTime = total * (params.autoprice ? 2 : 1) * 1000; - const aSecond = 1 * 1000; - const aMin = 1 * 60 * 1000; - const anHour = 1 * 60 * 60 * 1000; - - bot.sendMessage( - steamID, - `ā³ Running automatic add items... Total items to add: ${total}` + - `\n${params.autoprice ? 2 : 1} seconds in between items, so it will be about ${ - totalTime < aMin - ? `${Math.round(totalTime / aSecond)} seconds` - : totalTime < anHour - ? `${Math.round(totalTime / aMin)} minutes` - : `${Math.round(totalTime / anHour)} hours` - } to complete. Send "!stopautoadd" to abort.` - ); - - let added = 0; - let skipped = 0; - let failed = 0; - - for (const sku in dict) { - if (stopAutoAdd) { - bot.sendMessage(steamID, '----------\nšŸ›‘ Stopped auto-add items'); - stopAutoAdd = false; - break; - } - - if (!Object.prototype.hasOwnProperty.call(dict, sku)) { - continue; - } - - if (pricelist.some(entry => entry.sku === sku)) { - skipped++; - bot.sendMessage( - steamID, - `----------\nāš ļø ${bot.schema.getName(SKU.fromString(sku))} (${sku}) already in pricelist, skipping...` + - `\nšŸ“œ Status: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total, ${ - total - added - skipped - failed - } remaining` - ); - // Prevent spamming detection and cause the bot to stop sending messages - await sleepasync().Promise.sleep(1 * 1000); - continue; - } - - if (params.autoprice === true) { - await sleepasync().Promise.sleep(2 * 1000); - } else { - await sleepasync().Promise.sleep(1 * 1000); - } - - params.sku = sku; - - bot.pricelist - .addPrice(params as EntryData, true, PricelistChangedSource.Command) - .then(entry => { - added++; - bot.sendMessage( - steamID, - `----------\nāœ… Added "${entry.name}"` + - generateAddedReply(bot, isPremium, entry) + - `\n\nšŸ“œ Status: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total, ${ - total - added - skipped - failed - } remaining` - ); - }) - .catch(err => { - failed++; - bot.sendMessage( - steamID, - `----------\nāŒ Failed to add the item to the pricelist: ${(err as Error).message}` + - `\n\nšŸ“œ Status: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total, ${ - total - added - skipped - failed - } remaining` - ); - }); - } - - bot.sendMessage( - steamID, - `----------\nāœ… Done, summary: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total` - ); - - stopAutoAdd = false; -} - -function generateAddedReply(bot: Bot, isPremium: boolean, entry: Entry): string { - const amount = bot.inventoryManager.getInventory.getAmount(entry.sku); - return ( - `\nšŸ’² Buy: ${entry.buy.toString()} | Sell: ${entry.sell.toString()}` + - `\nšŸ›’ Intent: ${entry.intent === 2 ? 'bank' : entry.intent === 1 ? 'sell' : 'buy'}` + - `\nšŸ“¦ Stock: ${amount} | Min: ${entry.min} | Max: ${entry.max}` + - `\nšŸ“‹ Enabled: ${entry.enabled ? 'āœ…' : 'āŒ'}` + - `\nšŸ”„ Autoprice: ${entry.autoprice ? 'āœ…' : 'āŒ'}` + - (isPremium ? `\nšŸ“¢ Promoted: ${entry.promoted === 1 ? 'āœ…' : 'āŒ'}` : '') + - `${entry.group !== 'all' ? `\nšŸ”° Group: ${entry.group}` : ''}` + - `${entry.note.buy !== null ? `\nšŸ“„ Custom buying note: ${entry.note.buy}` : ''}` + - `${entry.note.sell !== null ? `\nšŸ“¤ Custom selling note: ${entry.note.sell}` : ''}` - ); -} - -export async function updateCommand(steamID: SteamID, message: string, bot: Bot): Promise { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - - if (typeof params.intent === 'string') { - const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); - - if (intent !== -1) { - params.intent = intent; - } - } - - if (params.all === true) { - // TODO: Must have at least one other param - const pricelist = bot.pricelist.getPrices; - const keyPrice = bot.pricelist.getKeyPrice; - - let targetedPricelist: Entry[]; - let unTargetedPricelist: Entry[]; - let newPricelist: Entry[]; - - if (params.promoted) { - return bot.sendMessage(steamID, `āŒ Parameter "promoted" can't be used with "!update all=true".`); - } - - if (params.withgroup && params.withoutgroup) { - return bot.sendMessage( - steamID, - `āŒ Don't be dumb. Please choose only "withgroup" OR "withoutgroup", not both. Thanks.` - ); - } - - if (params.withgroup) { - targetedPricelist = pricelist.filter(entry => - entry.group ? [(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) : false - ); - unTargetedPricelist = pricelist.filter(entry => - entry.group ? ![(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) : true - ); - - if (targetedPricelist.length === 0) { - return bot.sendMessage( - steamID, - `āŒ There is no entry with "${params.withgroup as string}" group found in your pricelist.` - ); - } - - newPricelist = targetedPricelist; - - if (typeof params.buy === 'object' || typeof params.sell === 'object') { - if ( - new Currencies(params.buy).toValue(keyPrice.metal) >= - new Currencies(params.sell).toValue(keyPrice.metal) - ) { - return bot.sendMessage(steamID, `āŒ Buying price can't be higher than selling price.`); - } else if ( - (params.buy !== null && params.sell === undefined) || - (params.buy === undefined && params.sell !== null) - ) { - return bot.sendMessage(steamID, `āŒ You must include both buying and selling prices.`); - } - } - } else if (params.withoutgroup) { - // reverse of withgroup - targetedPricelist = pricelist.filter(entry => - entry.group - ? ![(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) - : true - ); - unTargetedPricelist = pricelist.filter(entry => - entry.group - ? [(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) - : false - ); - - if (targetedPricelist.length === 0) { - return bot.sendMessage( - steamID, - `āŒ There is no entry other than "${params.withoutgroup as string}" group found in your pricelist.` - ); - } - - newPricelist = targetedPricelist; - - if (typeof params.buy === 'object' || typeof params.sell === 'object') { - if ( - new Currencies(params.buy).toValue(keyPrice.metal) >= - new Currencies(params.sell).toValue(keyPrice.metal) - ) { - return bot.sendMessage(steamID, `āŒ Buying price can't be higher than selling price.`); - } else if ( - (params.buy !== null && params.sell === undefined) || - (params.buy === undefined && params.sell !== null) - ) { - return bot.sendMessage(steamID, `āŒ You must include both buying and selling prices.`); - } - } - } else { - newPricelist = pricelist; - - if (bot.options.autokeys.enable) { - // Autokeys is a feature, so when updating multiple entry with - // "!update all=true", key entry will be removed from newPricelist. - // https://github.com/idinium96/tf2autobot/issues/131 - const keyEntry = bot.pricelist.getPrice('5021;6'); - if (keyEntry !== null) { - const index = bot.pricelist.getIndex('5021;6'); - newPricelist.splice(index, 1); - } - } - } - - if (newPricelist.length === 0) { - return bot.sendMessage(steamID, 'Your pricelist is empty.'); - } - - if (!params.withgroup && !params.withoutgroup) { - if (typeof params.note === 'object') { - return bot.sendMessage( - steamID, - `āŒ Please specify "withgroup" or "withoutgroup" to change note.` + - `\nExample: "!update all=true&withgroup=¬e.buy="` - ); - } - - if (typeof params.buy === 'object' || typeof params.sell === 'object') { - return bot.sendMessage( - steamID, - `āŒ Please specify "withgroup" or "withoutgroup" to change buying/selling price.\nExample:\n` + - `"!update all=true&withgroup=&(buy.keys|buy.metal)=&(sell.keys|sell.metal)="` - ); - } - } - - newPricelist.forEach((entry, i) => { - if (params.intent || params.intent === 0) { - entry.intent = params.intent as 0 | 1 | 2; - } - - if (params.min === 0 || typeof params.min === 'number') { - entry.min = params.min as number; - } - - if (params.max === 0 || typeof params.max === 'number') { - entry.max = params.max as number; - } - - if (typeof params.enabled === 'boolean') { - entry.enabled = params.enabled; - } - - if (params.group) { - entry.group = (params.group as string).toString(); - } - - if (params.removenote && typeof params.removenote === 'boolean' && params.removenote === true) { - // Sending "!update all=true&removenote=true" will set both - // note.buy and note.sell for entire/withgroup entries to null. - if (entry.note) { - entry.note.buy = null; - entry.note.sell = null; - } - } - - if (typeof params.autoprice === 'boolean') { - if (params.autoprice === false) { - entry.time = null; - } - entry.autoprice = params.autoprice; - } - - if (params.withgroup || params.withoutgroup) { - if (typeof params.note === 'object') { - // can change note if have withgroup/withoutgroup parameter - entry.note.buy = params.note.buy || entry.note.buy; - entry.note.sell = params.note.sell || entry.note.sell; - } - - if (typeof params.buy === 'object' && params.buy !== null) { - entry.buy.keys = params.buy.keys || 0; - entry.buy.metal = params.buy.metal || 0; - - if (params.autoprice === undefined) { - entry.autoprice = false; - } - } - - if (typeof params.sell === 'object' && params.sell !== null) { - entry.sell.keys = params.sell.keys || 0; - entry.sell.metal = params.sell.metal || 0; - - if (params.autoprice === undefined) { - entry.autoprice = false; - } - } - } - - if (i === 0) { - const errors = validator( - { - sku: entry.sku, - enabled: entry.enabled, - intent: entry.intent, - max: entry.max, - min: entry.min, - autoprice: entry.autoprice, - buy: entry.buy.toJSON(), - sell: entry.sell.toJSON(), - promoted: entry.promoted, - group: entry.group, - note: entry.note, - time: entry.time - }, - 'pricelist' - ); - - if (errors !== null) { - throw new Error(errors.join(', ')); - } - } - }); - - if (params.removenote) { - delete params.removenote; - } - - if (params.withgroup) { - newPricelist = unTargetedPricelist.concat(newPricelist); - delete params.withgroup; - } - - if (params.withoutgroup) { - newPricelist = unTargetedPricelist.concat(newPricelist); - delete params.withoutgroup; - } - - // FIXME: Make it so that it is not needed to remove all listings - - if (params.autoprice !== true) { - await bot.handler.onPricelist(newPricelist); - bot.sendMessage(steamID, 'āœ… Updated pricelist!'); - - return await bot.listings.redoListings(); - } - - bot.sendMessage(steamID, 'āŒ› Updating prices...'); - - return bot.pricelist - .setupPricelist() - .then(async () => { - bot.sendMessage(steamID, 'āœ… Updated pricelist!'); - await bot.listings.redoListings(); - }) - .catch(err => { - log.warn('Failed to update prices: ', err); - return bot.sendMessage(steamID, `āŒ Failed to update prices: ${(err as Error).message}`); - }); - } - - if (params.sku !== undefined && !testSKU(params.sku as string)) { - return bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); - } - - if (typeof params.buy === 'object' && params.buy !== null) { - params.buy.keys = params.buy.keys || 0; - params.buy.metal = params.buy.metal || 0; - - if (params.autoprice === undefined) { - params.autoprice = false; - } - } - - if (typeof params.sell === 'object' && params.sell !== null) { - params.sell.keys = params.sell.keys || 0; - params.sell.metal = params.sell.metal || 0; - - if (params.autoprice === undefined) { - params.autoprice = false; - } - } - - if (params.resetgroup) { - // if resetgroup (when sending "!update item=&resetgroup=true") is defined, - // first check if it's a booelan type - if (typeof params.resetgroup === 'boolean') { - // if it's boolean, then check if it's true - if (params.resetgroup === true) { - // if it's true, then set group key to null. - params.group = 'all'; - } - } else { - // else if it's not a boolean type, then send message. - return bot.sendMessage(steamID, 'āŒ "resetgroup" must be either "true" or "false".'); - } - // delete resetgroup key from params so it will not trying to be added into pricelist (validator error) - delete params.resetgroup; - } - - const isPremium = bot.handler.getBotInfo.premium; - if (params.promoted !== undefined) { - if (!isPremium) { - return bot.sendMessage( - steamID, - `āŒ This account is not Backpack.tf Premium. You can't use "promoted" paramter.` - ); - } - - if (typeof params.promoted === 'boolean') { - if (params.promoted === true) { - params.promoted = 1; - } else { - params.promoted = 0; - } - } else { - if (typeof params.promoted !== 'number') { - return bot.sendMessage(steamID, 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)'); - } - - if (params.promoted < 0 || params.promoted > 1) { - return bot.sendMessage(steamID, 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)'); - } - } - } - - if (params.item !== undefined) { - // Remove by full name - let match = bot.pricelist.searchByName(params.item as string, false); - if (match === null) { - return bot.sendMessage( - steamID, - `āŒ I could not find any items in my pricelist that contain "${params.item as string}"` - ); - } else if (Array.isArray(match)) { - const matchCount = match.length; - if (match.length > 20) { - match = match.splice(0, 20); - } - - let reply = `I've found ${match.length} items. Try with one of the items shown below:\n${match.join( - ',\n' - )}`; - - if (matchCount > match.length) { - const other = matchCount - match.length; - reply += `,\nand ${other} other ${pluralize('item', other)}.`; - } - - return bot.sendMessage(steamID, reply); - } - - delete params.item; - params.sku = match.sku; - } else if (params.sku === undefined) { - const item = getItemFromParams(steamID, params, bot); - - if (item === null) { - return bot.sendMessage(steamID, `āŒ Item not found.`); - } - - params.sku = SKU.fromObject(item); - } - - if (!bot.pricelist.hasPrice(params.sku as string)) { - return bot.sendMessage(steamID, 'āŒ Item is not in the pricelist.'); - } - - const itemEntry = bot.pricelist.getPrice(params.sku as string, false); - - if (typeof params.note === 'object') { - params.note.buy = params.note.buy || itemEntry.note.buy; - params.note.sell = params.note.sell || itemEntry.note.sell; - } - - if (params.removenote) { - // removenote to set both note.buy and note.sell to null. - if (typeof params.removenote === 'boolean') { - if (params.removenote === true) { - params.note = { buy: null, sell: null }; - } - } else { - return bot.sendMessage(steamID, 'āŒ "removenote" must be either "true" or "false".'); - } - - delete params.removenote; - } - - if (params.removebuynote && params.removesellnote) { - return bot.sendMessage( - steamID, - 'āŒ Please only use either "removebuynote" or "removesellnote", not both, or if you wish to remove both buy and sell note, please use "removenote".' - ); - } - - if (params.removebuynote) { - if (typeof params.removebuynote === 'boolean') { - if (params.removebuynote === true) { - params.note = { buy: null, sell: itemEntry.note.sell }; - } - } else { - return bot.sendMessage(steamID, 'āŒ "removebuynote" must be either "true" or "false".'); - } - - delete params.removebuynote; - } - - if (params.removesellnote) { - if (typeof params.removesellnote === 'boolean') { - if (params.removesellnote === true) { - params.note = { buy: itemEntry.note.buy, sell: null }; - } - } else { - return bot.sendMessage(steamID, 'āŒ "removesellnote" must be either "true" or "false".'); - } - - delete params.removesellnote; - } - - if (params.group && typeof params.group !== 'string') { - // if group parameter is defined, convert anything to string - params.group = (params.group as string).toString(); - } - - const entryData = bot.pricelist.getPrice(params.sku as string, false).getJSON(); - delete entryData.time; - delete params.sku; - - if (Object.keys(params).length === 0) { - return bot.sendMessage(steamID, 'āš ļø Missing properties to update.'); - } - - // Update entry - for (const property in params) { - if (!Object.prototype.hasOwnProperty.call(params, property)) { - continue; - } - - entryData[property] = params[property]; - } - - const generateUpdateReply = (bot: Bot, isPremium: boolean, oldEntry: Entry, newEntry: Entry) => { - const keyPrice = bot.pricelist.getKeyPrice; - const amount = bot.inventoryManager.getInventory.getAmount(oldEntry.sku); - return ( - `\nšŸ’² Buy: ${ - oldEntry.buy.toValue(keyPrice.metal) !== newEntry.buy.toValue(keyPrice.metal) - ? `${oldEntry.buy.toString()} ā†’ ${newEntry.buy.toString()}` - : newEntry.buy.toString() - } | Sell: ${ - oldEntry.sell.toValue(keyPrice.metal) !== newEntry.sell.toValue(keyPrice.metal) - ? `${oldEntry.sell.toString()} ā†’ ${newEntry.sell.toString()}` - : newEntry.sell.toString() - }` + - `\nšŸ“¦ Stock: ${amount}` + - ` | Min: ${oldEntry.min !== newEntry.min ? `${oldEntry.min} ā†’ ${newEntry.min}` : newEntry.min} | Max: ${ - oldEntry.max !== newEntry.max ? `${oldEntry.max} ā†’ ${newEntry.max}` : newEntry.max - }` + - `\nšŸ›’ Intent: ${ - oldEntry.intent !== newEntry.intent - ? `${oldEntry.intent === 2 ? 'bank' : oldEntry.intent === 1 ? 'sell' : 'buy'} ā†’ ${ - newEntry.intent === 2 ? 'bank' : newEntry.intent === 1 ? 'sell' : 'buy' - }` - : `${newEntry.intent === 2 ? 'bank' : newEntry.intent === 1 ? 'sell' : 'buy'}` - }` + - `\nšŸ“‹ Enabled: ${ - oldEntry.enabled !== newEntry.enabled - ? `${oldEntry.enabled ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.enabled ? 'āœ…' : 'āŒ'}` - : `${newEntry.enabled ? 'āœ…' : 'āŒ'}` - }` + - `\nšŸ”„ Autoprice: ${ - oldEntry.autoprice !== newEntry.autoprice - ? `${oldEntry.autoprice ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.autoprice ? 'āœ…' : 'āŒ'}` - : `${newEntry.autoprice ? 'āœ…' : 'āŒ'}` - }` + - (isPremium - ? `\nšŸ“¢ Promoted: ${ - oldEntry.promoted !== newEntry.promoted - ? `${oldEntry.promoted === 1 ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.promoted === 1 ? 'āœ…' : 'āŒ'}` - : `${newEntry.promoted === 1 ? 'āœ…' : 'āŒ'}` - }` - : '') + - `${ - newEntry.group !== 'all' - ? `\nšŸ”° Group: ${ - oldEntry.group !== newEntry.group ? `${oldEntry.group} ā†’ ${newEntry.group}` : newEntry.group - }` - : '' - }` + - `${newEntry.note.buy !== null ? `\nšŸ“„ Custom buying note: ${newEntry.note.buy}` : ''}` + - `${newEntry.note.sell !== null ? `\nšŸ“¤ Custom selling note: ${newEntry.note.sell}` : ''}` - ); - }; - - bot.pricelist - .updatePrice(entryData, true, PricelistChangedSource.Command) - .then(entry => { - bot.sendMessage( - steamID, - `āœ… Updated "${entry.name}"` + generateUpdateReply(bot, isPremium, itemEntry, entry) - ); - }) - .catch((err: ErrorRequest) => { - bot.sendMessage( - steamID, - 'āŒ Failed to update pricelist entry: ' + - (err.body && err.body.message ? err.body.message : err.message) - ); - }); -} - -let executed = false; -let lastExecutedTime: number | null = null; -let executeTimeout: NodeJS.Timeout; - -export async function shuffleCommand(steamID: SteamID, bot: Bot): Promise { - const newExecutedTime = dayjs().valueOf(); - const timeDiff = newExecutedTime - lastExecutedTime; - - const pricelist = bot.pricelist.getPrices; - - if (pricelist.length === 0) { - return bot.sendMessage(steamID, 'āŒ Pricelist is empty!'); - } - - if (executed === true) { - return bot.sendMessage( - steamID, - `āš ļø You need to wait ${Math.trunc( - (30 * 60 * 1000 - timeDiff) / (1000 * 60) - )} minutes before you can shuffle pricelist/shuffle again.` - ); - } else { - clearTimeout(executeTimeout); - lastExecutedTime = dayjs().valueOf(); - - const shufflePricelist = (arr: Entry[]) => { - return arr.sort(() => Math.random() - 0.5); - }; - - await bot.handler.onPricelist(shufflePricelist(pricelist)); - bot.sendMessage(steamID, 'āœ… Pricelist shuffled!'); - await bot.listings.redoListings(); - - executed = true; - executeTimeout = setTimeout(() => { - lastExecutedTime = null; - executed = false; - clearTimeout(executeTimeout); - }, 30 * 60 * 1000); - } -} - -export async function removeCommand(steamID: SteamID, message: string, bot: Bot): Promise { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - - if (params.all === true) { - // Remove entire pricelist - const pricelistLength = bot.pricelist.getLength; - if (pricelistLength === 0) { - return bot.sendMessage(steamID, 'āŒ Your pricelist is already empty!'); - } - - const pricelist = bot.pricelist.getPrices; - let newPricelist: Entry[] = []; - let newPricelistCount: Entry[] = []; - - if (params.withgroup && params.withoutgroup) { - return bot.sendMessage( - steamID, - `āŒ Don't be dumb. Please choose only "withgroup" OR "withoutgroup", not both. Thanks.` - ); - } - - if (params.withgroup) { - // first filter out pricelist with ONLY "withgroup" value. - newPricelistCount = pricelist.filter(entry => - entry.group ? [(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) : false - ); - - if (newPricelistCount.length === 0) { - return bot.sendMessage( - steamID, - `āŒ There is no entry with "${params.withgroup as string}" group found in your pricelist.` - ); - } - - // then filter out pricelist with NOT "withgroup" value. - newPricelist = pricelist.filter(entry => - entry.group ? ![(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) : true - ); - } else if (params.withoutgroup) { - // reverse of withgroup - newPricelistCount = pricelist.filter(entry => - entry.group - ? ![(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) - : true - ); - - if (newPricelistCount.length === 0) { - return bot.sendMessage( - steamID, - `āŒ There is no entry other than "${params.withoutgroup as string}" group found in your pricelist.` - ); - } - - newPricelist = pricelist.filter(entry => - entry.group - ? [(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) - : false - ); - } else { - newPricelistCount = pricelist; - } - - if (params.i_am_sure !== 'yes_i_am') { - return bot.sendMessage( - steamID, - '/pre āš ļø Are you sure that you want to remove ' + - pluralize('item', newPricelistCount.length, true) + - '? Try again with i_am_sure=yes_i_am' - ); - } - - if (params.withgroup || params.withoutgroup) { - try { - await bot.pricelist.removeByGroup(newPricelist); - bot.sendMessage(steamID, `āœ… Removed ${newPricelistCount.length} items from pricelist.`); - return await bot.listings.redoListings(); - } catch (err) { - return bot.sendMessage(steamID, `āŒ Failed to clear pricelist: ${(err as Error).message}`); - } - } else { - try { - await bot.pricelist.removeAll(); - return bot.sendMessage(steamID, 'āœ… Cleared pricelist!'); - } catch (err) { - return bot.sendMessage(steamID, `āŒ Failed to clear pricelist: ${(err as Error).message}`); - } - } - } - - if (params.sku !== undefined && !testSKU(params.sku as string)) { - return bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); - } - - if (params.item !== undefined) { - // Remove by full name - let match = bot.pricelist.searchByName(params.item as string, false); - if (match === null) { - return bot.sendMessage( - steamID, - `āŒ I could not find any items in my pricelist that contain "${params.item as string}"` - ); - } else if (Array.isArray(match)) { - const matchCount = match.length; - if (match.length > 20) match = match.splice(0, 20); - - let reply = `I've found ${match.length} items. Try with one of the items shown below:\n${match.join( - ',\n' - )}`; - - if (matchCount > match.length) { - const other = matchCount - match.length; - reply += `,\nand ${other} other ${pluralize('item', other)}.`; - } - - return bot.sendMessage(steamID, reply); - } - - delete params.item; - params.sku = match.sku; - } else if (params.sku === undefined) { - const item = getItemFromParams(steamID, params, bot); - - if (item === null) { - return bot.sendMessage(steamID, `āŒ Item not found.`); - } - - params.sku = SKU.fromObject(item); - } - - bot.pricelist - .removePrice(params.sku as string, true) - .then(entry => bot.sendMessage(steamID, `āœ… Removed "${entry.name}".`)) - .catch(err => bot.sendMessage(steamID, `āŒ Failed to remove pricelist entry: ${(err as Error).message}`)); -} - -export function getCommand(steamID: SteamID, message: string, bot: Bot): void { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - if (params.sku !== undefined && !testSKU(params.sku as string)) { - return bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); - } - - if (params.item !== undefined) { - // Remove by full name - let match = bot.pricelist.searchByName(params.item as string, false); - - if (match === null) { - return bot.sendMessage( - steamID, - `āŒ I could not find any items in my pricelist that contain "${params.item as string}"` - ); - } else if (Array.isArray(match)) { - const matchCount = match.length; - if (match.length > 20) { - match = match.splice(0, 20); - } - - let reply = `I've found ${match.length} items. Try with one of the items shown below:\n${match.join( - ',\n' - )}`; - - if (matchCount > match.length) { - const other = matchCount - match.length; - reply += `,\nand ${other} other ${pluralize('item', other)}.`; - } - - return bot.sendMessage(steamID, reply); - } - - delete params.item; - params.sku = match.sku; - } else if (params.sku === undefined) { - const item = getItemFromParams(steamID, params, bot); - - if (item === null) { - return bot.sendMessage(steamID, `āŒ Item not found.`); - } - - params.sku = SKU.fromObject(item); - } - - if (params.sku === undefined) { - return bot.sendMessage(steamID, 'āŒ Missing item'); - } - - const match = bot.pricelist.getPrice(params.sku as string); - if (match === null) { - bot.sendMessage(steamID, `āŒ Could not find item "${params.sku as string}" in the pricelist`); - } else { - bot.sendMessage(steamID, `/code ${generateOutput(match)}`); - } -} - -export async function findCommand(steamID: SteamID, message: string, bot: Bot): Promise { - const params = CommandParser.parseParams(CommandParser.removeCommand(message)); - if ( - !( - params.enabled !== undefined || - params.max !== undefined || - params.min !== undefined || - params.intent !== undefined || - params.autoprice !== undefined || - params.group !== undefined || - params.promoted !== undefined - ) - ) { - return bot.sendMessage( - steamID, - 'āš ļø Only parameters available for !find command: enabled, max, min, intent, promoted, autoprice or group\nExample: !find intent=bank&max=2' - ); - } - - let filter = bot.pricelist.getPrices; - if (params.enabled !== undefined) { - if (typeof params.enabled !== 'boolean') { - return bot.sendMessage(steamID, 'āš ļø enabled parameter must be "true" or "false"'); - } - filter = filter.filter(entry => entry.enabled === params.enabled); - } - - if (params.max !== undefined) { - if (typeof params.max !== 'number') { - return bot.sendMessage(steamID, 'āš ļø max parameter must be an integer'); - } - filter = filter.filter(entry => entry.max === params.max); - } - - if (params.min !== undefined) { - if (typeof params.min !== 'number') { - return bot.sendMessage(steamID, 'āš ļø min parameter must be an integer'); - } - filter = filter.filter(entry => entry.min === params.min); - } - - if (params.promoted !== undefined) { - if (typeof params.promoted === 'boolean') { - if (params.promoted === true) { - filter = filter.filter(entry => entry.promoted === 1); - } else { - filter = filter.filter(entry => entry.promoted === 0); - } - } else { - if (typeof params.promoted !== 'number') { - return bot.sendMessage(steamID, 'āš ļø promoted parameter must be either 0 (false) or 1 (true)'); - } - - if (params.promoted < 0 || params.promoted > 1) { - return bot.sendMessage(steamID, 'āš ļø "promoted" parameter must be either 0 (false) or 1 (true)'); - } - filter = filter.filter(entry => entry.promoted === params.promoted); - } - } - - if (params.intent !== undefined) { - if (typeof params.intent === 'string') { - const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); - if (intent !== -1) { - params.intent = intent; - } - } - - if (typeof params.intent !== 'number' || params.intent < 0) { - return bot.sendMessage( - steamID, - 'āš ļø intent parameter must be "buy", "sell", or "bank" OR an integer of "0", "1" or "2" respectively' - ); - } - filter = filter.filter(entry => entry.intent === params.intent); - } - - if (params.autoprice !== undefined) { - if (typeof params.autoprice !== 'boolean') { - return bot.sendMessage(steamID, 'āš ļø autoprice parameter must be "true" or "false"'); - } - filter = filter.filter(entry => entry.autoprice === params.autoprice); - } - - if (params.group !== undefined) { - if (typeof params.group !== 'string') { - return bot.sendMessage(steamID, 'āš ļø group parameter must be a string'); - } - filter = filter.filter(entry => entry.group === params.group); - } - - const parametersUsed = { - enabled: params.enabled !== undefined ? `enabled=${(params.enabled as boolean).toString()}` : '', - autoprice: params.autoprice !== undefined ? `autoprice=${(params.autoprice as boolean).toString()}` : '', - max: params.max !== undefined ? `max=${params.max as number}` : '', - min: params.min !== undefined ? `min=${params.min as number}` : '', - intent: - params.intent !== undefined - ? `intent=${ - (params.intent as number) === 0 ? 'buy' : (params.intent as number) === 1 ? 'sell' : 'bank' - }` - : '', - promoted: - params.promoted !== undefined ? `promoted=${(params.promoted as number) === 1 ? 'true' : 'false'}` : '', - group: params.group !== undefined ? `group=${params.group as string}` : '' - }; - - const parameters = Object.values(parametersUsed); - const display = parameters.filter(param => param !== ''); - - const length = filter.length; - if (length === 0) { - bot.sendMessage(steamID, `No items found with ${display.join('&')}.`); - } else if (length > 20) { - bot.sendMessage( - steamID, - `Found ${pluralize('item', length, true)} with ${display.join('&')}, showing only a max of 100 items` - ); - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 0, 20)}`); - if (length <= 40) { - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 20, length > 40 ? 40 : length)}`); - } else if (length <= 60) { - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 20, 40)}`); - await sleepasync().Promise.sleep(1 * 1000); - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 40, length > 60 ? 60 : length)}`); - } else if (length <= 80) { - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 20, 40)}`); - await sleepasync().Promise.sleep(1 * 1000); - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 40, 60)}`); - await sleepasync().Promise.sleep(1 * 1000); - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 60, length > 80 ? 80 : length)}`); - } else if (length > 80) { - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 20, 40)}`); - await sleepasync().Promise.sleep(1 * 1000); - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 40, 60)}`); - await sleepasync().Promise.sleep(1 * 1000); - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 60, 80)}`); - await sleepasync().Promise.sleep(1 * 1000); - bot.sendMessage(steamID, `/code ${generateOutput(filter, true, 80, length > 100 ? 100 : length)}`); - } - } else { - bot.sendMessage(steamID, `Found ${pluralize('item', filter.length, true)} with ${display.join('&')}`); - bot.sendMessage(steamID, `/code ${generateOutput(filter)}`); - } -} - -function generateOutput(filtered: Entry[] | Entry, isSlice = false, start?: number, end?: number): string { - return isSlice - ? JSON.stringify((filtered as Entry[]).slice(start, end), null, 4) - : JSON.stringify(filtered, null, 4); -} - -interface ErrorRequest { - body?: ErrorBody; - message?: string; -} - -interface ErrorBody { - message: string; -} diff --git a/src/classes/Commands/functions/request.ts b/src/classes/Commands/functions/request.ts deleted file mode 100644 index b6545db81..000000000 --- a/src/classes/Commands/functions/request.ts +++ /dev/null @@ -1,251 +0,0 @@ -import SteamID from 'steamid'; -import SKU from 'tf2-sku-2'; -import pluralize from 'pluralize'; -import dayjs from 'dayjs'; -import sleepasync from 'sleep-async'; -import Currencies from 'tf2-currencies'; -import { removeLinkProtocol, getItemFromParams, testSKU } from './utils'; -import Bot from '../../Bot'; -import CommandParser from '../../CommandParser'; -import log from '../../../lib/logger'; -import { fixItem } from '../../../lib/items'; -import { GetPriceFn, GetSalesFn, RequestCheckFn, RequestCheckResponse } from '../../Pricer'; - -export async function getSalesCommand( - steamID: SteamID, - message: string, - bot: Bot, - getSales: GetSalesFn -): Promise { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - if (params.sku === undefined) { - const item = getItemFromParams(steamID, params, bot); - - if (item === null) { - return bot.sendMessage(steamID, `āŒ Item not found.`); - } - - params.sku = SKU.fromObject(item); - } else { - params.sku = SKU.fromObject(fixItem(SKU.fromString(params.sku), bot.schema)); - } - - const name = bot.schema.getName(SKU.fromString(params.sku)); - try { - const salesData = await getSales(params.sku, 'bptf'); - if (!salesData) { - return bot.sendMessage( - steamID, - `āŒ No recorded snapshots found for ${name === null ? (params.sku as string) : name}.` - ); - } - - if (salesData.sales.length === 0) { - return bot.sendMessage( - steamID, - `āŒ No recorded snapshots found for ${name === null ? (params.sku as string) : name}.` - ); - } - - const sales: Sales[] = []; - salesData.sales.forEach(sale => - sales.push({ - seller: 'https://backpack.tf/profiles/' + sale.steamid, - itemHistory: 'https://backpack.tf/item/' + sale.id.replace('440_', ''), - keys: sale.currencies.keys, - metal: sale.currencies.metal, - date: sale.time - }) - ); - sales.sort((a, b) => b.date - a.date); - - let left = 0; - const SalesList: string[] = []; - for (let i = 0; i < sales.length; i++) { - if (SalesList.length > 40) { - left += 1; - } else { - SalesList.push( - `Listed #${i + 1}-----\nā€¢ Date: ${dayjs.unix(sales[i].date).utc().toString()}\nā€¢ Item: ${ - sales[i].itemHistory - }\nā€¢ Seller: ${sales[i].seller}\nā€¢ Was selling for: ${ - sales[i].keys > 0 ? `${sales[i].keys} keys,` : '' - } ${sales[i].metal} ref` - ); - } - } - - let reply = `šŸ”Ž Recorded removed sell listings from backpack.tf\n\nItem name: ${ - salesData.name - }\n\n-----${SalesList.join('\n\n-----')}`; - if (left > 0) { - reply += `,\n\nand ${left} other ${pluralize('sale', left)}`; - } - - bot.sendMessage(steamID, reply); - } catch (err) { - return bot.sendMessage( - steamID, - `āŒ Error getting sell snapshots for ${name === null ? (params.sku as string) : name}: ${ - (err as ErrorRequest).body && (err as ErrorRequest).body.message - ? (err as ErrorRequest).body.message - : (err as ErrorRequest).message - }` - ); - } -} - -// Request commands - -export function pricecheckCommand(steamID: SteamID, message: string, bot: Bot, requestCheck: RequestCheckFn): void { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - if (params.sku !== undefined && !testSKU(params.sku as string)) { - return bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); - } - - if (params.sku === undefined) { - const item = getItemFromParams(steamID, params, bot); - if (item === null) { - return bot.sendMessage(steamID, `āŒ Item not found.`); - } - - params.sku = SKU.fromObject(item); - } else { - params.sku = SKU.fromObject(fixItem(SKU.fromString(params.sku), bot.schema)); - } - - const name = bot.schema.getName(SKU.fromString(params.sku), false); - void requestCheck(params.sku, 'bptf').asCallback((err: ErrorRequest, body: RequestCheckResponse) => { - if (err) { - return bot.sendMessage( - steamID, - `āŒ Error while requesting price check: ${ - err.body && err.body.message ? err.body.message : err.message - }` - ); - } - - if (!body) { - bot.sendMessage(steamID, 'āŒ Error while requesting price check (returned null/undefined)'); - } else { - bot.sendMessage( - steamID, - `āŒ› Price check requested for ${ - body.name.includes('War Paint') || - body.name.includes('Mann Co. Supply Crate Series #') || - body.name.includes('Salvaged Mann Co. Supply Crate #') - ? name - : body.name - }, the item will be checked.` - ); - } - }); -} - -export async function pricecheckAllCommand(steamID: SteamID, bot: Bot, requestCheck: RequestCheckFn): Promise { - const pricelist = bot.pricelist.getPrices; - - const total = pricelist.length; - const totalTime = total * 2 * 1000; - const aSecond = 1 * 1000; - const aMin = 1 * 60 * 1000; - const anHour = 1 * 60 * 60 * 1000; - bot.sendMessage( - steamID, - `āŒ› Price check requested for ${total} items. It will be completed in approximately ${ - totalTime < aMin - ? `${Math.round(totalTime / aSecond)} seconds.` - : totalTime < anHour - ? `${Math.round(totalTime / aMin)} minutes.` - : `${Math.round(totalTime / anHour)} hours.` - } (about 2 seconds for each item).` - ); - - const skus = pricelist.map(entry => entry.sku); - let submitted = 0; - let success = 0; - let failed = 0; - for (const sku of skus) { - await sleepasync().Promise.sleep(2 * 1000); - void requestCheck(sku, 'bptf').asCallback(err => { - if (err) { - submitted++; - failed++; - log.warn(`pricecheck failed for ${sku}: ${JSON.stringify(err)}`); - log.debug( - `pricecheck for ${sku} failed, status: ${submitted}/${total}, ${success} success, ${failed} failed.` - ); - } else { - submitted++; - success++; - log.debug( - `pricecheck for ${sku} success, status: ${submitted}/${total}, ${success} success, ${failed} failed.` - ); - } - if (submitted === total) { - bot.sendMessage( - steamID, - `āœ… Successfully completed pricecheck for all ${total} ${pluralize('item', total)}!` - ); - } - }); - } -} - -export async function checkCommand(steamID: SteamID, message: string, bot: Bot, getPrice: GetPriceFn): Promise { - const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); - if (params.sku !== undefined && !testSKU(params.sku as string)) { - return bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); - } - - if (params.sku === undefined) { - const item = getItemFromParams(steamID, params, bot); - if (item === null) { - return bot.sendMessage(steamID, `āŒ Item not found.`); - } - - params.sku = SKU.fromObject(item); - } else { - params.sku = SKU.fromObject(fixItem(SKU.fromString(params.sku), bot.schema)); - } - - const name = bot.schema.getName(SKU.fromString(params.sku)); - try { - const price = await getPrice(params.sku, 'bptf'); - const currBuy = new Currencies(price.buy); - const currSell = new Currencies(price.sell); - - bot.sendMessage( - steamID, - `šŸ”Ž ${name}:\nā€¢ Buy : ${currBuy.toString()}\nā€¢ Sell : ${currSell.toString()}\n\nPrices.TF: https://prices.tf/items/${ - params.sku as string - }` - ); - } catch (err) { - return bot.sendMessage( - steamID, - `Error getting price for ${name === null ? (params.sku as string) : name}: ${ - (err as ErrorRequest).body && (err as ErrorRequest).body.message - ? (err as ErrorRequest).body.message - : (err as ErrorRequest).message - }` - ); - } -} - -interface Sales { - seller: string; - itemHistory: string; - keys: number; - metal: number; - date: number; -} - -interface ErrorRequest { - body?: ErrorBody; - message?: string; -} - -interface ErrorBody { - message: string; -} diff --git a/src/classes/Commands/functions/review.ts b/src/classes/Commands/functions/review.ts deleted file mode 100644 index ca02bef1a..000000000 --- a/src/classes/Commands/functions/review.ts +++ /dev/null @@ -1,335 +0,0 @@ -import SteamID from 'steamid'; -import pluralize from 'pluralize'; -import TradeOfferManager, { Action, OfferData, OurTheirItemsDict } from 'steam-tradeoffer-manager'; -import Currencies from 'tf2-currencies'; -import { UnknownDictionaryKnownValues } from '../../../types/common'; -import SKU from 'tf2-sku-2'; -import SchemaManager from 'tf2-schema-2'; -import Bot from '../../Bot'; -import CommandParser from '../../CommandParser'; -import { generateLinks } from '../../../lib/tools/export'; - -// Manual review commands - -export function tradesCommand(steamID: SteamID, bot: Bot): void { - // Go through polldata and find active offers - const pollData = bot.manager.pollData; - - const offersForReview: UnknownDictionaryKnownValues[] = []; - const activeOffersNotForReview: UnknownDictionaryKnownValues[] = []; - - for (const id in pollData.received) { - if (!Object.prototype.hasOwnProperty.call(pollData.received, id)) { - continue; - } - - if (pollData.received[id] !== TradeOfferManager.ETradeOfferState['Active']) { - continue; - } - - const data = pollData?.offerData[id] || null; - - if (data === null) { - continue; - } else if (data?.action?.action !== 'skip') { - activeOffersNotForReview.push({ id: id, data: data }); - continue; - } - - offersForReview.push({ id: id, data: data }); - } - - if (offersForReview.length === 0 && activeOffersNotForReview.length === 0) { - return bot.sendMessage(steamID, 'āŒ There are no active offers/ pending review.'); - } - - bot.sendMessage( - steamID, - (offersForReview.length > 0 ? generateTradesReply(offersForReview.sort((a, b) => a.id - b.id)) : '') + - (offersForReview.length > 0 ? '\n\n-----------------\n\n' : '') + - (activeOffersNotForReview.length > 0 - ? generateActiveOfferReply(activeOffersNotForReview.sort((a, b) => a.id - b.id)) - : '') - ); -} - -function generateTradesReply(offers: UnknownDictionaryKnownValues[]): string { - let reply = `There ${pluralize('is', offers.length, true)} active ${pluralize( - 'offer', - offers.length - )} that you can review:\n`; - for (let i = 0; i < offers.length; i++) { - reply += - `\n- Offer #${offers[i].id as string} from ${(offers[i].data as OfferData).partner} (reason: ${(offers[i] - .data as OfferData).action.meta.uniqueReasons.join(', ')})` + - `\nāš ļø Send "!trade ${offers[i].id as string}" for more details.\n`; - } - - return reply; -} - -function generateActiveOfferReply(offers: UnknownDictionaryKnownValues[]): string { - let reply = `There ${pluralize('is', offers.length, true)} ${pluralize( - 'offer', - offers.length - )} that currently still active:\n`; - for (let i = 0; i < offers.length; i++) { - reply += - `\n- Offer #${offers[i].id as string} from ${(offers[i].data as OfferData).partner}` + - `\nāš ļø Send "!trade ${offers[i].id as string}" for more details or "!faccept ${ - offers[i].id as string - }" to force accept the trade.\n`; - } - - return reply; -} - -export function tradeCommand(steamID: SteamID, message: string, bot: Bot): void { - const offerId = CommandParser.removeCommand(message).trim(); - - if (offerId === '') { - return bot.sendMessage(steamID, 'āš ļø Missing offer id. Example: "!trade 3957959294"'); - } - - const state = bot.manager.pollData.received[offerId]; - - if (state === undefined) { - return bot.sendMessage(steamID, 'Offer does not exist. āŒ'); - } - - if (state !== TradeOfferManager.ETradeOfferState['Active']) { - // TODO: Add what the offer is now, accepted / declined and why - return bot.sendMessage(steamID, 'Offer is not active. āŒ'); - } - - const offerData = bot.manager.pollData.offerData[offerId]; - - // Log offer details - - let reply = - offerData?.action?.action === 'skip' - ? `āš ļø Offer #${offerId} from ${offerData.partner} is pending for review` + - `\nReason: ${offerData.action.meta.uniqueReasons.join(', ')}).\n\nSummary:\n\n` - : `āš ļø Offer #${offerId} from ${offerData.partner} is still active.\n\nSummary:\n\n`; - - const keyPrice = bot.pricelist.getKeyPrice; - const value = offerData.value; - const items = offerData.dict || { our: null, their: null }; - const summarizeItems = (dict: OurTheirItemsDict, schema: SchemaManager.Schema) => { - if (dict === null) { - return 'unknown items'; - } - - const summary: string[] = []; - - for (const sku in dict) { - if (!Object.prototype.hasOwnProperty.call(dict, sku)) { - continue; - } - - summary.push(schema.getName(SKU.fromString(sku), false) + (dict[sku] > 1 ? ` x${dict[sku]}` : '')); // dict[sku] = amount - } - - if (summary.length === 0) { - return 'nothing'; - } - - return summary.join(', '); - }; - - if (!value) { - reply += - 'Asked: ' + summarizeItems(items.our, bot.schema) + '\nOffered: ' + summarizeItems(items.their, bot.schema); - } else { - const valueDiff = - new Currencies(value.their).toValue(keyPrice.metal) - new Currencies(value.our).toValue(keyPrice.metal); - const valueDiffRef = Currencies.toRefined(Currencies.toScrap(Math.abs(valueDiff * (1 / 9)))).toString(); - reply += - 'Asked: ' + - new Currencies(value.our).toString() + - ' (' + - summarizeItems(items.our, bot.schema) + - ')\nOffered: ' + - new Currencies(value.their).toString() + - ' (' + - summarizeItems(items.their, bot.schema) + - (valueDiff > 0 - ? `)\nšŸ“ˆ Profit from overpay: ${valueDiffRef} ref` - : valueDiff < 0 - ? `)\nšŸ“‰ Loss from underpay: ${valueDiffRef} ref` - : ')'); - } - - const links = generateLinks(offerData.partner.toString()); - reply += - `\n\nSteam: ${links.steam}\nBackpack.tf: ${links.bptf}\nSteamREP: ${links.steamrep}` + - (offerData?.action?.action === 'skip' - ? `\n\nāš ļø Send "!accept ${offerId}" to accept or "!decline ${offerId}" to decline this offer.` - : `\n\nāš ļø Send "!faccept ${offerId}" to force accept the trade now!`); - - bot.sendMessage(steamID, reply); -} - -type ActionOnTrade = 'accept' | 'accepttrade' | 'decline' | 'declinetrade'; - -export async function actionOnTradeCommand( - steamID: SteamID, - message: string, - bot: Bot, - command: ActionOnTrade -): Promise { - const offerIdAndMessage = CommandParser.removeCommand(message); - const offerIdRegex = /\d+/.exec(offerIdAndMessage); - - const isAccepting = ['accept', 'accepttrade'].includes(command); - - if (isNaN(+offerIdRegex) || !offerIdRegex) { - return bot.sendMessage( - steamID, - `āš ļø Missing offer id. Example: "!${isAccepting ? 'accept' : 'decline'} 3957959294"` - ); - } - - const offerId = offerIdRegex[0]; - const state = bot.manager.pollData.received[offerId]; - if (state === undefined) { - return bot.sendMessage(steamID, 'Offer does not exist. āŒ'); - } - - if (state !== TradeOfferManager.ETradeOfferState['Active']) { - // TODO: Add what the offer is now, accepted / declined and why - return bot.sendMessage(steamID, 'Offer is not active. āŒ'); - } - - const offerData = bot.manager.pollData.offerData[offerId]; - if (offerData?.action?.action !== 'skip') { - return bot.sendMessage(steamID, "Offer can't be reviewed. āŒ"); - } - - try { - const offer = await bot.trades.getOffer(offerId); - bot.sendMessage(steamID, `${isAccepting ? 'Accepting' : 'Declining'} offer...`); - - const partnerId = new SteamID(bot.manager.pollData.offerData[offerId].partner); - const reply = offerIdAndMessage.substr(offerId.length); - const adminDetails = bot.friends.getFriend(steamID); - - try { - await bot.trades.applyActionToOffer( - isAccepting ? 'accept' : 'decline', - 'MANUAL', - isAccepting ? (offer.data('action') as Action).meta : {}, - offer - ); - - if (isAccepting) { - const isManyItems = offer.itemsToGive.length + offer.itemsToReceive.length > 50; - - if (isManyItems) { - bot.sendMessage( - offer.partner, - bot.options.customMessage.accepted.manual.largeOffer - ? bot.options.customMessage.accepted.manual.largeOffer - : '.\nMy owner has manually accepted your offer. The trade may take a while to finalize due to it being a large offer.' + - ' If the trade does not finalize after 5-10 minutes has passed, please send your offer again, or add me and use ' + - 'the !sell/!sellcart or !buy/!buycart command.' - ); - } else { - bot.sendMessage( - offer.partner, - bot.options.customMessage.accepted.manual.smallOffer - ? bot.options.customMessage.accepted.manual.smallOffer - : '.\nMy owner has manually accepted your offer. The trade should be finalized shortly.' + - ' If the trade does not finalize after 1-2 minutes has passed, please send your offer again, or add me and use ' + - 'the !sell/!sellcart or !buy/!buycart command.' - ); - } - } - - // Send message to recipient if includes some messages - if (reply) { - bot.sendMessage( - partnerId, - `/quote šŸ’¬ Message from ${adminDetails ? adminDetails.player_name : 'admin'}: ${reply}` - ); - } - } catch (err) { - return bot.sendMessage( - steamID, - `āŒ Ohh nooooes! Something went wrong while trying to ${ - isAccepting ? 'accept' : 'decline' - } the offer: ${(err as Error).message}` - ); - } - } catch (err) { - return bot.sendMessage( - steamID, - `āŒ Ohh nooooes! Something went wrong while trying to ${isAccepting ? 'accept' : 'decline'} the offer: ${ - (err as Error).message - }` - ); - } -} - -type ForceAction = 'faccept' | 'fdecline'; - -export async function forceAction(steamID: SteamID, message: string, bot: Bot, command: ForceAction): Promise { - const offerIdAndMessage = CommandParser.removeCommand(message); - const offerIdRegex = /\d+/.exec(offerIdAndMessage); - - const isForceAccepting = command === 'faccept'; - - if (isNaN(+offerIdRegex) || !offerIdRegex) { - return bot.sendMessage( - steamID, - `āš ļø Missing offer id. Example: "!${isForceAccepting ? 'faccept' : 'fdecline'} 3957959294"` - ); - } - - const offerId = offerIdRegex[0]; - - const state = bot.manager.pollData.received[offerId]; - if (state === undefined) { - return bot.sendMessage(steamID, 'Offer does not exist. āŒ'); - } - - try { - const offer = await bot.trades.getOffer(offerId); - bot.sendMessage(steamID, `Force ${isForceAccepting ? 'accepting' : 'declining'} offer...`); - - const partnerId = new SteamID(bot.manager.pollData.offerData[offerId].partner); - const reply = offerIdAndMessage.substr(offerId.length); - const adminDetails = bot.friends.getFriend(steamID); - - try { - await bot.trades.applyActionToOffer( - isForceAccepting ? 'accept' : 'decline', - 'MANUAL-FORCE', - isForceAccepting ? (offer.data('action') as Action).meta : {}, - offer - ); - - // Send message to recipient if includes some messages - if (reply) { - bot.sendMessage( - partnerId, - `/quote šŸ’¬ Message from ${adminDetails ? adminDetails.player_name : 'admin'}: ${reply}` - ); - } - } catch (err) { - return bot.sendMessage( - steamID, - `āŒ Ohh nooooes! Something went wrong while trying to force ${ - isForceAccepting ? 'accept' : 'decline' - } the offer: ${(err as Error).message}` - ); - } - } catch (err) { - return bot.sendMessage( - steamID, - `āŒ Ohh nooooes! Something went wrong while trying to force ${ - isForceAccepting ? 'accept' : 'decline' - } the offer: ${(err as Error).message}` - ); - } -} diff --git a/src/classes/Commands/functions/status.ts b/src/classes/Commands/functions/status.ts deleted file mode 100644 index c9985d211..000000000 --- a/src/classes/Commands/functions/status.ts +++ /dev/null @@ -1,117 +0,0 @@ -import SteamID from 'steamid'; -import pluralize from 'pluralize'; -import Currencies from 'tf2-currencies'; -import Bot from '../../Bot'; -import { stats, profit } from '../../../lib/tools/export'; -import { sendStats } from '../../../lib/DiscordWebhook/export'; - -// Bot status - -export function statsCommand(steamID: SteamID, bot: Bot): void { - const tradesFromEnv = bot.options.statistics.lastTotalTrades; - const trades = stats(bot); - const profits = profit(bot, Math.floor((Date.now() - 86400000) / 1000)); //since -24h - - const keyPrices = bot.pricelist.getKeyPrices; - - const timedProfitmadeFull = Currencies.toCurrencies(profits.profitTimed, keyPrices.sell.metal).toString(); - const timedProfitmadeInRef = timedProfitmadeFull.includes('key') - ? ` (${Currencies.toRefined(profits.profitTimed)} ref)` - : ''; - - const profitmadeFull = Currencies.toCurrencies(profits.tradeProfit, keyPrices.sell.metal).toString(); - const profitmadeInRef = profitmadeFull.includes('key') ? ` (${Currencies.toRefined(profits.tradeProfit)} ref)` : ''; - - const profitOverpayFull = Currencies.toCurrencies(profits.overpriceProfit, keyPrices.sell.metal).toString(); - const profitOverpayInRef = profitOverpayFull.includes('key') - ? ` (${Currencies.toRefined(profits.overpriceProfit)} ref)` - : ''; - - bot.sendMessage( - steamID, - `All trades (accepted) are recorded from ${pluralize('day', trades.totalDays, true)}` + - ' ago šŸ“Š\n Total accepted trades: ' + - (tradesFromEnv !== 0 - ? String(tradesFromEnv + trades.totalAcceptedTrades) - : String(trades.totalAcceptedTrades)) + - `\n\n--- Last 24 hours ---` + - `\nā€¢ Processed: ${trades.hours24.processed}` + - `\nā€¢ Accepted: ${trades.hours24.accepted.offer + trades.hours24.accepted.sent}` + - `\n---ā€¢ Received offer: ${trades.hours24.accepted.offer}` + - `\n---ā€¢ Sent offer: ${trades.hours24.accepted.sent}` + - `\nā€¢ Declined: ${trades.hours24.decline.offer + trades.hours24.decline.sent}` + - `\n---ā€¢ Received offer: ${trades.hours24.decline.offer}` + - `\n---ā€¢ Sent offer: ${trades.hours24.decline.sent}` + - `\nā€¢ Skipped: ${trades.hours24.skipped}` + - `\nā€¢ Traded away: ${trades.hours24.invalid}` + - `\nā€¢ Canceled: ${trades.hours24.canceled.total}` + - `\n---ā€¢ by user: ${trades.hours24.canceled.byUser}` + - `\n---ā€¢ confirmation failed: ${trades.hours24.canceled.failedConfirmation}` + - `\n---ā€¢ unknown reason: ${trades.hours24.canceled.unknown}` + - `\n\n--- Since beginning of today ---` + - `\nā€¢ Processed: ${trades.today.processed}` + - `\nā€¢ Accepted: ${trades.today.accepted.offer + trades.today.accepted.sent}` + - `\n---ā€¢ Received offer: ${trades.today.accepted.offer}` + - `\n---ā€¢ Sent offer: ${trades.today.accepted.sent}` + - `\nā€¢ Declined: ${trades.today.decline.offer + trades.today.decline.sent}` + - `\n---ā€¢ Received offer: ${trades.today.decline.offer}` + - `\n---ā€¢ Sent offer: ${trades.today.decline.sent}` + - `\nā€¢ Skipped: ${trades.today.skipped}` + - `\nā€¢ Traded away: ${trades.today.invalid}` + - `\nā€¢ Canceled: ${trades.today.canceled.total}` + - `\n---ā€¢ by user: ${trades.today.canceled.byUser}` + - `\n---ā€¢ confirmation failed: ${trades.today.canceled.failedConfirmation}` + - `\n---ā€¢ unknown reason: ${trades.today.canceled.unknown}` + - `\n\n Profit (last 24h): ${timedProfitmadeFull + timedProfitmadeInRef}` + - `\nProfit made: ${profitmadeFull + profitmadeInRef} ${ - profits.since !== 0 ? ` (since ${pluralize('day', profits.since, true)} ago)` : '' - }` + - `\nProfit from overpay: ${profitOverpayFull + profitOverpayInRef}` + - `\nKey rate: ${keyPrices.buy.metal}/${keyPrices.sell.metal} ref` - ); -} - -export function statsDWCommand(steamID: SteamID, bot: Bot): void { - const opt = bot.options.discordWebhook.sendStats; - - if (!opt.enable) { - return bot.sendMessage(steamID, 'āŒ Sending stats to Discord Webhook is disabled.'); - } - - if (opt.url === '') { - return bot.sendMessage(steamID, 'āŒ Your discordWebhook.sendStats.url is empty.'); - } - - sendStats(bot, true, steamID); -} - -export function inventoryCommand(steamID: SteamID, bot: Bot): void { - bot.sendMessage( - steamID, - `šŸŽ’ My current items in my inventory: ${ - String(bot.inventoryManager.getInventory.getTotalItems) + '/' + String(bot.tf2.backpackSlots) - }` - ); -} - -export function versionCommand(steamID: SteamID, bot: Bot): void { - bot.sendMessage(steamID, `Currently running TF2Autobot@v${process.env.BOT_VERSION}. Checking for a new version...`); - - bot.checkForUpdates - .then(({ hasNewVersion, latestVersion }) => { - if (!hasNewVersion) { - bot.sendMessage(steamID, 'You are running the latest version of TF2Autobot!'); - } else if (bot.lastNotifiedVersion === latestVersion) { - bot.sendMessage( - steamID, - `āš ļø Update available! Current: v${process.env.BOT_VERSION}, Latest: v${latestVersion}.\n\n` + - `Release note: https://github.com/idinium96/tf2autobot/releases` + - `\n\nRun "!updaterepo" if you're running your bot with PM2 to update now!"` + - '\n\nContact IdiNium if you have any other problem. Thank you.' - ); - } - }) - .catch(err => { - bot.sendMessage(steamID, `āŒ Failed to check for updates: ${JSON.stringify(err)}`); - }); -} diff --git a/src/classes/Commands/functions/utils.ts b/src/classes/Commands/functions/utils.ts index 34fda618a..39323f597 100644 --- a/src/classes/Commands/functions/utils.ts +++ b/src/classes/Commands/functions/utils.ts @@ -162,11 +162,11 @@ export function getItemAndAmount( } else if (Array.isArray(match)) { const matchCount = match.length; - if (match.length > 20) { + if (matchCount > 20) { match = match.splice(0, 20); } - let reply = `I've found ${match.length} items. Try with one of the items shown below:\n${match.join(',\n')}`; + let reply = `I've found ${matchCount} items. Try with one of the items shown below:\n${match.join(',\n')}`; if (matchCount > match.length) { const other = matchCount - match.length; reply += `,\nand ${other} other ${pluralize('item', other)}.`; @@ -197,28 +197,33 @@ export function getItemFromParams( const match: SchemaManager.SchemaItem[] = []; - for (let i = 0; i < bot.schema.raw.schema.items.length; i++) { - if (bot.schema.raw.schema.items[i].item_name === params.name) { - match.push(bot.schema.raw.schema.items[i]); + const items = bot.schema.raw.schema.items; + const itemsCount = items.length; + + for (let i = 0; i < itemsCount; i++) { + if (items[i].item_name === params.name) { + match.push(items[i]); } } - if (match.length === 0) { + const matchCount = match.length; + + if (matchCount === 0) { bot.sendMessage( steamID, `āŒ Could not find an item in the schema with the name "${params.name as string}".` ); return null; - } else if (match.length !== 1) { - const matchCount = match.length; + } else if (matchCount !== 1) { const parsed = match.splice(0, 20).map(schemaItem => `${schemaItem.defindex} (${schemaItem.name})`); + const parsedCount = parsed.length; let reply = `I've found ${matchCount} items with a matching name. Please use one of the defindexes below as "defindex":\n${parsed.join( ',\n' )}`; - if (matchCount > parsed.length) { - const other = matchCount - parsed.length; + if (matchCount > parsedCount) { + const other = matchCount - parsedCount; reply += `,\nand ${other} other ${pluralize('item', other)}.`; } @@ -450,28 +455,33 @@ export function getItemFromParams( // Look for all items that have the same name const match: SchemaManager.SchemaItem[] = []; - for (let i = 0; i < bot.schema.raw.schema.items.length; i++) { - if (bot.schema.raw.schema.items[i].item_name === params.name) { - match.push(bot.schema.raw.schema.items[i]); + const items = bot.schema.raw.schema.items; + const itemsCount = bot.schema.raw.schema.items.length; + + for (let i = 0; i < itemsCount; i++) { + if (items[i].item_name === params.name) { + match.push(items[i]); } } - if (match.length === 0) { + const matchCount = match.length; + + if (matchCount === 0) { bot.sendMessage( steamID, `āŒ Could not find an item in the schema with the name "${params.name as string}".` ); return null; - } else if (match.length !== 1) { - const matchCount = match.length; + } else if (matchCount !== 1) { const parsed = match.splice(0, 20).map(schemaItem => `${schemaItem.defindex} (${schemaItem.name})`); + const parsedCount = parsed.length; let reply = `I've found ${matchCount} items with a matching name. Please use one of the defindexes below as "output":\n${parsed.join( ',\n' )}`; - if (matchCount > parsed.length) { - const other = matchCount - parsed.length; + if (matchCount > parsedCount) { + const other = matchCount - parsedCount; reply += `,\nand ${other} other ${pluralize('item', other)}.`; } diff --git a/src/classes/Commands/sub-classes/Help.ts b/src/classes/Commands/sub-classes/Help.ts new file mode 100644 index 000000000..dff6d6bf1 --- /dev/null +++ b/src/classes/Commands/sub-classes/Help.ts @@ -0,0 +1,141 @@ +import SteamID from 'steamid'; +import Bot from '../../Bot'; + +export default class HelpCommands { + private readonly bot: Bot; + + constructor(bot: Bot) { + this.bot = bot; + } + + helpCommand(steamID: SteamID): void { + const isAdmin = this.bot.isAdmin(steamID); + + this.bot.sendMessage( + steamID, + `šŸ“Œ Note šŸ“Œ${ + isAdmin + ? '\nā€¢ a = Directly add "a"' + + '\nā€¢ [a] = Optionally add "a"' + + '\nā€¢ (a|b) = Directly input "a" OR "b"' + + '\nā€¢ = Replace "a" with relevant content' + + '\n\nDo not include characters <>, ( | ) nor [ ] when typing it. For more info, please refer to the wiki: https://github.com/idinium96/tf2autobot/wiki/What-is-the-pricelist%3F#table-of-contents' + : `\nDo not include characters <> nor [ ] - <> means required and [] means optional.` + }\n\nšŸ“œ Here's a list of my commands:\n- ${ + isAdmin + ? [ + '!deposit (sku|name|defindex)=&amount= - Deposit items', + '!withdraw (sku|name|defindex)=&amount= - Withdraw items\n\nāœØ=== Pricelist manager ===āœØ', + '!add (sku|name|defindex)=&[Listing-parameters] - Add a pricelist entry āž•', + '!autoadd [Listing-parameters] - Perform automatic adding items to the pricelist based on items that are currently available in your bot inventory (about 2 seconds every item) šŸ¤–', + '!stopautoadd - Stop automatic add items operation šŸ›‘', + '!update (sku|name|defindex|item)=&[Listing-parameters] - Update a pricelist entry', + '!remove (sku|name|defindex|item)= - Remove a pricelist entry āž–', + '!shuffle - Shuffle pricelist entries.', + '!get (sku|name|defindex|item)= - Get raw information about a pricelist entry\n\nāœØ=== Bot manager ===āœØ', + "!expand craftable=(true|false) - Use Backpack Expanders to increase the bot's inventory limit", + '!use (sku|assetid)= - Use an item (such as Gift-Stuffed Stocking 2020 - sku: 5923;6;untradable)', + "!delete (sku|assetid)= - Delete an item from the bot's inventory (SKU input only) šŸš®", + '!message - Send a message to a specific user šŸ’¬', + '!block - Block a specific user', + '!unblock - Unblock a specific user', + '!clearfriends - Clear friendlist (will keep admins and friendsToKeep) šŸ‘‹', + '!stop - Stop the bot šŸ”“', + '!restart - Restart the bot šŸ”„', + '!updaterepo - Update your bot to the latest version (only if cloned and running with PM2)', + "!refreshautokeys - Refresh the bot's autokeys settings.", + '!refreshlist - Refresh sell listings šŸ”„', + "!name - Change the bot's name", + "!avatar - Change the bot's avatar\n\nāœØ=== Bot status ===āœØ", + '!stats - Get statistics for accepted trades šŸ“Š', + '!statsdw - Send statistics to Discord Webhook šŸ“Š', + "!inventory - Get the bot's current inventory spaces šŸŽ’", + '!version - Get the TF2Autobot version that the bot is running\n\nāœØ=== Manual review ===āœØ', + '!trades - Get a list of trade offers pending for manual review šŸ”', + '!trade - Get information about a trade', + '!offerinfo - Get information about the offer from polldata šŸ”', + '!accept [Your Message] - Manually accept an active offer āœ…šŸ”', + '!decline [Your Message] - Manually decline an active offer āŒšŸ”', + '!faccept [Your Message] - Force accept any failed to accept offer āœ…šŸ”‚', + '!fdecline [Your Message] - Force decline any failed to decline offer āŒšŸ”‚\n\nāœØ=== Request ===āœØ', + '!check (sku|name|defindex)= - Request the current price for an item from Prices.TF', + '!pricecheck (sku|name|defindex|item)= - Request an item to be price checked by Prices.TF', + "!pricecheckall - Request all items in the bot's pricelist to be price checked by Prices.TF\n\nāœØ=== Misc ===āœØ", + "!autokeys - Get info on the bot's current autokeys settings šŸ”‘", + "!time - Show the owner's current time šŸ•„", + '!uptime - Show the bot uptime šŸ”Œ', + "!pure - Get the bot's current pure stock šŸ’°", + "!rate - Get the bot's current key rates šŸ”‘", + '!stock - Get a list of items that the bot owns', + "!craftweapon - Get a list of the bot's craftable weapon stock šŸ”«", + "!uncraftweapon - Get a list of the bot's uncraftable weapon stock šŸ”«", + '!paints - Get a list of paints partial sku šŸŽØ', + '!sales (sku|name|defindex)= - Get the sales history for an item šŸ”', + '!find - Get the list of filtered items detail based on the parameters šŸ”', + '!options [OptionsKey] - Get options.json content (current bot option settings) šŸ”§', + '!config =[&OtherOptions] - Update the current options (example: !config game.customName=Selling Tools!) šŸ”§', + '!donatebptf (sku|name|defindex)=&amount= - Donate to backpack.tf (https://backpack.tf/donate) šŸ’°', + '!premium months= - Purchase backpack.tf premium using keys (https://backpack.tf/premium/subscribe) šŸ‘‘' + ].join('\n- ') + : [ + '!help - Get a list of commands', + '!how2trade - Guide on how to trade with the bot', + '!price [amount] - Get the price and stock of an item šŸ’²šŸ“¦\n\nāœØ=== Instant item trade ===āœØ', + '!buy [amount] - Instantly buy an item šŸ’²', + '!sell [amount] - Instantly sell an item šŸ’²\n\nāœØ=== Multiple items trade ===āœØ', + '!buycart [amount] - Add an item you want to buy to your cart šŸ›’', + '!sellcart [amount] - Add an item you want to sell to your cart šŸ›’', + '!cart - View your cart šŸ›’', + '!clearcart - Clear your cart āŽšŸ›’', + '!checkout - Have the bot send an offer with the items in your cart āœ…šŸ›’\n\nāœØ=== Trade actions ===āœØ', + '!cancel - Cancel the trade offer āŒ', + '!queue - Check your position in the queue\n\nāœØ=== Contact Owner ===āœØ', + "!owner - Get the owner's Steam profile and Backpack.tf links", + '!message - Send a message to the owner of the bot šŸ’¬', + "!discord - Get a link to join TF2Autobot and/or the owner's discord server\n\nāœØ=== Other Commands ===āœØ", + '!more - Show the advanced commands list' + ].join('\n- ') + }` + ); + } + + moreCommand(steamID: SteamID): void { + const opt = this.bot.options.commands.more; + + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); + } + } + + this.bot.sendMessage( + steamID, + `Advanced commands list:\n- ${[ + "!autokeys - Get info on the bot's current autokeys settings šŸ”‘", + "!time - Show the owner's current time šŸ•„", + '!uptime - Show the bot uptime šŸ”Œ', + "!pure - Get the bot's current pure stock šŸ’°", + "!rate - Get the bot's current key rates šŸ”‘", + '!stock - Get a list of items that the bot owns', + "!craftweapon - Get a list of the bot's craftable weapon stock šŸ”«", + "!uncraftweapon - Get a list of the bot's uncraftable weapon stock šŸ”«" + ].join('\n- ')}` + ); + } + + howToTradeCommand(steamID: SteamID): void { + const custom = this.bot.options.commands.how2trade.customReply.reply; + + this.bot.sendMessage( + steamID, + custom + ? custom + : '/quote You can either send me an offer yourself, or use one of my commands to request a trade. ' + + 'Say you want to buy a Team Captain, just type "!buy Team Captain", if want to buy more, ' + + 'just add the [amount] - "!buy 2 Team Captain". Type "!help" for all the commands.' + + '\nYou can also buy or sell multiple items by using the "!buycart [amount] " or ' + + '"!sellcart [amount] " commands.' + ); + } +} diff --git a/src/classes/Commands/sub-classes/Manager.ts b/src/classes/Commands/sub-classes/Manager.ts new file mode 100644 index 000000000..0fa972919 --- /dev/null +++ b/src/classes/Commands/sub-classes/Manager.ts @@ -0,0 +1,721 @@ +import SteamID from 'steamid'; +import SKU from 'tf2-sku-2'; +import pluralize from 'pluralize'; +import Currencies from 'tf2-currencies'; +import validUrl from 'valid-url'; +import child from 'child_process'; +import fs from 'graceful-fs'; +import sleepasync from 'sleep-async'; +import path from 'path'; +import dayjs from 'dayjs'; +import { EPersonaState } from 'steam-user'; +import { testSKU } from '../functions/utils'; +import Bot from '../../Bot'; +import CommandParser from '../../CommandParser'; +import log from '../../../lib/logger'; +import { pure } from '../../../lib/tools/export'; + +// Bot manager commands + +type TF2GC = 'expand' | 'use' | 'delete'; +type NameAvatar = 'name' | 'avatar'; +type BlockUnblock = 'block' | 'unblock'; + +export default class ManagerCommands { + private readonly bot: Bot; + + private pricelistLength = 0; + + private executed = false; + + private lastExecutedTime: number | null = null; + + private executeTimeout: NodeJS.Timeout; + + constructor(bot: Bot) { + this.bot = bot; + } + + TF2GCCommand(steamID: SteamID, message: string, command: TF2GC): void { + const params = CommandParser.parseParams(CommandParser.removeCommand(message)); + + if (command === 'expand') { + // Expand command + if (typeof params.craftable !== 'boolean') { + return this.bot.sendMessage(steamID, 'āš ļø Missing `craftable=true|false`'); + } + + const item = SKU.fromString('5050;6'); + if (params.craftable === false) { + item.craftable = false; + } + + const assetids = this.bot.inventoryManager.getInventory.findBySKU(SKU.fromObject(item), false); + if (assetids.length === 0) { + // No backpack expanders + return this.bot.sendMessage( + steamID, + `āŒ I couldn't find any ${!item.craftable ? 'Non-Craftable' : ''} Backpack Expander` + ); + } + + this.bot.tf2gc.useItem(assetids[0], err => { + if (err) { + log.warn('Error trying to expand inventory: ', err); + return this.bot.sendMessage(steamID, `āŒ Failed to expand inventory: ${err.message}`); + } + + this.bot.sendMessage(steamID, `āœ… Used ${!item.craftable ? 'Non-Craftable' : ''} Backpack Expander!`); + }); + } else { + // For use and delete commands + if (params.sku !== undefined && !testSKU(params.sku as string)) { + return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); + } + + if (params.assetid !== undefined && params.sku === undefined) { + const targetedAssetId = params.assetid as string; + const sku = this.bot.inventoryManager.getInventory.findByAssetid(targetedAssetId); + + if (params.i_am_sure !== 'yes_i_am') { + return this.bot.sendMessage( + steamID, + `āš ļø Are you sure that you want to ${command} ${ + sku === null + ? `the item with asset ID ${targetedAssetId}` + : `${this.bot.schema.getName(SKU.fromString(sku), false)}` + }?` + + `\n- This process is irreversible and will ${command} the item from your bot's backpack!` + + `\n- If you are sure, try again with i_am_sure=yes_i_am as a parameter` + ); + } + + return this.bot.tf2gc[command === 'use' ? 'useItem' : 'deleteItem'](targetedAssetId, err => { + const theItem = + sku === null + ? targetedAssetId + : `${this.bot.schema.getName(SKU.fromString(sku), false)} (${targetedAssetId})`; + + if (err) { + log.warn(`Error trying to ${command} ${theItem}: `, err); + return this.bot.sendMessage(steamID, `āŒ Failed to ${command} ${theItem}: ${err.message}`); + } + + this.bot.sendMessage(steamID, `āœ… ${command === 'use' ? 'Used' : 'Deleted'} ${theItem}!`); + }); + } + + if (params.name !== undefined || params.item !== undefined) { + return this.bot.sendMessage( + steamID, + command === 'use' + ? 'āš ļø Please only use sku property.' + + '\n\nBelow are some common items to use:\n ā€¢ ' + + [ + 'Gift-Stuffed Stocking 2013: 5718;6;untradable', + 'Gift-Stuffed Stocking 2017: 5886;6;untradable', + 'Gift-Stuffed Stocking 2018: 5900;6;untradable', + 'Gift-Stuffed Stocking 2019: 5910;6;untradable', + 'Gift-Stuffed Stocking 2020: 5923;6;untradable' + ].join('\nā€¢ ') + : 'āš ļø Please only use sku property.' + + '\n\nBelow are some common items to delete:\n ā€¢ ' + + [ + 'Smissmas Sweater: 16391;15;untradable;w1;pk391', + 'Soul Gargoyle: 5826;6;uncraftable;untradable', + 'Noise Maker - TF Birthday: 536;6;untradable', + 'Bronze Dueling Badge: 242;6;untradable', + 'Silver Dueling Badge: 243;6;untradable', + 'Gold Dueling Badge: 244;6;untradable', + 'Platinum Dueling Badge: 245;6;untradable', + 'Mercenary: 166;6;untradable', + 'Soldier of Fortune: 165;6;untradable', + 'Grizzled Veteran: 164;6;untradable', + 'Primeval Warrior: 170;6;untradable', + 'Professor Speks: 343;6;untradable', + 'Mann Co. Cap: 261;6;untradable', + 'Mann Co. Online Cap: 994;6;untradable', + 'Proof of Purchase: 471;6;untradable', + 'Mildly Disturbing Halloween Mask: 115;6;untradable', + 'Seal Mask: 582;6;untradable', + 'Pyrovision Goggles: 743;6;untradable', + 'Giftapult: 5083;6;untradable', + 'Spirit Of Giving: 655;11;untradable', + 'Party Hat: 537;6;untradable', + 'Name Tag: 5020;6;untradable', + 'Description Tag: 5044;6;untradable', + 'Ghastly Gibus: 584;6;untradable', + 'Ghastlier Gibus: 279;6;untradable', + 'Power Up Canteen: 489;6;untradable', + 'Bombinomicon: 583;6;untradable', + 'Skull Island Topper: 941;6;untradable', + 'Spellbook Page: 8935;6;untradable', + 'Gun Mettle Campaign Coin: 5809;6;untradable', + 'MONOCULUS!: 581;6;untradable' + ].join('\nā€¢ ') + ); + } + + if (params.sku === undefined) { + return this.bot.sendMessage( + steamID, + `āš ļø Missing sku property. Example: "!${command} sku=5923;6;untradable"` + ); + } + + const targetedSKU = params.sku as string; + const [uncraft, untrade] = [targetedSKU.includes(';uncraftable'), targetedSKU.includes(';untradable')]; + + const item = SKU.fromString(targetedSKU.replace(';uncraftable', '').replace(';untradable', '')); + + if (uncraft) { + item.craftable = !uncraft; + } + if (untrade) { + item.tradable = !untrade; + } + + const assetids = this.bot.inventoryManager.getInventory.findBySKU(SKU.fromObject(item), false); + const name = this.bot.schema.getName(item, false); + + if (assetids.length === 0) { + // Item not found + return this.bot.sendMessage(steamID, `āŒ I couldn't find any ${pluralize(name, 0)}`); + } + + let assetid: string; + if (params.assetid !== undefined) { + const targetedAssetId = params.assetid as string; + + if (assetids.includes(targetedAssetId)) { + assetid = targetedAssetId; + } else { + return this.bot.sendMessage( + steamID, + `āŒ Looks like an assetid ${targetedAssetId} did not match any assetids associated with ${name}` + + ` in my inventory. Try using the sku to use a random assetid.` + ); + } + } else { + assetid = assetids[0]; + } + + if (params.i_am_sure !== 'yes_i_am') { + return this.bot.sendMessage( + steamID, + `/pre āš ļø Are you sure that you want to ${command} ${name}?` + + `\n- This process is irreversible and will ${command} the item from your bot's backpack!` + + `\n- If you are sure, try again with i_am_sure=yes_i_am as a parameter` + ); + } + + this.bot.tf2gc[command === 'use' ? 'useItem' : 'deleteItem'](assetid, err => { + if (err) { + log.warn(`Error trying to ${command} ${name}: `, err); + return this.bot.sendMessage( + steamID, + `āŒ Failed to ${command} ${name} (${assetid}): ${err.message}` + ); + } + + this.bot.sendMessage(steamID, `āœ… ${command === 'use' ? 'Used' : 'Deleted'} ${name} (${assetid})!`); + }); + } + } + + nameAvatarCommand(steamID: SteamID, message: string, command: NameAvatar): void { + const example = + 'https://steamcdn-a.akamaihd.net/steamcommunity/public/images/avatars/f5/f57685d33224e32436f366d1acb4a1769bdfa60f_full.jpg'; + const input = CommandParser.removeCommand(message); + + if (!input || input === `!${command}`) { + return this.bot.sendMessage( + steamID, + `āŒ You forgot to add ${command === 'name' ? 'a name' : 'an image url'}. Example: "!${ + command === 'name' ? 'name IdiNium' : `avatar ${example}` + } "` + ); + } + + if (command === 'name') { + this.bot.community.editProfile( + { + name: input + }, + err => { + if (err) { + log.warn('Error while changing name: ', err); + return this.bot.sendMessage(steamID, `āŒ Error while changing name: ${err.message}`); + } + + this.bot.sendMessage(steamID, 'āœ… Successfully changed name.'); + } + ); + } else { + if (!validUrl.isUri(input)) { + return this.bot.sendMessage(steamID, `āŒ Your url is not valid. Example: "!avatar ${example}"`); + } + + this.bot.community.uploadAvatar(input, err => { + if (err) { + log.warn('Error while uploading new avatar: ', err); + return this.bot.sendMessage(steamID, `āŒ Error while uploading a new avatar: ${err.message}`); + } + + this.bot.sendMessage(steamID, 'āœ… Successfully uploaded a new avatar.'); + }); + } + } + + blockUnblockCommand(steamID: SteamID, message: string, command: BlockUnblock): void { + const steamid = CommandParser.removeCommand(message); + + if (!steamid || steamid === `!${command}`) { + return this.bot.sendMessage( + steamID, + `āŒ You forgot to add their SteamID64. Example: "!${command} 76561198798404909"` + ); + } + + const targetSteamID64 = new SteamID(steamid); + if (!targetSteamID64.isValid()) { + return this.bot.sendMessage(steamID, `āŒ SteamID is not valid. Example: "!${command} 76561198798404909"`); + } + + this.bot.client[command === 'block' ? 'blockUser' : 'unblockUser'](targetSteamID64, err => { + if (err) { + log.warn(`Failed to ${command} user ${targetSteamID64.getSteamID64()}: `, err); + return this.bot.sendMessage( + steamID, + `āŒ Failed to ${command} user ${targetSteamID64.getSteamID64()}: ${err.message}` + ); + } + this.bot.sendMessage( + steamID, + `āœ… Successfully ${ + command === 'block' ? 'blocked' : 'unblocked' + } user ${targetSteamID64.getSteamID64()}` + ); + }); + } + + async clearFriendsCommand(steamID: SteamID): Promise { + const friendsToRemove = this.bot.friends.getFriends.filter( + steamid => !this.bot.handler.friendsToKeep.includes(steamid) + ); + + for (const steamid of friendsToRemove) { + this.bot.sendMessage( + steamid, + this.bot.options.customMessage.clearFriends + ? this.bot.options.customMessage.clearFriends + : `/quote Hey ${ + this.bot.friends.getFriend(steamid).player_name + }! My owner has performed friend list clearance. Please feel free to add me again if you want to trade at a later time!` + ); + + this.bot.client.removeFriend(steamid); + + // Prevent Steam from detecting the bot as spamming + await sleepasync().Promise.sleep(2 * 1000); + } + + this.bot.sendMessage(steamID, `āœ… Friendlist clearance success! Removed ${friendsToRemove.length} friends.`); + } + + stopCommand(steamID: SteamID): void { + this.bot.sendMessage(steamID, 'āŒ› Stopping...'); + + this.bot.botManager.stopProcess().catch(err => { + log.warn('Error occurred while trying to stop: ', err); + this.bot.sendMessage(steamID, `āŒ An error occurred while trying to stop: ${(err as Error).message}`); + }); + } + + restartCommand(steamID: SteamID): void { + this.bot.sendMessage(steamID, 'āŒ› Restarting...'); + + this.bot.botManager + .restartProcess() + .then(restarting => { + if (!restarting) { + this.bot.sendMessage( + steamID, + 'āŒ You are not running the bot with PM2! Get a VPS and run ' + + 'your bot with PM2: https://github.com/TF2Autobot/tf2autobot/wiki/Getting-a-VPS' + ); + } + }) + .catch(err => { + log.warn('Error occurred while trying to restart: ', err); + this.bot.sendMessage( + steamID, + `āŒ An error occurred while trying to restart: ${(err as Error).message}` + ); + }); + } + + updaterepoCommand(steamID: SteamID, message: string): void { + if (!fs.existsSync(path.resolve(__dirname, '..', '..', '..', '..', '.git'))) { + return this.bot.sendMessage(steamID, 'āŒ You did not clone the bot from Github.'); + } + + if (process.env.pm_id === undefined) { + return this.bot.sendMessage( + steamID, + `āŒ You're not running the bot with PM2!` + + `\n\nNavigate to your bot folder and run ` + + `[git reset HEAD --hard && git checkout master && git pull && npm install && npm run build] ` + + `and then restart your bot.` + ); + } + + const params = CommandParser.parseParams(CommandParser.removeCommand(message)); + if (params.i_am_sure !== 'yes_i_am') { + this.bot.sendMessage( + steamID, + `Currently running TF2Autobot@v${process.env.BOT_VERSION}. Checking for a new version...` + ); + + this.bot.checkForUpdates + .then(({ hasNewVersion, latestVersion }) => { + if (!hasNewVersion) { + return this.bot.sendMessage(steamID, 'You are running the latest version of TF2Autobot!'); + } else if (this.bot.lastNotifiedVersion === latestVersion) { + return this.bot.sendMessage( + steamID, + `āš ļø Update available! Current: v${process.env.BOT_VERSION}, Latest: v${latestVersion}.` + + '\nSend "!updaterepo i_am_sure=yes_i_am" to update your repo now!' + + `\n\nRelease note: https://github.com/TF2Autobot/tf2autobot/releases` + ); + } + }) + .catch(err => this.bot.sendMessage(steamID, `āŒ Failed to check for updates: ${JSON.stringify(err)}`)); + } else { + this.bot.sendMessage(steamID, 'āŒ› Updating...'); + // Make the bot snooze on Steam, that way people will know it is not running + this.bot.client.setPersona(EPersonaState.Snooze); + + // Set isUpdating status, so any command will not be processed + this.bot.handler.isUpdatingStatus = true; + + // Stop polling offers + this.bot.manager.pollInterval = -1; + + // Callback hell šŸ˜ˆ + + // git reset HEAD --hard + child.exec('git reset HEAD --hard', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { + // ignore err + + // git checkout master + child.exec('git checkout master', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { + // ignore err + + this.bot.sendMessage(steamID, 'āŒ› Pulling changes...'); + + // git pull + child.exec('git pull', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { + // ignore err + + void promiseDelay(3 * 1000); + + this.bot.sendMessage(steamID, 'āŒ› Installing packages...'); + + // npm install + child.exec('npm install', { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, () => { + // ignore err + + // 10 seconds delay, because idk why this always cause some problem + void promiseDelay(10 * 1000); + + this.bot.sendMessage(steamID, 'āŒ› Compiling TypeScript codes into JavaScript...'); + + // tsc -p . + child.exec( + 'npm run build', + { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, + () => { + // ignore err + + // 5 seconds delay? + void promiseDelay(5 * 1000); + + this.bot.sendMessage(steamID, 'āŒ› Restarting...'); + + child.exec( + 'pm2 restart ecosystem.json', + { cwd: path.resolve(__dirname, '..', '..', '..', '..') }, + () => { + // ignore err + } + ); + } + ); + }); + }); + }); + }); + } + } + + autokeysCommand(steamID: SteamID): void { + const opt = this.bot.options.commands.autokeys; + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); + } + } + + this.bot.sendMessage(steamID, '/pre ' + this.generateAutokeysReply(steamID, this.bot)); + } + + refreshAutokeysCommand(steamID: SteamID): void { + if (this.bot.handler.autokeys.isEnabled === false) { + return this.bot.sendMessage(steamID, `This feature is disabled.`); + } + + this.bot.handler.autokeys.refresh(); + this.bot.sendMessage(steamID, 'āœ… Successfully refreshed Autokeys.'); + } + + refreshListingsCommand(steamID: SteamID): void { + const newExecutedTime = dayjs().valueOf(); + const timeDiff = newExecutedTime - this.lastExecutedTime; + + if (this.executed === true) { + return this.bot.sendMessage( + steamID, + `āš ļø You need to wait ${Math.trunc( + ((this.pricelistLength > 1000 ? 60 : 30) * 60 * 1000 - timeDiff) / (1000 * 60) + )} minutes before you run refresh listings command again.` + ); + } else { + const listingsSKUs: string[] = []; + this.bot.listingManager.getListings(async err => { + if (err) { + return this.bot.sendMessage( + steamID, + 'āŒ Unable to refresh listings, please try again later: ' + JSON.stringify(err) + ); + } + + const inventory = this.bot.inventoryManager; + const isFilterCantAfford = this.bot.options.pricelist.filterCantAfford.enable; + + this.bot.listingManager.listings.forEach(listing => { + let listingSKU = listing.getSKU(); + if (listing.intent === 1) { + if (this.bot.options.normalize.painted.our && /;[p][0-9]+/.test(listingSKU)) { + listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); + } + + if (this.bot.options.normalize.festivized.our && listingSKU.includes(';festive')) { + listingSKU = listingSKU.replace(';festive', ''); + } + + if (this.bot.options.normalize.strangeAsSecondQuality.our && listingSKU.includes(';strange')) { + listingSKU = listingSKU.replace(';strange', ''); + } + } else { + if (/;[p][0-9]+/.test(listingSKU)) { + listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); + } + } + + const match = this.bot.pricelist.getPrice(listingSKU); + + if (isFilterCantAfford && listing.intent === 0 && match !== null) { + const canAffordToBuy = inventory.isCanAffordToBuy(match.buy, inventory.getInventory); + + if (!canAffordToBuy) { + // Listing for buying exist but we can't afford to buy, remove. + log.debug(`Intent buy, removed because can't afford: ${match.sku}`); + listing.remove(); + } + } + + listingsSKUs.push(listingSKU); + }); + + // Remove duplicate elements + const newlistingsSKUs: string[] = []; + listingsSKUs.forEach(sku => { + if (!newlistingsSKUs.includes(sku)) { + newlistingsSKUs.push(sku); + } + }); + + const pricelist = this.bot.pricelist.getPrices.filter(entry => { + // First find out if lising for this item from bptf already exist. + const isExist = newlistingsSKUs.find(sku => entry.sku === sku); + + if (!isExist) { + // undefined - listing does not exist but item is in the pricelist + + // Get amountCanBuy and amountCanSell (already cover intent and so on) + const amountCanBuy = inventory.amountCanTrade(entry.sku, true); + const amountCanSell = inventory.amountCanTrade(entry.sku, false); + + if ( + (amountCanBuy > 0 && inventory.isCanAffordToBuy(entry.buy, inventory.getInventory)) || + amountCanSell > 0 + ) { + // if can amountCanBuy is more than 0 and isCanAffordToBuy is true OR amountCanSell is more than 0 + // return this entry + log.debug( + `Missing${isFilterCantAfford ? '/Re-adding can afford' : ' listings'}: ${entry.sku}` + ); + return true; + } + + // Else ignore + return false; + } + + // Else if listing already exist on backpack.tf, ignore + return false; + }); + + const pricelistCount = pricelist.length; + + if (pricelistCount > 0) { + clearTimeout(this.executeTimeout); + this.lastExecutedTime = dayjs().valueOf(); + + log.debug( + 'Checking listings for ' + + pluralize('item', pricelistCount, true) + + ` [${pricelist.map(entry => entry.sku).join(', ')}] ...` + ); + + this.bot.sendMessage( + steamID, + 'Refreshing listings for ' + pluralize('item', pricelistCount, true) + '...' + ); + + this.bot.handler.isRecentlyExecuteRefreshlistCommand = true; + this.bot.handler.setRefreshlistExecutedDelay = (this.pricelistLength > 1000 ? 60 : 30) * 60 * 1000; + this.pricelistLength = pricelistCount; + this.executed = true; + this.executeTimeout = setTimeout(() => { + this.lastExecutedTime = null; + this.executed = false; + this.bot.handler.isRecentlyExecuteRefreshlistCommand = false; + clearTimeout(this.executeTimeout); + }, (this.pricelistLength > 1000 ? 60 : 30) * 60 * 1000); + + await this.bot.listings.recursiveCheckPricelist(pricelist, true); + + log.debug('Done checking ' + pluralize('item', pricelistCount, true)); + this.bot.sendMessage(steamID, 'āœ… Done refreshing ' + pluralize('item', pricelistCount, true)); + } else { + this.bot.sendMessage(steamID, 'āŒ Nothing to refresh.'); + } + }); + } + } + + private generateAutokeysReply(steamID: SteamID, bot: Bot): string { + const pureNow = pure.currPure(bot); + const currKey = pureNow.key; + const currRef = pureNow.refTotalInScrap; + + const keyPrices = bot.pricelist.getKeyPrices; + + const autokeys = bot.handler.autokeys; + const userPure = autokeys.userPure; + const status = autokeys.getOverallStatus; + + const keyBlMin = ` X`; + const keyAbMax = ` X`; + const keyAtBet = ` X`; + const keyAtMin = ` X`; + const keyAtMax = ` X`; + const keysLine = `Keys ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶`; + const refBlMin = ` X`; + const refAbMax = ` X`; + const refAtBet = ` X`; + const refAtMin = ` X`; + const refAtMax = ` X`; + const refsLine = `Refs ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶`; + const xAxisRef = ` min max`; + const keysPosition = + currKey < userPure.minKeys + ? keyBlMin + : currKey > userPure.maxKeys + ? keyAbMax + : currKey > userPure.minKeys && currKey < userPure.maxKeys + ? keyAtBet + : currKey === userPure.minKeys + ? keyAtMin + : currKey === userPure.maxKeys + ? keyAtMax + : ''; + const refsPosition = + currRef < userPure.minRefs + ? refBlMin + : currRef > userPure.maxRefs + ? refAbMax + : currRef > userPure.minRefs && currRef < userPure.maxRefs + ? refAtBet + : currRef === userPure.minRefs + ? refAtMin + : currRef === userPure.maxRefs + ? refAtMax + : ''; + const summary = `\nā€¢ ${userPure.minKeys} ā‰¤ ${pluralize('key', currKey)}(${currKey}) ā‰¤ ${ + userPure.maxKeys + }\nā€¢ ${Currencies.toRefined(userPure.minRefs)} < ${pluralize( + 'ref', + Currencies.toRefined(currRef) + )}(${Currencies.toRefined(currRef)}) < ${Currencies.toRefined(userPure.maxRefs)}`; + + let reply = + (bot.isAdmin(steamID) ? 'Your ' : 'My ') + + `current Autokeys settings:\n${summary}\n\nDiagram:\n${keysPosition}\n${keysLine}\n${refsPosition}\n${refsLine}\n${xAxisRef}\n`; + reply += `\n Key prices: ${keyPrices.buy.toString()}/${keyPrices.sell.toString()} (${ + keyPrices.src === 'manual' ? 'manual' : 'prices.tf' + })`; + + const scrapAdjustmentEnabled = autokeys.isEnableScrapAdjustment; + const scrapAdjustmentValue = autokeys.scrapAdjustmentValue; + const keyBankingEnabled = autokeys.isKeyBankingEnabled; + + reply += `\nScrap Adjustment: ${scrapAdjustmentEnabled ? 'Enabled āœ…' : 'Disabled āŒ'}`; + reply += `\n Auto-banking: ${keyBankingEnabled ? 'Enabled āœ…' : 'Disabled āŒ'}`; + reply += `\n Autokeys status: ${ + autokeys.getActiveStatus + ? status.isBankingKeys + ? 'Banking' + (scrapAdjustmentEnabled ? ' (default price)' : '') + : status.isBuyingKeys + ? 'Buying for ' + + Currencies.toRefined( + keyPrices.buy.toValue() + (scrapAdjustmentEnabled ? scrapAdjustmentValue : 0) + ).toString() + + ' ref' + + (scrapAdjustmentEnabled ? ` (+${scrapAdjustmentValue} scrap)` : '') + : 'Selling for ' + + Currencies.toRefined( + keyPrices.sell.toValue() - (scrapAdjustmentEnabled ? scrapAdjustmentValue : 0) + ).toString() + + ' ref' + + (scrapAdjustmentEnabled ? ` (-${scrapAdjustmentValue} scrap)` : '') + : 'Not active' + }`; + /* + * X + * Keys ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶ + * X + * Refs ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”ā€”|ā€”ā€”ā€”ā€”ā–¶ + * min max + */ + + return reply; + } +} + +function promiseDelay(ms: number): Promise { + return new Promise(resolve => setTimeout(() => resolve(), ms)); +} diff --git a/src/classes/Commands/sub-classes/Message.ts b/src/classes/Commands/sub-classes/Message.ts new file mode 100644 index 000000000..38d111844 --- /dev/null +++ b/src/classes/Commands/sub-classes/Message.ts @@ -0,0 +1,163 @@ +import SteamID from 'steamid'; +import Bot from '../../Bot'; +import CommandParser from '../../CommandParser'; +import { generateLinks, timeNow } from '../../../lib/tools/export'; +import { sendPartnerMessage, sendAdminMessage } from '../../../lib/DiscordWebhook/export'; + +export default class MessageCommand { + private readonly bot: Bot; + + constructor(bot: Bot) { + this.bot = bot; + } + + message(steamID: SteamID, message: string): void { + const isAdmin = this.bot.isAdmin(steamID); + const custom = this.bot.options.commands.message.customReply; + if (!this.bot.options.commands.enable) { + if (isAdmin) { + this.bot.sendMessage( + steamID, + 'āŒ The message command is disabled. Enable it by sending `!config commands.message.enable=true`.' + ); + } else { + this.bot.sendMessage( + steamID, + custom.disabled ? custom.disabled : 'āŒ The owner has disabled messages.' + ); + } + return; + } + + const senderDetails = this.bot.friends.getFriend(steamID); + const optDW = this.bot.options.discordWebhook.messages; + + if (isAdmin) { + const steamIdAndMessage = CommandParser.removeCommand(message); + const parts = steamIdAndMessage.split(' '); + let recipientSteamID: SteamID; + + try { + recipientSteamID = new SteamID(parts[0]); + } catch (err) { + return this.bot.sendMessage( + steamID, + 'āŒ Your syntax is wrong or the SteamID is incorrectly formatted. Here\'s an example: "!message 76561198120070906 Hi"' + + "\n\nHow to get the targeted user's SteamID?" + + '\n1. Go to his/her profile page.' + + '\n2. Go to https://steamrep.com/' + + '\n3. View this gif: https://user-images.githubusercontent.com/47635037/96715154-be80b580-13d5-11eb-9bd5-39613f600f6d.gif' + ); + } + + const steamIDString = recipientSteamID.getSteamID64(); + + if (!recipientSteamID.isValid()) { + return this.bot.sendMessage( + steamID, + `āŒ "${steamIDString}" is not a valid SteamID.` + + "\n\nHow to get the targeted user's SteamID?" + + '\n1. Go to his/her profile page.' + + '\n2. Go to https://steamrep.com/' + + '\n3. View this gif: https://user-images.githubusercontent.com/47635037/96715154-be80b580-13d5-11eb-9bd5-39613f600f6d.gif' + ); + } else if (!this.bot.friends.isFriend(recipientSteamID)) { + return this.bot.sendMessage(steamID, `āŒ I am not friends with the user.`); + } + + const recipientDetails = this.bot.friends.getFriend(recipientSteamID); + const reply = steamIdAndMessage.substr(steamIDString.length); + + // Send message to recipient + this.bot.sendMessage( + recipientSteamID, + custom.fromOwner + ? custom.fromOwner.replace(/%reply%/g, reply) + : `/quote šŸ’¬ Message from the owner: ${reply}` + + '\n\nā” Hint: You can use the !message command to respond to the owner of this bot.' + + '\nExample: !message Hi Thanks!' + ); + + // Send a notification to the admin with message contents & details + if (optDW.enable && optDW.url !== '') { + sendAdminMessage( + recipientSteamID.toString(), + reply, + recipientDetails, + generateLinks(steamID.toString()), + timeNow(this.bot.options).time, + this.bot + ); + } else { + const customInitializer = this.bot.options.steamChat.customInitializer.message.toOtherAdmins; + this.bot.messageAdmins( + `${ + customInitializer ? customInitializer : '/quote' + } šŸ’¬ Message sent to #${recipientSteamID.toString()} (${ + recipientDetails.player_name + }): "${reply}". `, + [] + ); + } + + this.bot.sendMessage(steamID, custom.success ? custom.success : 'āœ… Your message has been sent.'); + + // Send message to all other admins that an admin replied + return this.bot.messageAdmins( + `${ + senderDetails ? `${senderDetails.player_name} (${steamID.toString()})` : steamID.toString() + } sent a message to ${ + recipientDetails + ? recipientDetails.player_name + ` (${recipientSteamID.toString()})` + : recipientSteamID.toString() + } with "${reply}".`, + [steamID] + ); + } else { + const admins = this.bot.getAdmins; + if (!admins || admins.length === 0) { + // Just default to same message as if it was disabled + return this.bot.sendMessage( + steamID, + custom.disabled ? custom.disabled : 'āŒ The owner has disabled messages.' + ); + } + + const msg = message.substr(message.toLowerCase().indexOf('message') + 8); + if (!msg) { + return this.bot.sendMessage( + steamID, + custom.wrongSyntax + ? custom.wrongSyntax + : 'āŒ Please include a message. Here\'s an example: "!message Hi"' + ); + } + + const links = generateLinks(steamID.toString()); + if (optDW.enable && optDW.url !== '') { + sendPartnerMessage( + steamID.toString(), + msg, + senderDetails, + links, + timeNow(this.bot.options).time, + this.bot + ); + } else { + const customInitializer = this.bot.options.steamChat.customInitializer.message.onReceive; + this.bot.messageAdmins( + `${ + customInitializer ? customInitializer : '/quote' + } šŸ’¬ You've got a message from #${steamID.toString()} (${senderDetails.player_name}):` + + `"${msg}". ` + + `\nSteam: ${links.steam}` + + `\nBackpack.tf: ${links.bptf}` + + `\nSteamREP: ${links.steamrep}`, + [] + ); + } + + this.bot.sendMessage(steamID, custom.success ? custom.success : 'āœ… Your message has been sent.'); + } + } +} diff --git a/src/classes/Commands/sub-classes/Misc.ts b/src/classes/Commands/sub-classes/Misc.ts new file mode 100644 index 000000000..2c2b36793 --- /dev/null +++ b/src/classes/Commands/sub-classes/Misc.ts @@ -0,0 +1,257 @@ +import SteamID from 'steamid'; +import SKU from 'tf2-sku-2'; +import pluralize from 'pluralize'; +import Bot from '../../Bot'; +import { Discord, Stock } from '../../Options'; +import { pure, timeNow, uptime } from '../../../lib/tools/export'; + +type Misc = 'time' | 'uptime' | 'pure' | 'rate' | 'owner' | 'discord' | 'stock'; +type CraftUncraft = 'craftweapon' | 'uncraftweapon'; + +export default class MiscCommands { + private readonly bot: Bot; + + constructor(bot: Bot) { + this.bot = bot; + } + + miscCommand(steamID: SteamID, command: Misc): void { + const opt = this.bot.options.commands[command]; + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); + } + } + + const custom = opt.customReply.reply; + if (command === 'time') { + const timeWithEmojis = timeNow(this.bot.options); + this.bot.sendMessage( + steamID, + custom + ? custom + .replace(/%emoji%/g, timeWithEmojis.emoji) + .replace(/%time%/g, timeWithEmojis.time) + .replace(/%note%/g, timeWithEmojis.note) + : `It is currently the following time in my owner's timezone: ${timeWithEmojis.emoji} ${ + timeWithEmojis.time + (timeWithEmojis.note !== '' ? `.\n\n${timeWithEmojis.note}.` : '.') + }` + ); + } else if (command === 'uptime') { + const botUptime = uptime(); + this.bot.sendMessage(steamID, custom ? custom.replace(/%uptime%/g, botUptime) : botUptime); + } else if (command === 'pure') { + const pureStock = pure.stock(this.bot); + this.bot.sendMessage( + steamID, + custom + ? custom.replace(/%pure%/g, pureStock.join(' and ')) + : `šŸ’° I have ${pureStock.join(' and ')} in my inventory.` + ); + } else if (command === 'rate') { + const key = this.bot.pricelist.getKeyPrices; + const keyRate = key.sell.toString(); + const source = key.src === 'manual' ? 'manual' : 'https://api.prices.tf/items/5021;6?src=bptf'; + + this.bot.sendMessage( + steamID, + custom + ? custom + .replace(/%keyRate%/g, keyRate) + .replace(/%keyPrices%/g, `${key.buy.metal} / ${key.sell.toString()}`) + .replace(/%source%/g, source) + : 'I value šŸ”‘ Mann Co. Supply Crate Keys at ' + + keyRate + + '. This means that one key is the same as ' + + keyRate + + ', and ' + + keyRate + + ' is the same as one key.' + + `\n\nKey rate source: ${source}` + ); + } else if (command === 'owner') { + const firstAdmin = this.bot.getAdmins[0]; + const steamURL = `https://steamcommunity.com/profiles/${firstAdmin.toString()}`; + const bptfURL = `https://backpack.tf/profiles/${firstAdmin.toString()}`; + + this.bot.sendMessage( + steamID, + custom + ? custom + .replace(/%steamurl%/g, steamURL) + .replace(/%bptfurl%/g, bptfURL) + .replace(/%steamid%/, firstAdmin.toString()) + : `ā€¢ Steam: ${steamURL}\nā€¢ Backpack.tf: ${bptfURL}` + ); + } else if (command === 'discord') { + const inviteURL = (opt as Discord).inviteURL; + let reply: string; + if (custom) { + reply = + 'TF2Autobot Discord Server: https://discord.gg/D2GNnp7tv8\n\n' + + custom.replace(/%discordurl%/g, inviteURL); + } else { + if (inviteURL) { + reply = `TF2Autobot Discord Server: https://discord.gg/D2GNnp7tv8\nOwner's Discord Server: ${inviteURL}`; + // + } else reply = 'TF2Autobot Discord Server: https://discord.gg/D2GNnp7tv8'; + } + this.bot.sendMessage(steamID, reply); + } else { + const inventory = this.bot.inventoryManager.getInventory; + const dict = inventory.getItems; + const items: { amount: number; name: string }[] = []; + + for (const sku in dict) { + if (!Object.prototype.hasOwnProperty.call(dict, sku)) { + continue; + } + + if (['5021;6', '5002;6', '5001;6', '5000;6'].includes(sku)) { + continue; + } + + items.push({ + name: this.bot.schema.getName(SKU.fromString(sku), false), + amount: dict[sku].length + }); + } + + items.sort((a, b) => { + if (a.amount === b.amount) { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + } + return b.amount - a.amount; + }); + + const pure = [ + { + name: 'Mann Co. Supply Crate Key', + amount: inventory.getAmount('5021;6') + }, + { + name: 'Refined Metal', + amount: inventory.getAmount('5002;6') + }, + { + name: 'Reclaimed Metal', + amount: inventory.getAmount('5001;6') + }, + { + name: 'Scrap Metal', + amount: inventory.getAmount('5000;6') + } + ]; + + const parsed = pure.concat(items); + + const stock: string[] = []; + let left = 0; + + const max = (opt as Stock).maximumItems; + + const parsedCount = parsed.length; + + for (let i = 0; i < parsedCount; i++) { + if (stock.length > max) { + left += parsed[i].amount; + } else { + stock.push(`${parsed[i].name}: ${parsed[i].amount}`); + } + } + + const custom = opt.customReply.reply; + let reply = custom + ? custom.replace(/%stocklist%/g, stock.join(', \n')) + : `/pre šŸ“œ Here's a list of all the items that I have in my inventory:\n${stock.join(', \n')}`; + + if (left > 0) { + reply += `,\nand ${left} other ${pluralize('item', left)}`; + } + + this.bot.sendMessage(steamID, reply); + } + } + + weaponCommand(steamID: SteamID, type: CraftUncraft): void { + const opt = this.bot.options.commands[type]; + if (!opt.enable) { + if (!this.bot.isAdmin(steamID)) { + const custom = opt.customReply.disabled; + return this.bot.sendMessage(steamID, custom ? custom : 'āŒ This command is disabled by the owner.'); + } + } + + const weaponStock = this.getWeaponsStock( + this.bot, + type === 'craftweapon' ? this.bot.craftWeapons : this.bot.uncraftWeapons + ); + + let reply: string; + if (weaponStock.length > 0) { + const custom = opt.customReply.have; + reply = custom + ? custom.replace(/%list%/g, weaponStock.join(', \n')) + : `šŸ“ƒ Here's a list of all ${ + type === 'craftweapon' ? 'craft' : 'uncraft' + } weapons stock in my inventory:\n\n` + weaponStock.join(', \n'); + } else { + const custom = opt.customReply.dontHave; + reply = custom + ? custom + : `āŒ I don't have any ${ + type === 'craftweapon' ? 'craftable' : 'uncraftable' + } weapons in my inventory.`; + } + + this.bot.sendMessage(steamID, reply); + } + + paintsCommand(steamID: SteamID): void { + this.bot.sendMessage(steamID, '/code ' + JSON.stringify(this.bot.paints, null, 4)); + } + + private getWeaponsStock(bot: Bot, type: string[]): string[] { + const items: { amount: number; name: string }[] = []; + + type.forEach(sku => { + const amount = bot.inventoryManager.getInventory.getAmount(sku); + if (amount > 0) { + items.push({ + name: bot.schema.getName(SKU.fromString(sku), false), + amount: amount + }); + } + }); + + items.sort((a, b) => { + if (a.amount === b.amount) { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } else { + return 0; + } + } + return b.amount - a.amount; + }); + + const stock: string[] = []; + const itemsCount = items.length; + + if (itemsCount > 0) { + for (let i = 0; i < itemsCount; i++) { + stock.push(`${items[i].name}: ${items[i].amount}`); + } + } + return stock; + } +} diff --git a/src/classes/Commands/sub-classes/Options.ts b/src/classes/Commands/sub-classes/Options.ts new file mode 100644 index 000000000..657e90d5b --- /dev/null +++ b/src/classes/Commands/sub-classes/Options.ts @@ -0,0 +1,280 @@ +import SteamID from 'steamid'; +import { promises as fsp } from 'fs'; +import sleepasync from 'sleep-async'; +import Bot from '../../Bot'; +import CommandParser from '../../CommandParser'; +import { getOptionsPath, JsonOptions, removeCliOptions } from '../../Options'; +import validator from '../../../lib/validator'; +import log from '../../../lib/logger'; +import { deepMerge } from '../../../lib/tools/deep-merge'; + +export type OptionsKeys = + | 'miscSettings' + | 'sendAlert' + | 'pricelist' + | 'bypass' + | 'tradeSummary' + | 'steamChat' + | 'highValue' + | 'normalize' + | 'details' + | 'statistics' + | 'autokeys' + | 'crafting' + | 'offerReceived' + | 'manualReview' + | 'discordWebhook' + | 'customMessage' + | 'commands' + | 'detailsExtra'; + +export default class OptionsCommands { + private readonly bot: Bot; + + constructor(bot: Bot) { + this.bot = bot; + } + + async optionsCommand(steamID: SteamID, message: string): Promise { + const liveOptions = deepMerge({}, this.bot.options) as JsonOptions; + // remove any CLI stuff + removeCliOptions(liveOptions); + + const key = CommandParser.removeCommand(message); + + if (!key || key === '!options') { + this.bot.sendMessage( + steamID, + `/code ${JSON.stringify( + { + miscSettings: liveOptions.miscSettings, + sendAlert: liveOptions.sendAlert, + pricelist: liveOptions.pricelist, + bypass: liveOptions.bypass, + tradeSummary: liveOptions.tradeSummary + }, + null, + 4 + )}` + ); + + await sleepasync().Promise.sleep(1 * 1000); + + this.bot.sendMessage( + steamID, + `/code ${JSON.stringify( + { + steamChat: liveOptions.steamChat, + highValue: liveOptions.highValue, + normalize: liveOptions.normalize, + details: liveOptions.details, + statistics: liveOptions.statistics, + autokeys: liveOptions.autokeys, + crafting: liveOptions.crafting + }, + null, + 4 + )}` + ); + + await sleepasync().Promise.sleep(1 * 1000); + + this.bot.sendMessage( + steamID, + `/code ${JSON.stringify( + { + offerReceived: liveOptions.offerReceived, + manualReview: liveOptions.manualReview + }, + null, + 4 + )}` + ); + + await sleepasync().Promise.sleep(1 * 1000); + + this.bot.sendMessage( + steamID, + `/code ${JSON.stringify( + { + discordWebhook: liveOptions.discordWebhook, + customMessage: liveOptions.customMessage + }, + null, + 4 + )}` + ); + + await sleepasync().Promise.sleep(1 * 1000); + + this.bot.sendMessage(steamID, `/code ${JSON.stringify({ commands: liveOptions.commands }, null, 4)}`); + + await sleepasync().Promise.sleep(1 * 1000); + + this.bot.sendMessage( + steamID, + `/code ${JSON.stringify({ detailsExtra: liveOptions.detailsExtra }, null, 4)}` + ); + + this.bot.sendMessage( + steamID, + `\n\nYou can also get only the part the you want by sending "!options [OptionsParentKey]"` + ); + } else { + const optionsKeys = Object.keys(liveOptions); + + if (!optionsKeys.includes(key)) { + return this.bot.sendMessage( + steamID, + `āŒ "${key}" parent key does not exist in options.` + + `\n\nValid parent keys:\nā€¢ ` + + [ + 'miscSettings', + 'sendAlert', + 'pricelist', + 'bypass', + 'tradeSummary', + 'steamChat', + 'highValue', + 'normalize', + 'details', + 'statistics', + 'autokeys', + 'crafting', + 'offerReceived', + 'manualReview', + 'discordWebhook', + 'customMessage', + 'commands', + 'detailsExtra' + ].join('\nā€¢ ') + ); + } + + const show = {}; + + show[key] = liveOptions[key as OptionsKeys]; + + this.bot.sendMessage(steamID, `/code ${JSON.stringify(show, null, 4)}`); + } + } + + updateOptionsCommand(steamID: SteamID, message: string): void { + const opt = this.bot.options; + const params = CommandParser.parseParams(CommandParser.removeCommand(message)) as unknown; + + const optionsPath = getOptionsPath(opt.steamAccountName); + const saveOptions = deepMerge({}, opt) as JsonOptions; + removeCliOptions(saveOptions); + + if (Object.keys(params).length === 0) { + const msg = 'āš ļø Missing properties to update.'; + if (steamID) { + this.bot.sendMessage(steamID, msg); + } else { + log.warn(msg); + } + + return; + } + + const knownParams = params as JsonOptions; + + if (knownParams.discordWebhook?.ownerID !== undefined) { + // Stringify numbers + knownParams.discordWebhook.ownerID = String(knownParams.discordWebhook.ownerID); + } + + if (knownParams.discordWebhook?.embedColor !== undefined) { + // Stringify numbers + knownParams.discordWebhook.embedColor = String(knownParams.discordWebhook.embedColor); + } + + const result: JsonOptions = deepMerge(saveOptions, knownParams); + + const errors = validator(result, 'options'); + if (errors !== null) { + const msg = 'āŒ Error updating options: ' + errors.join(', '); + if (steamID) { + this.bot.sendMessage(steamID, msg); + } else { + log.error(msg); + } + + return; + } + + fsp.writeFile(optionsPath, JSON.stringify(saveOptions, null, 4), { encoding: 'utf8' }) + .then(() => { + deepMerge(opt, saveOptions); + const msg = 'āœ… Updated options!'; + + if (knownParams.miscSettings?.game?.playOnlyTF2 === true) { + this.bot.client.gamesPlayed([]); + this.bot.client.gamesPlayed(440); + } + + if (typeof knownParams.miscSettings?.game?.customName === 'string') { + this.bot.client.gamesPlayed([]); + this.bot.client.gamesPlayed( + ( + knownParams.miscSettings?.game?.playOnlyTF2 !== undefined + ? knownParams.miscSettings.game.playOnlyTF2 + : opt.miscSettings.game.playOnlyTF2 + ) + ? 440 + : [knownParams.miscSettings.game.customName, 440] + ); + } + + if (knownParams.miscSettings?.autobump?.enable === true) { + this.bot.listings.setupAutorelist(); + this.bot.handler.disableAutoRefreshListings(); + } else if (knownParams.miscSettings?.autobump?.enable === false) { + this.bot.listings.disableAutorelistOption(); + this.bot.handler.enableAutoRefreshListings(); + } + + if (knownParams.statistics?.sendStats?.enable === true) { + this.bot.handler.sendStats(); + } else if (knownParams.statistics?.sendStats?.enable === false) { + this.bot.handler.disableSendStats(); + } + + if (knownParams.statistics?.sendStats?.time !== undefined) { + this.bot.handler.sendStats(); + } + + if (typeof knownParams.highValue !== undefined) { + void this.bot.inventoryManager.getInventory.fetch(); + } + + if (typeof knownParams.normalize === 'object') { + void this.bot.inventoryManager.getInventory.fetch(); + } + + if (typeof knownParams.autokeys === 'object') { + if (knownParams.autokeys.enable !== undefined && !knownParams.autokeys.enable) { + this.bot.handler.autokeys.disable(this.bot.pricelist.getKeyPrices); + } + this.bot.handler.autokeys.check(); + } + + if (steamID) { + return this.bot.sendMessage(steamID, msg); + } else { + return log.info(msg); + } + }) + .catch(err => { + const msg = `āŒ Error saving options file to disk: ${JSON.stringify(err)}`; + if (steamID) { + this.bot.sendMessage(steamID, msg); + } else { + log.error(msg); + } + + return; + }); + } +} diff --git a/src/classes/Commands/sub-classes/PricelistManager.ts b/src/classes/Commands/sub-classes/PricelistManager.ts new file mode 100644 index 000000000..d84117a2b --- /dev/null +++ b/src/classes/Commands/sub-classes/PricelistManager.ts @@ -0,0 +1,1339 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + +import SteamID from 'steamid'; +import SKU from 'tf2-sku-2'; +import Currencies from 'tf2-currencies'; +import pluralize from 'pluralize'; +import dayjs from 'dayjs'; +import sleepasync from 'sleep-async'; +import { removeLinkProtocol, testSKU, getItemFromParams } from '../functions/utils'; +import Bot from '../../Bot'; +import CommandParser from '../../CommandParser'; +import { Entry, EntryData, PricelistChangedSource } from '../../Pricelist'; +import validator from '../../../lib/validator'; +import log from '../../../lib/logger'; + +// Pricelist manager + +export default class PricelistManagerCommands { + private readonly bot: Bot; + + private stopAutoAdd = false; + + private executed = false; + + private lastExecutedTime: number | null = null; + + private executeTimeout: NodeJS.Timeout; + + stopAutoAddCommand(): void { + this.stopAutoAdd = true; + } + + constructor(bot: Bot) { + this.bot = bot; + } + + addCommand(steamID: SteamID, message: string): void { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + + if (params.enabled === undefined) { + params.enabled = true; + } + + if (params.max === undefined) { + params.max = 1; + } + + if (params.min === undefined) { + params.min = 0; + } + + if (params.intent === undefined) { + params.intent = 2; + } else if (typeof params.intent === 'string') { + const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); + + if (intent !== -1) { + params.intent = intent; + } + } + + if (typeof params.buy === 'object') { + params.buy.keys = params.buy.keys || 0; + params.buy.metal = params.buy.metal || 0; + + if (params.autoprice === undefined) { + params.autoprice = false; + } + } + if (typeof params.sell === 'object') { + params.sell.keys = params.sell.keys || 0; + params.sell.metal = params.sell.metal || 0; + + if (params.autoprice === undefined) { + params.autoprice = false; + } + } + + const isPremium = this.bot.handler.getBotInfo.premium; + if (params.promoted !== undefined) { + if (!isPremium) { + return this.bot.sendMessage( + steamID, + `āŒ This account is not Backpack.tf Premium. You can't use "promoted" parameter.` + ); + } + + if (typeof params.promoted === 'boolean') { + if (params.promoted === true) { + params.promoted = 1; + } else { + params.promoted = 0; + } + } else { + if (typeof params.promoted !== 'number') { + return this.bot.sendMessage( + steamID, + 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)' + ); + } else if (params.promoted < 0 || params.promoted > 1) { + return this.bot.sendMessage( + steamID, + 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)' + ); + } + } + } else { + params['promoted'] = 0; + } + + if (typeof params.note === 'object') { + params.note.buy = params.note.buy || null; + params.note.sell = params.note.sell || null; + } + + if (params.note === undefined) { + // If note parameter is not defined, set both note.buy and note.sell to null. + params['note'] = { buy: null, sell: null }; + } + + if (params.group && typeof params.group !== 'string') { + // if group parameter is defined, convert anything to string + params.group = (params.group as string).toString(); + } + + if (typeof params.group === undefined) { + // If group parameter is not defined, set it to null. + params['group'] = 'all'; + } + + if (params.autoprice === undefined) { + params.autoprice = true; + } + + if (params.sku !== undefined && !testSKU(params.sku as string)) { + return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); + } + + if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } + + this.bot.pricelist + .addPrice(params as EntryData, true, PricelistChangedSource.Command) + .then(entry => { + this.bot.sendMessage( + steamID, + `āœ… Added "${entry.name}"` + this.generateAddedReply(this.bot, isPremium, entry) + ); + }) + .catch(err => { + this.bot.sendMessage(steamID, `āŒ Failed to add the item to the pricelist: ${(err as Error).message}`); + }); + } + + private generateAddedReply(bot: Bot, isPremium: boolean, entry: Entry): string { + const amount = bot.inventoryManager.getInventory.getAmount(entry.sku); + return ( + `\nšŸ’² Buy: ${entry.buy.toString()} | Sell: ${entry.sell.toString()}` + + `\nšŸ›’ Intent: ${entry.intent === 2 ? 'bank' : entry.intent === 1 ? 'sell' : 'buy'}` + + `\nšŸ“¦ Stock: ${amount} | Min: ${entry.min} | Max: ${entry.max}` + + `\nšŸ“‹ Enabled: ${entry.enabled ? 'āœ…' : 'āŒ'}` + + `\nšŸ”„ Autoprice: ${entry.autoprice ? 'āœ…' : 'āŒ'}` + + (isPremium ? `\nšŸ“¢ Promoted: ${entry.promoted === 1 ? 'āœ…' : 'āŒ'}` : '') + + `\nšŸ”° Group: ${entry.group}` + + `${entry.note.buy !== null ? `\nšŸ“„ Custom buying note: ${entry.note.buy}` : ''}` + + `${entry.note.sell !== null ? `\nšŸ“¤ Custom selling note: ${entry.note.sell}` : ''}` + ); + } + + async autoAddCommand(steamID: SteamID, message: string): Promise { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + + if (params.sku !== undefined || params.name !== undefined || params.defindex !== undefined) { + return this.bot.sendMessage( + steamID, + `āŒ Please only define item listing settings parameters, more info:` + + ` https://github.com/TF2Autobot/tf2autobot/wiki/What-is-the-pricelist%3F#i2---item-listing-settings-parameters.` + ); + } + + if (!params) { + this.bot.sendMessage(steamID, `ā³ Adding all items with default settings...`); + } + + if (params.enabled === undefined) { + params.enabled = true; + } + + if (params.max === undefined) { + params.max = 1; + } + + if (params.min === undefined) { + params.min = 0; + } + + if (params.intent === undefined) { + params.intent = 2; + } else if (typeof params.intent === 'string') { + const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); + + if (intent !== -1) { + params.intent = intent; + } + } + + if (typeof params.buy === 'object') { + params.buy.keys = params.buy.keys || 0; + params.buy.metal = params.buy.metal || 0; + + if (params.autoprice === undefined) { + params.autoprice = false; + } + } + if (typeof params.sell === 'object') { + params.sell.keys = params.sell.keys || 0; + params.sell.metal = params.sell.metal || 0; + + if (params.autoprice === undefined) { + params.autoprice = false; + } + } + + if ( + (params.sell !== undefined && params.buy === undefined) || + (params.sell === undefined && params.buy !== undefined) + ) { + return this.bot.sendMessage(steamID, `āŒ Please set both buy and sell prices.`); + } + + const isPremium = this.bot.handler.getBotInfo.premium; + + if (params.promoted !== undefined) { + if (!isPremium) { + return this.bot.sendMessage( + steamID, + `āŒ This account is not Backpack.tf Premium. You can't use "promoted" paramter.` + ); + } + + if (typeof params.promoted === 'boolean') { + if (params.promoted === true) { + params.promoted = 1; + } else { + params.promoted = 0; + } + } else { + if (typeof params.promoted !== 'number') { + return this.bot.sendMessage( + steamID, + 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)' + ); + } + + if (params.promoted < 0 || params.promoted > 1) { + return this.bot.sendMessage( + steamID, + 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)' + ); + } + } + } else { + params['promoted'] = 0; + } + + if (typeof params.note === 'object') { + params.note.buy = params.note.buy || null; + params.note.sell = params.note.sell || null; + } + + if (params.note === undefined) { + // If note parameter is not defined, set both note.buy and note.sell to null. + params['note'] = { buy: null, sell: null }; + } + + if (params.group && typeof params.group !== 'string') { + // if group parameter is defined, convert anything to string + params.group = (params.group as string).toString(); + } + + if (typeof params.group === undefined) { + // If group parameter is not defined, set it to null. + params['group'] = 'all'; + } + + if (params.autoprice === undefined) { + params.autoprice = true; + } + + const pricelist = this.bot.pricelist.getPrices; + const dict = this.bot.inventoryManager.getInventory.getItems; + + const pureAndWeapons = ['5021;6', '5000;6', '5001;6', '5002;6'].concat( + this.bot.craftWeapons.concat(this.bot.uncraftWeapons) + ); + + for (const sku in dict) { + if (!Object.prototype.hasOwnProperty.call(dict, sku)) { + continue; + } + + if (pureAndWeapons.some(pureOrWeaponsSKU => pureOrWeaponsSKU === sku)) { + delete dict[sku]; + } + } + + const total = Object.keys(dict).length; + + const totalTime = total * (params.autoprice ? 2 : 1) * 1000; + const aSecond = 1 * 1000; + const aMin = 1 * 60 * 1000; + const anHour = 1 * 60 * 60 * 1000; + + this.bot.sendMessage( + steamID, + `ā³ Running automatic add items... Total items to add: ${total}` + + `\n${params.autoprice ? 2 : 1} seconds in between items, so it will be about ${ + totalTime < aMin + ? `${Math.round(totalTime / aSecond)} seconds` + : totalTime < anHour + ? `${Math.round(totalTime / aMin)} minutes` + : `${Math.round(totalTime / anHour)} hours` + } to complete. Send "!stopautoadd" to abort.` + ); + + let added = 0; + let skipped = 0; + let failed = 0; + + for (const sku in dict) { + if (this.stopAutoAdd) { + this.bot.sendMessage(steamID, '----------\nšŸ›‘ Stopped auto-add items'); + this.stopAutoAdd = false; + break; + } + + if (!Object.prototype.hasOwnProperty.call(dict, sku)) { + continue; + } + + if (pricelist.some(entry => entry.sku === sku)) { + skipped++; + this.bot.sendMessage( + steamID, + `----------\nāš ļø ${this.bot.schema.getName( + SKU.fromString(sku) + )} (${sku}) already in pricelist, skipping...` + + `\nšŸ“œ Status: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total, ${ + total - added - skipped - failed + } remaining` + ); + // Prevent spamming detection and cause the bot to stop sending messages + await sleepasync().Promise.sleep(1 * 1000); + continue; + } + + if (params.autoprice === true) { + await sleepasync().Promise.sleep(2 * 1000); + } else { + await sleepasync().Promise.sleep(1 * 1000); + } + + params.sku = sku; + + this.bot.pricelist + .addPrice(params as EntryData, true, PricelistChangedSource.Command) + .then(entry => { + added++; + this.bot.sendMessage( + steamID, + `----------\nāœ… Added "${entry.name}"` + + this.generateAddedReply(this.bot, isPremium, entry) + + `\n\nšŸ“œ Status: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total, ${ + total - added - skipped - failed + } remaining` + ); + }) + .catch(err => { + failed++; + this.bot.sendMessage( + steamID, + `----------\nāŒ Failed to add the item to the pricelist: ${(err as Error).message}` + + `\n\nšŸ“œ Status: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total, ${ + total - added - skipped - failed + } remaining` + ); + }); + } + + this.bot.sendMessage( + steamID, + `----------\nāœ… Done, summary: ${added} added, ${skipped} skipped, ${failed} failed / ${total} total` + ); + + this.stopAutoAdd = false; + } + + async updateCommand(steamID: SteamID, message: string): Promise { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + + if (typeof params.intent === 'string') { + const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); + + if (intent !== -1) { + params.intent = intent; + } + } + + if (params.all === true) { + // TODO: Must have at least one other param + const pricelist = this.bot.pricelist.getPrices; + const keyPrice = this.bot.pricelist.getKeyPrice; + + let targetedPricelist: Entry[]; + let unTargetedPricelist: Entry[]; + let newPricelist: Entry[]; + + if (params.promoted) { + return this.bot.sendMessage(steamID, `āŒ Parameter "promoted" can't be used with "!update all=true".`); + } + + if (params.withgroup && params.withoutgroup) { + return this.bot.sendMessage( + steamID, + `āŒ Don't be dumb. Please choose only "withgroup" OR "withoutgroup", not both. Thanks.` + ); + } + + if (params.withgroup) { + targetedPricelist = pricelist.filter(entry => + entry.group + ? [(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : false + ); + unTargetedPricelist = pricelist.filter(entry => + entry.group + ? ![(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : true + ); + + if (targetedPricelist.length === 0) { + return this.bot.sendMessage( + steamID, + `āŒ There is no entry with "${params.withgroup as string}" group found in your pricelist.` + ); + } + + newPricelist = targetedPricelist; + + if (typeof params.buy === 'object' || typeof params.sell === 'object') { + if ( + new Currencies(params.buy).toValue(keyPrice.metal) >= + new Currencies(params.sell).toValue(keyPrice.metal) + ) { + return this.bot.sendMessage(steamID, `āŒ Buying price can't be higher than selling price.`); + } else if ( + (params.buy !== null && params.sell === undefined) || + (params.buy === undefined && params.sell !== null) + ) { + return this.bot.sendMessage(steamID, `āŒ You must include both buying and selling prices.`); + } + } + } else if (params.withoutgroup) { + // reverse of withgroup + targetedPricelist = pricelist.filter(entry => + entry.group + ? ![(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : true + ); + unTargetedPricelist = pricelist.filter(entry => + entry.group + ? [(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : false + ); + + if (targetedPricelist.length === 0) { + return this.bot.sendMessage( + steamID, + `āŒ There is no entry other than "${ + params.withoutgroup as string + }" group found in your pricelist.` + ); + } + + newPricelist = targetedPricelist; + + if (typeof params.buy === 'object' || typeof params.sell === 'object') { + if ( + new Currencies(params.buy).toValue(keyPrice.metal) >= + new Currencies(params.sell).toValue(keyPrice.metal) + ) { + return this.bot.sendMessage(steamID, `āŒ Buying price can't be higher than selling price.`); + } else if ( + (params.buy !== null && params.sell === undefined) || + (params.buy === undefined && params.sell !== null) + ) { + return this.bot.sendMessage(steamID, `āŒ You must include both buying and selling prices.`); + } + } + } else { + newPricelist = pricelist; + + if (this.bot.options.autokeys.enable) { + // Autokeys is a feature, so when updating multiple entry with + // "!update all=true", key entry will be removed from newPricelist. + // https://github.com/idinium96/tf2autobot/issues/131 + const keyEntry = this.bot.pricelist.getPrice('5021;6'); + if (keyEntry !== null) { + const index = this.bot.pricelist.getIndex('5021;6'); + newPricelist.splice(index, 1); + } + } + } + + if (newPricelist.length === 0) { + return this.bot.sendMessage(steamID, 'Your pricelist is empty.'); + } + + if (!params.withgroup && !params.withoutgroup) { + if (typeof params.note === 'object') { + return this.bot.sendMessage( + steamID, + `āŒ Please specify "withgroup" or "withoutgroup" to change note.` + + `\nExample: "!update all=true&withgroup=¬e.buy="` + ); + } + + if (typeof params.buy === 'object' || typeof params.sell === 'object') { + return this.bot.sendMessage( + steamID, + `āŒ Please specify "withgroup" or "withoutgroup" to change buying/selling price.\nExample:\n` + + `"!update all=true&withgroup=&(buy.keys|buy.metal)=&(sell.keys|sell.metal)="` + ); + } + } + + newPricelist.forEach((entry, i) => { + if (params.intent || params.intent === 0) { + entry.intent = params.intent as 0 | 1 | 2; + } + + if (params.min === 0 || typeof params.min === 'number') { + entry.min = params.min as number; + } + + if (params.max === 0 || typeof params.max === 'number') { + entry.max = params.max as number; + } + + if (typeof params.enabled === 'boolean') { + entry.enabled = params.enabled; + } + + if (params.group) { + entry.group = (params.group as string).toString(); + } + + if (params.removenote && typeof params.removenote === 'boolean' && params.removenote === true) { + // Sending "!update all=true&removenote=true" will set both + // note.buy and note.sell for entire/withgroup entries to null. + if (entry.note) { + entry.note.buy = null; + entry.note.sell = null; + } + } + + if (typeof params.autoprice === 'boolean') { + if (params.autoprice === false) { + entry.time = null; + } + entry.autoprice = params.autoprice; + } + + if (params.withgroup || params.withoutgroup) { + if (typeof params.note === 'object') { + // can change note if have withgroup/withoutgroup parameter + entry.note.buy = params.note.buy || entry.note.buy; + entry.note.sell = params.note.sell || entry.note.sell; + } + + if (typeof params.buy === 'object' && params.buy !== null) { + entry.buy.keys = params.buy.keys || 0; + entry.buy.metal = params.buy.metal || 0; + + if (params.autoprice === undefined) { + entry.autoprice = false; + } + } + + if (typeof params.sell === 'object' && params.sell !== null) { + entry.sell.keys = params.sell.keys || 0; + entry.sell.metal = params.sell.metal || 0; + + if (params.autoprice === undefined) { + entry.autoprice = false; + } + } + } + + if (i === 0) { + const errors = validator( + { + sku: entry.sku, + enabled: entry.enabled, + intent: entry.intent, + max: entry.max, + min: entry.min, + autoprice: entry.autoprice, + buy: entry.buy.toJSON(), + sell: entry.sell.toJSON(), + promoted: entry.promoted, + group: entry.group, + note: entry.note, + time: entry.time + }, + 'pricelist' + ); + + if (errors !== null) { + throw new Error(errors.join(', ')); + } + } + }); + + if (params.removenote) { + delete params.removenote; + } + + if (params.withgroup) { + newPricelist = unTargetedPricelist.concat(newPricelist); + delete params.withgroup; + } + + if (params.withoutgroup) { + newPricelist = unTargetedPricelist.concat(newPricelist); + delete params.withoutgroup; + } + + // FIXME: Make it so that it is not needed to remove all listings + + if (params.autoprice !== true) { + await this.bot.handler.onPricelist(newPricelist); + this.bot.sendMessage(steamID, 'āœ… Updated pricelist!'); + + return await this.bot.listings.redoListings(); + } + + this.bot.sendMessage(steamID, 'āŒ› Updating prices...'); + + return this.bot.pricelist + .setupPricelist() + .then(async () => { + this.bot.sendMessage(steamID, 'āœ… Updated pricelist!'); + await this.bot.listings.redoListings(); + }) + .catch(err => { + log.warn('Failed to update prices: ', err); + return this.bot.sendMessage(steamID, `āŒ Failed to update prices: ${(err as Error).message}`); + }); + } + + if (params.sku !== undefined && !testSKU(params.sku as string)) { + return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); + } + + if (typeof params.buy === 'object' && params.buy !== null) { + params.buy.keys = params.buy.keys || 0; + params.buy.metal = params.buy.metal || 0; + + if (params.autoprice === undefined) { + params.autoprice = false; + } + } + + if (typeof params.sell === 'object' && params.sell !== null) { + params.sell.keys = params.sell.keys || 0; + params.sell.metal = params.sell.metal || 0; + + if (params.autoprice === undefined) { + params.autoprice = false; + } + } + + if (params.resetgroup) { + // if resetgroup (when sending "!update item=&resetgroup=true") is defined, + // first check if it's a booelan type + if (typeof params.resetgroup === 'boolean') { + // if it's boolean, then check if it's true + if (params.resetgroup === true) { + // if it's true, then set group key to null. + params.group = 'all'; + } + } else { + // else if it's not a boolean type, then send message. + return this.bot.sendMessage(steamID, 'āŒ "resetgroup" must be either "true" or "false".'); + } + // delete resetgroup key from params so it will not trying to be added into pricelist (validator error) + delete params.resetgroup; + } + + const isPremium = this.bot.handler.getBotInfo.premium; + if (params.promoted !== undefined) { + if (!isPremium) { + return this.bot.sendMessage( + steamID, + `āŒ This account is not Backpack.tf Premium. You can't use "promoted" paramter.` + ); + } + + if (typeof params.promoted === 'boolean') { + if (params.promoted === true) { + params.promoted = 1; + } else { + params.promoted = 0; + } + } else { + if (typeof params.promoted !== 'number') { + return this.bot.sendMessage( + steamID, + 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)' + ); + } + + if (params.promoted < 0 || params.promoted > 1) { + return this.bot.sendMessage( + steamID, + 'āŒ "promoted" parameter must be either 0 (false) or 1 (true)' + ); + } + } + } + + if (params.item !== undefined) { + // Remove by full name + let match = this.bot.pricelist.searchByName(params.item as string, false); + if (match === null) { + return this.bot.sendMessage( + steamID, + `āŒ I could not find any items in my pricelist that contain "${params.item as string}"` + ); + } else if (Array.isArray(match)) { + const matchCount = match.length; + if (matchCount > 20) { + match = match.splice(0, 20); + } + + let reply = `I've found ${matchCount} items. Try with one of the items shown below:\n${match.join( + ',\n' + )}`; + + if (matchCount > match.length) { + const other = matchCount - match.length; + reply += `,\nand ${other} other ${pluralize('item', other)}.`; + } + + return this.bot.sendMessage(steamID, reply); + } + + delete params.item; + params.sku = match.sku; + } else if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } + + if (!this.bot.pricelist.hasPrice(params.sku as string)) { + return this.bot.sendMessage(steamID, 'āŒ Item is not in the pricelist.'); + } + + const itemEntry = this.bot.pricelist.getPrice(params.sku as string, false); + + if (typeof params.note === 'object') { + params.note.buy = params.note.buy || itemEntry.note.buy; + params.note.sell = params.note.sell || itemEntry.note.sell; + } + + if (params.removenote) { + // removenote to set both note.buy and note.sell to null. + if (typeof params.removenote === 'boolean') { + if (params.removenote === true) { + params.note = { buy: null, sell: null }; + } + } else { + return this.bot.sendMessage(steamID, 'āŒ "removenote" must be either "true" or "false".'); + } + + delete params.removenote; + } + + if (params.removebuynote && params.removesellnote) { + return this.bot.sendMessage( + steamID, + 'āŒ Please only use either "removebuynote" or "removesellnote", not both, or if you wish to remove both buy and sell note, please use "removenote".' + ); + } + + if (params.removebuynote) { + if (typeof params.removebuynote === 'boolean') { + if (params.removebuynote === true) { + params.note = { buy: null, sell: itemEntry.note.sell }; + } + } else { + return this.bot.sendMessage(steamID, 'āŒ "removebuynote" must be either "true" or "false".'); + } + + delete params.removebuynote; + } + + if (params.removesellnote) { + if (typeof params.removesellnote === 'boolean') { + if (params.removesellnote === true) { + params.note = { buy: itemEntry.note.buy, sell: null }; + } + } else { + return this.bot.sendMessage(steamID, 'āŒ "removesellnote" must be either "true" or "false".'); + } + + delete params.removesellnote; + } + + if (params.group && typeof params.group !== 'string') { + // if group parameter is defined, convert anything to string + params.group = (params.group as string).toString(); + } + + const entryData = this.bot.pricelist.getPrice(params.sku as string, false).getJSON(); + delete entryData.time; + delete params.sku; + + if (Object.keys(params).length === 0) { + return this.bot.sendMessage(steamID, 'āš ļø Missing properties to update.'); + } + + // Update entry + for (const property in params) { + if (!Object.prototype.hasOwnProperty.call(params, property)) { + continue; + } + + entryData[property] = params[property]; + } + + this.bot.pricelist + .updatePrice(entryData, true, PricelistChangedSource.Command) + .then(entry => { + this.bot.sendMessage( + steamID, + `āœ… Updated "${entry.name}"` + this.generateUpdateReply(this.bot, isPremium, itemEntry, entry) + ); + }) + .catch((err: ErrorRequest) => { + this.bot.sendMessage( + steamID, + 'āŒ Failed to update pricelist entry: ' + + (err.body && err.body.message ? err.body.message : err.message) + ); + }); + } + + private generateUpdateReply(bot: Bot, isPremium: boolean, oldEntry: Entry, newEntry: Entry): string { + const keyPrice = bot.pricelist.getKeyPrice; + const amount = bot.inventoryManager.getInventory.getAmount(oldEntry.sku); + return ( + `\nšŸ’² Buy: ${ + oldEntry.buy.toValue(keyPrice.metal) !== newEntry.buy.toValue(keyPrice.metal) + ? `${oldEntry.buy.toString()} ā†’ ${newEntry.buy.toString()}` + : newEntry.buy.toString() + } | Sell: ${ + oldEntry.sell.toValue(keyPrice.metal) !== newEntry.sell.toValue(keyPrice.metal) + ? `${oldEntry.sell.toString()} ā†’ ${newEntry.sell.toString()}` + : newEntry.sell.toString() + }` + + `\nšŸ“¦ Stock: ${amount}` + + ` | Min: ${oldEntry.min !== newEntry.min ? `${oldEntry.min} ā†’ ${newEntry.min}` : newEntry.min} | Max: ${ + oldEntry.max !== newEntry.max ? `${oldEntry.max} ā†’ ${newEntry.max}` : newEntry.max + }` + + `\nšŸ›’ Intent: ${ + oldEntry.intent !== newEntry.intent + ? `${oldEntry.intent === 2 ? 'bank' : oldEntry.intent === 1 ? 'sell' : 'buy'} ā†’ ${ + newEntry.intent === 2 ? 'bank' : newEntry.intent === 1 ? 'sell' : 'buy' + }` + : `${newEntry.intent === 2 ? 'bank' : newEntry.intent === 1 ? 'sell' : 'buy'}` + }` + + `\nšŸ“‹ Enabled: ${ + oldEntry.enabled !== newEntry.enabled + ? `${oldEntry.enabled ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.enabled ? 'āœ…' : 'āŒ'}` + : `${newEntry.enabled ? 'āœ…' : 'āŒ'}` + }` + + `\nšŸ”„ Autoprice: ${ + oldEntry.autoprice !== newEntry.autoprice + ? `${oldEntry.autoprice ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.autoprice ? 'āœ…' : 'āŒ'}` + : `${newEntry.autoprice ? 'āœ…' : 'āŒ'}` + }` + + (isPremium + ? `\nšŸ“¢ Promoted: ${ + oldEntry.promoted !== newEntry.promoted + ? `${oldEntry.promoted === 1 ? 'āœ…' : 'āŒ'} ā†’ ${newEntry.promoted === 1 ? 'āœ…' : 'āŒ'}` + : `${newEntry.promoted === 1 ? 'āœ…' : 'āŒ'}` + }` + : '') + + `\nšŸ”° Group: ${ + oldEntry.group !== newEntry.group ? `${oldEntry.group} ā†’ ${newEntry.group}` : newEntry.group + }` + + `${newEntry.note.buy !== null ? `\nšŸ“„ Custom buying note: ${newEntry.note.buy}` : ''}` + + `${newEntry.note.sell !== null ? `\nšŸ“¤ Custom selling note: ${newEntry.note.sell}` : ''}` + ); + } + + async shuffleCommand(steamID: SteamID): Promise { + const newExecutedTime = dayjs().valueOf(); + const timeDiff = newExecutedTime - this.lastExecutedTime; + + const pricelist = this.bot.pricelist.getPrices; + + if (pricelist.length === 0) { + return this.bot.sendMessage(steamID, 'āŒ Pricelist is empty!'); + } + + if (this.executed === true) { + return this.bot.sendMessage( + steamID, + `āš ļø You need to wait ${Math.trunc( + (30 * 60 * 1000 - timeDiff) / (1000 * 60) + )} minutes before you can shuffle pricelist/shuffle again.` + ); + } else { + clearTimeout(this.executeTimeout); + this.lastExecutedTime = dayjs().valueOf(); + + const shufflePricelist = (arr: Entry[]) => { + return arr.sort(() => Math.random() - 0.5); + }; + + await this.bot.handler.onPricelist(shufflePricelist(pricelist)); + this.bot.sendMessage(steamID, 'āœ… Pricelist shuffled!'); + await this.bot.listings.redoListings(); + + this.executed = true; + this.executeTimeout = setTimeout(() => { + this.lastExecutedTime = null; + this.executed = false; + clearTimeout(this.executeTimeout); + }, 30 * 60 * 1000); + } + } + + async removeCommand(steamID: SteamID, message: string): Promise { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + + if (params.all === true) { + // Remove entire pricelist + const pricelistLength = this.bot.pricelist.getLength; + if (pricelistLength === 0) { + return this.bot.sendMessage(steamID, 'āŒ Your pricelist is already empty!'); + } + + const pricelist = this.bot.pricelist.getPrices; + let newPricelist: Entry[] = []; + let newPricelistCount: Entry[] = []; + + if (params.withgroup && params.withoutgroup) { + return this.bot.sendMessage( + steamID, + `āŒ Don't be dumb. Please choose only "withgroup" OR "withoutgroup", not both. Thanks.` + ); + } + + if (params.withgroup) { + // first filter out pricelist with ONLY "withgroup" value. + newPricelistCount = pricelist.filter(entry => + entry.group + ? [(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : false + ); + + if (newPricelistCount.length === 0) { + return this.bot.sendMessage( + steamID, + `āŒ There is no entry with "${params.withgroup as string}" group found in your pricelist.` + ); + } + + // then filter out pricelist with NOT "withgroup" value. + newPricelist = pricelist.filter(entry => + entry.group + ? ![(params.withgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : true + ); + } else if (params.withoutgroup) { + // reverse of withgroup + newPricelistCount = pricelist.filter(entry => + entry.group + ? ![(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : true + ); + + if (newPricelistCount.length === 0) { + return this.bot.sendMessage( + steamID, + `āŒ There is no entry other than "${ + params.withoutgroup as string + }" group found in your pricelist.` + ); + } + + newPricelist = pricelist.filter(entry => + entry.group + ? [(params.withoutgroup as string).toLowerCase()].includes(entry.group.toLowerCase()) + : false + ); + } else { + newPricelistCount = pricelist; + } + + if (params.i_am_sure !== 'yes_i_am') { + return this.bot.sendMessage( + steamID, + '/pre āš ļø Are you sure that you want to remove ' + + pluralize('item', newPricelistCount.length, true) + + '? Try again with i_am_sure=yes_i_am' + ); + } + + if (params.withgroup || params.withoutgroup) { + try { + await this.bot.pricelist.removeByGroup(newPricelist); + this.bot.sendMessage(steamID, `āœ… Removed ${newPricelistCount.length} items from pricelist.`); + return await this.bot.listings.redoListings(); + } catch (err) { + return this.bot.sendMessage(steamID, `āŒ Failed to clear pricelist: ${(err as Error).message}`); + } + } else { + try { + await this.bot.pricelist.removeAll(); + return this.bot.sendMessage(steamID, 'āœ… Cleared pricelist!'); + } catch (err) { + return this.bot.sendMessage(steamID, `āŒ Failed to clear pricelist: ${(err as Error).message}`); + } + } + } + + if (params.sku !== undefined && !testSKU(params.sku as string)) { + return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); + } + + if (params.item !== undefined) { + // Remove by full name + let match = this.bot.pricelist.searchByName(params.item as string, false); + if (match === null) { + return this.bot.sendMessage( + steamID, + `āŒ I could not find any items in my pricelist that contain "${params.item as string}"` + ); + } else if (Array.isArray(match)) { + const matchCount = match.length; + if (matchCount > 20) { + match = match.splice(0, 20); + } + + let reply = `I've found ${matchCount} items. Try with one of the items shown below:\n${match.join( + ',\n' + )}`; + + if (matchCount > match.length) { + const other = matchCount - match.length; + reply += `,\nand ${other} other ${pluralize('item', other)}.`; + } + + return this.bot.sendMessage(steamID, reply); + } + + delete params.item; + params.sku = match.sku; + } else if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } + + this.bot.pricelist + .removePrice(params.sku as string, true) + .then(entry => this.bot.sendMessage(steamID, `āœ… Removed "${entry.name}".`)) + .catch(err => + this.bot.sendMessage(steamID, `āŒ Failed to remove pricelist entry: ${(err as Error).message}`) + ); + } + + getCommand(steamID: SteamID, message: string): void { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + if (params.sku !== undefined && !testSKU(params.sku as string)) { + return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); + } + + if (params.item !== undefined) { + // Remove by full name + let match = this.bot.pricelist.searchByName(params.item as string, false); + + if (match === null) { + return this.bot.sendMessage( + steamID, + `āŒ I could not find any items in my pricelist that contain "${params.item as string}"` + ); + } else if (Array.isArray(match)) { + const matchCount = match.length; + if (matchCount > 20) { + match = match.splice(0, 20); + } + + let reply = `I've found ${matchCount} items. Try with one of the items shown below:\n${match.join( + ',\n' + )}`; + + if (matchCount > match.length) { + const other = matchCount - match.length; + reply += `,\nand ${other} other ${pluralize('item', other)}.`; + } + + return this.bot.sendMessage(steamID, reply); + } + + delete params.item; + params.sku = match.sku; + } else if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } + + if (params.sku === undefined) { + return this.bot.sendMessage(steamID, 'āŒ Missing item'); + } + + const match = this.bot.pricelist.getPrice(params.sku as string); + if (match === null) { + this.bot.sendMessage(steamID, `āŒ Could not find item "${params.sku as string}" in the pricelist`); + } else { + this.bot.sendMessage(steamID, `/code ${this.generateOutput(match)}`); + } + } + + async findCommand(steamID: SteamID, message: string): Promise { + const params = CommandParser.parseParams(CommandParser.removeCommand(message)); + if ( + !( + params.enabled !== undefined || + params.max !== undefined || + params.min !== undefined || + params.intent !== undefined || + params.autoprice !== undefined || + params.group !== undefined || + params.promoted !== undefined + ) + ) { + return this.bot.sendMessage( + steamID, + 'āš ļø Only parameters available for !find command: enabled, max, min, intent, promoted, autoprice or group\nExample: !find intent=bank&max=2' + ); + } + + let filter = this.bot.pricelist.getPrices; + if (params.enabled !== undefined) { + if (typeof params.enabled !== 'boolean') { + return this.bot.sendMessage(steamID, 'āš ļø enabled parameter must be "true" or "false"'); + } + filter = filter.filter(entry => entry.enabled === params.enabled); + } + + if (params.max !== undefined) { + if (typeof params.max !== 'number') { + return this.bot.sendMessage(steamID, 'āš ļø max parameter must be an integer'); + } + filter = filter.filter(entry => entry.max === params.max); + } + + if (params.min !== undefined) { + if (typeof params.min !== 'number') { + return this.bot.sendMessage(steamID, 'āš ļø min parameter must be an integer'); + } + filter = filter.filter(entry => entry.min === params.min); + } + + if (params.promoted !== undefined) { + if (typeof params.promoted === 'boolean') { + if (params.promoted === true) { + filter = filter.filter(entry => entry.promoted === 1); + } else { + filter = filter.filter(entry => entry.promoted === 0); + } + } else { + if (typeof params.promoted !== 'number') { + return this.bot.sendMessage(steamID, 'āš ļø promoted parameter must be either 0 (false) or 1 (true)'); + } + + if (params.promoted < 0 || params.promoted > 1) { + return this.bot.sendMessage( + steamID, + 'āš ļø "promoted" parameter must be either 0 (false) or 1 (true)' + ); + } + filter = filter.filter(entry => entry.promoted === params.promoted); + } + } + + if (params.intent !== undefined) { + if (typeof params.intent === 'string') { + const intent = ['buy', 'sell', 'bank'].indexOf(params.intent.toLowerCase()); + if (intent !== -1) { + params.intent = intent; + } + } + + if (typeof params.intent !== 'number' || params.intent < 0) { + return this.bot.sendMessage( + steamID, + 'āš ļø intent parameter must be "buy", "sell", or "bank" OR an integer of "0", "1" or "2" respectively' + ); + } + filter = filter.filter(entry => entry.intent === params.intent); + } + + if (params.autoprice !== undefined) { + if (typeof params.autoprice !== 'boolean') { + return this.bot.sendMessage(steamID, 'āš ļø autoprice parameter must be "true" or "false"'); + } + filter = filter.filter(entry => entry.autoprice === params.autoprice); + } + + if (params.group !== undefined) { + if (typeof params.group !== 'string') { + return this.bot.sendMessage(steamID, 'āš ļø group parameter must be a string'); + } + filter = filter.filter(entry => entry.group === params.group); + } + + const parametersUsed = { + enabled: params.enabled !== undefined ? `enabled=${(params.enabled as boolean).toString()}` : '', + autoprice: params.autoprice !== undefined ? `autoprice=${(params.autoprice as boolean).toString()}` : '', + max: params.max !== undefined ? `max=${params.max as number}` : '', + min: params.min !== undefined ? `min=${params.min as number}` : '', + intent: + params.intent !== undefined + ? `intent=${ + (params.intent as number) === 0 ? 'buy' : (params.intent as number) === 1 ? 'sell' : 'bank' + }` + : '', + promoted: + params.promoted !== undefined ? `promoted=${(params.promoted as number) === 1 ? 'true' : 'false'}` : '', + group: params.group !== undefined ? `group=${params.group as string}` : '' + }; + + const parameters = Object.values(parametersUsed); + const display = parameters.filter(param => param !== ''); + + const filterCount = filter.length; + if (filterCount === 0) { + this.bot.sendMessage(steamID, `No items found with ${display.join('&')}.`); + } else if (filterCount > 20) { + this.bot.sendMessage( + steamID, + `Found ${pluralize('item', filterCount, true)} with ${display.join( + '&' + )}, showing only a max of 100 items` + ); + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter, true, 0, 20)}`); + if (filterCount <= 40) { + this.bot.sendMessage( + steamID, + `/code ${this.generateOutput(filter, true, 20, filterCount > 40 ? 40 : filterCount)}` + ); + } else if (filterCount <= 60) { + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter, true, 20, 40)}`); + await sleepasync().Promise.sleep(1 * 1000); + this.bot.sendMessage( + steamID, + `/code ${this.generateOutput(filter, true, 40, filterCount > 60 ? 60 : filterCount)}` + ); + } else if (filterCount <= 80) { + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter, true, 20, 40)}`); + await sleepasync().Promise.sleep(1 * 1000); + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter, true, 40, 60)}`); + await sleepasync().Promise.sleep(1 * 1000); + this.bot.sendMessage( + steamID, + `/code ${this.generateOutput(filter, true, 60, filterCount > 80 ? 80 : filterCount)}` + ); + } else if (filterCount > 80) { + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter, true, 20, 40)}`); + await sleepasync().Promise.sleep(1 * 1000); + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter, true, 40, 60)}`); + await sleepasync().Promise.sleep(1 * 1000); + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter, true, 60, 80)}`); + await sleepasync().Promise.sleep(1 * 1000); + this.bot.sendMessage( + steamID, + `/code ${this.generateOutput(filter, true, 80, filterCount > 100 ? 100 : filterCount)}` + ); + } + } else { + this.bot.sendMessage(steamID, `Found ${pluralize('item', filterCount, true)} with ${display.join('&')}`); + this.bot.sendMessage(steamID, `/code ${this.generateOutput(filter)}`); + } + } + + private generateOutput(filtered: Entry[] | Entry, isSlice = false, start?: number, end?: number): string { + return isSlice + ? JSON.stringify((filtered as Entry[]).slice(start, end), null, 4) + : JSON.stringify(filtered, null, 4); + } +} + +interface ErrorRequest { + body?: ErrorBody; + message?: string; +} + +interface ErrorBody { + message: string; +} diff --git a/src/classes/Commands/sub-classes/Request.ts b/src/classes/Commands/sub-classes/Request.ts new file mode 100644 index 000000000..0e37d696a --- /dev/null +++ b/src/classes/Commands/sub-classes/Request.ts @@ -0,0 +1,266 @@ +import SteamID from 'steamid'; +import SKU from 'tf2-sku-2'; +import pluralize from 'pluralize'; +import dayjs from 'dayjs'; +import sleepasync from 'sleep-async'; +import Currencies from 'tf2-currencies'; +import { removeLinkProtocol, getItemFromParams, testSKU } from '../functions/utils'; +import Bot from '../../Bot'; +import CommandParser from '../../CommandParser'; +import log from '../../../lib/logger'; +import { fixItem } from '../../../lib/items'; +import Pricer, { GetPriceFn, GetSalesFn, RequestCheckFn, RequestCheckResponse } from '../../Pricer'; + +export default class RequestCommands { + private readonly bot: Bot; + + private getSales: GetSalesFn; + + private requestCheck: RequestCheckFn; + + private getPrice: GetPriceFn; + + constructor(bot: Bot, private priceSource: Pricer) { + this.bot = bot; + + this.getSales = this.priceSource.getSales.bind(this.priceSource); + this.requestCheck = this.priceSource.requestCheck.bind(this.priceSource); + this.getPrice = this.priceSource.getPrice.bind(this.priceSource); + } + + async getSalesCommand(steamID: SteamID, message: string): Promise { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } else { + params.sku = SKU.fromObject(fixItem(SKU.fromString(params.sku), this.bot.schema)); + } + + const name = this.bot.schema.getName(SKU.fromString(params.sku)); + try { + const salesData = await this.getSales(params.sku, 'bptf'); + if (!salesData) { + return this.bot.sendMessage( + steamID, + `āŒ No recorded snapshots found for ${name === null ? (params.sku as string) : name}.` + ); + } + + if (salesData.sales.length === 0) { + return this.bot.sendMessage( + steamID, + `āŒ No recorded snapshots found for ${name === null ? (params.sku as string) : name}.` + ); + } + + const sales: Sales[] = []; + salesData.sales.forEach(sale => + sales.push({ + seller: 'https://backpack.tf/profiles/' + sale.steamid, + itemHistory: 'https://backpack.tf/item/' + sale.id.replace('440_', ''), + keys: sale.currencies.keys, + metal: sale.currencies.metal, + date: sale.time + }) + ); + sales.sort((a, b) => b.date - a.date); + + let left = 0; + const salesList: string[] = []; + const salesListCount = salesList.length; + const salesCount = sales.length; + + for (let i = 0; i < salesCount; i++) { + if (salesListCount > 40) { + left += 1; + } else { + const sale = sales[i]; + salesList.push( + `Listed #${i + 1}-----\nā€¢ Date: ${dayjs.unix(sale.date).utc().toString()}\nā€¢ Item: ${ + sale.itemHistory + }\nā€¢ Seller: ${sale.seller}\nā€¢ Was selling for: ${sale.keys > 0 ? `${sale.keys} keys,` : ''} ${ + sale.metal + } ref` + ); + } + } + + let reply = `šŸ”Ž Recorded removed sell listings from backpack.tf\n\nItem name: ${ + salesData.name + }\n\n-----${salesList.join('\n\n-----')}`; + if (left > 0) { + reply += `,\n\nand ${left} other ${pluralize('sale', left)}`; + } + + this.bot.sendMessage(steamID, reply); + } catch (err) { + return this.bot.sendMessage( + steamID, + `āŒ Error getting sell snapshots for ${name === null ? (params.sku as string) : name}: ${ + (err as ErrorRequest).body && (err as ErrorRequest).body.message + ? (err as ErrorRequest).body.message + : (err as ErrorRequest).message + }` + ); + } + } + + pricecheckCommand(steamID: SteamID, message: string): void { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + if (params.sku !== undefined && !testSKU(params.sku as string)) { + return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); + } + + if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } else { + params.sku = SKU.fromObject(fixItem(SKU.fromString(params.sku), this.bot.schema)); + } + + const name = this.bot.schema.getName(SKU.fromString(params.sku), false); + void this.requestCheck(params.sku, 'bptf').asCallback((err: ErrorRequest, body: RequestCheckResponse) => { + if (err) { + return this.bot.sendMessage( + steamID, + `āŒ Error while requesting price check: ${ + err.body && err.body.message ? err.body.message : err.message + }` + ); + } + + if (!body) { + this.bot.sendMessage(steamID, 'āŒ Error while requesting price check (returned null/undefined)'); + } else { + this.bot.sendMessage( + steamID, + `āŒ› Price check requested for ${ + body.name.includes('War Paint') || + body.name.includes('Mann Co. Supply Crate Series #') || + body.name.includes('Salvaged Mann Co. Supply Crate #') + ? name + : body.name + }, the item will be checked.` + ); + } + }); + } + + async pricecheckAllCommand(steamID: SteamID): Promise { + const pricelist = this.bot.pricelist.getPrices; + + const total = pricelist.length; + const totalTime = total * 2 * 1000; + const aSecond = 1 * 1000; + const aMin = 1 * 60 * 1000; + const anHour = 1 * 60 * 60 * 1000; + this.bot.sendMessage( + steamID, + `āŒ› Price check requested for ${total} items. It will be completed in approximately ${ + totalTime < aMin + ? `${Math.round(totalTime / aSecond)} seconds.` + : totalTime < anHour + ? `${Math.round(totalTime / aMin)} minutes.` + : `${Math.round(totalTime / anHour)} hours.` + } (about 2 seconds for each item).` + ); + + const skus = pricelist.map(entry => entry.sku); + let submitted = 0; + let success = 0; + let failed = 0; + for (const sku of skus) { + await sleepasync().Promise.sleep(2 * 1000); + void this.requestCheck(sku, 'bptf').asCallback(err => { + if (err) { + submitted++; + failed++; + log.warn(`pricecheck failed for ${sku}: ${JSON.stringify(err)}`); + log.debug( + `pricecheck for ${sku} failed, status: ${submitted}/${total}, ${success} success, ${failed} failed.` + ); + } else { + submitted++; + success++; + log.debug( + `pricecheck for ${sku} success, status: ${submitted}/${total}, ${success} success, ${failed} failed.` + ); + } + if (submitted === total) { + this.bot.sendMessage( + steamID, + `āœ… Successfully completed pricecheck for all ${total} ${pluralize('item', total)}!` + ); + } + }); + } + } + + async checkCommand(steamID: SteamID, message: string): Promise { + const params = CommandParser.parseParams(CommandParser.removeCommand(removeLinkProtocol(message))); + if (params.sku !== undefined && !testSKU(params.sku as string)) { + return this.bot.sendMessage(steamID, `āŒ "sku" should not be empty or wrong format.`); + } + + if (params.sku === undefined) { + const item = getItemFromParams(steamID, params, this.bot); + if (item === null) { + return; + } + + params.sku = SKU.fromObject(item); + } else { + params.sku = SKU.fromObject(fixItem(SKU.fromString(params.sku), this.bot.schema)); + } + + const name = this.bot.schema.getName(SKU.fromString(params.sku)); + try { + const price = await this.getPrice(params.sku, 'bptf'); + const currBuy = new Currencies(price.buy); + const currSell = new Currencies(price.sell); + + this.bot.sendMessage( + steamID, + `šŸ”Ž ${name}:\nā€¢ Buy : ${currBuy.toString()}\nā€¢ Sell : ${currSell.toString()}\n\nPrices.TF: https://prices.tf/items/${ + params.sku as string + }` + ); + } catch (err) { + return this.bot.sendMessage( + steamID, + `Error getting price for ${name === null ? (params.sku as string) : name}: ${ + (err as ErrorRequest).body && (err as ErrorRequest).body.message + ? (err as ErrorRequest).body.message + : (err as ErrorRequest).message + }` + ); + } + } +} + +interface Sales { + seller: string; + itemHistory: string; + keys: number; + metal: number; + date: number; +} + +interface ErrorRequest { + body?: ErrorBody; + message?: string; +} + +interface ErrorBody { + message: string; +} diff --git a/src/classes/Commands/sub-classes/Review.ts b/src/classes/Commands/sub-classes/Review.ts new file mode 100644 index 000000000..52ac915b9 --- /dev/null +++ b/src/classes/Commands/sub-classes/Review.ts @@ -0,0 +1,377 @@ +import SteamID from 'steamid'; +import pluralize from 'pluralize'; +import TradeOfferManager, { Meta, OfferData, OurTheirItemsDict } from 'steam-tradeoffer-manager'; +import Currencies from 'tf2-currencies'; +import { UnknownDictionaryKnownValues } from '../../../types/common'; +import SKU from 'tf2-sku-2'; +import SchemaManager from 'tf2-schema-2'; +import Bot from '../../Bot'; +import CommandParser from '../../CommandParser'; +import { generateLinks } from '../../../lib/tools/export'; + +// Manual review commands + +type ActionOnTrade = 'accept' | 'accepttrade' | 'decline' | 'declinetrade'; +type ForceAction = 'faccept' | 'fdecline'; + +export default class ReviewCommands { + private readonly bot: Bot; + + constructor(bot: Bot) { + this.bot = bot; + } + + tradesCommand(steamID: SteamID): void { + // Go through polldata and find active offers + const pollData = this.bot.manager.pollData; + + const offersForReview: UnknownDictionaryKnownValues[] = []; + const activeOffersNotForReview: UnknownDictionaryKnownValues[] = []; + + for (const id in pollData.received) { + if (!Object.prototype.hasOwnProperty.call(pollData.received, id)) { + continue; + } + + if (pollData.received[id] !== TradeOfferManager.ETradeOfferState['Active']) { + continue; + } + + const data = pollData?.offerData[id] || null; + + if (data === null) { + continue; + } else if (data?.action?.action !== 'skip') { + activeOffersNotForReview.push({ id: id, data: data }); + continue; + } + + offersForReview.push({ id: id, data: data }); + } + + if (offersForReview.length === 0 && activeOffersNotForReview.length === 0) { + return this.bot.sendMessage(steamID, 'āŒ There are no active offers/ pending review.'); + } + + this.bot.sendMessage( + steamID, + (offersForReview.length > 0 ? this.generateTradesReply(offersForReview.sort((a, b) => a.id - b.id)) : '') + + (offersForReview.length > 0 ? '\n\n-----------------\n\n' : '') + + (activeOffersNotForReview.length > 0 + ? this.generateActiveOfferReply(activeOffersNotForReview.sort((a, b) => a.id - b.id)) + : '') + ); + } + + private generateTradesReply(offers: UnknownDictionaryKnownValues[]): string { + const offersCount = offers.length; + + let reply = `There ${pluralize('is', offersCount, true)} active ${pluralize( + 'offer', + offersCount + )} that you can review:\n`; + + for (let i = 0; i < offersCount; i++) { + const offer = offers[i]; + + reply += + `\n- Offer #${offer.id as string} from ${ + (offer.data as OfferData).partner + } (reason: ${(offer.data as OfferData).meta.uniqueReasons.join(', ')})` + + `\nāš ļø Send "!trade ${offer.id as string}" for more details.\n`; + } + + return reply; + } + + private generateActiveOfferReply(offers: UnknownDictionaryKnownValues[]): string { + const offersCount = offers.length; + + let reply = `There ${pluralize('is', offersCount, true)} ${pluralize( + 'offer', + offersCount + )} that currently still active:\n`; + + for (let i = 0; i < offersCount; i++) { + const offer = offers[i]; + + reply += + `\n- Offer #${offer.id as string} from ${(offer.data as OfferData).partner}` + + `\nāš ļø Send "!trade ${offer.id as string}" for more details or "!faccept ${ + offer.id as string + }" to force accept the trade.\n`; + } + + return reply; + } + + tradeCommand(steamID: SteamID, message: string): void { + const offerId = CommandParser.removeCommand(message).trim(); + + if (offerId === '') { + return this.bot.sendMessage(steamID, 'āš ļø Missing offer id. Example: "!trade 3957959294"'); + } + + const state = this.bot.manager.pollData.received[offerId]; + + if (state === undefined) { + return this.bot.sendMessage(steamID, 'Offer does not exist. āŒ'); + } + + if (state !== TradeOfferManager.ETradeOfferState['Active']) { + // TODO: Add what the offer is now, accepted / declined and why + return this.bot.sendMessage(steamID, 'Offer is not active. āŒ'); + } + + const offerData = this.bot.manager.pollData.offerData[offerId]; + + // Log offer details + + let reply = + offerData?.action?.action === 'skip' + ? `āš ļø Offer #${offerId} from ${offerData.partner} is pending for review` + + `\nReason: ${offerData.meta.uniqueReasons.join(', ')}).\n\nSummary:\n\n` + : `āš ļø Offer #${offerId} from ${offerData.partner} is still active.\n\nSummary:\n\n`; + + const keyPrice = this.bot.pricelist.getKeyPrice; + const value = offerData.value; + const items = offerData.dict || { our: null, their: null }; + const summarizeItems = (dict: OurTheirItemsDict, schema: SchemaManager.Schema) => { + if (dict === null) { + return 'unknown items'; + } + + const summary: string[] = []; + + for (const sku in dict) { + if (!Object.prototype.hasOwnProperty.call(dict, sku)) { + continue; + } + + summary.push(schema.getName(SKU.fromString(sku), false) + (dict[sku] > 1 ? ` x${dict[sku]}` : '')); // dict[sku] = amount + } + + if (summary.length === 0) { + return 'nothing'; + } + + return summary.join(', '); + }; + + if (!value) { + reply += + 'Asked: ' + + summarizeItems(items.our, this.bot.schema) + + '\nOffered: ' + + summarizeItems(items.their, this.bot.schema); + } else { + const valueDiff = + new Currencies(value.their).toValue(keyPrice.metal) - new Currencies(value.our).toValue(keyPrice.metal); + const valueDiffRef = Currencies.toRefined(Currencies.toScrap(Math.abs(valueDiff * (1 / 9)))).toString(); + reply += + 'Asked: ' + + new Currencies(value.our).toString() + + ' (' + + summarizeItems(items.our, this.bot.schema) + + ')\nOffered: ' + + new Currencies(value.their).toString() + + ' (' + + summarizeItems(items.their, this.bot.schema) + + (valueDiff > 0 + ? `)\nšŸ“ˆ Profit from overpay: ${valueDiffRef} ref` + : valueDiff < 0 + ? `)\nšŸ“‰ Loss from underpay: ${valueDiffRef} ref` + : ')'); + } + + const links = generateLinks(offerData.partner.toString()); + reply += + `\n\nSteam: ${links.steam}\nBackpack.tf: ${links.bptf}\nSteamREP: ${links.steamrep}` + + (offerData?.action?.action === 'skip' + ? `\n\nāš ļø Send "!accept ${offerId}" to accept or "!decline ${offerId}" to decline this offer.` + : `\n\nāš ļø Send "!faccept ${offerId}" to force accept the trade now!`); + + this.bot.sendMessage(steamID, reply); + } + + async actionOnTradeCommand(steamID: SteamID, message: string, command: ActionOnTrade): Promise { + const offerIdAndMessage = CommandParser.removeCommand(message); + const offerIdRegex = /\d+/.exec(offerIdAndMessage); + + const isAccepting = ['accept', 'accepttrade'].includes(command); + + if (isNaN(+offerIdRegex) || !offerIdRegex) { + return this.bot.sendMessage( + steamID, + `āš ļø Missing offer id. Example: "!${isAccepting ? 'accept' : 'decline'} 3957959294"` + ); + } + + const offerId = offerIdRegex[0]; + const state = this.bot.manager.pollData.received[offerId]; + if (state === undefined) { + return this.bot.sendMessage(steamID, 'Offer does not exist. āŒ'); + } + + if (state !== TradeOfferManager.ETradeOfferState['Active']) { + // TODO: Add what the offer is now, accepted / declined and why + return this.bot.sendMessage(steamID, 'Offer is not active. āŒ'); + } + + const offerData = this.bot.manager.pollData.offerData[offerId]; + if (offerData?.action?.action !== 'skip') { + return this.bot.sendMessage(steamID, "Offer can't be reviewed. āŒ"); + } + + try { + const offer = await this.bot.trades.getOffer(offerId); + this.bot.sendMessage(steamID, `${isAccepting ? 'Accepting' : 'Declining'} offer...`); + + const partnerId = new SteamID(this.bot.manager.pollData.offerData[offerId].partner); + const reply = offerIdAndMessage.substr(offerId.length); + const adminDetails = this.bot.friends.getFriend(steamID); + + try { + await this.bot.trades.applyActionToOffer( + isAccepting ? 'accept' : 'decline', + 'MANUAL', + isAccepting ? (offer.data('meta') as Meta) : {}, + offer + ); + + if (isAccepting) { + const isManyItems = offer.itemsToGive.length + offer.itemsToReceive.length > 50; + + if (isManyItems) { + this.bot.sendMessage( + offer.partner, + this.bot.options.customMessage.accepted.manual.largeOffer + ? this.bot.options.customMessage.accepted.manual.largeOffer + : '.\nMy owner has manually accepted your offer. The trade may take a while to finalize due to it being a large offer.' + + ' If the trade does not finalize after 5-10 minutes has passed, please send your offer again, or add me and use ' + + 'the !sell/!sellcart or !buy/!buycart command.' + ); + } else { + this.bot.sendMessage( + offer.partner, + this.bot.options.customMessage.accepted.manual.smallOffer + ? this.bot.options.customMessage.accepted.manual.smallOffer + : '.\nMy owner has manually accepted your offer. The trade should be finalized shortly.' + + ' If the trade does not finalize after 1-2 minutes has passed, please send your offer again, or add me and use ' + + 'the !sell/!sellcart or !buy/!buycart command.' + ); + } + } + + // Send message to recipient if includes some messages + if (reply) { + this.bot.sendMessage( + partnerId, + `/quote šŸ’¬ Message from ${adminDetails ? adminDetails.player_name : 'admin'}: ${reply}` + ); + } + } catch (err) { + return this.bot.sendMessage( + steamID, + `āŒ Ohh nooooes! Something went wrong while trying to ${ + isAccepting ? 'accept' : 'decline' + } the offer: ${(err as Error).message}` + ); + } + } catch (err) { + return this.bot.sendMessage( + steamID, + `āŒ Ohh nooooes! Something went wrong while trying to ${ + isAccepting ? 'accept' : 'decline' + } the offer: ${(err as Error).message}` + ); + } + } + + async forceAction(steamID: SteamID, message: string, command: ForceAction): Promise { + const offerIdAndMessage = CommandParser.removeCommand(message); + const offerIdRegex = /\d+/.exec(offerIdAndMessage); + + const isForceAccepting = command === 'faccept'; + + if (isNaN(+offerIdRegex) || !offerIdRegex) { + return this.bot.sendMessage( + steamID, + `āš ļø Missing offer id. Example: "!${isForceAccepting ? 'faccept' : 'fdecline'} 3957959294"` + ); + } + + const offerId = offerIdRegex[0]; + + const state = this.bot.manager.pollData.received[offerId]; + if (state === undefined) { + return this.bot.sendMessage(steamID, 'Offer does not exist. āŒ'); + } + + try { + const offer = await this.bot.trades.getOffer(offerId); + this.bot.sendMessage(steamID, `Force ${isForceAccepting ? 'accepting' : 'declining'} offer...`); + + const partnerId = new SteamID(this.bot.manager.pollData.offerData[offerId].partner); + const reply = offerIdAndMessage.substr(offerId.length); + const adminDetails = this.bot.friends.getFriend(steamID); + + try { + await this.bot.trades.applyActionToOffer( + isForceAccepting ? 'accept' : 'decline', + 'MANUAL-FORCE', + isForceAccepting ? (offer.data('meta') as Meta) : {}, + offer + ); + + // Send message to recipient if includes some messages + if (reply) { + this.bot.sendMessage( + partnerId, + `/quote šŸ’¬ Message from ${adminDetails ? adminDetails.player_name : 'admin'}: ${reply}` + ); + } + } catch (err) { + return this.bot.sendMessage( + steamID, + `āŒ Ohh nooooes! Something went wrong while trying to force ${ + isForceAccepting ? 'accept' : 'decline' + } the offer: ${(err as Error).message}` + ); + } + } catch (err) { + return this.bot.sendMessage( + steamID, + `āŒ Ohh nooooes! Something went wrong while trying to force ${ + isForceAccepting ? 'accept' : 'decline' + } the offer: ${(err as Error).message}` + ); + } + } + + offerInfo(steamID: SteamID, message: string): void { + const offerIdAndMessage = CommandParser.removeCommand(message); + const offerIdRegex = /\d+/.exec(offerIdAndMessage); + + if (isNaN(+offerIdRegex) || !offerIdRegex) { + return this.bot.sendMessage(steamID, `āš ļø Missing offer id. Example: "!offerinfo 3957959294"`); + } + + const offerId = offerIdRegex[0]; + + const state = this.bot.manager.pollData.received[offerId]; + if (state === undefined) { + return this.bot.sendMessage(steamID, 'Offer does not exist. āŒ'); + } + + try { + const offer = this.bot.manager.pollData.offerData[offerId]; + const show = {}; + show[offerId] = offer; + + this.bot.sendMessage(steamID, '/code ' + JSON.stringify(show, null, 4)); + } catch (err) { + return this.bot.sendMessage(steamID, `āŒ Error getting offer #${offerId} info: ${(err as Error).message}`); + } + } +} diff --git a/src/classes/Commands/sub-classes/Status.ts b/src/classes/Commands/sub-classes/Status.ts new file mode 100644 index 000000000..9000677ac --- /dev/null +++ b/src/classes/Commands/sub-classes/Status.ts @@ -0,0 +1,130 @@ +import SteamID from 'steamid'; +import pluralize from 'pluralize'; +import Currencies from 'tf2-currencies'; +import Bot from '../../Bot'; +import { stats, profit } from '../../../lib/tools/export'; +import { sendStats } from '../../../lib/DiscordWebhook/export'; + +// Bot status + +export default class StatusCommands { + private readonly bot: Bot; + + constructor(bot: Bot) { + this.bot = bot; + } + + statsCommand(steamID: SteamID): void { + const tradesFromEnv = this.bot.options.statistics.lastTotalTrades; + const trades = stats(this.bot); + const profits = profit(this.bot, Math.floor((Date.now() - 86400000) / 1000)); //since -24h + + const keyPrices = this.bot.pricelist.getKeyPrices; + + const timedProfitmadeFull = Currencies.toCurrencies(profits.profitTimed, keyPrices.sell.metal).toString(); + const timedProfitmadeInRef = timedProfitmadeFull.includes('key') + ? ` (${Currencies.toRefined(profits.profitTimed)} ref)` + : ''; + + const profitmadeFull = Currencies.toCurrencies(profits.tradeProfit, keyPrices.sell.metal).toString(); + const profitmadeInRef = profitmadeFull.includes('key') + ? ` (${Currencies.toRefined(profits.tradeProfit)} ref)` + : ''; + + const profitOverpayFull = Currencies.toCurrencies(profits.overpriceProfit, keyPrices.sell.metal).toString(); + const profitOverpayInRef = profitOverpayFull.includes('key') + ? ` (${Currencies.toRefined(profits.overpriceProfit)} ref)` + : ''; + + this.bot.sendMessage( + steamID, + `All trades (accepted) are recorded from ${pluralize('day', trades.totalDays, true)}` + + ' ago šŸ“Š\n Total accepted trades: ' + + (tradesFromEnv !== 0 + ? String(tradesFromEnv + trades.totalAcceptedTrades) + : String(trades.totalAcceptedTrades)) + + `\n\n--- Last 24 hours ---` + + `\nā€¢ Processed: ${trades.hours24.processed}` + + `\nā€¢ Accepted: ${trades.hours24.accepted.offer + trades.hours24.accepted.sent}` + + `\n---ā€¢ Received offer: ${trades.hours24.accepted.offer}` + + `\n---ā€¢ Sent offer: ${trades.hours24.accepted.sent}` + + `\nā€¢ Declined: ${trades.hours24.decline.offer + trades.hours24.decline.sent}` + + `\n---ā€¢ Received offer: ${trades.hours24.decline.offer}` + + `\n---ā€¢ Sent offer: ${trades.hours24.decline.sent}` + + `\nā€¢ Skipped: ${trades.hours24.skipped}` + + `\nā€¢ Traded away: ${trades.hours24.invalid}` + + `\nā€¢ Canceled: ${trades.hours24.canceled.total}` + + `\n---ā€¢ by user: ${trades.hours24.canceled.byUser}` + + `\n---ā€¢ confirmation failed: ${trades.hours24.canceled.failedConfirmation}` + + `\n---ā€¢ unknown reason: ${trades.hours24.canceled.unknown}` + + `\n\n--- Since beginning of today ---` + + `\nā€¢ Processed: ${trades.today.processed}` + + `\nā€¢ Accepted: ${trades.today.accepted.offer + trades.today.accepted.sent}` + + `\n---ā€¢ Received offer: ${trades.today.accepted.offer}` + + `\n---ā€¢ Sent offer: ${trades.today.accepted.sent}` + + `\nā€¢ Declined: ${trades.today.decline.offer + trades.today.decline.sent}` + + `\n---ā€¢ Received offer: ${trades.today.decline.offer}` + + `\n---ā€¢ Sent offer: ${trades.today.decline.sent}` + + `\nā€¢ Skipped: ${trades.today.skipped}` + + `\nā€¢ Traded away: ${trades.today.invalid}` + + `\nā€¢ Canceled: ${trades.today.canceled.total}` + + `\n---ā€¢ by user: ${trades.today.canceled.byUser}` + + `\n---ā€¢ confirmation failed: ${trades.today.canceled.failedConfirmation}` + + `\n---ā€¢ unknown reason: ${trades.today.canceled.unknown}` + + `\n\n Profit (last 24h): ${timedProfitmadeFull + timedProfitmadeInRef}` + + `\nProfit made: ${profitmadeFull + profitmadeInRef} ${ + profits.since !== 0 ? ` (since ${pluralize('day', profits.since, true)} ago)` : '' + }` + + `\nProfit from overpay: ${profitOverpayFull + profitOverpayInRef}` + + `\nKey rate: ${keyPrices.buy.metal}/${keyPrices.sell.metal} ref` + ); + } + + statsDWCommand(steamID: SteamID): void { + const opt = this.bot.options.discordWebhook.sendStats; + + if (!opt.enable) { + return this.bot.sendMessage(steamID, 'āŒ Sending stats to Discord Webhook is disabled.'); + } + + if (opt.url === '') { + return this.bot.sendMessage(steamID, 'āŒ Your discordWebhook.sendStats.url is empty.'); + } + + sendStats(this.bot, true, steamID); + } + + inventoryCommand(steamID: SteamID): void { + this.bot.sendMessage( + steamID, + `šŸŽ’ My current items in my inventory: ${ + String(this.bot.inventoryManager.getInventory.getTotalItems) + '/' + String(this.bot.tf2.backpackSlots) + }` + ); + } + + versionCommand(steamID: SteamID): void { + this.bot.sendMessage( + steamID, + `Currently running TF2Autobot@v${process.env.BOT_VERSION}. Checking for a new version...` + ); + + this.bot.checkForUpdates + .then(({ hasNewVersion, latestVersion }) => { + if (!hasNewVersion) { + this.bot.sendMessage(steamID, 'You are running the latest version of TF2Autobot!'); + } else if (this.bot.lastNotifiedVersion === latestVersion) { + this.bot.sendMessage( + steamID, + `āš ļø Update available! Current: v${process.env.BOT_VERSION}, Latest: v${latestVersion}.\n\n` + + `Release note: https://github.com/idinium96/tf2autobot/releases` + + `\n\nRun "!updaterepo" if you're running your bot with PM2 to update now!"` + + '\n\nContact IdiNium if you have any other problem. Thank you.' + ); + } + }) + .catch(err => { + this.bot.sendMessage(steamID, `āŒ Failed to check for updates: ${JSON.stringify(err)}`); + }); + } +} diff --git a/src/classes/Commands/sub-classes/export.ts b/src/classes/Commands/sub-classes/export.ts new file mode 100644 index 000000000..efd4f0256 --- /dev/null +++ b/src/classes/Commands/sub-classes/export.ts @@ -0,0 +1,21 @@ +import StatusCommands from './Status'; +import HelpCommands from './Help'; +import MessageCommand from './Message'; +import MiscCommands from './Misc'; +import PricelistManager from './PricelistManager'; +import ReviewCommands from './Review'; +import OptionsCommand from './Options'; +import ManagerCommands from './Manager'; +import RequestCommands from './Request'; + +export { + StatusCommands, + HelpCommands, + MessageCommand, + MiscCommands, + PricelistManager, + ReviewCommands, + OptionsCommand, + ManagerCommands, + RequestCommands +}; diff --git a/src/classes/Inventory.ts b/src/classes/Inventory.ts index b875ea85e..6d1bd8986 100644 --- a/src/classes/Inventory.ts +++ b/src/classes/Inventory.ts @@ -246,7 +246,9 @@ export default class Inventory { ? Object.keys(paints).map(paint => paint.toLowerCase()) : opt.highValue.painted.map(paint => paint.toLowerCase()); - for (let i = 0; i < items.length; i++) { + const itemsCount = items.length; + + for (let i = 0; i < itemsCount; i++) { const sku = items[i].getSKU( schema, opt.normalize.festivized[which], @@ -257,6 +259,8 @@ export default class Inventory { ); const attributes = highValue(items[i], opt, paints, strangeParts); + const attributesCount = Object.keys(attributes).length; + const isUses = sku === '241;6' ? isFull(items[i], 'duel') @@ -264,7 +268,7 @@ export default class Inventory { ? isFull(items[i], 'noise') : null; - if (Object.keys(attributes).length === 0 && isUses === null) { + if (attributesCount === 0 && isUses === null) { (dict[sku] = dict[sku] || []).push({ id: items[i].id }); } else { if (isUses !== null) { diff --git a/src/classes/InventoryManager.ts b/src/classes/InventoryManager.ts index 6955acbdc..225d0b72f 100644 --- a/src/classes/InventoryManager.ts +++ b/src/classes/InventoryManager.ts @@ -92,14 +92,15 @@ export default class InventoryManager { const avaiableCurrencies = inventory.getCurrencies([]); - const availableKeys = avaiableCurrencies['5021;6'].length * keyPrice.toValue(); + const availableKeysValue = avaiableCurrencies['5021;6'].length * keyPrice.toValue(); const availableMetalsValue = avaiableCurrencies['5002;6'].length * 9 + avaiableCurrencies['5001;6'].length * 3 + avaiableCurrencies['5000;6'].length; return ( - (buyingPrice.keys > 0 ? availableKeys >= buyingKeysValue : true) && availableMetalsValue >= buyingMetalValue + (buyingPrice.keys > 0 ? availableKeysValue >= buyingKeysValue : true) && + availableMetalsValue >= buyingMetalValue ); } diff --git a/src/classes/Listings.ts b/src/classes/Listings.ts index d94011573..7a6cc8277 100644 --- a/src/classes/Listings.ts +++ b/src/classes/Listings.ts @@ -5,13 +5,13 @@ import request from 'request-retry-dayjs'; import async from 'async'; import dayjs from 'dayjs'; import Currencies from 'tf2-currencies'; +import sleepasync from 'sleep-async'; import Bot from './Bot'; import { Entry } from './Pricelist'; import { BPTFGetUserInfo, UserSteamID } from './MyHandler/interfaces'; import log from '../lib/logger'; import { exponentialBackoff } from '../lib/helpers'; import { noiseMakers, spellsData, killstreakersData, sheensData } from '../lib/data'; -import { updateOptionsCommand } from './Commands/functions/options'; import { DictItem } from './Inventory'; import { PaintedNames } from './Options'; import { Paints, StrangeParts } from 'tf2-schema-2'; @@ -134,7 +134,7 @@ export default class Listings { this.enableAutoRelist(); } else if (this.isAutoRelistEnabled && info.premium === 1) { log.warn('Disabling autobump! - Your account is premium, no need to forcefully bump listings'); - updateOptionsCommand(null, '!config miscSettings.autobump.enable=false', this.bot); + this.bot.handler.commands.useUpdateOptionsCommand(null, '!config miscSettings.autobump.enable=false'); } }); } @@ -182,7 +182,10 @@ export default class Listings { const amountCanBuy = this.bot.inventoryManager.amountCanTrade(sku, true, generics); const amountCanSell = this.bot.inventoryManager.amountCanTrade(sku, false, generics); - const inventory = this.bot.inventoryManager.getInventory; + const invManager = this.bot.inventoryManager; + const inventory = invManager.getInventory; + + const isFilterCantAfford = this.bot.options.pricelist.filterCantAfford.enable; // false by default this.bot.listingManager.findListings(sku).forEach(listing => { if (listing.intent === 1 && hasSellListing) { @@ -203,6 +206,15 @@ export default class Listings { } else if ((listing.intent === 0 && amountCanBuy <= 0) || (listing.intent === 1 && amountCanSell <= 0)) { // We are not buying / selling more, remove the listing listing.remove(); + } else if ( + match !== null && + listing.intent === 0 && + !invManager.isCanAffordToBuy(match.buy, invManager.getInventory) && + isFilterCantAfford + ) { + // Listing for buying exist but we can't afford to buy, remove. + log.debug(`Intent buy, removed because can't afford: ${match.sku}`); + listing.remove(); } else { if (listing.intent === 0 && /;[p][0-9]+/.test(sku)) { // do nothing @@ -241,7 +253,11 @@ export default class Listings { // TODO: Check if we are already making a listing for same type of item + intent - if (!hasBuyListing && amountCanBuy > 0 && !/;[p][0-9]+/.test(sku)) { + const canAffordToBuy = isFilterCantAfford + ? invManager.isCanAffordToBuy(matchNew.buy, invManager.getInventory) + : true; + + if (!hasBuyListing && amountCanBuy > 0 && canAffordToBuy && !/;[p][0-9]+/.test(sku)) { // We have no buy order and we can buy more items, create buy listing this.bot.listingManager.createListing({ time: matchNew.time || dayjs().unix(), @@ -297,6 +313,30 @@ export default class Listings { const keyPrice = this.bot.pricelist.getKeyPrice; const pricelist = this.bot.pricelist.getPrices + .filter(entry => { + if (!this.bot.options.pricelist.filterCantAfford.enable) { + // if this option is set to false, then always return true + return true; + } + + // Filter pricelist to only items we can sell and we can afford to buy + + const amountCanBuy = this.bot.inventoryManager.amountCanTrade(entry.sku, true); + const amountCanSell = this.bot.inventoryManager.amountCanTrade(entry.sku, false); + + if ( + (amountCanBuy > 0 && + inventoryManager.isCanAffordToBuy(entry.buy, inventoryManager.getInventory)) || + amountCanSell > 0 + ) { + // if can amountCanBuy is more than 0 and isCanAffordToBuy is true OR amountCanSell is more than 0 + // return this entry + return true; + } + + // Else ignore + return false; + }) .sort((a, b) => { return ( currentPure.keys - @@ -328,32 +368,31 @@ export default class Listings { }); } - recursiveCheckPricelist(pricelist: Entry[], withDelay = false): Promise { + recursiveCheckPricelist(pricelist: Entry[], withDelay = false, time?: number): Promise { return new Promise(resolve => { let index = 0; - const iteration = (): void => { + const iteration = async (): Promise => { if (pricelist.length <= index || this.cancelCheckingListings) { this.cancelCheckingListings = false; return resolve(); } if (withDelay) { - setTimeout(() => { - this.checkBySKU(pricelist[index].sku, pricelist[index]); - index++; - iteration(); - }, 200); + this.checkBySKU(pricelist[index].sku, pricelist[index]); + index++; + await sleepasync().Promise.sleep(time ? time : 200); + void iteration(); } else { setImmediate(() => { this.checkBySKU(pricelist[index].sku, pricelist[index]); index++; - iteration(); + void iteration(); }); } }; - iteration(); + void iteration(); }); } diff --git a/src/classes/MyHandler/MyHandler.ts b/src/classes/MyHandler/MyHandler.ts index 124282721..57d32f214 100644 --- a/src/classes/MyHandler/MyHandler.ts +++ b/src/classes/MyHandler/MyHandler.ts @@ -37,8 +37,6 @@ import Inventory, { Dict } from '../Inventory'; import TF2Inventory from '../TF2Inventory'; import Autokeys from '../Autokeys/Autokeys'; -import { statsCommand } from '../Commands/functions/status'; - import { Paths } from '../../resources/paths'; import log from '../../lib/logger'; import * as files from '../../lib/files'; @@ -50,9 +48,10 @@ import { summarize, uptime, getHighValueItems } from '../../lib/tools/export'; import genPaths from '../../resources/paths'; import Pricer, { RequestCheckFn } from '../Pricer'; +import Options from '../Options'; export default class MyHandler extends Handler { - private readonly commands: Commands; + readonly commands: Commands; readonly autokeys: Autokeys; @@ -62,9 +61,13 @@ export default class MyHandler extends Handler { private requestCheck: RequestCheckFn; + private get opt(): Options { + return this.bot.options; + } + private get groups(): string[] { if (!this.groupsStore) { - const groups = this.bot.options.groups; + const groups = this.opt.groups; if (groups !== null && Array.isArray(groups)) { groups.forEach(groupID64 => { @@ -85,9 +88,7 @@ export default class MyHandler extends Handler { get friendsToKeep(): string[] { if (!this.friendsToKeepStore) { - const friendsToKeep = this.bot.options.keep.concat( - this.bot.getAdmins.map(steamID => steamID.getSteamID64()) - ); + const friendsToKeep = this.opt.keep.concat(this.bot.getAdmins.map(steamID => steamID.getSteamID64())); if (friendsToKeep !== null && Array.isArray(friendsToKeep)) { friendsToKeep.forEach(steamID64 => { @@ -105,56 +106,54 @@ export default class MyHandler extends Handler { } private get minimumScrap(): number { - return this.bot.options.crafting.metals.minScrap; + return this.opt.crafting.metals.minScrap; } private get minimumReclaimed(): number { - return this.bot.options.crafting.metals.minRec; + return this.opt.crafting.metals.minRec; } private get combineThreshold(): number { - return this.bot.options.crafting.metals.threshold; + return this.opt.crafting.metals.threshold; } get dupeCheckEnabled(): boolean { - return this.bot.options.offerReceived.duped.enableCheck; + return this.opt.offerReceived.duped.enableCheck; } get minimumKeysDupeCheck(): number { - return this.bot.options.offerReceived.duped.minKeys; + return this.opt.offerReceived.duped.minKeys; } private get isPriceUpdateWebhook(): boolean { - return ( - this.bot.options.discordWebhook.priceUpdate.enable && this.bot.options.discordWebhook.priceUpdate.url !== '' - ); + return this.opt.discordWebhook.priceUpdate.enable && this.opt.discordWebhook.priceUpdate.url !== ''; } get isWeaponsAsCurrency(): { enable: boolean; withUncraft: boolean } { return { - enable: this.bot.options.miscSettings.weaponsAsCurrency.enable, - withUncraft: this.bot.options.miscSettings.weaponsAsCurrency.withUncraft + enable: this.opt.miscSettings.weaponsAsCurrency.enable, + withUncraft: this.opt.miscSettings.weaponsAsCurrency.withUncraft }; } private get isAutoRelistEnabled(): boolean { - return this.bot.options.miscSettings.autobump.enable; + return this.opt.miscSettings.autobump.enable; } private get invalidValueException(): number { - return Currencies.toScrap(this.bot.options.offerReceived.invalidValue.exceptionValue.valueInRef); + return Currencies.toScrap(this.opt.offerReceived.invalidValue.exceptionValue.valueInRef); } private hasInvalidValueException = false; private get sendStatsEnabled(): boolean { - return this.bot.options.statistics.sendStats.enable; + return this.opt.statistics.sendStats.enable; } private isTradingKeys = false; get customGameName(): string { - const customGameName = this.bot.options.miscSettings.game.customName; + const customGameName = this.opt.miscSettings.game.customName; if (customGameName === '' || customGameName === 'TF2Autobot') { return `TF2Autobot v${process.env.BOT_VERSION}`; @@ -189,7 +188,7 @@ export default class MyHandler extends Handler { private poller: NodeJS.Timeout; - private refreshInterval: NodeJS.Timeout; + private refreshTimeout: NodeJS.Timeout; private sendStatsInterval: NodeJS.Timeout; @@ -197,6 +196,18 @@ export default class MyHandler extends Handler { private autoRefreshListingsInterval: NodeJS.Timeout; + private alreadyExecutedRefreshlist = false; + + set isRecentlyExecuteRefreshlistCommand(setExecuted: boolean) { + this.alreadyExecutedRefreshlist = setExecuted; + } + + private executedDelayTime = 30 * 60 * 1000; + + set setRefreshlistExecutedDelay(delay: number) { + this.executedDelayTime = delay; + } + constructor(bot: Bot, private priceSource: Pricer) { super(bot); @@ -204,7 +215,7 @@ export default class MyHandler extends Handler { this.cartQueue = new CartQueue(bot); this.autokeys = new Autokeys(bot); - this.paths = genPaths(this.bot.options.steamAccountName); + this.paths = genPaths(this.opt.steamAccountName); this.requestCheck = this.priceSource.requestCheck.bind(this.priceSource); } @@ -234,7 +245,7 @@ export default class MyHandler extends Handler { )} | Startup time: ${process.uptime().toFixed(0)} s` ); - this.bot.client.gamesPlayed(this.bot.options.miscSettings.game.playOnlyTF2 ? 440 : [this.customGameName, 440]); + this.bot.client.gamesPlayed(this.opt.miscSettings.game.playOnlyTF2 ? 440 : [this.customGameName, 440]); this.bot.client.setPersona(EPersonaState.Online); this.botSteamID = this.bot.client.steamID; @@ -273,7 +284,7 @@ export default class MyHandler extends Handler { this.sendStats(); // Check for missing listings every 30 minutes, initiate setInterval 5 minutes after start - this.refreshInterval = setTimeout(() => { + this.refreshTimeout = setTimeout(() => { this.enableAutoRefreshListings(); }, 5 * 60 * 1000); } @@ -283,8 +294,8 @@ export default class MyHandler extends Handler { clearInterval(this.poller); } - if (this.refreshInterval) { - clearInterval(this.refreshInterval); + if (this.refreshTimeout) { + clearInterval(this.refreshTimeout); } if (this.sendStatsInterval) { @@ -306,7 +317,7 @@ export default class MyHandler extends Handler { this.bot.listings.disableAutorelistOption(); return new Promise(resolve => { - if (this.bot.options.autokeys.enable && this.autokeys.getActiveStatus) { + if (this.opt.autokeys.enable && this.autokeys.getActiveStatus) { log.debug('Disabling Autokeys and disabling key entry in the pricelist...'); this.autokeys.disable(this.bot.pricelist.getKeyPrices); } @@ -329,16 +340,14 @@ export default class MyHandler extends Handler { onLoggedOn(): void { if (this.bot.isReady) { this.bot.client.setPersona(EPersonaState.Online); - this.bot.client.gamesPlayed( - this.bot.options.miscSettings.game.playOnlyTF2 ? 440 : [this.customGameName, 440] - ); + this.bot.client.gamesPlayed(this.opt.miscSettings.game.playOnlyTF2 ? 440 : [this.customGameName, 440]); } } onMessage(steamID: SteamID, message: string): void { - if (!this.bot.options.commands.enable) { + if (!this.opt.commands.enable) { if (!this.bot.isAdmin(steamID)) { - const custom = this.bot.options.commands.customDisableReply; + const custom = this.opt.commands.customDisableReply; return this.bot.sendMessage(steamID, custom ? custom : 'āŒ Command function is disabled by the owner.'); } } @@ -424,92 +433,139 @@ export default class MyHandler extends Handler { return; } - this.autoRefreshListingsInterval = setInterval(() => { - log.debug('Running automatic check for missing listings...'); + let pricelistLength = 0; - const listingsSKUs: string[] = []; - this.bot.listingManager.getListings(async err => { - if (err) { + this.autoRefreshListingsInterval = setInterval( + () => { + if (this.alreadyExecutedRefreshlist) { + log.debug( + 'āŒ Just recently executed refreshlist command, will not run automatic check for missing listings.' + ); setTimeout(() => { this.enableAutoRefreshListings(); - }, 30 * 60 * 1000); + }, this.executedDelayTime); + + // reset to default + this.setRefreshlistExecutedDelay = 30 * 60 * 1000; clearInterval(this.autoRefreshListingsInterval); return; } - this.bot.listingManager.listings.forEach(listing => { - let listingSKU = listing.getSKU(); - if (listing.intent === 1) { - if (this.bot.options.normalize.painted.our && /;[p][0-9]+/.test(listingSKU)) { - listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); - } + pricelistLength = 0; + log.debug('Running automatic check for missing listings...'); - if (this.bot.options.normalize.festivized.our && listingSKU.includes(';festive')) { - listingSKU = listingSKU.replace(';festive', ''); - } + const listingsSKUs: string[] = []; + this.bot.listingManager.getListings(async err => { + if (err) { + setTimeout(() => { + this.enableAutoRefreshListings(); + }, 30 * 60 * 1000); + clearInterval(this.autoRefreshListingsInterval); + return; + } - if (this.bot.options.normalize.strangeAsSecondQuality.our && listingSKU.includes(';strange')) { - listingSKU = listingSKU.replace(';strange', ''); - } - } else { - if (/;[p][0-9]+/.test(listingSKU)) { - listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); + const inventory = this.bot.inventoryManager; + const isFilterCantAfford = this.opt.pricelist.filterCantAfford.enable; + + this.bot.listingManager.listings.forEach(listing => { + let listingSKU = listing.getSKU(); + if (listing.intent === 1) { + if (this.opt.normalize.painted.our && /;[p][0-9]+/.test(listingSKU)) { + listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); + } + + if (this.opt.normalize.festivized.our && listingSKU.includes(';festive')) { + listingSKU = listingSKU.replace(';festive', ''); + } + + if (this.opt.normalize.strangeAsSecondQuality.our && listingSKU.includes(';strange')) { + listingSKU = listingSKU.replace(';strange', ''); + } + } else { + if (/;[p][0-9]+/.test(listingSKU)) { + listingSKU = listingSKU.replace(/;[p][0-9]+/, ''); + } } - } - listingsSKUs.push(listingSKU); - }); + const match = this.bot.pricelist.getPrice(listingSKU); - // Remove duplicate elements - const newlistingsSKUs: string[] = []; - listingsSKUs.forEach(sku => { - if (!newlistingsSKUs.includes(sku)) { - newlistingsSKUs.push(sku); - } - }); + if (isFilterCantAfford && listing.intent === 0 && match !== null) { + const canAffordToBuy = inventory.isCanAffordToBuy(match.buy, inventory.getInventory); + if (!canAffordToBuy) { + // Listing for buying exist but we can't afford to buy, remove. + log.debug(`Intent buy, removed because can't afford: ${match.sku}`); + listing.remove(); + } + } - const inventory = this.bot.inventoryManager; - const pricelist = this.bot.pricelist.getPrices.filter(entry => { - // First find out if lising for this item from bptf already exist. - const isExist = newlistingsSKUs.find(sku => entry.sku === sku); + listingsSKUs.push(listingSKU); + }); - if (!isExist) { - // undefined - listing does not exist but item is in the pricelist + // Remove duplicate elements + const newlistingsSKUs: string[] = []; + listingsSKUs.forEach(sku => { + if (!newlistingsSKUs.includes(sku)) { + newlistingsSKUs.push(sku); + } + }); - // Get amountCanBuy and amountCanSell (already cover intent and so on) - const amountCanBuy = inventory.amountCanTrade(entry.sku, true); - const amountCanSell = inventory.amountCanTrade(entry.sku, false); + const pricelist = this.bot.pricelist.getPrices.filter(entry => { + // First find out if lising for this item from bptf already exist. + const isExist = newlistingsSKUs.find(sku => entry.sku === sku); + + if (!isExist) { + // undefined - listing does not exist but item is in the pricelist + + // Get amountCanBuy and amountCanSell (already cover intent and so on) + const amountCanBuy = inventory.amountCanTrade(entry.sku, true); + const amountCanSell = inventory.amountCanTrade(entry.sku, false); + + if ( + (amountCanBuy > 0 && inventory.isCanAffordToBuy(entry.buy, inventory.getInventory)) || + amountCanSell > 0 + ) { + // if can amountCanBuy is more than 0 and isCanAffordToBuy is true OR amountCanSell is more than 0 + // return this entry + log.debug( + `Missing${isFilterCantAfford ? '/Re-adding can afford' : ' listings'}: ${entry.sku}` + ); + return true; + } - if ( - (amountCanBuy > 0 && inventory.isCanAffordToBuy(entry.buy, inventory.getInventory)) || - amountCanSell > 0 - ) { - // if can amountCanBuy is more than 0 and isCanAffordToBuy is true OR amountCanSell is more than 0 - // return this entry - return true; + // Else ignore + return false; } - // Else ignore + // Else if listing already exist on backpack.tf, ignore return false; + }); + + const pricelistCount = pricelist.length; + + if (pricelistCount > 0) { + log.debug( + 'Checking listings for ' + + pluralize('item', pricelistCount, true) + + ` [${pricelist.map(entry => entry.sku).join(', ')}]...` + ); + + await this.bot.listings.recursiveCheckPricelist( + pricelist, + true, + pricelistCount > 1000 ? 1000 : 200 + ); + + log.debug('āœ… Done checking ' + pluralize('item', pricelistCount, true)); + } else { + log.debug('āŒ Nothing to refresh.'); } - // Else if listing already exist on backpack.tf, ignore - return false; + pricelistLength = pricelistCount; }); - - if (pricelist.length > 0) { - log.debug( - 'Checking listings for ' + - pluralize('item', pricelist.length, true) + - ` [${pricelist.map(entry => entry.sku).join(', ')}] ...` - ); - await this.bot.listings.recursiveCheckPricelist(pricelist, true); - log.debug('āœ… Done checking ' + pluralize('item', pricelist.length, true)); - } else { - log.debug('āŒ Nothing to refresh.'); - } - }); - }, 30 * 60 * 1000); + }, + // set check every 60 minutes if pricelist to check was more than 1000 items + pricelistLength > 1000 ? 60 * 60 * 1000 : 30 * 60 * 1000 + ); } disableAutoRefreshListings(): void { @@ -543,7 +599,7 @@ export default class MyHandler extends Handler { sendStats(this.bot); } else { this.bot.getAdmins.forEach(admin => { - statsCommand(admin, this.bot); + this.commands.useStatsCommand(admin); }); } } @@ -564,7 +620,7 @@ export default class MyHandler extends Handler { // If crafting class weapons still waiting, cancel it. clearTimeout(this.classWeaponsTimeout); - const opt = this.bot.options; + const opt = this.opt; const items = { our: Inventory.fromItems( @@ -669,6 +725,8 @@ export default class MyHandler extends Handler { } } }); + } else if (item.isFullUses !== undefined) { + getHighValue[which].items[sku] = { isFull: item.isFullUses }; } }); } @@ -710,22 +768,18 @@ export default class MyHandler extends Handler { )}` ); - if (isContainsHighValue) { - return { - action: 'accept', - reason: 'ADMIN', - meta: { highValue: highValueMeta(input) } - }; - } else { - return { - action: 'accept', - reason: 'ADMIN' - }; - } + return { + action: 'accept', + reason: 'ADMIN', + meta: isContainsHighValue ? { highValue: highValueMeta(input) } : undefined + }; } + const itemsToGiveCount = offer.itemsToGive.length; + const itemsToReceiveCount = offer.itemsToReceive.length; + // check if the trade is valid - const isCannotProceedProcessingOffer = offer.itemsToGive.length === 0 && offer.itemsToReceive.length === 0; + const isCannotProceedProcessingOffer = itemsToGiveCount === 0 && itemsToReceiveCount === 0; if (isCannotProceedProcessingOffer) { log.warn('isCannotProceedProcessingOffer', { @@ -740,9 +794,9 @@ export default class MyHandler extends Handler { ` My owner has been informed, and they might manually act on your offer later.` ); - const optDw = this.bot.options.discordWebhook; + const optDw = opt.discordWebhook; - if (this.bot.options.sendAlert.unableToProcessOffer) { + if (opt.sendAlert.unableToProcessOffer) { if (optDw.sendAlert.enable && optDw.sendAlert.url !== '') { sendAlert('failed-processing-offer', this.bot, null, null, null, [ offer.partner.getSteamID64(), @@ -795,7 +849,7 @@ export default class MyHandler extends Handler { 'cute' // right? ].some(word => offerMessage.includes(word)); - if (offer.itemsToGive.length === 0 && isGift) { + if (itemsToGiveCount === 0 && isGift) { offer.log( 'trade', `is a gift offer, accepting. Summary:\n${JSON.stringify( @@ -804,42 +858,28 @@ export default class MyHandler extends Handler { 4 )}` ); - if (isContainsHighValue) { - return { - action: 'accept', - reason: 'GIFT', - meta: { highValue: highValueMeta(input) } - }; - } else { - return { - action: 'accept', - reason: 'GIFT' - }; - } - } else if (offer.itemsToGive.length === 0 && offer.itemsToReceive.length > 0 && !isGift) { + return { + action: 'accept', + reason: 'GIFT', + meta: isContainsHighValue ? { highValue: highValueMeta(input) } : undefined + }; + } else if (itemsToGiveCount === 0 && itemsToReceiveCount > 0 && !isGift) { if (opt.bypass.giftWithoutMessage.allow) { offer.log( 'info', 'is a gift offer without any offer message, but allowed to be accepted, accepting...' ); - if (isContainsHighValue) { - return { - action: 'accept', - reason: 'GIFT', - meta: { highValue: highValueMeta(input) } - }; - } else { - return { - action: 'accept', - reason: 'GIFT' - }; - } + return { + action: 'accept', + reason: 'GIFT', + meta: isContainsHighValue ? { highValue: highValueMeta(input) } : undefined + }; } else { offer.log('info', 'is a gift offer without any offer message, declining...'); return { action: 'decline', reason: 'GIFT_NO_NOTE' }; } - } else if (offer.itemsToGive.length > 0 && offer.itemsToReceive.length === 0) { + } else if (itemsToGiveCount > 0 && itemsToReceiveCount === 0) { offer.log('info', 'is taking our items for free, declining...'); return { action: 'decline', reason: 'CRIME_ATTEMPT' }; } @@ -900,14 +940,16 @@ export default class MyHandler extends Handler { } } + const ourItemsHVCount = Object.keys(getHighValue.our.items).length; + const isInPricelist = - Object.keys(getHighValue.our.items).length > 0 // Only check if this not empty + ourItemsHVCount > 0 // Only check if this not empty ? Object.keys(getHighValue.our.items).some(sku => { return checkExist.getPrice(sku, false) !== null; // Return true if exist in pricelist, enabled or not. }) : null; - if (Object.keys(getHighValue.our.items).length > 0 && isInPricelist === false) { + if (ourItemsHVCount > 0 && isInPricelist === false) { // Decline trade that offer overpay on high valued (spelled) items that are not in our pricelist. offer.log('info', 'contains higher value item on our side that is not in our pricelist.'); @@ -1377,8 +1419,10 @@ export default class MyHandler extends Handler { return { action: 'decline', reason: 'OVERPAY' }; } - if (this.dupeCheckEnabled && assetidsToCheck.length > 0) { - offer.log('info', 'checking ' + pluralize('item', assetidsToCheck.length, true) + ' for dupes...'); + const assetidsToCheckCount = assetidsToCheck.length; + + if (this.dupeCheckEnabled && assetidsToCheckCount > 0) { + offer.log('info', 'checking ' + pluralize('item', assetidsToCheckCount, true) + ' for dupes...'); const inventory = new TF2Inventory(offer.partner, this.bot.manager); const requests = assetidsToCheck.map(assetid => { @@ -1398,7 +1442,9 @@ export default class MyHandler extends Handler { log.debug('Got result from dupe checks on ' + assetidsToCheck.join(', '), { result: result }); const declineDupes = opt.offerReceived.duped.autoDecline.enable; - for (let i = 0; i < result.length; i++) { + const resultCount = result.length; + + for (let i = 0; i < resultCount; i++) { if (result[i] === true) { // Found duped item if (declineDupes) { @@ -1463,7 +1509,8 @@ export default class MyHandler extends Handler { reason: 'ā¬œ_ESCROW_CHECK_FAILED', meta: { uniqueReasons: filterReasons(uniqueReasons), - reasons: wrongAboutOffer + reasons: wrongAboutOffer, + highValue: isContainsHighValue ? highValueMeta(input) : undefined } }; } else { @@ -1505,7 +1552,8 @@ export default class MyHandler extends Handler { reason: 'ā¬œ_BANNED_CHECK_FAILED', meta: { uniqueReasons: filterReasons(uniqueReasons), - reasons: wrongAboutOffer + reasons: wrongAboutOffer, + highValue: isContainsHighValue ? highValueMeta(input) : undefined } }; } else { @@ -1599,7 +1647,7 @@ export default class MyHandler extends Handler { )}` ); - if (offer.itemsToGive.length + offer.itemsToReceive.length > 50) { + if (itemsToGiveCount + itemsToReceiveCount > 50) { this.bot.sendMessage( offer.partner, opt.customMessage.accepted.automatic.largeOffer @@ -1619,26 +1667,15 @@ export default class MyHandler extends Handler { ); } - if (isContainsHighValue) { - return { - action: 'accept', - reason: 'VALID_WITH_OVERPAY', - meta: { - uniqueReasons: uniqueReasons, - reasons: wrongAboutOffer, - highValue: highValueMeta(input) - } - }; - } else { - return { - action: 'accept', - reason: 'VALID_WITH_OVERPAY', - meta: { - uniqueReasons: uniqueReasons, - reasons: wrongAboutOffer - } - }; - } + return { + action: 'accept', + reason: 'VALID_WITH_OVERPAY', + meta: { + uniqueReasons: uniqueReasons, + reasons: wrongAboutOffer, + highValue: isContainsHighValue ? highValueMeta(input) : undefined + } + }; } else if ( opt.offerReceived.invalidValue.autoDecline.enable && isInvalidValue && @@ -1705,7 +1742,7 @@ export default class MyHandler extends Handler { meta: { uniqueReasons: uniqueReasons, reasons: wrongAboutOffer, - highValue: highValueMeta(input) + highValue: isContainsHighValue ? highValueMeta(input) : undefined } }; } @@ -1716,7 +1753,7 @@ export default class MyHandler extends Handler { `accepting. Summary:\n${JSON.stringify(summarize(offer, this.bot, 'summary-accepting', false), null, 4)}` ); - if (offer.itemsToGive.length + offer.itemsToReceive.length > 50) { + if (itemsToGiveCount + itemsToReceiveCount > 50) { this.bot.sendMessage( offer.partner, opt.customMessage.accepted.automatic.largeOffer @@ -1736,18 +1773,11 @@ export default class MyHandler extends Handler { ); } - if (isContainsHighValue) { - return { - action: 'accept', - reason: 'VALID', - meta: { highValue: highValueMeta(input) } - }; - } else { - return { - action: 'accept', - reason: 'VALID' - }; - } + return { + action: 'accept', + reason: 'VALID', + meta: isContainsHighValue ? { highValue: highValueMeta(input) } : undefined + }; } onTradeOfferChanged(offer: TradeOffer, oldState: number, processTime?: number): void { @@ -1781,10 +1811,13 @@ export default class MyHandler extends Handler { } else if (offer.state === TradeOfferManager.ETradeOfferState['Declined']) { declined(offer, this.bot, this.isTradingKeys); this.isTradingKeys = false; // reset + this.removePolldataKeys(offer); } else if (offer.state === TradeOfferManager.ETradeOfferState['Canceled']) { cancelled(offer, oldState, this.bot); + this.removePolldataKeys(offer); } else if (offer.state === TradeOfferManager.ETradeOfferState['InvalidItems']) { invalid(offer, this.bot); + this.removePolldataKeys(offer); } } @@ -1832,9 +1865,17 @@ export default class MyHandler extends Handler { // Invite to group this.inviteToGroups(offer.partner); + + // delete notify and meta keys from polldata after each successful trades + this.removePolldataKeys(offer); } } + private removePolldataKeys(offer: TradeOffer): void { + offer.data('notify', undefined); + offer.data('meta', undefined); + } + onOfferAction(offer: TradeOffer, action: 'accept' | 'decline' | 'skip', reason: string, meta: Meta): void { if (offer.data('notify') !== true) { return; @@ -1846,14 +1887,14 @@ export default class MyHandler extends Handler { } private sortInventory(): void { - if (this.bot.options.miscSettings.sortInventory.enable) { - const type = this.bot.options.miscSettings.sortInventory.type; + if (this.opt.miscSettings.sortInventory.enable) { + const type = this.opt.miscSettings.sortInventory.type; this.bot.tf2gc.sortInventory([1, 2, 3, 4, 5, 101, 102].includes(type) ? type : 3); } } private inviteToGroups(steamID: SteamID | string): void { - if (!this.bot.options.miscSettings.sendGroupInvite.enable) { + if (!this.opt.miscSettings.sendGroupInvite.enable) { return; } @@ -1890,7 +1931,7 @@ export default class MyHandler extends Handler { } private respondToFriendRequest(steamID: SteamID | string): void { - if (!this.bot.options.miscSettings.addFriends.enable) { + if (!this.opt.miscSettings.addFriends.enable) { if (!this.bot.isAdmin(steamID)) { return this.bot.client.removeFriend(steamID); } @@ -1927,8 +1968,8 @@ export default class MyHandler extends Handler { return this.bot.sendMessage( steamID, - this.bot.options.customMessage.welcome - ? this.bot.options.customMessage.welcome + this.opt.customMessage.welcome + ? this.opt.customMessage.welcome .replace(/%name%/g, '') .replace(/%admin%/g, isAdmin ? '!help' : '!how2trade') + ` - TF2Autobot v${process.env.BOT_VERSION}` @@ -1950,8 +1991,8 @@ export default class MyHandler extends Handler { this.bot.sendMessage( steamID, - this.bot.options.customMessage.welcome - ? this.bot.options.customMessage.welcome + this.opt.customMessage.welcome + ? this.opt.customMessage.welcome .replace(/%name%/g, friend.player_name) .replace(/%admin%/g, isAdmin ? '!help' : '!how2trade') + ` - TF2Autobot v${process.env.BOT_VERSION}` @@ -1998,8 +2039,8 @@ export default class MyHandler extends Handler { friendsToRemove.forEach(element => { this.bot.sendMessage( element.steamID, - this.bot.options.customMessage.clearFriends - ? this.bot.options.customMessage.clearFriends.replace( + this.opt.customMessage.clearFriends + ? this.opt.customMessage.clearFriends.replace( /%name%/g, this.bot.friends.getFriend(element.steamID).player_name ) @@ -2019,7 +2060,7 @@ export default class MyHandler extends Handler { url: 'https://backpack.tf/api/users/info/v1', method: 'GET', qs: { - key: this.bot.options.bptfAPIKey, + key: this.opt.bptfAPIKey, steamids: steamID64 }, gzip: true, @@ -2120,7 +2161,7 @@ export default class MyHandler extends Handler { onTF2QueueCompleted(): void { log.debug('Queue finished'); - this.bot.client.gamesPlayed(this.bot.options.miscSettings.game.playOnlyTF2 ? 440 : [this.customGameName, 440]); + this.bot.client.gamesPlayed(this.opt.miscSettings.game.playOnlyTF2 ? 440 : [this.customGameName, 440]); } } diff --git a/src/classes/MyHandler/offer/accepted/processAccepted.ts b/src/classes/MyHandler/offer/accepted/processAccepted.ts index 33937b977..797874bec 100644 --- a/src/classes/MyHandler/offer/accepted/processAccepted.ts +++ b/src/classes/MyHandler/offer/accepted/processAccepted.ts @@ -25,6 +25,7 @@ export default function processAccepted( }; const offerReceived = offer.data('action') as i.Action; + const meta = offer.data('meta') as i.Meta; const offerSent = offer.data('highValue') as i.HighValueOutput; const isWebhookEnabled = opt.discordWebhook.tradeSummary.enable && opt.discordWebhook.tradeSummary.url.length > 0; @@ -33,27 +34,23 @@ export default function processAccepted( // doing this because if an offer is being made by bot (from command), then this is undefined if (['VALID_WITH_OVERPAY', 'MANUAL', 'MANUAL-FORCE', 'AUTO-RETRY'].includes(offerReceived.reason)) { // only for accepted overpay with INVALID_ITEMS/OVERSTOCKED/UNDERSTOCKED or MANUAL offer - if (offerReceived.meta?.uniqueReasons?.includes('šŸŸØ_INVALID_ITEMS')) { + if (meta?.uniqueReasons?.includes('šŸŸØ_INVALID_ITEMS')) { // doing this so it will only executed if includes šŸŸØ_INVALID_ITEMS reason. - (offerReceived.meta.reasons.filter(el => el.reason === 'šŸŸØ_INVALID_ITEMS') as i.InvalidItems[]).forEach( - el => { - accepted.invalidItems.push( - `${ - isWebhookEnabled - ? `_${bot.schema.getName(SKU.fromString(el.sku), false)}_` - : bot.schema.getName(SKU.fromString(el.sku), false) - } - ${el.price}` - ); - } - ); + (meta.reasons.filter(el => el.reason === 'šŸŸØ_INVALID_ITEMS') as i.InvalidItems[]).forEach(el => { + accepted.invalidItems.push( + `${ + isWebhookEnabled + ? `_${bot.schema.getName(SKU.fromString(el.sku), false)}_` + : bot.schema.getName(SKU.fromString(el.sku), false) + } - ${el.price}` + ); + }); } - if (offerReceived.meta?.uniqueReasons?.includes('šŸŸ§_DISABLED_ITEMS')) { + if (meta?.uniqueReasons?.includes('šŸŸ§_DISABLED_ITEMS')) { // doing this so it will only executed if includes šŸŸ§_DISABLED_ITEMS reason. - (offerReceived.meta.reasons.filter( - el => el.reason === 'šŸŸ§_DISABLED_ITEMS' - ) as i.DisabledItems[]).forEach(el => { + (meta.reasons.filter(el => el.reason === 'šŸŸ§_DISABLED_ITEMS') as i.DisabledItems[]).forEach(el => { accepted.disabledItems.push( isWebhookEnabled ? `_${bot.schema.getName(SKU.fromString(el.sku), false)}_` @@ -61,12 +58,10 @@ export default function processAccepted( ); }); } - if (offerReceived.meta?.uniqueReasons?.includes('šŸŸ¦_OVERSTOCKED')) { + if (meta?.uniqueReasons?.includes('šŸŸ¦_OVERSTOCKED')) { // doing this so it will only executed if includes šŸŸ¦_OVERSTOCKED reason. - (offerReceived.meta.reasons.filter(el => - el.reason.includes('šŸŸ¦_OVERSTOCKED') - ) as i.Overstocked[]).forEach(el => { + (meta.reasons.filter(el => el.reason.includes('šŸŸ¦_OVERSTOCKED')) as i.Overstocked[]).forEach(el => { accepted.overstocked.push( `${ isWebhookEnabled @@ -77,12 +72,10 @@ export default function processAccepted( }); } - if (offerReceived.meta?.uniqueReasons?.includes('šŸŸ©_UNDERSTOCKED')) { + if (meta?.uniqueReasons?.includes('šŸŸ©_UNDERSTOCKED')) { // doing this so it will only executed if includes šŸŸ©_UNDERSTOCKED reason. - (offerReceived.meta.reasons.filter(el => - el.reason.includes('šŸŸ©_UNDERSTOCKED') - ) as i.Understocked[]).forEach(el => { + (meta.reasons.filter(el => el.reason.includes('šŸŸ©_UNDERSTOCKED')) as i.Understocked[]).forEach(el => { accepted.understocked.push( `${ isWebhookEnabled @@ -94,15 +87,10 @@ export default function processAccepted( } } - if (offerReceived.meta?.highValue && offerReceived.meta?.highValue['has'] === undefined) { - if (Object.keys(offerReceived.meta.highValue.items.their).length > 0) { + if (meta?.highValue && meta.highValue['has'] === undefined) { + if (Object.keys(meta.highValue.items.their).length > 0) { // doing this to check if their side have any high value items, if so, push each name into accepted.highValue const. - const itemsName = t.getHighValueItems( - offerReceived.meta.highValue.items.their, - bot, - bot.paints, - bot.strangeParts - ); + const itemsName = t.getHighValueItems(meta.highValue.items.their, bot, bot.paints, bot.strangeParts); for (const name in itemsName) { if (!Object.prototype.hasOwnProperty.call(itemsName, name)) { @@ -113,8 +101,8 @@ export default function processAccepted( theirHighValuedItems.push(`${isWebhookEnabled ? `_${name}_` : name}` + itemsName[name]); } - if (offerReceived.meta.highValue.isMention.their) { - Object.keys(offerReceived.meta.highValue.items.their).forEach(sku => isDisableSKU.push(sku)); + if (meta.highValue.isMention.their) { + Object.keys(meta.highValue.items.their).forEach(sku => isDisableSKU.push(sku)); if (!bot.isAdmin(offer.partner)) { accepted.isMention = true; @@ -122,14 +110,9 @@ export default function processAccepted( } } - if (Object.keys(offerReceived.meta.highValue.items.our).length > 0) { + if (Object.keys(meta.highValue.items.our).length > 0) { // doing this to check if our side have any high value items, if so, push each name into accepted.highValue const. - const itemsName = t.getHighValueItems( - offerReceived.meta.highValue.items.our, - bot, - bot.paints, - bot.strangeParts - ); + const itemsName = t.getHighValueItems(meta.highValue.items.our, bot, bot.paints, bot.strangeParts); for (const name in itemsName) { if (!Object.prototype.hasOwnProperty.call(itemsName, name)) { @@ -139,7 +122,7 @@ export default function processAccepted( accepted.highValue.push(`${isWebhookEnabled ? `_${name}_` : name}` + itemsName[name]); } - if (offerReceived.meta.highValue.isMention.our) { + if (meta.highValue.isMention.our) { if (!bot.isAdmin(offer.partner)) { accepted.isMention = true; } @@ -251,7 +234,7 @@ export default function processAccepted( return { theirHighValuedItems, isDisableSKU, - items: offerReceived?.meta?.highValue?.items?.their || offerSent?.items?.their + items: meta?.highValue?.items?.their || offerSent?.items?.their }; } diff --git a/src/classes/MyHandler/offer/accepted/updateListings.ts b/src/classes/MyHandler/offer/accepted/updateListings.ts index 1ecfabbc3..3b49f8c01 100644 --- a/src/classes/MyHandler/offer/accepted/updateListings.ts +++ b/src/classes/MyHandler/offer/accepted/updateListings.ts @@ -187,7 +187,9 @@ export default function updateListings( let msg = 'I have received a high-valued items which is not in my pricelist.' + '\n\nItem information:\n\n- '; - for (let i = 0; i < highValue.theirItems.length; i++) { + const theirCount = highValue.theirItems.length; + + for (let i = 0; i < theirCount; i++) { if (highValue.theirItems[i].includes(name)) { msg += `${highValue.isDisableSKU[i]}: ` + highValue.theirItems[i]; } @@ -265,7 +267,9 @@ export default function updateListings( ` or just re-enable it with "!update sku=${sku}&enabled=true".` + '\n\nItem information:\n\n- '; - for (let i = 0; i < highValue.theirItems.length; i++) { + const theirCount = highValue.theirItems.length; + + for (let i = 0; i < theirCount; i++) { if (highValue.theirItems[i].includes(name)) msg += highValue.theirItems[i]; } diff --git a/src/classes/MyHandler/offer/notify/declined.ts b/src/classes/MyHandler/offer/notify/declined.ts index 9837f89c1..0934a2bed 100644 --- a/src/classes/MyHandler/offer/notify/declined.ts +++ b/src/classes/MyHandler/offer/notify/declined.ts @@ -1,4 +1,4 @@ -import { Action, TradeOffer } from 'steam-tradeoffer-manager'; +import { Action, Meta, TradeOffer } from 'steam-tradeoffer-manager'; import { valueDiff, summarizeToChat } from '../../../../lib/tools/export'; import Bot from '../../../Bot'; @@ -6,108 +6,99 @@ export default function declined(offer: TradeOffer, bot: Bot, isTradingKeys: boo const opt = bot.options; const offerReason = offer.data('action') as Action; + const meta = offer.data('meta') as Meta; const keyPrices = bot.pricelist.getKeyPrices; const value = valueDiff(offer, keyPrices, isTradingKeys, opt.miscSettings.showOnlyMetal.enable); const manualReviewDisabled = !opt.manualReview.enable; offer.data('isDeclined', true); + const declined = '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined'; + let reasonForInvalidValue = false; let reply: string; if (!offerReason) { const custom = opt.customMessage.decline.general; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined.'; + reply = custom ? custom : declined + '.'; } else if (offerReason.reason === 'GIFT_NO_NOTE') { // const custom = opt.customMessage.decline.giftNoNote; reply = custom ? custom - : `/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because` + - `the offer you've sent is an empty offer on my side without any offer message. ` + + : declined + + ` because the offer you've sent is an empty offer on my side without any offer message. ` + `If you wish to give it as a gift, please include "gift" in the offer message. Thank you.`; // } else if (offerReason.reason === 'CRIME_ATTEMPT') { // const custom = opt.customMessage.decline.crimeAttempt; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "you're taking free items. No."; + reply = custom ? custom : declined + " because you're taking free items. No."; // } else if (offerReason.reason === 'ONLY_METAL') { // const custom = opt.customMessage.decline.onlyMetal; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - 'you might forgot to add items into the trade.'; + reply = custom ? custom : declined + ' because you might forgot to add items into the trade.'; // } else if (offerReason.reason === 'DUELING_NOT_5_USES') { // const custom = opt.customMessage.decline.duelingNot5Uses; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - 'your offer contains Dueling Mini-Game(s) that does not have 5 uses.'; + : declined + ' because your offer contains Dueling Mini-Game(s) that does not have 5 uses.'; // } else if (offerReason.reason === 'NOISE_MAKER_NOT_25_USES') { // const custom = opt.customMessage.decline.noiseMakerNot25Uses; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - 'your offer contains Noise Maker(s) that does not have 25 uses.'; + reply = custom ? custom : declined + ' because your offer contains Noise Maker(s) that does not have 25 uses.'; // } else if (offerReason.reason === 'HIGH_VALUE_ITEMS_NOT_SELLING') { // const custom = opt.customMessage.decline.highValueItemsNotSelling; - const highValueName = offerReason.meta.highValueName.join(', '); + const highValueName = meta.highValueName.join(', '); reply = custom ? custom.replace(/%highValueName%/g, highValueName) - : `/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ` + - `you're attempting to purchase ${highValueName}, but I am not selling it right now.`; + : declined + ` because you're attempting to purchase ${highValueName}, but I am not selling it right now.`; // } else if (offerReason.reason === 'NOT_TRADING_KEYS') { // const custom = opt.customMessage.decline.notTradingKeys; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - 'I am no longer trading keys. You can confirm this by typing "!price Mann Co. Supply Crate Key" or "!autokeys".'; + : declined + + ' because I am no longer trading keys. You can confirm this by typing "!price Mann Co. Supply Crate Key" or "!autokeys".'; // } else if (offerReason.reason === 'NOT_SELLING_KEYS') { // const custom = opt.customMessage.decline.notSellingKeys; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - 'I am no longer selling keys. You can confirm this by typing "!price Mann Co. Supply Crate Key" or "!autokeys".'; + : declined + + ' because I am no longer selling keys. You can confirm this by typing "!price Mann Co. Supply Crate Key" or "!autokeys".'; // } else if (offerReason.reason === 'NOT_BUYING_KEYS') { // const custom = opt.customMessage.decline.notBuyingKeys; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - 'I am no longer buying keys. You can confirm this by typing "!price Mann Co. Supply Crate Key" or "!autokeys".'; + : declined + + ' because I am no longer buying keys. You can confirm this by typing "!price Mann Co. Supply Crate Key" or "!autokeys".'; // } else if (offerReason.reason === 'BANNED') { // const custom = opt.customMessage.decline.banned; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "you're currently banned on backpack.tf or labeled as a scammer on steamrep.com or another community."; + : declined + + " because you're currently banned on backpack.tf or labeled as a scammer on steamrep.com or another community."; // } else if (offerReason.reason === 'ESCROW') { // const custom = opt.customMessage.decline.escrow; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - 'I do not accept escrow (trade holds). To prevent this from happening in the future, please enable Steam Guard Mobile Authenticator.' + + : declined + + ' because I do not accept escrow (trade holds). To prevent this from happening in the future, ' + + 'please enable Steam Guard Mobile Authenticator.' + '\nRead:\n' + 'ā€¢ Steam Guard Mobile Authenticator - https://support.steampowered.com/kb_article.php?ref=8625-WRAH-9030' + '\nā€¢ How to set up Steam Guard Mobile Authenticator - https://support.steampowered.com/kb_article.php?ref=4440-RTUI-9218'; @@ -115,9 +106,7 @@ export default function declined(offer: TradeOffer, bot: Bot, isTradingKeys: boo } else if (offerReason.reason === 'MANUAL') { // const custom = opt.customMessage.decline.manual; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined by the owner.'; + reply = custom ? custom : declined + ' by the owner.'; // } else if ( offerReason.reason === 'ONLY_INVALID_VALUE' || @@ -128,8 +117,8 @@ export default function declined(offer: TradeOffer, bot: Bot, isTradingKeys: boo reasonForInvalidValue = true; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "you've sent a trade with an invalid value (your side and my side do not hold equal value)."; + : declined + + " because you've sent a trade with an invalid value (your side and my side do not hold equal value)."; // } else if ( offerReason.reason === 'ONLY_INVALID_ITEMS' || @@ -140,8 +129,7 @@ export default function declined(offer: TradeOffer, bot: Bot, isTradingKeys: boo reasonForInvalidValue = value.diff < 0; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "you've sent a trade with an invalid items (not exist in my pricelist)."; + : declined + " because you've sent a trade with an invalid items (not exist in my pricelist)."; // } else if ( offerReason.reason === 'ONLY_DISABLED_ITEMS' || @@ -150,10 +138,7 @@ export default function declined(offer: TradeOffer, bot: Bot, isTradingKeys: boo // const custom = opt.offerReceived.disabledItems.autoDecline.declineReply; reasonForInvalidValue = value.diff < 0; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "the item(s) you're trying to take/give is currently disabled"; + reply = custom ? custom : declined + " because the item(s) you're trying to take/give is currently disabled"; // } else if ( offerReason.reason === 'ONLY_OVERSTOCKED' || @@ -162,10 +147,7 @@ export default function declined(offer: TradeOffer, bot: Bot, isTradingKeys: boo // const custom = opt.offerReceived.overstocked.autoDecline.declineReply; reasonForInvalidValue = value.diff < 0; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "you're attempting to sell item(s) that I can't buy more of."; + reply = custom ? custom : declined + " because you're attempting to sell item(s) that I can't buy more of."; // } else if ( offerReason.reason === 'ONLY_UNDERSTOCKED' || @@ -176,23 +158,17 @@ export default function declined(offer: TradeOffer, bot: Bot, isTradingKeys: boo reasonForInvalidValue = value.diff < 0; reply = custom ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "you're attempting to purchase item(s) that I can't sell more of."; + : declined + " because you're attempting to purchase item(s) that I can't sell more of."; // } else if (offerReason.reason === 'šŸŸ«_DUPED_ITEMS') { // const custom = opt.offerReceived.duped.autoDecline.declineReply; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined because ' + - "I don't accept duped items."; + reply = custom ? custom : declined + " because I don't accept duped items."; // } else { // const custom = opt.customMessage.decline.general; - reply = custom - ? custom - : '/pre āŒ Ohh nooooes! The offer is no longer available. Reason: The offer has been declined.'; + reply = custom ? custom : declined + '.'; // } diff --git a/src/classes/MyHandler/offer/review/reasons/disabledItems.ts b/src/classes/MyHandler/offer/review/reasons/disabledItems.ts index d6317e87e..d8d95ca38 100644 --- a/src/classes/MyHandler/offer/review/reasons/disabledItems.ts +++ b/src/classes/MyHandler/offer/review/reasons/disabledItems.ts @@ -21,14 +21,16 @@ export default function disabledItems(meta: Meta, bot: Bot): { note: string; nam disabledForTheir.push(bot.schema.getName(SKU.fromString(el.sku), false)); }); + const disabledForTheirCount = disabledForTheir.length; + return { note: bot.options.manualReview.disabledItems.note ? `šŸŸ§_DISABLED_ITEMS - ${bot.options.manualReview.disabledItems.note}` .replace(/%itemsName%/g, disabledForTheir.join(', ')) - .replace(/%isOrAre%/g, pluralize('is', disabledForTheir.length)) + .replace(/%isOrAre%/g, pluralize('is', disabledForTheirCount)) : `šŸŸ§_DISABLED_ITEMS - ${disabledForTheir.join(', ')} ${pluralize( 'is', - disabledForTheir.length + disabledForTheirCount )} currently disabled.`, // Default note: %itemsName% %isOrAre% currently disabled. name: disabledForOur diff --git a/src/classes/MyHandler/offer/review/reasons/duped.ts b/src/classes/MyHandler/offer/review/reasons/duped.ts index d88d1b3d0..41eaeca49 100644 --- a/src/classes/MyHandler/offer/review/reasons/duped.ts +++ b/src/classes/MyHandler/offer/review/reasons/duped.ts @@ -29,14 +29,16 @@ export default function duped(meta: Meta, bot: Bot): { note: string; name: strin ); }); + const dupedItemsNameTheirCount = dupedItemsNameTheir.length; + return { note: bot.options.manualReview.duped.note ? `šŸŸ«_DUPED_ITEMS - ${bot.options.manualReview.duped.note}` .replace(/%itemsName%/g, dupedItemsNameTheir.join(', ')) - .replace(/%isOrAre%/g, pluralize('is', dupedItemsNameTheir.length)) + .replace(/%isOrAre%/g, pluralize('is', dupedItemsNameTheirCount)) : `šŸŸ«_DUPED_ITEMS - ${dupedItemsNameTheir.join(', ')} ${pluralize( 'is', - dupedItemsNameTheir.length + dupedItemsNameTheirCount )} appeared to be duped.`, // Default note: %itemsName% is|are appeared to be duped. name: dupedItemsNameOur diff --git a/src/classes/MyHandler/offer/review/reasons/invalidItems.ts b/src/classes/MyHandler/offer/review/reasons/invalidItems.ts index 1d3f20fa6..e3784abf1 100644 --- a/src/classes/MyHandler/offer/review/reasons/invalidItems.ts +++ b/src/classes/MyHandler/offer/review/reasons/invalidItems.ts @@ -21,14 +21,16 @@ export default function invalidItems(meta: Meta, bot: Bot): { note: string; name invalidForTheir.push(bot.schema.getName(SKU.fromString(el.sku), false)); }); + const invalidForTheirCount = invalidForTheir.length; + return { note: bot.options.manualReview.invalidItems.note ? `šŸŸØ_INVALID_ITEMS - ${bot.options.manualReview.invalidItems.note}` .replace(/%itemsName%/g, invalidForTheir.join(', ')) - .replace(/%isOrAre%/g, pluralize('is', invalidForTheir.length)) + .replace(/%isOrAre%/g, pluralize('is', invalidForTheirCount)) : `šŸŸØ_INVALID_ITEMS - ${invalidForTheir.join(', ')} ${pluralize( 'is', - invalidForTheir.length + invalidForTheirCount )} not in my pricelist.`, // Default note: %itemsName% %isOrAre% not in my pricelist. name: invalidForOur diff --git a/src/classes/MyHandler/utils/craftClassWeapons.ts b/src/classes/MyHandler/utils/craftClassWeapons.ts index 0180a8e2f..5f78bdac5 100644 --- a/src/classes/MyHandler/utils/craftClassWeapons.ts +++ b/src/classes/MyHandler/utils/craftClassWeapons.ts @@ -17,12 +17,14 @@ function craftEachClassWeapons(bot: Bot, weapons: string[], currencies: { [sku: return new Promise(resolve => { let stopLoop = false; - for (let i = 0; i < weapons.length; i++) { + const weaponsCount = weapons.length; + + for (let i = 0; i < weaponsCount; i++) { // first loop // check if that weapon1 only have 1 in inventory AND it's not in pricelist const isWep1 = currencies[weapons[i]].length === 1 && bot.pricelist.getPrice(weapons[i], true) === null; - for (let j = 0; j < weapons.length; j++) { + for (let j = 0; j < weaponsCount; j++) { if (j === i) { // second loop inside first loop, but ignore same index (same weapons) continue; diff --git a/src/classes/Options.ts b/src/classes/Options.ts index a2db02021..508afde06 100644 --- a/src/classes/Options.ts +++ b/src/classes/Options.ts @@ -65,6 +65,9 @@ export const DEFAULTS = { }, pricelist: { + filterCantAfford: { + enable: false + }, autoRemoveIntentSell: { enable: false }, @@ -98,6 +101,7 @@ export const DEFAULTS = { showStockChanges: false, showTimeTakenInMS: false, showItemPrices: true, + showPureInEmoji: false, customText: { summary: { steamChat: 'Summary', @@ -1023,6 +1027,7 @@ interface HighValueAlert { // ------------ Pricelist ------------ interface Pricelist { + filterCantAfford?: OnlyEnable; autoRemoveIntentSell?: OnlyEnable; autoAddInvalidItems?: OnlyEnable; autoAddPaintedItems?: OnlyEnable; @@ -1052,6 +1057,7 @@ interface TradeSummary { showStockChanges?: boolean; showTimeTakenInMS?: boolean; showItemPrices?: boolean; + showPureInEmoji?: boolean; customText?: TradeSummaryCustomText; } diff --git a/src/classes/Pricelist.ts b/src/classes/Pricelist.ts index 14d596d05..857f81f9e 100644 --- a/src/classes/Pricelist.ts +++ b/src/classes/Pricelist.ts @@ -241,8 +241,9 @@ export default class Pricelist extends EventEmitter { search = search.toLowerCase(); const match: Entry[] = []; + const pricesCount = this.prices.length; - for (let i = 0; i < this.prices.length; i++) { + for (let i = 0; i < pricesCount; i++) { const entry = this.prices[i]; if (enabledOnly && entry.enabled === false) { @@ -282,10 +283,12 @@ export default class Pricelist extends EventEmitter { } } - if (match.length === 0) { + const matchCount = match.length; + + if (matchCount === 0) { // No match return null; - } else if (match.length === 1) { + } else if (matchCount === 1) { // Found one that matched the search return match[0]; } @@ -681,7 +684,9 @@ export default class Pricelist extends EventEmitter { let pricesChanged = false; // Go through our pricelist - for (let i = 0; i < old.length; i++) { + const oldCount = old.length; + + for (let i = 0; i < oldCount; i++) { // const currPrice = old[i]; if (old[i].autoprice !== true) { continue; @@ -692,8 +697,11 @@ export default class Pricelist extends EventEmitter { const name = this.schema.getName(item, true); // Go through pricestf prices - for (let j = 0; j < groupedPrices[item.quality][item.killstreak].length; j++) { - const newestPrice = groupedPrices[item.quality][item.killstreak][j]; + const grouped = groupedPrices[item.quality][item.killstreak]; + const groupedCount = grouped.length; + + for (let j = 0; j < groupedCount; j++) { + const newestPrice = grouped[j]; if (name === newestPrice.name) { // Found matching items @@ -800,7 +808,9 @@ export default class Pricelist extends EventEmitter { static groupPrices(prices: Item[]): Group { const sorted: Group = {}; - for (let i = 0; i < prices.length; i++) { + const pricesCount = prices.length; + + for (let i = 0; i < pricesCount; i++) { if (prices[i].buy === null) { continue; } diff --git a/src/classes/TF2GC.ts b/src/classes/TF2GC.ts index 91754a8fd..ae199b670 100644 --- a/src/classes/TF2GC.ts +++ b/src/classes/TF2GC.ts @@ -466,7 +466,9 @@ export default class TF2GC { .findBySKU(String(job.defindex) + ';6', true) .filter(assetid => !this.bot.trades.isInTrade(assetid)); - return (job.type === 'smelt' && assetids.length > 0) || (job.type === 'combine' && assetids.length >= 3); + const assetidsCount = assetids.length; + + return (job.type === 'smelt' && assetidsCount > 0) || (job.type === 'combine' && assetidsCount >= 3); } else if (job.type === 'use' || job.type === 'delete') { return this.bot.inventoryManager.getInventory.findByAssetid(job.assetid) !== null; } diff --git a/src/classes/Trades.ts b/src/classes/Trades.ts index 17970bb2b..7399ecbf0 100644 --- a/src/classes/Trades.ts +++ b/src/classes/Trades.ts @@ -63,14 +63,18 @@ export default class Trades { } // Go through all sent / received offers and mark the items as in trade - for (let i = 0; i < activeOrCreatedNeedsConfirmation.length; i++) { + const activeCount = activeOrCreatedNeedsConfirmation.length; + + for (let i = 0; i < activeCount; i++) { const id = activeOrCreatedNeedsConfirmation[i]; const offerData: UnknownDictionaryKnownValues = pollData.offerData === undefined ? {} : pollData.offerData[id] || {}; const items = (offerData.items || []) as TradeOfferManager.TradeOfferItem[]; - for (let i = 0; i < items.length; i++) { + const itemsCount = items.length; + + for (let i = 0; i < itemsCount; i++) { this.setItemInTrade = items[i].assetid; } } @@ -103,27 +107,30 @@ export default class Trades { }); const activeReceived = received.filter(offer => offer.state === TradeOfferManager.ETradeOfferState['Active']); + const activeReceivedCount = activeReceived.length; if ( filter === TradeOfferManager.EOfferFilter['ActiveOnly'] && - (this.pollCount * this.bot.manager.pollInterval) / (2 * 60 * 1000) >= 1 + (this.pollCount * this.bot.manager.pollInterval) / (2 * 30 * 60 * 1000) >= 1 ) { this.pollCount = 0; const activeSent = sent.filter(offer => offer.state === TradeOfferManager.ETradeOfferState['Active']); + const activeSentCount = activeSent.length; const receivedOnHold = received.filter( offer => offer.state === TradeOfferManager.ETradeOfferState['InEscrow'] ).length; + const sentOnHold = sent.filter(offer => offer.state === TradeOfferManager.ETradeOfferState['InEscrow']) .length; log.verbose( - `${activeReceived.length} incoming ${pluralize('offer', activeReceived.length)}${ - activeReceived.length > 0 ? ` [${activeReceived.map(offer => offer.id).join(', ')}]` : '' - } (${receivedOnHold} on hold), ${activeSent.length} outgoing ${pluralize( + `${activeReceivedCount} incoming ${pluralize('offer', activeReceivedCount)}${ + activeReceivedCount > 0 ? ` [${activeReceived.map(offer => offer.id).join(', ')}]` : '' + } (${receivedOnHold} on hold), ${activeSentCount} outgoing ${pluralize( 'offer', - activeSent.length + activeSentCount )} (${sentOnHold} on hold)` ); } @@ -303,10 +310,11 @@ export default class Trades { offer.data('action', { action: action, - reason: reason, - meta: meta + reason: reason } as Action); + offer.data('meta', meta); + if (actionFunc === undefined) { return Promise.resolve(); } @@ -337,7 +345,7 @@ export default class Trades { this.bot, `Failed to ${action} on the offer #${offer.id}` + summary + - `\n\nRetrying in 3 seconds, or you can try to force ${action} this trade, send "!f${action} ${offer.id}" now.`, + `\n\nRetrying in 30 seconds, or you can try to force ${action} this trade, send "!f${action} ${offer.id}" now.`, null, err, [offer.id] @@ -357,10 +365,14 @@ export default class Trades { this.bot.messageAdmins( `Failed to ${action} on the offer #${offer.id}:` + summary + - `\n\nRetrying in 3 seconds, you can try to force ${action} this trade, reply "!f${action} ${offer.id}" now.` + + `\n\nRetrying in 30 seconds, you can try to force ${action} this trade, reply "!f${action} ${offer.id}" now.` + `\n\nError: ${ - TradeOfferManager.EResult[(err as CustomError).eresult] as string - } (https://steamerrors.com/${(err as CustomError).eresult})`, + (err as CustomError).eresult + ? `${ + TradeOfferManager.EResult[(err as CustomError).eresult] as string + } (https://steamerrors.com/${(err as CustomError).eresult})` + : JSON.stringify(err, null, 4) + }`, [] ); } @@ -368,9 +380,9 @@ export default class Trades { if (!['MANUAL-FORCE', 'AUTO-RETRY'].includes(reason) && ['accept', 'decline'].includes(action)) { setTimeout(() => { - // Auto-retry after 3 seconds + // Auto-retry after 30 seconds void this.retryActionAfterFailure(offer.id, action as 'accept' | 'decline'); - }, 3 * 1000); + }, 30 * 1000); } }) .finally(() => { @@ -399,7 +411,7 @@ export default class Trades { await this.applyActionToOffer( isRetryAccept ? 'accept' : 'decline', 'AUTO-RETRY', - isRetryAccept ? (offer.data('action') as Action).meta : {}, + isRetryAccept ? (offer.data('meta') as Meta) : {}, offer ); @@ -569,7 +581,73 @@ export default class Trades { // Maybe wait for confirmation to be accepted and then resolve? this.acceptConfirmation(offer).catch(err => { log.debug(`Error while trying to accept mobile confirmation on offer #${offer.id}: `, err); - return reject(err); + + if (!(err as CustomError).message?.includes('Could not act on confirmation')) { + // Only notify is error is not "Could not act on confirmation" + + const opt = this.bot.options; + if (opt.sendAlert.failedAccept) { + const keyPrices = this.bot.pricelist.getKeyPrices; + const value = t.valueDiff( + offer, + keyPrices, + false, + opt.miscSettings.showOnlyMetal.enable + ); + + if (opt.discordWebhook.sendAlert.enable && opt.discordWebhook.sendAlert.url !== '') { + const summary = t.summarizeToChat( + offer, + this.bot, + 'summary-accepting', + true, + value, + keyPrices, + false, + false + ); + sendAlert( + `error-accept`, + this.bot, + `Error while trying to accept mobile confirmation on offer #${offer.id}` + + summary + + `\n\nThe offer might already get cancelled. You can check if this offer is still active by` + + ` sending "!trade ${offer.id}"`, + null, + err, + [offer.id] + ); + } else { + const summary = t.summarizeToChat( + offer, + this.bot, + 'summary-accepting', + false, + value, + keyPrices, + true, + false + ); + + this.bot.messageAdmins( + `Error while trying to accept mobile confirmation on offer #${offer.id}:` + + summary + + `\n\nThe offer might already get cancelled. You can check if this offer is still active by` + + ` sending "!trade ${offer.id}` + + `\n\nError: ${ + (err as CustomError).eresult + ? `${ + TradeOfferManager.EResult[ + (err as CustomError).eresult + ] as string + } (https://steamerrors.com/${(err as CustomError).eresult})` + : (err as Error).message + }`, + [] + ); + } + } + } }); } @@ -1064,7 +1142,9 @@ export default class Trades { } const copy = b.slice(0); - for (let i = 0; i < a.length; i++) { + const aCount = a.length; + + for (let i = 0; i < aCount; i++) { // Find index of matching item const index = copy.findIndex(item => Trades.itemEquals(item, a[i])); if (index === -1) { diff --git a/src/lib/DiscordWebhook/sendAlert.ts b/src/lib/DiscordWebhook/sendAlert.ts index cbf2917ec..853e2ca69 100644 --- a/src/lib/DiscordWebhook/sendAlert.ts +++ b/src/lib/DiscordWebhook/sendAlert.ts @@ -33,7 +33,8 @@ type AlertType = | 'failed-decline' | 'failed-processing-offer' | 'retry-success' - | 'retry-failed'; + | 'retry-failed' + | 'error-accept'; export default function sendAlert( type: AlertType, @@ -131,18 +132,39 @@ export default function sendAlert( title = 'Failed to accept trade'; description = msg + - `\n\nError: [${ - TradeOfferManager.EResult[(err as CustomError).eresult] as string - }](https://steamerrors.com/${(err as CustomError).eresult})`; + `\n\nError: ${ + (err as CustomError).eresult + ? `[${TradeOfferManager.EResult[(err as CustomError).eresult] as string}](https://steamerrors.com/${ + (err as CustomError).eresult + })` + : (err as Error).message + }`; content = items[0]; // offer id color = '16711680'; // red } else if (type === 'failed-decline') { title = 'Failed to decline trade'; description = msg + - `\n\nError: [${ - TradeOfferManager.EResult[(err as CustomError).eresult] as string - }](https://steamerrors.com/${(err as CustomError).eresult})`; + `\n\nError: ${ + (err as CustomError).eresult + ? `[${TradeOfferManager.EResult[(err as CustomError).eresult] as string}](https://steamerrors.com/${ + (err as CustomError).eresult + })` + : (err as Error).message + }`; + content = items[0]; // offer id + color = '16711680'; // red + } else if (type === 'error-accept') { + title = 'Error while trying to accept mobile confirmation'; + description = + msg + + `\n\nError: ${ + (err as CustomError).eresult + ? `[${TradeOfferManager.EResult[(err as CustomError).eresult] as string}](https://steamerrors.com/${ + (err as CustomError).eresult + })` + : (err as Error).message + }`; content = items[0]; // offer id color = '16711680'; // red } else if (type === 'failed-processing-offer') { @@ -190,7 +212,8 @@ export default function sendAlert( 'escrow-check-failed-not-restart-bptf-down', 'queue-problem-not-restart-bptf-down', 'autoAddPaintedItemsFailed', - 'failed-accept' + 'failed-accept', + 'error-accept' ].includes(type) && optDW.sendAlert.isMention ? `<@!${optDW.ownerID}>` : '') + (content ? ` - ${content}` : ''), diff --git a/src/lib/DiscordWebhook/sendOfferReview.ts b/src/lib/DiscordWebhook/sendOfferReview.ts index 4d46b236a..6198d1545 100644 --- a/src/lib/DiscordWebhook/sendOfferReview.ts +++ b/src/lib/DiscordWebhook/sendOfferReview.ts @@ -146,17 +146,18 @@ export default function sendOfferReview( webhookReview.embeds[0].fields.length = 0; const separate = itemList.split('@'); + const separateCount = separate.length; let newSentences = ''; let j = 1; separate.forEach((sentence, i) => { - if ((newSentences.length >= 800 || i === separate.length - 1) && !(j > 4)) { + if ((newSentences.length >= 800 || i === separateCount - 1) && !(j > 4)) { webhookReview.embeds[0].fields.push({ name: `__Item list ${j}__`, value: newSentences.replace(/@/g, '') }); - if (i === separate.length - 1 || j > 4) { + if (i === separateCount - 1 || j > 4) { webhookReview.embeds[0].fields.push(statusElement); } diff --git a/src/lib/DiscordWebhook/sendTradeSummary.ts b/src/lib/DiscordWebhook/sendTradeSummary.ts index 024816ac0..4e6e70391 100644 --- a/src/lib/DiscordWebhook/sendTradeSummary.ts +++ b/src/lib/DiscordWebhook/sendTradeSummary.ts @@ -166,17 +166,18 @@ export default async function sendTradeSummary( acceptedTradeSummary.embeds[0].fields.length = 0; const separate = itemList.split('@'); + const separateCount = separate.length; let newSentences = ''; let j = 1; separate.forEach((sentence, i) => { - if ((newSentences.length >= 800 || i === separate.length - 1) && !(j > 4)) { + if ((newSentences.length >= 800 || i === separateCount - 1) && !(j > 4)) { acceptedTradeSummary.embeds[0].fields.push({ name: `__Item list ${j}__`, value: newSentences.replace(/@/g, '') }); - if (i === separate.length - 1 || j > 4) { + if (i === separateCount - 1 || j > 4) { acceptedTradeSummary.embeds[0].fields.push(statusElement); } diff --git a/src/lib/extend/item/getSKU.ts b/src/lib/extend/item/getSKU.ts index 537e255b8..eeb7fd6fa 100644 --- a/src/lib/extend/item/getSKU.ts +++ b/src/lib/extend/item/getSKU.ts @@ -178,7 +178,9 @@ function getPaintKit(item: EconItem, schema: SchemaManager.Schema): number | nul let hasCaseCollection = false; let skin: string | null = null; - for (let i = 0; i < item.descriptions.length; i++) { + const descriptionsCount = item.descriptions.length; + + for (let i = 0; i < descriptionsCount; i++) { if (!hasCaseCollection && item.descriptions[i].value.endsWith('Collection')) { hasCaseCollection = true; } else if ( @@ -243,7 +245,9 @@ function getOutput( ): { target: number | null; output: number | null; outputQuality: number | null } { let index = -1; - for (let i = 0; i < item.descriptions.length; i++) { + const descriptionsCount = item.descriptions.length; + + for (let i = 0; i < descriptionsCount; i++) { if ( item.descriptions[i].value == 'You will receive all of the following outputs once all of the inputs are fulfilled.' @@ -330,6 +334,8 @@ function getTarget(item: EconItem, schema: SchemaManager.Schema): number | null throw new Error('Could not find target for item "' + item.market_hash_name + '"'); } + const itemHashNameLength = item.market_hash_name.length; + if ( [ 6527, // general @@ -364,22 +370,19 @@ function getTarget(item: EconItem, schema: SchemaManager.Schema): number | null // Killstreak Kit return schema.getItemByItemName( item.market_hash_name - .substring(10, item.market_hash_name.length - 3) + .substring(10, itemHashNameLength - 3) .replace('Killstreak', '') .trim() ).defindex; } else if (defindex === 6523) { // Specialized Killstreak Kit - return schema.getItemByItemName(item.market_hash_name.substring(22, item.market_hash_name.length - 3).trim()) - .defindex; + return schema.getItemByItemName(item.market_hash_name.substring(22, itemHashNameLength - 3).trim()).defindex; } else if (defindex === 6526) { // Professional Killstreak Kit - return schema.getItemByItemName(item.market_hash_name.substring(23, item.market_hash_name.length - 3).trim()) - .defindex; + return schema.getItemByItemName(item.market_hash_name.substring(23, itemHashNameLength - 3).trim()).defindex; } else if (defindex === 9258) { // Unusualifier - return schema.getItemByItemName(item.market_hash_name.substring(7, item.market_hash_name.length - 12).trim()) - .defindex; + return schema.getItemByItemName(item.market_hash_name.substring(7, itemHashNameLength - 12).trim()).defindex; } return null; @@ -458,14 +461,16 @@ function getCrateSeries(item: EconItem): number | null { } }; - if (defindex === 5022 && Object.keys(crates.is5022).includes(item.market_hash_name)) { - series = crates.is5022[item.market_hash_name]; - } else if (defindex === 5041 && Object.keys(crates.is5041).includes(item.market_hash_name)) { - series = crates.is5041[item.market_hash_name]; - } else if (defindex === 5045 && Object.keys(crates.is5045).includes(item.market_hash_name)) { - series = crates.is5045[item.market_hash_name]; - } else if (defindex === 5068 && Object.keys(crates.is5068).includes(item.market_hash_name)) { - series = crates.is5068[item.market_hash_name]; + const itemHashMarketName = item.market_hash_name; + + if (defindex === 5022 && Object.keys(crates.is5022).includes(itemHashMarketName)) { + series = crates.is5022[itemHashMarketName]; + } else if (defindex === 5041 && Object.keys(crates.is5041).includes(itemHashMarketName)) { + series = crates.is5041[itemHashMarketName]; + } else if (defindex === 5045 && Object.keys(crates.is5045).includes(itemHashMarketName)) { + series = crates.is5045[itemHashMarketName]; + } else if (defindex === 5068 && Object.keys(crates.is5068).includes(itemHashMarketName)) { + series = crates.is5068[itemHashMarketName]; } if (series !== null) { @@ -488,8 +493,9 @@ function getPainted( } const descriptions = item.descriptions; + const descriptionCount = descriptions.length; - for (let i = 0; i < descriptions.length; i++) { + for (let i = 0; i < descriptionCount; i++) { if (descriptions[i].value.startsWith('Paint Color: ') && descriptions[i].color === '756b5e') { const name = descriptions[i].value.replace('Paint Color: ', '').trim(); diff --git a/src/lib/items.ts b/src/lib/items.ts index 9bbb3a940..647201647 100644 --- a/src/lib/items.ts +++ b/src/lib/items.ts @@ -12,13 +12,13 @@ export function fixItem(item: Item, schema: SchemaManager.Schema): Item { return item; } + const items = schema.raw.schema.items; + const itemsCount = items.length; + if (schemaItem.name.includes(schemaItem.item_class.toUpperCase())) { - for (let i = 0; i < schema.raw.schema.items.length; i++) { - if ( - schema.raw.schema.items[i].item_class === schemaItem.item_class && - schema.raw.schema.items[i].name.startsWith('Upgradeable ') - ) { - item.defindex = schema.raw.schema.items[i].defindex; + for (let i = 0; i < itemsCount; i++) { + if (items[i].item_class === schemaItem.item_class && items[i].name.startsWith('Upgradeable ')) { + item.defindex = items[i].defindex; } } } @@ -30,23 +30,18 @@ export function fixItem(item: Item, schema: SchemaManager.Schema): Item { } const isPromo = isPromoItem(schemaItem); + if (isPromo && item.quality != 1) { - for (let i = 0; i < schema.raw.schema.items.length; i++) { - if ( - !isPromoItem(schema.raw.schema.items[i]) && - schema.raw.schema.items[i].item_name == schemaItem.item_name - ) { + for (let i = 0; i < itemsCount; i++) { + if (!isPromoItem(items[i]) && items[i].item_name == schemaItem.item_name) { // This is the non-promo version, use that defindex instead - item.defindex = schema.raw.schema.items[i].defindex; + item.defindex = items[i].defindex; } } } else if (!isPromo && item.quality == 1) { - for (let i = 0; i < schema.raw.schema.items.length; i++) { - if ( - isPromoItem(schema.raw.schema.items[i]) && - schema.raw.schema.items[i].item_name == schemaItem.item_name - ) { - item.defindex = schema.raw.schema.items[i].defindex; + for (let i = 0; i < itemsCount; i++) { + if (isPromoItem(items[i]) && items[i].item_name == schemaItem.item_name) { + item.defindex = items[i].defindex; } } } @@ -55,7 +50,9 @@ export function fixItem(item: Item, schema: SchemaManager.Schema): Item { let series: number | null = null; if (schemaItem.attributes !== undefined) { - for (let i = 0; i < schemaItem.attributes.length; i++) { + const attributesCount = schemaItem.attributes.length; + + for (let i = 0; i < attributesCount; i++) { if (schemaItem.attributes[i].name === 'set supply crate series') { series = schemaItem.attributes[i].value; } diff --git a/src/lib/tools/getHighValue.ts b/src/lib/tools/getHighValue.ts index f417b7730..71e34a546 100644 --- a/src/lib/tools/getHighValue.ts +++ b/src/lib/tools/getHighValue.ts @@ -27,7 +27,9 @@ export default function getHighValueItems( const toJoin: string[] = []; Object.keys(items[sku]).forEach(attachment => { - if (attachment === 's') { + if (attachment === 'isFull') { + toString += `\nšŸ’Æ Full uses: ${items[sku].isFull ? 'āœ…' : 'āŒ'}`; + } else if (attachment === 's') { toString += `\n${cT.spells ? cT.spells : 'šŸŽƒ Spells:'} `; items[sku].s.forEach(spellSKU => { diff --git a/src/lib/tools/profit.ts b/src/lib/tools/profit.ts index d276e08ab..c61cb181a 100644 --- a/src/lib/tools/profit.ts +++ b/src/lib/tools/profit.ts @@ -48,13 +48,20 @@ export default function profit( const tracker = new itemTracker(); - for (let i = 0; i < trades.length; i++) { + const tradesCount = trades.length; + + for (let i = 0; i < tradesCount; i++) { // const trade = trades[i]; if (!(trades[i].handledByUs && trades[i].isAccepted)) { // trade was not accepted, go to next trade continue; } + if (trades[i].action?.reason === 'ADMIN' || bot.isAdmin(trades[i].partner)) { + // trades was from ADMIN, ignore + continue; + } + let isGift = false; if (!Object.prototype.hasOwnProperty.call(trades[i], 'dict')) { @@ -62,9 +69,11 @@ export default function profit( continue; } - if (typeof Object.keys(trades[i].dict.our).length === 'undefined') { + const ourDictCount = Object.keys(trades[i].dict.our).length; + + if (typeof ourDictCount === 'undefined') { isGift = true; // no items on our side, so it is probably gift - } else if (Object.keys(trades[i].dict.our).length > 0) { + } else if (ourDictCount > 0) { // trade is not a gift if (!Object.prototype.hasOwnProperty.call(trades[i], 'value')) { // trade is missing value object diff --git a/src/lib/tools/pure.ts b/src/lib/tools/pure.ts index 9f8c0e822..53431201e 100644 --- a/src/lib/tools/pure.ts +++ b/src/lib/tools/pure.ts @@ -29,7 +29,10 @@ export function stock(bot: Bot): string[] { }` } ]; - for (let i = 0; i < pureCombine.length; i++) { + + const pureCombineCount = pureCombine.length; + + for (let i = 0; i < pureCombineCount; i++) { if (i < 1 && totalKeys < 1) { continue; } diff --git a/src/lib/tools/summarizeItems.ts b/src/lib/tools/summarizeItems.ts index 2d565cd53..74d9ed9f2 100644 --- a/src/lib/tools/summarizeItems.ts +++ b/src/lib/tools/summarizeItems.ts @@ -21,46 +21,50 @@ export default function listItems( const itemsPrices = bot.options.tradeSummary.showItemPrices ? listPrices(offer, bot, isSteamChat) : ''; let list = itemsPrices; + const itemsPricesLength = itemsPrices.length; + const invalidCount = items.invalid.length; + const disabledCount = items.disabled.length; + const overstockedCount = items.overstock.length; + const understockedCount = items.understock.length; + const dupedCount = items.duped.length; + const dupedFailedCount = items.dupedFailed.length; + const highValueCount = items.highValue.length; + list += - items.invalid.length > 0 - ? (itemsPrices.length > 0 ? '\n\n' : '') + + invalidCount > 0 + ? (itemsPricesLength > 0 ? '\n\n' : '') + (isSteamChat ? 'šŸŸØ_INVALID_ITEMS:\n- ' + items.invalid.join(',\n- ') : 'šŸŸØ`_INVALID_ITEMS:`\n- ' + items.invalid.join(',@\n- ')) : ''; list += - items.disabled.length > 0 - ? (itemsPrices.length > 0 || items.invalid.length > 0 ? '\n\n' : '') + + disabledCount > 0 + ? (itemsPricesLength > 0 || invalidCount > 0 ? '\n\n' : '') + (isSteamChat ? 'šŸŸ§_DISABLED_ITEMS:\n- ' + items.disabled.join(',\n- ') : 'šŸŸ§`_DISABLED_ITEMS:`\n- ' + items.disabled.join(',@\n- ')) : ''; list += - items.overstock.length > 0 - ? (itemsPrices.length > 0 || items.invalid.length > 0 || items.disabled.length > 0 ? '\n\n' : '') + + overstockedCount > 0 + ? (itemsPricesLength > 0 || invalidCount > 0 || disabledCount > 0 ? '\n\n' : '') + (isSteamChat ? 'šŸŸ¦_OVERSTOCKED:\n- ' + items.overstock.join(',\n- ') : 'šŸŸ¦`_OVERSTOCKED:`\n- ' + items.overstock.join(',@\n- ')) : ''; list += - items.understock.length > 0 - ? (itemsPrices.length > 0 || - items.invalid.length > 0 || - items.disabled.length > 0 || - items.overstock.length > 0 - ? '\n\n' - : '') + + understockedCount > 0 + ? (itemsPricesLength > 0 || invalidCount > 0 || disabledCount > 0 || overstockedCount > 0 ? '\n\n' : '') + (isSteamChat ? 'šŸŸ©_UNDERSTOCKED:\n- ' + items.understock.join(',\n- ') : 'šŸŸ©`_UNDERSTOCKED:`\n- ' + items.understock.join(',@\n- ')) : ''; list += - items.duped.length > 0 - ? (itemsPrices.length > 0 || - items.invalid.length > 0 || - items.disabled.length > 0 || - items.overstock.length > 0 || - items.understock.length > 0 + dupedCount > 0 + ? (itemsPricesLength > 0 || + invalidCount > 0 || + disabledCount > 0 || + overstockedCount > 0 || + understockedCount > 0 ? '\n\n' : '') + (isSteamChat @@ -68,13 +72,13 @@ export default function listItems( : 'šŸŸ«`_DUPED_ITEMS:`\n- ' + items.duped.join(',@\n- ')) : ''; list += - items.dupedFailed.length > 0 - ? (itemsPrices.length > 0 || - items.invalid.length > 0 || - items.disabled.length > 0 || - items.overstock.length > 0 || - items.understock.length > 0 || - items.duped.length > 0 + dupedFailedCount > 0 + ? (itemsPricesLength > 0 || + invalidCount > 0 || + disabledCount > 0 || + overstockedCount > 0 || + understockedCount > 0 || + dupedCount > 0 ? '\n\n' : '') + (isSteamChat @@ -82,14 +86,14 @@ export default function listItems( : 'šŸŸŖ`_DUPE_CHECK_FAILED:`\n- ' + items.dupedFailed.join(',@\n- ')) : ''; list += - items.highValue.length > 0 - ? (itemsPrices.length > 0 || - items.invalid.length > 0 || - items.disabled.length > 0 || - items.overstock.length > 0 || - items.understock.length > 0 || - items.duped.length > 0 || - items.dupedFailed.length > 0 + highValueCount > 0 + ? (itemsPricesLength > 0 || + invalidCount > 0 || + disabledCount > 0 || + overstockedCount > 0 || + understockedCount > 0 || + dupedCount > 0 || + dupedFailedCount > 0 ? '\n\n' : '') + (isSteamChat diff --git a/src/lib/tools/summarizeOffer.ts b/src/lib/tools/summarizeOffer.ts index 16a5cb400..046d81eaf 100644 --- a/src/lib/tools/summarizeOffer.ts +++ b/src/lib/tools/summarizeOffer.ts @@ -102,13 +102,15 @@ export default function summarize( }; } else { // If trade with trade partner + const opening = showStockChanges ? '怚' : ' ('; + const closing = showStockChanges ? '怛' : ')'; return { asked: `${new Currencies(value.our).toString()}` + - ` (${getSummary(items.our, bot, 'our', type, withLink, showStockChanges)})`, + `${opening}${getSummary(items.our, bot, 'our', type, withLink, showStockChanges)}${closing}`, offered: `${new Currencies(value.their).toString()}` + - ` (${getSummary(items.their, bot, 'their', type, withLink, showStockChanges)})` + `${opening}${getSummary(items.their, bot, 'their', type, withLink, showStockChanges)}${closing}` }; } } @@ -150,7 +152,19 @@ function getSummary( if (withLink) { summary.push( - `[${name}](https://www.prices.tf/items/${sku})${amount > 1 ? ` x${amount}` : ''} (${ + `[${ + bot.options.tradeSummary.showPureInEmoji + ? sku === '5021;6' + ? '<:tf2key:742725387968184371>' + : sku === '5002;6' + ? '<:tf2refined:735533220942053396>' + : sku === '5001;6' + ? '<:tf2reclaimed:809644301633323048>' + : sku === '5000;6' + ? '<:tf2scrap:809644301067091968>' + : name + : name + }](https://www.prices.tf/items/${sku})${amount > 1 ? ` x${amount}` : ''} (${ type === 'summary-accepted' && oldStock !== null ? `${oldStock} ā†’ ` : '' }${currentStock}${maxStock ? `/${maxStock.max}` : ''})` ); @@ -168,7 +182,19 @@ function getSummary( } else { if (withLink) { summary.push( - '[' + name + '](https://www.prices.tf/items/' + sku + ')' + (amount > 1 ? ` x${amount}` : '') + `[${ + bot.options.tradeSummary.showPureInEmoji + ? sku === '5021;6' + ? '<:tf2key:742725387968184371>' + : sku === '5002;6' + ? '<:tf2refined:735533220942053396>' + : sku === '5001;6' + ? '<:tf2reclaimed:809644301633323048>' + : sku === '5000;6' + ? '<:tf2scrap:809644301067091968>' + : name + : name + }](https://www.prices.tf/items/${sku})${amount > 1 ? ` x${amount}` : ''}` ); } else { summary.push(name + (amount > 1 ? ` x${amount}` : '')); @@ -176,14 +202,16 @@ function getSummary( } } - if (summary.length === 0) { + const summaryCount = summary.length; + + if (summaryCount === 0) { return 'nothing'; } if (withLink) { let left = 0; - if (summary.length > 15) { - left = summary.length - 15; + if (summaryCount > 15) { + left = summaryCount - 15; summary.splice(15); } diff --git a/src/schemas/options-json/array-string-url.ts b/src/schemas/options-json/array-string-url.ts index d07bc6dad..568426e1a 100644 --- a/src/schemas/options-json/array-string-url.ts +++ b/src/schemas/options-json/array-string-url.ts @@ -7,5 +7,6 @@ export const stringArrayURLSchema: jsonschema.Schema = { type: 'string', pattern: '^$|https://discord(app)?.com/api/webhooks/[0-9]+/(.)+' }, - required: false + required: false, + additionalProperties: false }; diff --git a/src/schemas/options-json/options.ts b/src/schemas/options-json/options.ts index a13a1adf6..4ec080cd0 100644 --- a/src/schemas/options-json/options.ts +++ b/src/schemas/options-json/options.ts @@ -9,7 +9,8 @@ export const optionsSchema: jsonschema.Schema = { items: { type: 'string' }, - required: false + required: false, + additionalProperties: false }, 'only-enable': { type: 'object', @@ -18,7 +19,8 @@ export const optionsSchema: jsonschema.Schema = { type: 'boolean' } }, - required: ['enable'] + required: ['enable'], + additionalProperties: false }, 'normalize-which': { type: 'object', @@ -65,7 +67,8 @@ export const optionsSchema: jsonschema.Schema = { $ref: '#/definitions/only-enable-declineReply' } }, - required: ['autoAcceptOverpay', 'autoDecline'] + required: ['autoAcceptOverpay', 'autoDecline'], + additionalProperties: false }, 'only-ignore-failed': { type: 'object', @@ -84,7 +87,8 @@ export const optionsSchema: jsonschema.Schema = { type: 'string' } }, - required: ['note'] + required: ['note'], + additionalProperties: false }, 'discord-webhook-misc': { type: 'object', @@ -102,7 +106,8 @@ export const optionsSchema: jsonschema.Schema = { type: 'boolean' } }, - required: ['showQuickLinks', 'showKeyRate', 'showPureStock', 'showInventory'] + required: ['showQuickLinks', 'showKeyRate', 'showPureStock', 'showInventory'], + additionalProperties: false }, 'discord-webhook-enable-url': { type: 'object', @@ -115,7 +120,8 @@ export const optionsSchema: jsonschema.Schema = { pattern: '^$|https://discord(app)?.com/api/webhooks/[0-9]+/(.)+' } }, - required: ['enable', 'url'] + required: ['enable', 'url'], + additionalProperties: false }, 'only-customReply-reply': { type: 'object', @@ -141,7 +147,8 @@ export const optionsSchema: jsonschema.Schema = { type: 'string' } }, - required: ['disabled'] + required: ['disabled'], + additionalProperties: false }, 'only-enable-customReply-disabled': { type: 'object', @@ -223,7 +230,8 @@ export const optionsSchema: jsonschema.Schema = { type: 'string' } }, - required: ['disabled', 'dontHave', 'have'] + required: ['disabled', 'dontHave', 'have'], + additionalProperties: false } }, required: ['enable', 'customReply'], @@ -401,7 +409,8 @@ export const optionsSchema: jsonschema.Schema = { 'weaponsAsCurrency', 'checkUses', 'game' - ] + ], + additionalProperties: false }, sendAlert: { properties: { @@ -424,7 +433,8 @@ export const optionsSchema: jsonschema.Schema = { type: 'boolean' } }, - required: ['lowPure', 'failedToAdd', 'failedToUpdate', 'failedToDisable'] + required: ['lowPure', 'failedToAdd', 'failedToUpdate', 'failedToDisable'], + additionalProperties: false }, backpackFull: { type: 'boolean' @@ -442,7 +452,8 @@ export const optionsSchema: jsonschema.Schema = { type: 'boolean' } }, - required: ['gotDisabled', 'receivedNotInPricelist', 'tryingToTake'] + required: ['gotDisabled', 'receivedNotInPricelist', 'tryingToTake'], + additionalProperties: false }, autoRemoveIntentSellFailed: { type: 'boolean' @@ -472,6 +483,9 @@ export const optionsSchema: jsonschema.Schema = { pricelist: { type: 'object', properties: { + filterCantAfford: { + $ref: '#/definitions/only-enable' + }, autoRemoveIntentSell: { $ref: '#/definitions/only-enable' }, @@ -493,7 +507,14 @@ export const optionsSchema: jsonschema.Schema = { additionalProperties: false } }, - required: ['autoRemoveIntentSell', 'autoAddInvalidItems', 'autoAddPaintedItems', 'priceAge'] + required: [ + 'filterCantAfford', + 'autoRemoveIntentSell', + 'autoAddInvalidItems', + 'autoAddPaintedItems', + 'priceAge' + ], + additionalProperties: false }, bypass: { type: 'object', @@ -526,6 +547,9 @@ export const optionsSchema: jsonschema.Schema = { showItemPrices: { type: 'boolean' }, + showPureInEmoji: { + type: 'boolean' + }, customText: { type: 'object', properties: { @@ -572,11 +596,26 @@ export const optionsSchema: jsonschema.Schema = { type: 'string' } }, - required: ['spells', 'strangeParts', 'killstreaker', 'sheen', 'painted'], + required: [ + 'summary', + 'asked', + 'offered', + 'profitFromOverpay', + 'lossFromUnderpay', + 'timeTaken', + 'keyRate', + 'pureStock', + 'totalItems', + 'spells', + 'strangeParts', + 'killstreaker', + 'sheen', + 'painted' + ], additionalProperties: false } }, - required: ['showStockChanges', 'showTimeTakenInMS', 'showItemPrices'], + required: ['showStockChanges', 'showTimeTakenInMS', 'showItemPrices', 'showPureInEmoji', 'customText'], additionalProperties: false }, @@ -899,19 +938,20 @@ export const optionsSchema: jsonschema.Schema = { additionalProperties: false }, invalidItems: { - allOf: [ - { - properties: { - givePrice: { - type: 'boolean' - } - }, - required: ['givePrice'] + type: 'object', + properties: { + givePrice: { + type: 'boolean' }, - { - $ref: '#/definitions/only-autoAcceptOverpay-autoDecline' + autoAcceptOverpay: { + type: 'boolean' + }, + autoDecline: { + $ref: '#/definitions/only-enable-declineReply' } - ] + }, + required: ['givePrice', 'autoAcceptOverpay', 'autoDecline'], + additionalProperties: false }, disabledItems: { $ref: '#/definitions/only-autoAcceptOverpay-autoDecline' @@ -1052,14 +1092,26 @@ export const optionsSchema: jsonschema.Schema = { $ref: 'array-string-url' }, misc: { - allOf: [ - { - $ref: '#/definitions/discord-webhook-misc' + type: 'object', + properties: { + showQuickLinks: { + type: 'boolean' }, - { - $ref: '#/definitions/only-note' + showKeyRate: { + type: 'boolean' + }, + showPureStock: { + type: 'boolean' + }, + showInventory: { + type: 'boolean' + }, + note: { + type: 'string' } - ] + }, + required: ['showQuickLinks', 'showKeyRate', 'showPureStock', 'showInventory', 'note'], + additionalProperties: false }, mentionOwner: { properties: { @@ -1082,71 +1134,97 @@ export const optionsSchema: jsonschema.Schema = { additionalProperties: false }, offerReview: { - allOf: [ - { - $ref: '#/definitions/discord-webhook-enable-url' + type: 'object', + properties: { + enable: { + type: 'boolean' }, - { + url: { + type: 'string', + pattern: '^$|https://discord(app)?.com/api/webhooks/[0-9]+/(.)+' + }, + mentionInvalidValue: { + type: 'boolean' + }, + isMention: { + type: 'boolean' + }, + misc: { type: 'object', properties: { - mentionInvalidValue: { + showQuickLinks: { type: 'boolean' }, - isMention: { + showKeyRate: { type: 'boolean' }, - misc: { - $ref: '#/definitions/discord-webhook-misc' + showPureStock: { + type: 'boolean' + }, + showInventory: { + type: 'boolean' } }, - required: ['mentionInvalidValue', 'isMention', 'misc'] + required: ['showQuickLinks', 'showKeyRate', 'showPureStock', 'showInventory'], + additionalProperties: false } - ] + }, + required: ['enable', 'url', 'mentionInvalidValue', 'isMention', 'misc'], + additionalProperties: false }, messages: { - allOf: [ - { - $ref: '#/definitions/discord-webhook-enable-url' + type: 'object', + properties: { + enable: { + type: 'boolean' }, - { - type: 'object', - properties: { - isMention: { - type: 'boolean' - }, - showQuickLinks: { - type: 'boolean' - } - }, - required: ['isMention', 'showQuickLinks'] + url: { + type: 'string', + pattern: '^$|https://discord(app)?.com/api/webhooks/[0-9]+/(.)+' + }, + isMention: { + type: 'boolean' + }, + showQuickLinks: { + type: 'boolean' } - ] + }, + required: ['enable', 'url', 'isMention', 'showQuickLinks'], + additionalProperties: false }, priceUpdate: { - allOf: [ - { - $ref: '#/definitions/discord-webhook-enable-url' + type: 'object', + properties: { + enable: { + type: 'boolean' }, - { - $ref: '#/definitions/only-note' + url: { + type: 'string', + pattern: '^$|https://discord(app)?.com/api/webhooks/[0-9]+/(.)+' + }, + note: { + type: 'string' } - ] + }, + required: ['enable', 'url', 'note'], + additionalProperties: false }, sendAlert: { - allOf: [ - { - $ref: '#/definitions/discord-webhook-enable-url' + type: 'object', + properties: { + enable: { + type: 'boolean' }, - { - type: 'object', - properties: { - isMention: { - type: 'boolean' - } - }, - required: ['isMention'] + url: { + type: 'string', + pattern: '^$|https://discord(app)?.com/api/webhooks/[0-9]+/(.)+' + }, + isMention: { + type: 'boolean' } - ] + }, + required: ['enable', 'url', 'isMention'], + additionalProperties: false }, sendStats: { $ref: '#/definitions/discord-webhook-enable-url' @@ -1277,6 +1355,7 @@ export const optionsSchema: jsonschema.Schema = { 'success', 'successEscrow', 'decline', + 'accepted', 'tradedAway', 'failedMobileConfirmation', 'cancelledActiveForAwhile', @@ -1449,7 +1528,7 @@ export const optionsSchema: jsonschema.Schema = { additionalProperties: false }, owner: { - $ref: '#/definitions/only-enable-customReply-disabled' + $ref: '#/definitions/only-enabled-disabled-reply' }, discord: { type: 'object', @@ -1458,7 +1537,17 @@ export const optionsSchema: jsonschema.Schema = { type: 'boolean' }, customReply: { - $ref: '#/definitions/only-disabled' + type: 'object', + properties: { + disabled: { + type: 'string' + }, + reply: { + type: 'string' + } + }, + required: ['disabled', 'reply'], + additionalProperties: false }, inviteURL: { type: 'string' @@ -1731,88 +1820,88 @@ export const optionsSchema: jsonschema.Schema = { $ref: '#/definitions/painted-properties' }, 'A Deep Commitment to Purple': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'A Distinctive Lack of Hue': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, "A Mann's Mint": { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'After Eight': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Aged Moustache Grey': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'An Extraordinary Abundance of Tinge': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Australium Gold': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Color No. 216-190-216': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Dark Salmon Injustice': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Drably Olive': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Indubitably Green': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Mann Co. Orange': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, Muskelmannbraun: { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, "Noble Hatter's Violet": { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Peculiarly Drab Tincture': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Pink as Hell': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Radigan Conagher Brown': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'The Bitter Taste of Defeat and Lime': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, "The Color of a Gentlemann's Business Pants": { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Ye Olde Rustic Colour': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, "Zepheniah's Greed": { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'An Air of Debonair': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Balaclavas Are Forever': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, "Operator's Overalls": { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Cream Spirit': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Team Spirit': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'The Value of Teamwork': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' }, 'Waterlogged Lab Coat': { - type: '#/definitions/painted-properties' + $ref: '#/definitions/painted-properties' } }, required: [ diff --git a/src/types/modules/steam-tradeoffer-manager/index.d.ts b/src/types/modules/steam-tradeoffer-manager/index.d.ts index 71fd69aea..982d9e6a4 100644 --- a/src/types/modules/steam-tradeoffer-manager/index.d.ts +++ b/src/types/modules/steam-tradeoffer-manager/index.d.ts @@ -105,6 +105,7 @@ declare module 'steam-tradeoffer-manager' { finishTimestamp?: number; // checked isAccepted?: boolean; // checked action?: Action; // checked + meta?: Meta; highValue?: HighValueOutput; // Only offer sent // checked _dupeCheck?: string[]; // Only offer sent // checked _ourItems?: OutItems[]; // checked @@ -151,7 +152,6 @@ declare module 'steam-tradeoffer-manager' { export interface Action { action: 'accept' | 'decline' | 'skip'; reason: string; - meta: Meta; } export interface Overstocked { @@ -244,6 +244,7 @@ declare module 'steam-tradeoffer-manager' { ks?: PartialSKUWithMention; ke?: PartialSKUWithMention; p?: PartialSKUWithMention; + isFull?: boolean; } interface Items {