mirror of
https://github.com/ZeppelinBot/Zeppelin.git
synced 2025-05-20 16:25:03 +00:00
commit
7dcdb5f72f
143 changed files with 4708 additions and 419 deletions
624
backend/package-lock.json
generated
624
backend/package-lock.json
generated
|
@ -89,6 +89,11 @@
|
|||
"integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@sqltools/formatter": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.2.tgz",
|
||||
"integrity": "sha512-/5O7Fq6Vnv8L6ucmPjaWbVG1XkP4FO+w5glqfkIsq3Xw4oyNAdJddbnYodNDAfjVUvo/rrSCTom4kAND7T1o5Q=="
|
||||
},
|
||||
"@szmarczak/http-timer": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz",
|
||||
|
@ -359,12 +364,14 @@
|
|||
"ansi-regex": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
|
||||
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
|
||||
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^1.9.0"
|
||||
}
|
||||
|
@ -385,9 +392,9 @@
|
|||
}
|
||||
},
|
||||
"app-root-path": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-2.2.1.tgz",
|
||||
"integrity": "sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA=="
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
|
||||
"integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw=="
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
|
@ -726,9 +733,9 @@
|
|||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"base64-js": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
|
||||
"integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
|
||||
},
|
||||
"base64url": {
|
||||
"version": "3.0.1",
|
||||
|
@ -922,12 +929,12 @@
|
|||
}
|
||||
},
|
||||
"buffer": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",
|
||||
"integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
|
||||
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
|
||||
"requires": {
|
||||
"base64-js": "^1.0.2",
|
||||
"ieee754": "^1.1.4"
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"buffer-from": {
|
||||
|
@ -990,12 +997,14 @@
|
|||
"camelcase": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
|
||||
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
|
||||
"dev": true
|
||||
},
|
||||
"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",
|
||||
|
@ -1069,15 +1078,61 @@
|
|||
}
|
||||
},
|
||||
"cli-highlight": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.1.tgz",
|
||||
"integrity": "sha512-0y0VlNmdD99GXZHYnvrQcmHxP8Bi6T00qucGgBgGv4kJ0RyDthNnnFPupHV7PYv/OXSVk+azFbOeaW6+vGmx9A==",
|
||||
"version": "2.1.10",
|
||||
"resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.10.tgz",
|
||||
"integrity": "sha512-CcPFD3JwdQ2oSzy+AMG6j3LRTkNjM82kzcSKzoVw6cLanDCJNlsLjeqVTOTfOfucnWv5F0rmBemVf1m9JiIasw==",
|
||||
"requires": {
|
||||
"chalk": "^2.3.0",
|
||||
"highlight.js": "^9.6.0",
|
||||
"chalk": "^4.0.0",
|
||||
"highlight.js": "^10.0.0",
|
||||
"mz": "^2.4.0",
|
||||
"parse5": "^4.0.0",
|
||||
"yargs": "^13.0.0"
|
||||
"parse5": "^5.1.1",
|
||||
"parse5-htmlparser2-tree-adapter": "^6.0.0",
|
||||
"yargs": "^16.0.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==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"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==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli-spinners": {
|
||||
|
@ -1131,13 +1186,43 @@
|
|||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
|
||||
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"requires": {
|
||||
"string-width": "^3.1.0",
|
||||
"strip-ansi": "^5.2.0",
|
||||
"wrap-ansi": "^5.1.0"
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
|
||||
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"clone": {
|
||||
|
@ -1364,11 +1449,6 @@
|
|||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"decamelize": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
|
||||
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
|
||||
},
|
||||
"decompress-response": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz",
|
||||
|
@ -1403,14 +1483,6 @@
|
|||
"integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==",
|
||||
"dev": true
|
||||
},
|
||||
"define-properties": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz",
|
||||
"integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==",
|
||||
"requires": {
|
||||
"object-keys": "^1.0.12"
|
||||
}
|
||||
},
|
||||
"del": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/del/-/del-5.1.0.tgz",
|
||||
|
@ -1587,32 +1659,10 @@
|
|||
"is-arrayish": "^0.2.1"
|
||||
}
|
||||
},
|
||||
"es-abstract": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.16.0.tgz",
|
||||
"integrity": "sha512-xdQnfykZ9JMEiasTAJZJdMWCQ1Vm00NBw79/AWi7ELfZuuPCSOMDZbT9mkOfSctVtfhb+sAAzrm+j//GjjLHLg==",
|
||||
"requires": {
|
||||
"es-to-primitive": "^1.2.0",
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
"has-symbols": "^1.0.0",
|
||||
"is-callable": "^1.1.4",
|
||||
"is-regex": "^1.0.4",
|
||||
"object-inspect": "^1.6.0",
|
||||
"object-keys": "^1.1.1",
|
||||
"string.prototype.trimleft": "^2.1.0",
|
||||
"string.prototype.trimright": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"es-to-primitive": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz",
|
||||
"integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==",
|
||||
"requires": {
|
||||
"is-callable": "^1.1.4",
|
||||
"is-date-object": "^1.0.1",
|
||||
"is-symbol": "^1.0.2"
|
||||
}
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
|
||||
},
|
||||
"escape-goat": {
|
||||
"version": "2.1.1",
|
||||
|
@ -1733,9 +1783,9 @@
|
|||
}
|
||||
},
|
||||
"figlet": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.2.4.tgz",
|
||||
"integrity": "sha512-mv8YA9RruB4C5QawPaD29rEVx3N97ZTyNrE4DAfbhuo6tpcMdKnPVo8MlyT3RP5uPcg5M14bEJBq7kjFf4kAWg=="
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.5.0.tgz",
|
||||
"integrity": "sha512-ZQJM4aifMpz6H19AW1VqvZ7l4pOE9p7i/3LyxgO2kp+PO/VcDYNqIHEMtkccqIhTXMKci4kjueJr/iCQEaT/Ww=="
|
||||
},
|
||||
"figures": {
|
||||
"version": "3.2.0",
|
||||
|
@ -1848,11 +1898,6 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
|
||||
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz",
|
||||
|
@ -1924,6 +1969,7 @@
|
|||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.5.tgz",
|
||||
"integrity": "sha512-J9dlskqUXK1OeTOYBEn5s8aMukWMwWfs+rPTn/jn50Ux4MNXVhubL1wu/j2t+H4NVI+cXEcCaYellqaPVGXNqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
|
@ -1989,14 +2035,6 @@
|
|||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
|
||||
"integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
|
||||
},
|
||||
"has": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
|
||||
"integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"has-ansi": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
|
||||
|
@ -2015,12 +2053,8 @@
|
|||
"has-flag": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
|
||||
},
|
||||
"has-symbols": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz",
|
||||
"integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q="
|
||||
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
|
||||
"dev": true
|
||||
},
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
|
@ -2034,9 +2068,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "9.16.2",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.16.2.tgz",
|
||||
"integrity": "sha512-feMUrVLZvjy0oC7FVJQcSQRqbBq9kwqnYE4+Kj9ZjbHh3g+BisiPgF49NyQbVLNdrL/qqZr3Ca9yOKwgn2i/tw=="
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.6.0.tgz",
|
||||
"integrity": "sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ=="
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.8",
|
||||
|
@ -2076,9 +2110,9 @@
|
|||
}
|
||||
},
|
||||
"ieee754": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="
|
||||
},
|
||||
"ignore": {
|
||||
"version": "5.1.8",
|
||||
|
@ -2170,11 +2204,6 @@
|
|||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-callable": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz",
|
||||
"integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA=="
|
||||
},
|
||||
"is-ci": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz",
|
||||
|
@ -2184,11 +2213,6 @@
|
|||
"ci-info": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-date-object": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz",
|
||||
"integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY="
|
||||
},
|
||||
"is-error": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/is-error/-/is-error-2.2.2.tgz",
|
||||
|
@ -2273,22 +2297,6 @@
|
|||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-regex": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz",
|
||||
"integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=",
|
||||
"requires": {
|
||||
"has": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"is-symbol": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz",
|
||||
"integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==",
|
||||
"requires": {
|
||||
"has-symbols": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"is-typedarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
|
||||
|
@ -2371,12 +2379,27 @@
|
|||
}
|
||||
},
|
||||
"knub": {
|
||||
"version": "30.0.0-beta.30",
|
||||
"resolved": "https://registry.npmjs.org/knub/-/knub-30.0.0-beta.30.tgz",
|
||||
"integrity": "sha512-K0aUlPVORy+p2XkZe6qYOT/ekjM43TN2TmzY76rIgkz7RtxpX5boSYj6dt4omLz6o7B5rCCBaNq9poEO+l2gfw==",
|
||||
"version": "30.0.0-beta.35",
|
||||
"resolved": "https://registry.npmjs.org/knub/-/knub-30.0.0-beta.35.tgz",
|
||||
"integrity": "sha512-3fVefmp8hq8DxR8RuR/bqaFsXCkt2yf7NyotXaI8wq1FbPjM4/tM+YAzHIg9ER1L96KFZ9SDnRvs18DOb4Nk1w==",
|
||||
"requires": {
|
||||
"knub-command-manager": "^8.1.2",
|
||||
"knub-command-manager": "^9.1.0",
|
||||
"ts-essentials": "^6.0.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
|
||||
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="
|
||||
},
|
||||
"knub-command-manager": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/knub-command-manager/-/knub-command-manager-9.1.0.tgz",
|
||||
"integrity": "sha512-pEtpWElbBoTRSL8kWSPRrTIuTIdvYGkP/wzOn77cieumC02adfwEt1Cc09HFvVT4ib35nf1y31oul36csaG7Vg==",
|
||||
"requires": {
|
||||
"escape-string-regexp": "^2.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"knub-command-manager": {
|
||||
|
@ -2834,25 +2857,6 @@
|
|||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
|
||||
},
|
||||
"object-inspect": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz",
|
||||
"integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ=="
|
||||
},
|
||||
"object-keys": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||
"integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="
|
||||
},
|
||||
"object.getownpropertydescriptors": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz",
|
||||
"integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=",
|
||||
"requires": {
|
||||
"define-properties": "^1.1.2",
|
||||
"es-abstract": "^1.5.1"
|
||||
}
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
|
@ -3071,9 +3075,24 @@
|
|||
"dev": true
|
||||
},
|
||||
"parse5": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz",
|
||||
"integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA=="
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
|
||||
"integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
|
||||
},
|
||||
"parse5-htmlparser2-tree-adapter": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz",
|
||||
"integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==",
|
||||
"requires": {
|
||||
"parse5": "^6.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"parse5": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz",
|
||||
"integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"parseurl": {
|
||||
"version": "1.3.3",
|
||||
|
@ -3462,7 +3481,8 @@
|
|||
"require-main-filename": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
|
||||
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
|
||||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.17.0",
|
||||
|
@ -3632,6 +3652,15 @@
|
|||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
|
||||
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
|
||||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"sharp": {
|
||||
"version": "0.23.4",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.23.4.tgz",
|
||||
|
@ -3876,6 +3905,7 @@
|
|||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
|
||||
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^7.0.1",
|
||||
"is-fullwidth-code-point": "^2.0.0",
|
||||
|
@ -3885,28 +3915,11 @@
|
|||
"emoji-regex": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
|
||||
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
|
||||
"integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"string.prototype.trimleft": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz",
|
||||
"integrity": "sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw==",
|
||||
"requires": {
|
||||
"define-properties": "^1.1.3",
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"string.prototype.trimright": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz",
|
||||
"integrity": "sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg==",
|
||||
"requires": {
|
||||
"define-properties": "^1.1.3",
|
||||
"function-bind": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
|
@ -3919,6 +3932,7 @@
|
|||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
|
||||
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^4.1.0"
|
||||
}
|
||||
|
@ -3984,6 +3998,7 @@
|
|||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
|
||||
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^3.0.0"
|
||||
}
|
||||
|
@ -4056,9 +4071,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"thenify": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz",
|
||||
"integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=",
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
|
||||
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
|
||||
"requires": {
|
||||
"any-promise": "^1.0.0"
|
||||
}
|
||||
|
@ -4180,9 +4195,9 @@
|
|||
}
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
|
@ -4239,43 +4254,115 @@
|
|||
}
|
||||
},
|
||||
"typeorm": {
|
||||
"version": "0.2.20",
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.20.tgz",
|
||||
"integrity": "sha512-VxB+9qH8D+PM19MIx18Zs3Fqv/ZINnnQvUGmBEiLYDrB9etdSdamgSTCIhWdFNndeJ6ldH4jbD0Z6HWsepMPlA==",
|
||||
"version": "0.2.31",
|
||||
"resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.31.tgz",
|
||||
"integrity": "sha512-dVvCEVHH48DG0QPXAKfo0l6ecQrl3A8ucGP4Yw4myz4YEDMProebTQo8as83uyES+nrwCbu3qdkL4ncC2+qcMA==",
|
||||
"requires": {
|
||||
"app-root-path": "^2.0.1",
|
||||
"buffer": "^5.1.0",
|
||||
"chalk": "^2.4.2",
|
||||
"cli-highlight": "^2.0.0",
|
||||
"@sqltools/formatter": "1.2.2",
|
||||
"app-root-path": "^3.0.0",
|
||||
"buffer": "^5.5.0",
|
||||
"chalk": "^4.1.0",
|
||||
"cli-highlight": "^2.1.10",
|
||||
"debug": "^4.1.1",
|
||||
"dotenv": "^6.2.0",
|
||||
"glob": "^7.1.2",
|
||||
"js-yaml": "^3.13.1",
|
||||
"mkdirp": "^0.5.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"glob": "^7.1.6",
|
||||
"js-yaml": "^3.14.0",
|
||||
"mkdirp": "^1.0.4",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"tslib": "^1.9.0",
|
||||
"xml2js": "^0.4.17",
|
||||
"sha.js": "^2.4.11",
|
||||
"tslib": "^1.13.0",
|
||||
"xml2js": "^0.4.23",
|
||||
"yargonaut": "^1.1.2",
|
||||
"yargs": "^13.2.1"
|
||||
"yargs": "^16.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
|
||||
"integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w=="
|
||||
"version": "8.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw=="
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"js-yaml": {
|
||||
"version": "3.14.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
|
||||
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
|
||||
"requires": {
|
||||
"argparse": "^1.0.7",
|
||||
"esprima": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
|
||||
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"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==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -4396,15 +4483,6 @@
|
|||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
|
||||
},
|
||||
"util.promisify": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz",
|
||||
"integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==",
|
||||
"requires": {
|
||||
"define-properties": "^1.1.2",
|
||||
"object.getownpropertydescriptors": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
|
@ -4456,7 +4534,8 @@
|
|||
"which-module": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
|
||||
"integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=",
|
||||
"dev": true
|
||||
},
|
||||
"which-pm-runs": {
|
||||
"version": "1.0.0",
|
||||
|
@ -4539,13 +4618,64 @@
|
|||
}
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
|
||||
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"requires": {
|
||||
"ansi-styles": "^3.2.0",
|
||||
"string-width": "^3.0.0",
|
||||
"strip-ansi": "^5.0.0"
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
|
||||
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
|
@ -4577,12 +4707,11 @@
|
|||
"dev": true
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.22",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.22.tgz",
|
||||
"integrity": "sha512-MWTbxAQqclRSTnehWWe5nMKzI3VmJ8ltiJEco8akcC6j3miOhjjfzKum5sId+CWhfxdOs/1xauYr8/ZDBtQiRw==",
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"util.promisify": "~1.0.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -4603,7 +4732,8 @@
|
|||
"y18n": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
|
||||
"integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==",
|
||||
"dev": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "2.1.2",
|
||||
|
@ -4664,30 +4794,58 @@
|
|||
}
|
||||
},
|
||||
"yargs": {
|
||||
"version": "13.3.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz",
|
||||
"integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==",
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"requires": {
|
||||
"cliui": "^5.0.0",
|
||||
"find-up": "^3.0.0",
|
||||
"get-caller-file": "^2.0.1",
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"require-main-filename": "^2.0.0",
|
||||
"set-blocking": "^2.0.0",
|
||||
"string-width": "^3.0.0",
|
||||
"which-module": "^2.0.0",
|
||||
"y18n": "^4.0.0",
|
||||
"yargs-parser": "^13.1.1"
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
|
||||
"integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
|
||||
"integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
|
||||
"integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.5.tgz",
|
||||
"integrity": "sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
|
||||
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
|
||||
"requires": {
|
||||
"camelcase": "^5.0.0",
|
||||
"decamelize": "^1.2.0"
|
||||
}
|
||||
"version": "20.2.5",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.5.tgz",
|
||||
"integrity": "sha512-jYRGS3zWy20NtDtK2kBgo/TlAoy5YUuhD9/LZ7z7W4j1Fdw2cqD0xEEclf8fxc8xjD6X5Qr+qQQwCEsP8iRiYg=="
|
||||
},
|
||||
"yawn-yaml": {
|
||||
"version": "github:dragory/yawn-yaml#77ab3870ca53c4693002c4a41336e7476e7934ed",
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
"watch": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node start-dev.js\"",
|
||||
"watch-yaml-parse-test": "cross-env NODE_ENV=development tsc-watch --onSuccess \"node dist/backend/src/yamlParseTest.js\"",
|
||||
"build": "rimraf dist && tsc",
|
||||
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --inspect=127.0.0.1:9229 dist/backend/src/index.js",
|
||||
"start-bot-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --inspect=0.0.0.0:9229 dist/backend/src/index.js",
|
||||
"start-bot-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict dist/backend/src/index.js",
|
||||
"watch-bot": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-bot-dev\"",
|
||||
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --inspect=127.0.0.1:9239 dist/backend/src/api/index.js",
|
||||
"start-api-dev": "cross-env NODE_ENV=development node -r ./register-tsconfig-paths.js --unhandled-rejections=strict --inspect=0.0.0.0:9239 dist/backend/src/api/index.js",
|
||||
"start-api-prod": "cross-env NODE_ENV=production node -r ./register-tsconfig-paths.js --unhandled-rejections=strict dist/backend/src/api/index.js",
|
||||
"watch-api": "cross-env NODE_ENV=development tsc-watch --onSuccess \"npm run start-api-dev\"",
|
||||
"typeorm": "node -r ./register-tsconfig-paths.js ./node_modules/typeorm/cli.js",
|
||||
|
@ -39,7 +39,7 @@
|
|||
"humanize-duration": "^3.15.0",
|
||||
"io-ts": "^2.0.0",
|
||||
"js-yaml": "^3.13.1",
|
||||
"knub": "^30.0.0-beta.30",
|
||||
"knub": "^30.0.0-beta.35",
|
||||
"knub-command-manager": "^8.1.2",
|
||||
"last-commit-log": "^2.1.0",
|
||||
"lodash.chunk": "^4.2.0",
|
||||
|
@ -65,7 +65,7 @@
|
|||
"tmp": "0.0.33",
|
||||
"tsconfig-paths": "^3.9.0",
|
||||
"twemoji": "^12.1.4",
|
||||
"typeorm": "^0.2.14",
|
||||
"typeorm": "^0.2.31",
|
||||
"uuid": "^3.3.2",
|
||||
"yawn-yaml": "github:dragory/yawn-yaml#string-number-fix-build",
|
||||
"zlib-sync": "^0.1.7"
|
||||
|
|
|
@ -7,6 +7,8 @@ export enum ERRORS {
|
|||
NO_USER_NOTIFICATION_CHANNEL,
|
||||
INVALID_USER_NOTIFICATION_CHANNEL,
|
||||
INVALID_USER,
|
||||
INVALID_MUTE_ROLE_ID,
|
||||
MUTE_ROLE_ABOVE_ZEP,
|
||||
}
|
||||
|
||||
export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
|
||||
|
@ -16,6 +18,8 @@ export const RECOVERABLE_PLUGIN_ERROR_MESSAGES = {
|
|||
[ERRORS.NO_USER_NOTIFICATION_CHANNEL]: "No user notify channel specified",
|
||||
[ERRORS.INVALID_USER_NOTIFICATION_CHANNEL]: "Invalid user notify channel specified",
|
||||
[ERRORS.INVALID_USER]: "Invalid user",
|
||||
[ERRORS.INVALID_MUTE_ROLE_ID]: "Specified mute role is not invalid",
|
||||
[ERRORS.MUTE_ROLE_ABOVE_ZEP]: "Specified mute role is above Zeppelin in the role hierarchy",
|
||||
};
|
||||
|
||||
export class RecoverablePluginError extends Error {
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
"MEMBER_NICK_CHANGE": "✏ {userMention(member)}: nickname changed from **{oldNick}** to **{newNick}**",
|
||||
"MEMBER_USERNAME_CHANGE": "✏ {userMention(user)}: username changed from **{oldName}** to **{newName}**",
|
||||
"MEMBER_RESTORE": "💿 Restored {restoredData} for {userMention(member)} on rejoin",
|
||||
"MEMBER_TIMED_BAN": "🔨 {userMention(user)} was tempbanned by {userMention(mod)} for {banTime}",
|
||||
"MEMBER_TIMED_UNBAN": "🔓 User (`{userId}`) was automatically unbanned by {userMention(mod)} after a tempban for {banTime}",
|
||||
|
||||
"CHANNEL_CREATE": "🖊 Channel {channelMention(channel)} was created",
|
||||
"CHANNEL_DELETE": "🗑 Channel {channelMention(channel)} was deleted",
|
||||
|
@ -38,6 +40,7 @@
|
|||
"VOICE_CHANNEL_MOVE": "🎙 ↔ {userMention(member)} moved from **{oldChannel.name}** to **{newChannel.name}**",
|
||||
"VOICE_CHANNEL_LEAVE": "🎙 🔴 {userMention(member)} left **{channel.name}**",
|
||||
"VOICE_CHANNEL_FORCE_MOVE": "\uD83C\uDF99 ✍ {userMention(member)} was moved from **{oldChannel.name}** to **{newChannel.name}** by {userMention(mod)}",
|
||||
"VOICE_CHANNEL_FORCE_DISCONNECT": "\uD83C\uDF99 🚫 {userMention(member)} was forcefully disconnected from **{oldChannel.name}** by {userMention(mod)}",
|
||||
|
||||
"COMMAND": "🤖 {userMention(member)} used command in {channelMention(channel)}:\n`{command}`",
|
||||
|
||||
|
@ -49,6 +52,7 @@
|
|||
"CASE_CREATE": "✏ {userMention(mod)} manually created new **{caseType}** case (#{caseNum})",
|
||||
"CASE_DELETE": "✂️ **Case #{case.case_number}** was deleted by {userMention(mod)}",
|
||||
|
||||
"MASSUNBAN": "⚒ {userMention(mod)} mass-unbanned {count} users",
|
||||
"MASSBAN": "⚒ {userMention(mod)} massbanned {count} users",
|
||||
"MASSMUTE": "📢🚫 {userMention(mod)} massmuted {count} users",
|
||||
|
||||
|
|
501
backend/src/data/GuildCounters.ts
Normal file
501
backend/src/data/GuildCounters.ts
Normal file
|
@ -0,0 +1,501 @@
|
|||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { getRepository, In, IsNull, LessThan, Not, Repository } from "typeorm";
|
||||
import { Counter } from "./entities/Counter";
|
||||
import { CounterValue } from "./entities/CounterValue";
|
||||
import { CounterTrigger, TRIGGER_COMPARISON_OPS, TriggerComparisonOp } from "./entities/CounterTrigger";
|
||||
import { CounterTriggerState } from "./entities/CounterTriggerState";
|
||||
import moment from "moment-timezone";
|
||||
import { DAYS, DBDateFormat, HOURS, MINUTES } from "../utils";
|
||||
import { connection } from "./db";
|
||||
|
||||
const comparisonStringRegex = new RegExp(`^(${TRIGGER_COMPARISON_OPS.join("|")})([1-9]\\d*)$`);
|
||||
|
||||
/**
|
||||
* @return Parsed comparison op and value, or null if the comparison string was invalid
|
||||
*/
|
||||
export function parseCondition(str: string): [TriggerComparisonOp, number] | null {
|
||||
const matches = str.match(comparisonStringRegex);
|
||||
return matches ? [matches[1] as TriggerComparisonOp, parseInt(matches[2], 10)] : null;
|
||||
}
|
||||
|
||||
export function buildConditionString(comparisonOp: TriggerComparisonOp, comparisonValue: number): string {
|
||||
return `${comparisonOp}${comparisonValue}`;
|
||||
}
|
||||
|
||||
function isValidComparisonOp(op: string): boolean {
|
||||
return TRIGGER_COMPARISON_OPS.includes(op as any);
|
||||
}
|
||||
|
||||
const REVERSE_OPS: Record<TriggerComparisonOp, TriggerComparisonOp> = {
|
||||
"=": "!=",
|
||||
"!=": "=",
|
||||
">": "<=",
|
||||
"<": ">=",
|
||||
">=": "<",
|
||||
"<=": ">",
|
||||
};
|
||||
|
||||
function getReverseComparisonOp(op: TriggerComparisonOp): TriggerComparisonOp {
|
||||
return REVERSE_OPS[op];
|
||||
}
|
||||
|
||||
const DELETE_UNUSED_COUNTERS_AFTER = 1 * DAYS;
|
||||
const DELETE_UNUSED_COUNTER_TRIGGERS_AFTER = 1 * DAYS;
|
||||
|
||||
const MAX_COUNTER_VALUE = 2147483647; // 2^31-1, for MySQL INT
|
||||
|
||||
async function deleteCountersMarkedToBeDeleted(): Promise<void> {
|
||||
await getRepository(Counter)
|
||||
.createQueryBuilder()
|
||||
.where("delete_at <= NOW()")
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
|
||||
async function deleteTriggersMarkedToBeDeleted(): Promise<void> {
|
||||
await getRepository(CounterTrigger)
|
||||
.createQueryBuilder()
|
||||
.where("delete_at <= NOW()")
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
|
||||
setInterval(deleteCountersMarkedToBeDeleted, 1 * HOURS);
|
||||
setInterval(deleteTriggersMarkedToBeDeleted, 1 * HOURS);
|
||||
|
||||
setTimeout(deleteCountersMarkedToBeDeleted, 1 * MINUTES);
|
||||
setTimeout(deleteTriggersMarkedToBeDeleted, 1 * MINUTES);
|
||||
|
||||
export class GuildCounters extends BaseGuildRepository {
|
||||
private counters: Repository<Counter>;
|
||||
private counterValues: Repository<CounterValue>;
|
||||
private counterTriggers: Repository<CounterTrigger>;
|
||||
private counterTriggerStates: Repository<CounterTriggerState>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.counters = getRepository(Counter);
|
||||
this.counterValues = getRepository(CounterValue);
|
||||
this.counterTriggers = getRepository(CounterTrigger);
|
||||
this.counterTriggerStates = getRepository(CounterTriggerState);
|
||||
}
|
||||
|
||||
async findOrCreateCounter(name: string, perChannel: boolean, perUser: boolean): Promise<Counter> {
|
||||
const existing = await this.counters.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// If the existing counter's properties match the ones we're looking for, return it.
|
||||
// Otherwise, delete the existing counter and re-create it with the proper properties.
|
||||
if (existing.per_channel === perChannel && existing.per_user === perUser) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
await this.counters.delete({ id: existing.id });
|
||||
}
|
||||
|
||||
const insertResult = await this.counters.insert({
|
||||
guild_id: this.guildId,
|
||||
name,
|
||||
per_channel: perChannel,
|
||||
per_user: perUser,
|
||||
last_decay_at: moment.utc().format(DBDateFormat),
|
||||
});
|
||||
|
||||
return (await this.counters.findOne({
|
||||
where: {
|
||||
id: insertResult.identifiers[0].id,
|
||||
},
|
||||
}))!;
|
||||
}
|
||||
|
||||
async markUnusedCountersToBeDeleted(idsToKeep: number[]): Promise<void> {
|
||||
if (idsToKeep.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteAt = moment
|
||||
.utc()
|
||||
.add(DELETE_UNUSED_COUNTERS_AFTER, "ms")
|
||||
.format(DBDateFormat);
|
||||
await this.counters.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
id: Not(In(idsToKeep)),
|
||||
delete_at: IsNull(),
|
||||
},
|
||||
{
|
||||
delete_at: deleteAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async deleteCountersMarkedToBeDeleted(): Promise<void> {
|
||||
await this.counters
|
||||
.createQueryBuilder()
|
||||
.where("delete_at <= NOW()")
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
|
||||
async changeCounterValue(id: number, channelId: string | null, userId: string | null, change: number): Promise<void> {
|
||||
if (typeof change !== "number" || Number.isNaN(change) || !Number.isFinite(change)) {
|
||||
throw new Error(`changeCounterValue() change argument must be a number`);
|
||||
}
|
||||
|
||||
channelId = channelId || "0";
|
||||
userId = userId || "0";
|
||||
|
||||
const rawUpdate =
|
||||
change >= 0 ? `value = LEAST(value + ${change}, ${MAX_COUNTER_VALUE})` : `value = GREATEST(value ${change}, 0)`;
|
||||
|
||||
await this.counterValues.query(
|
||||
`
|
||||
INSERT INTO counter_values (counter_id, channel_id, user_id, value)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE ${rawUpdate}
|
||||
`,
|
||||
[id, channelId, userId, Math.max(change, 0)],
|
||||
);
|
||||
}
|
||||
|
||||
async setCounterValue(id: number, channelId: string | null, userId: string | null, value: number): Promise<void> {
|
||||
if (typeof value !== "number" || Number.isNaN(value) || !Number.isFinite(value)) {
|
||||
throw new Error(`setCounterValue() value argument must be a number`);
|
||||
}
|
||||
|
||||
channelId = channelId || "0";
|
||||
userId = userId || "0";
|
||||
|
||||
value = Math.max(value, 0);
|
||||
|
||||
await this.counterValues.query(
|
||||
`
|
||||
INSERT INTO counter_values (counter_id, channel_id, user_id, value)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE value = ?
|
||||
`,
|
||||
[id, channelId, userId, value, value],
|
||||
);
|
||||
}
|
||||
|
||||
async decay(id: number, decayPeriodMs: number, decayAmount: number) {
|
||||
const counter = (await this.counters.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
}))!;
|
||||
|
||||
const diffFromLastDecayMs = moment.utc().diff(moment.utc(counter.last_decay_at!), "ms");
|
||||
if (diffFromLastDecayMs < decayPeriodMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const decayAmountToApply = Math.round((diffFromLastDecayMs / decayPeriodMs) * decayAmount);
|
||||
if (decayAmountToApply === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate new last_decay_at based on the rounded decay amount we applied. This makes it so that over time, the decayed amount will stay accurate, even if we round some here.
|
||||
const newLastDecayDate = moment
|
||||
.utc(counter.last_decay_at)
|
||||
.add((decayAmountToApply / decayAmount) * decayPeriodMs, "ms")
|
||||
.format(DBDateFormat);
|
||||
|
||||
const rawUpdate =
|
||||
decayAmountToApply >= 0
|
||||
? `GREATEST(value - ${decayAmountToApply}, 0)`
|
||||
: `LEAST(value + ${Math.abs(decayAmountToApply)}, ${MAX_COUNTER_VALUE})`;
|
||||
|
||||
await this.counterValues.update(
|
||||
{
|
||||
counter_id: id,
|
||||
},
|
||||
{
|
||||
value: () => rawUpdate,
|
||||
},
|
||||
);
|
||||
|
||||
await this.counters.update(
|
||||
{
|
||||
id,
|
||||
},
|
||||
{
|
||||
last_decay_at: newLastDecayDate,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async markAllTriggersTobeDeleted() {
|
||||
const deleteAt = moment
|
||||
.utc()
|
||||
.add(DELETE_UNUSED_COUNTER_TRIGGERS_AFTER, "ms")
|
||||
.format(DBDateFormat);
|
||||
await this.counterTriggers.update(
|
||||
{},
|
||||
{
|
||||
delete_at: deleteAt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async deleteTriggersMarkedToBeDeleted(): Promise<void> {
|
||||
await this.counterTriggers
|
||||
.createQueryBuilder()
|
||||
.where("delete_at <= NOW()")
|
||||
.delete()
|
||||
.execute();
|
||||
}
|
||||
|
||||
async initCounterTrigger(
|
||||
counterId: number,
|
||||
comparisonOp: TriggerComparisonOp,
|
||||
comparisonValue: number,
|
||||
): Promise<CounterTrigger> {
|
||||
if (!isValidComparisonOp(comparisonOp)) {
|
||||
throw new Error(`Invalid comparison op: ${comparisonOp}`);
|
||||
}
|
||||
|
||||
if (typeof comparisonValue !== "number") {
|
||||
throw new Error(`Invalid comparison value: ${comparisonValue}`);
|
||||
}
|
||||
|
||||
return connection.transaction(async entityManager => {
|
||||
const existing = await entityManager.findOne(CounterTrigger, {
|
||||
counter_id: counterId,
|
||||
comparison_op: comparisonOp,
|
||||
comparison_value: comparisonValue,
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Since all existing triggers are marked as to-be-deleted before they are re-initialized, this needs to be reset
|
||||
await entityManager.update(CounterTrigger, existing.id, { delete_at: null });
|
||||
return existing;
|
||||
}
|
||||
|
||||
const insertResult = await entityManager.insert(CounterTrigger, {
|
||||
counter_id: counterId,
|
||||
comparison_op: comparisonOp,
|
||||
comparison_value: comparisonValue,
|
||||
});
|
||||
|
||||
return (await entityManager.findOne(CounterTrigger, insertResult.identifiers[0].id))!;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a counter value with the given parameters triggers the specified comparison for the specified counter.
|
||||
* If it does, mark this comparison for these parameters as triggered.
|
||||
* Note that if this comparison for these parameters was already triggered previously, this function will return false.
|
||||
* This means that a specific comparison for the specific parameters specified will only trigger *once* until the reverse trigger is triggered.
|
||||
*
|
||||
* @param counterId
|
||||
* @param comparisonOp
|
||||
* @param comparisonValue
|
||||
* @param userId
|
||||
* @param channelId
|
||||
* @return Whether the given parameters newly triggered the given comparison
|
||||
*/
|
||||
async checkForTrigger(
|
||||
counterTrigger: CounterTrigger,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
): Promise<boolean> {
|
||||
channelId = channelId || "0";
|
||||
userId = userId || "0";
|
||||
|
||||
return connection.transaction(async entityManager => {
|
||||
const previouslyTriggered = await entityManager.findOne(CounterTriggerState, {
|
||||
trigger_id: counterTrigger.id,
|
||||
user_id: userId!,
|
||||
channel_id: channelId!,
|
||||
});
|
||||
|
||||
if (previouslyTriggered) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const matchingValue = await entityManager
|
||||
.createQueryBuilder(CounterValue, "cv")
|
||||
.leftJoin(
|
||||
CounterTriggerState,
|
||||
"triggerStates",
|
||||
"triggerStates.trigger_id = :triggerId AND triggerStates.user_id = cv.user_id AND triggerStates.channel_id = cv.channel_id",
|
||||
{ triggerId: counterTrigger.id },
|
||||
)
|
||||
.where(`cv.value ${counterTrigger.comparison_op} :value`, { value: counterTrigger.comparison_value })
|
||||
.andWhere(`cv.counter_id = :counterId`, { counterId: counterTrigger.counter_id })
|
||||
.andWhere("cv.channel_id = :channelId AND cv.user_id = :userId", { channelId, userId })
|
||||
.andWhere("triggerStates.id IS NULL")
|
||||
.getOne();
|
||||
|
||||
if (matchingValue) {
|
||||
await entityManager.insert(CounterTriggerState, {
|
||||
trigger_id: counterTrigger.id,
|
||||
user_id: userId!,
|
||||
channel_id: channelId!,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any counter values of the specified counter match the specified comparison.
|
||||
* Like checkForTrigger(), this can only happen *once* per unique counter value parameters until the reverse trigger is triggered for those values.
|
||||
*
|
||||
* @return Counter value parameters that triggered the condition
|
||||
*/
|
||||
async checkAllValuesForTrigger(
|
||||
counterTrigger: CounterTrigger,
|
||||
): Promise<Array<{ channelId: string; userId: string }>> {
|
||||
return connection.transaction(async entityManager => {
|
||||
const matchingValues = await entityManager
|
||||
.createQueryBuilder(CounterValue, "cv")
|
||||
.leftJoin(
|
||||
CounterTriggerState,
|
||||
"triggerStates",
|
||||
"triggerStates.trigger_id = :triggerId AND triggerStates.user_id = cv.user_id AND triggerStates.channel_id = cv.channel_id",
|
||||
{ triggerId: counterTrigger.id },
|
||||
)
|
||||
.where(`cv.value ${counterTrigger.comparison_op} :value`, { value: counterTrigger.comparison_value })
|
||||
.andWhere(`cv.counter_id = :counterId`, { counterId: counterTrigger.counter_id })
|
||||
.andWhere("triggerStates.id IS NULL")
|
||||
.getMany();
|
||||
|
||||
if (matchingValues.length) {
|
||||
await entityManager.insert(
|
||||
CounterTriggerState,
|
||||
matchingValues.map(row => ({
|
||||
trigger_id: counterTrigger.id,
|
||||
channelId: row.channel_id,
|
||||
userId: row.user_id,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
return matchingValues.map(row => ({
|
||||
channelId: row.channel_id,
|
||||
userId: row.user_id,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a counter value with the given parameters *no longer* matches the specified comparison, and thus triggers a "reverse trigger".
|
||||
* Like checkForTrigger(), this can only happen *once* until the comparison is triggered normally again.
|
||||
*
|
||||
* @param counterId
|
||||
* @param comparisonOp
|
||||
* @param comparisonValue
|
||||
* @param userId
|
||||
* @param channelId
|
||||
* @return Whether the given parameters triggered a reverse trigger for the given comparison
|
||||
*/
|
||||
async checkForReverseTrigger(
|
||||
counterTrigger: CounterTrigger,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
): Promise<boolean> {
|
||||
channelId = channelId || "0";
|
||||
userId = userId || "0";
|
||||
|
||||
return connection.transaction(async entityManager => {
|
||||
const reverseOp = getReverseComparisonOp(counterTrigger.comparison_op);
|
||||
const matchingValue = await entityManager
|
||||
.createQueryBuilder(CounterValue, "cv")
|
||||
.innerJoin(
|
||||
CounterTriggerState,
|
||||
"triggerStates",
|
||||
"triggerStates.trigger_id = :triggerId AND triggerStates.user_id = cv.user_id AND triggerStates.channel_id = cv.channel_id",
|
||||
{ triggerId: counterTrigger.id },
|
||||
)
|
||||
.where(`cv.value ${reverseOp} :value`, { value: counterTrigger.comparison_value })
|
||||
.andWhere(`cv.counter_id = :counterId`, { counterId: counterTrigger.counter_id })
|
||||
.andWhere(`cv.channel_id = :channelId AND cv.user_id = :userId`, { channelId, userId })
|
||||
.getOne();
|
||||
|
||||
if (matchingValue) {
|
||||
await entityManager.delete(CounterTriggerState, {
|
||||
trigger_id: counterTrigger.id,
|
||||
user_id: userId!,
|
||||
channel_id: channelId!,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any counter values of the specified counter *no longer* match the specified comparison, and thus triggers a "reverse trigger" for those values.
|
||||
* Like checkForTrigger(), this can only happen *once* per unique counter value parameters until the comparison is triggered normally again.
|
||||
*
|
||||
* @return Counter value parameters that triggered a reverse trigger
|
||||
*/
|
||||
async checkAllValuesForReverseTrigger(
|
||||
counterTrigger: CounterTrigger,
|
||||
): Promise<Array<{ channelId: string; userId: string }>> {
|
||||
return connection.transaction(async entityManager => {
|
||||
const reverseOp = getReverseComparisonOp(counterTrigger.comparison_op);
|
||||
const matchingValues: Array<{
|
||||
id: string;
|
||||
triggerStateId: string;
|
||||
user_id: string;
|
||||
channel_id: string;
|
||||
}> = await entityManager
|
||||
.createQueryBuilder(CounterValue, "cv")
|
||||
.innerJoin(
|
||||
CounterTriggerState,
|
||||
"triggerStates",
|
||||
"triggerStates.trigger_id = :triggerId AND triggerStates.user_id = cv.user_id AND triggerStates.channel_id = cv.channel_id",
|
||||
{ triggerId: counterTrigger.id },
|
||||
)
|
||||
.where(`cv.value ${reverseOp} :value`, { value: counterTrigger.comparison_value })
|
||||
.andWhere(`cv.counter_id = :counterId`, { counterId: counterTrigger.counter_id })
|
||||
.select([
|
||||
"cv.id AS id",
|
||||
"cv.user_id AS user_id",
|
||||
"cv.channel_id AS channel_id",
|
||||
"triggerStates.id AS triggerStateId",
|
||||
])
|
||||
.getRawMany();
|
||||
|
||||
if (matchingValues.length) {
|
||||
await entityManager.delete(CounterTriggerState, {
|
||||
id: In(matchingValues.map(v => v.triggerStateId)),
|
||||
});
|
||||
}
|
||||
|
||||
return matchingValues.map(row => ({
|
||||
channelId: row.channel_id,
|
||||
userId: row.user_id,
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async getCurrentValue(
|
||||
counterId: number,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
): Promise<number | undefined> {
|
||||
const value = await this.counterValues.findOne({
|
||||
where: {
|
||||
counter_id: counterId,
|
||||
channel_id: channelId || "0",
|
||||
user_id: userId || "0",
|
||||
},
|
||||
});
|
||||
|
||||
return value?.value;
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
return mute != null;
|
||||
}
|
||||
|
||||
async addMute(userId, expiryTime): Promise<Mute> {
|
||||
async addMute(userId, expiryTime, rolesToRestore?: string[]): Promise<Mute> {
|
||||
const expiresAt = expiryTime
|
||||
? moment
|
||||
.utc()
|
||||
|
@ -46,12 +46,13 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
expires_at: expiresAt,
|
||||
roles_to_restore: rolesToRestore ?? [],
|
||||
});
|
||||
|
||||
return (await this.mutes.findOne({ where: result.identifiers[0] }))!;
|
||||
}
|
||||
|
||||
async updateExpiryTime(userId, newExpiryTime) {
|
||||
async updateExpiryTime(userId, newExpiryTime, rolesToRestore?: string[]) {
|
||||
const expiresAt = newExpiryTime
|
||||
? moment
|
||||
.utc()
|
||||
|
@ -59,6 +60,18 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
.format("YYYY-MM-DD HH:mm:ss")
|
||||
: null;
|
||||
|
||||
if (rolesToRestore && rolesToRestore.length) {
|
||||
return this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
expires_at: expiresAt,
|
||||
roles_to_restore: rolesToRestore,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return this.mutes.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
|
@ -69,6 +82,7 @@ export class GuildMutes extends BaseGuildRepository {
|
|||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getActiveMutes(): Promise<Mute[]> {
|
||||
return this.mutes
|
||||
|
|
75
backend/src/data/GuildTempbans.ts
Normal file
75
backend/src/data/GuildTempbans.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
import moment from "moment-timezone";
|
||||
import { Mute } from "./entities/Mute";
|
||||
import { BaseGuildRepository } from "./BaseGuildRepository";
|
||||
import { Brackets, getRepository, Repository } from "typeorm";
|
||||
import { Tempban } from "./entities/Tempban";
|
||||
|
||||
export class GuildTempbans extends BaseGuildRepository {
|
||||
private tempbans: Repository<Tempban>;
|
||||
|
||||
constructor(guildId) {
|
||||
super(guildId);
|
||||
this.tempbans = getRepository(Tempban);
|
||||
}
|
||||
|
||||
async getExpiredTempbans(): Promise<Tempban[]> {
|
||||
return this.tempbans
|
||||
.createQueryBuilder("mutes")
|
||||
.where("guild_id = :guild_id", { guild_id: this.guildId })
|
||||
.andWhere("expires_at IS NOT NULL")
|
||||
.andWhere("expires_at <= NOW()")
|
||||
.getMany();
|
||||
}
|
||||
|
||||
async findExistingTempbanForUserId(userId: string): Promise<Tempban | undefined> {
|
||||
return this.tempbans.findOne({
|
||||
where: {
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async addTempban(userId, expiryTime, modId): Promise<Tempban> {
|
||||
const expiresAt = moment
|
||||
.utc()
|
||||
.add(expiryTime, "ms")
|
||||
.format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
const result = await this.tempbans.insert({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
mod_id: modId,
|
||||
expires_at: expiresAt,
|
||||
created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss"),
|
||||
});
|
||||
|
||||
return (await this.tempbans.findOne({ where: result.identifiers[0] }))!;
|
||||
}
|
||||
|
||||
async updateExpiryTime(userId, newExpiryTime, modId) {
|
||||
const expiresAt = moment
|
||||
.utc()
|
||||
.add(newExpiryTime, "ms")
|
||||
.format("YYYY-MM-DD HH:mm:ss");
|
||||
|
||||
return this.tempbans.update(
|
||||
{
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
},
|
||||
{
|
||||
created_at: moment.utc().format("YYYY-MM-DD HH:mm:ss"),
|
||||
expires_at: expiresAt,
|
||||
mod_id: modId,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async clear(userId) {
|
||||
await this.tempbans.delete({
|
||||
guild_id: this.guildId,
|
||||
user_id: userId,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -39,17 +39,21 @@ export enum LogType {
|
|||
|
||||
CASE_CREATE,
|
||||
|
||||
MASSUNBAN,
|
||||
MASSBAN,
|
||||
MASSMUTE,
|
||||
|
||||
MEMBER_TIMED_MUTE,
|
||||
MEMBER_TIMED_UNMUTE,
|
||||
MEMBER_TIMED_BAN,
|
||||
MEMBER_TIMED_UNBAN,
|
||||
|
||||
MEMBER_JOIN_WITH_PRIOR_RECORDS,
|
||||
OTHER_SPAM_DETECTED,
|
||||
|
||||
MEMBER_ROLE_CHANGES,
|
||||
VOICE_CHANNEL_FORCE_MOVE,
|
||||
VOICE_CHANNEL_FORCE_DISCONNECT,
|
||||
|
||||
CASE_UPDATE,
|
||||
|
||||
|
|
25
backend/src/data/entities/Counter.ts
Normal file
25
backend/src/data/entities/Counter.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
@Entity("counters")
|
||||
export class Counter {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column()
|
||||
per_channel: boolean;
|
||||
|
||||
@Column()
|
||||
per_user: boolean;
|
||||
|
||||
@Column()
|
||||
last_decay_at: string;
|
||||
|
||||
@Column({ type: "datetime", nullable: true })
|
||||
delete_at: string | null;
|
||||
}
|
23
backend/src/data/entities/CounterTrigger.ts
Normal file
23
backend/src/data/entities/CounterTrigger.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from "typeorm";
|
||||
|
||||
export const TRIGGER_COMPARISON_OPS = ["=", "!=", ">", "<", ">=", "<="] as const;
|
||||
|
||||
export type TriggerComparisonOp = typeof TRIGGER_COMPARISON_OPS[number];
|
||||
|
||||
@Entity("counter_triggers")
|
||||
export class CounterTrigger {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
counter_id: number;
|
||||
|
||||
@Column({ type: "varchar" })
|
||||
comparison_op: TriggerComparisonOp;
|
||||
|
||||
@Column()
|
||||
comparison_value: number;
|
||||
|
||||
@Column({ type: "datetime", nullable: true })
|
||||
delete_at: string | null;
|
||||
}
|
17
backend/src/data/entities/CounterTriggerState.ts
Normal file
17
backend/src/data/entities/CounterTriggerState.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("counter_trigger_states")
|
||||
export class CounterTriggerState {
|
||||
@Column({ type: "bigint", generated: "increment" })
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
trigger_id: number;
|
||||
|
||||
@Column({ type: "bigint" })
|
||||
channel_id: string;
|
||||
|
||||
@Column({ type: "bigint" })
|
||||
user_id: string;
|
||||
}
|
20
backend/src/data/entities/CounterValue.ts
Normal file
20
backend/src/data/entities/CounterValue.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("counter_values")
|
||||
export class CounterValue {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
counter_id: number;
|
||||
|
||||
@Column({ type: "bigint" })
|
||||
channel_id: string;
|
||||
|
||||
@Column({ type: "bigint" })
|
||||
user_id: string;
|
||||
|
||||
@Column()
|
||||
value: number;
|
||||
}
|
|
@ -15,4 +15,6 @@ export class Mute {
|
|||
@Column({ type: String, nullable: true }) expires_at: string | null;
|
||||
|
||||
@Column() case_id: number;
|
||||
|
||||
@Column("simple-array") roles_to_restore: string[];
|
||||
}
|
||||
|
|
18
backend/src/data/entities/Tempban.ts
Normal file
18
backend/src/data/entities/Tempban.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Column, Entity, PrimaryColumn } from "typeorm";
|
||||
|
||||
@Entity("tempbans")
|
||||
export class Tempban {
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
guild_id: string;
|
||||
|
||||
@Column()
|
||||
@PrimaryColumn()
|
||||
user_id: string;
|
||||
|
||||
@Column() mod_id: string;
|
||||
|
||||
@Column() created_at: string;
|
||||
|
||||
@Column() expires_at: string;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner, TableColumn } from "typeorm";
|
||||
|
||||
export class CreateRestoredRolesColumn1608608903570 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.addColumn(
|
||||
"mutes",
|
||||
new TableColumn({
|
||||
name: "roles_to_restore",
|
||||
type: "text",
|
||||
isNullable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.dropColumn("mutes", "roles_to_restore");
|
||||
}
|
||||
}
|
45
backend/src/migrations/1608753440716-CreateTempBansTable.ts
Normal file
45
backend/src/migrations/1608753440716-CreateTempBansTable.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";
|
||||
|
||||
export class CreateTempBansTable1608753440716 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
const table = await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "tempbans",
|
||||
columns: [
|
||||
{
|
||||
name: "guild_id",
|
||||
type: "bigint",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "user_id",
|
||||
type: "bigint",
|
||||
isPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "mod_id",
|
||||
type: "bigint",
|
||||
},
|
||||
{
|
||||
name: "created_at",
|
||||
type: "datetime",
|
||||
},
|
||||
{
|
||||
name: "expires_at",
|
||||
type: "datetime",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
queryRunner.createIndex(
|
||||
"tempbans",
|
||||
new TableIndex({
|
||||
columnNames: ["expires_at"],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.dropTable("tempbans");
|
||||
}
|
||||
}
|
203
backend/src/migrations/1612010765767-CreateCounterTables.ts
Normal file
203
backend/src/migrations/1612010765767-CreateCounterTables.ts
Normal file
|
@ -0,0 +1,203 @@
|
|||
import { MigrationInterface, QueryRunner, Table } from "typeorm";
|
||||
|
||||
export class CreateCounterTables1612010765767 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "counters",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "int",
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: "increment",
|
||||
},
|
||||
{
|
||||
name: "guild_id",
|
||||
type: "bigint",
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
type: "varchar",
|
||||
length: "255",
|
||||
},
|
||||
{
|
||||
name: "per_channel",
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
name: "per_user",
|
||||
type: "boolean",
|
||||
},
|
||||
{
|
||||
name: "last_decay_at",
|
||||
type: "datetime",
|
||||
},
|
||||
{
|
||||
name: "delete_at",
|
||||
type: "datetime",
|
||||
isNullable: true,
|
||||
default: null,
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
columnNames: ["guild_id", "name"],
|
||||
isUnique: true,
|
||||
},
|
||||
{
|
||||
columnNames: ["delete_at"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "counter_values",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "bigint",
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: "increment",
|
||||
},
|
||||
{
|
||||
name: "counter_id",
|
||||
type: "int",
|
||||
},
|
||||
{
|
||||
name: "channel_id",
|
||||
type: "bigint",
|
||||
},
|
||||
{
|
||||
name: "user_id",
|
||||
type: "bigint",
|
||||
},
|
||||
{
|
||||
name: "value",
|
||||
type: "int",
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
columnNames: ["counter_id", "channel_id", "user_id"],
|
||||
isUnique: true,
|
||||
},
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
columnNames: ["counter_id"],
|
||||
referencedTableName: "counters",
|
||||
referencedColumnNames: ["id"],
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "CASCADE",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "counter_triggers",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "int",
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: "increment",
|
||||
},
|
||||
{
|
||||
name: "counter_id",
|
||||
type: "int",
|
||||
},
|
||||
{
|
||||
name: "comparison_op",
|
||||
type: "varchar",
|
||||
length: "16",
|
||||
},
|
||||
{
|
||||
name: "comparison_value",
|
||||
type: "int",
|
||||
},
|
||||
{
|
||||
name: "delete_at",
|
||||
type: "datetime",
|
||||
isNullable: true,
|
||||
default: null,
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
columnNames: ["counter_id", "comparison_op", "comparison_value"],
|
||||
isUnique: true,
|
||||
},
|
||||
{
|
||||
columnNames: ["delete_at"],
|
||||
},
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
columnNames: ["counter_id"],
|
||||
referencedTableName: "counters",
|
||||
referencedColumnNames: ["id"],
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "CASCADE",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "counter_trigger_states",
|
||||
columns: [
|
||||
{
|
||||
name: "id",
|
||||
type: "bigint",
|
||||
isPrimary: true,
|
||||
isGenerated: true,
|
||||
generationStrategy: "increment",
|
||||
},
|
||||
{
|
||||
name: "trigger_id",
|
||||
type: "int",
|
||||
},
|
||||
{
|
||||
name: "channel_id",
|
||||
type: "bigint",
|
||||
},
|
||||
{
|
||||
name: "user_id",
|
||||
type: "bigint",
|
||||
},
|
||||
],
|
||||
indices: [
|
||||
{
|
||||
columnNames: ["trigger_id", "channel_id", "user_id"],
|
||||
isUnique: true,
|
||||
},
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
columnNames: ["trigger_id"],
|
||||
referencedTableName: "counter_triggers",
|
||||
referencedColumnNames: ["id"],
|
||||
onDelete: "CASCADE",
|
||||
onUpdate: "CASCADE",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<any> {
|
||||
await queryRunner.dropTable("counter_trigger_states");
|
||||
await queryRunner.dropTable("counter_triggers");
|
||||
await queryRunner.dropTable("counter_values");
|
||||
await queryRunner.dropTable("counters");
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* @file Utility functions that are plugin-instance-specific (i.e. use PluginData)
|
||||
*/
|
||||
|
||||
import { Member } from "eris";
|
||||
import { AdvancedMessageContent, AllowedMentions, GuildTextableChannel, Member, Message, TextableChannel } from "eris";
|
||||
import { CommandContext, configUtils, ConfigValidationError, GuildPluginData, helpers, PluginOptions } from "knub";
|
||||
import { decodeAndValidateStrict, StrictValidationError, validate } from "./validatorUtils";
|
||||
import { deepKeyIntersect, errorMessage, successMessage, tDeepPartial, tNullable } from "./utils";
|
||||
|
@ -137,17 +137,47 @@ export function getPluginConfigPreprocessor(
|
|||
};
|
||||
}
|
||||
|
||||
export function sendSuccessMessage(pluginData: AnyPluginData<any>, channel, body) {
|
||||
export function sendSuccessMessage(
|
||||
pluginData: AnyPluginData<any>,
|
||||
channel: TextableChannel,
|
||||
body: string,
|
||||
allowedMentions?: AllowedMentions,
|
||||
): Promise<Message | undefined> {
|
||||
const emoji = pluginData.fullConfig.success_emoji || undefined;
|
||||
return channel.createMessage(successMessage(body, emoji)).catch(err => {
|
||||
logger.warn(`Failed to send success message to ${channel.id} (${channel.guild?.id}): ${err.code} ${err.message}`);
|
||||
const formattedBody = successMessage(body, emoji);
|
||||
const content: AdvancedMessageContent = allowedMentions
|
||||
? { content: formattedBody, allowedMentions }
|
||||
: { content: formattedBody };
|
||||
return channel
|
||||
.createMessage(content) // Force line break
|
||||
.catch(err => {
|
||||
const channelInfo = (channel as GuildTextableChannel).guild
|
||||
? `${channel.id} (${(channel as GuildTextableChannel).guild.id})`
|
||||
: `${channel.id}`;
|
||||
logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
export function sendErrorMessage(pluginData: AnyPluginData<any>, channel, body) {
|
||||
export function sendErrorMessage(
|
||||
pluginData: AnyPluginData<any>,
|
||||
channel: TextableChannel,
|
||||
body: string,
|
||||
allowedMentions?: AllowedMentions,
|
||||
): Promise<Message | undefined> {
|
||||
const emoji = pluginData.fullConfig.error_emoji || undefined;
|
||||
return channel.createMessage(errorMessage(body, emoji)).catch(err => {
|
||||
logger.warn(`Failed to send error message to ${channel.id} (${channel.guild?.id}): ${err.code} ${err.message}`);
|
||||
const formattedBody = errorMessage(body, emoji);
|
||||
const content: AdvancedMessageContent = allowedMentions
|
||||
? { content: formattedBody, allowedMentions }
|
||||
: { content: formattedBody };
|
||||
return channel
|
||||
.createMessage(content) // Force line break
|
||||
.catch(err => {
|
||||
const channelInfo = (channel as GuildTextableChannel).guild
|
||||
? `${channel.id} (${(channel as GuildTextableChannel).guild.id})`
|
||||
: `${channel.id}`;
|
||||
logger.warn(`Failed to send error message to ${channelInfo}): ${err.code} ${err.message}`);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,12 @@ import { LogType } from "../../data/LogType";
|
|||
import { logger } from "../../logger";
|
||||
import { discardRegExpRunner, getRegExpRunner } from "../../regExpRunners";
|
||||
import { RunAutomodOnMemberUpdate } from "./events/RunAutomodOnMemberUpdate";
|
||||
import { CountersPlugin } from "../Counters/CountersPlugin";
|
||||
import { parseCondition } from "../../data/GuildCounters";
|
||||
import { runAutomodOnCounterTrigger } from "./events/runAutomodOnCounterTrigger";
|
||||
import { runAutomodOnModAction } from "./events/runAutomodOnModAction";
|
||||
import { registerEventListenersFromMap } from "../../utils/registerEventListenersFromMap";
|
||||
import { unregisterEventListenersFromMap } from "../../utils/unregisterEventListenersFromMap";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
|
@ -53,7 +59,7 @@ const defaultOptions = {
|
|||
};
|
||||
|
||||
/**
|
||||
* Config preprocessor to set default values for triggers
|
||||
* Config preprocessor to set default values for triggers and perform extra validation
|
||||
*/
|
||||
const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = options => {
|
||||
if (options.config?.rules) {
|
||||
|
@ -108,6 +114,15 @@ const configPreprocessor: ConfigPreprocessorFn<AutomodPluginType> = options => {
|
|||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (triggerName === "counter") {
|
||||
const parsedCondition = parseCondition(triggerObj[triggerName]!.condition);
|
||||
if (parsedCondition == null) {
|
||||
throw new StrictValidationError([
|
||||
`Invalid counter condition '${triggerObj[triggerName]!.condition}' in rule <${rule.name}>`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,7 +166,13 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()("automod",
|
|||
showInDocs: true,
|
||||
info: pluginInfo,
|
||||
|
||||
dependencies: [LogsPlugin, ModActionsPlugin, MutesPlugin],
|
||||
// prettier-ignore
|
||||
dependencies: [
|
||||
LogsPlugin,
|
||||
ModActionsPlugin,
|
||||
MutesPlugin,
|
||||
CountersPlugin,
|
||||
],
|
||||
|
||||
configSchema: ConfigSchema,
|
||||
defaultOptions,
|
||||
|
@ -161,6 +182,7 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()("automod",
|
|||
return criteria?.antiraid_level ? criteria.antiraid_level === pluginData.state.cachedAntiraidLevel : false;
|
||||
},
|
||||
|
||||
// prettier-ignore
|
||||
events: [
|
||||
RunAutomodOnJoinEvt,
|
||||
RunAutomodOnMemberUpdate,
|
||||
|
@ -204,6 +226,69 @@ export const AutomodPlugin = zeppelinGuildPlugin<AutomodPluginType>()("automod",
|
|||
pluginData.state.cachedAntiraidLevel = await pluginData.state.antiraidLevels.get();
|
||||
},
|
||||
|
||||
async onAfterLoad(pluginData) {
|
||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||
|
||||
pluginData.state.onCounterTrigger = (name, condition, channelId, userId) => {
|
||||
runAutomodOnCounterTrigger(pluginData, name, condition, channelId, userId, false);
|
||||
};
|
||||
|
||||
pluginData.state.onCounterReverseTrigger = (name, condition, channelId, userId) => {
|
||||
runAutomodOnCounterTrigger(pluginData, name, condition, channelId, userId, true);
|
||||
};
|
||||
|
||||
const config = pluginData.config.get();
|
||||
for (const rule of Object.values(config.rules)) {
|
||||
for (const trigger of rule.triggers) {
|
||||
if (trigger.counter) {
|
||||
await countersPlugin.initCounterTrigger(trigger.counter.name, trigger.counter.condition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
countersPlugin.onCounterEvent("trigger", pluginData.state.onCounterTrigger);
|
||||
countersPlugin.onCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger);
|
||||
|
||||
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
|
||||
pluginData.state.modActionsListeners = new Map();
|
||||
pluginData.state.modActionsListeners.set("note", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "note", userId),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("warn", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "warn", userId),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("kick", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "kick", userId),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("ban", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "ban", userId),
|
||||
);
|
||||
pluginData.state.modActionsListeners.set("unban", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "unban", userId),
|
||||
);
|
||||
registerEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners);
|
||||
|
||||
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
|
||||
pluginData.state.mutesListeners = new Map();
|
||||
pluginData.state.mutesListeners.set("mute", (userId: string) => runAutomodOnModAction(pluginData, "mute", userId));
|
||||
pluginData.state.mutesListeners.set("unmute", (userId: string) =>
|
||||
runAutomodOnModAction(pluginData, "unmute", userId),
|
||||
);
|
||||
registerEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners);
|
||||
},
|
||||
|
||||
async onBeforeUnload(pluginData) {
|
||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||
countersPlugin.offCounterEvent("trigger", pluginData.state.onCounterTrigger);
|
||||
countersPlugin.offCounterEvent("reverseTrigger", pluginData.state.onCounterReverseTrigger);
|
||||
|
||||
const modActionsEvents = pluginData.getPlugin(ModActionsPlugin).getEventEmitter();
|
||||
unregisterEventListenersFromMap(modActionsEvents, pluginData.state.modActionsListeners);
|
||||
|
||||
const mutesEvents = pluginData.getPlugin(MutesPlugin).getEventEmitter();
|
||||
unregisterEventListenersFromMap(mutesEvents, pluginData.state.mutesListeners);
|
||||
},
|
||||
|
||||
async onUnload(pluginData) {
|
||||
pluginData.state.queue.clear();
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ export const AlertAction = automodAction({
|
|||
const safeUser = safeUsers[0];
|
||||
const actionsTaken = Object.keys(pluginData.config.get().rules[ruleName].actions).join(", ");
|
||||
|
||||
const logMessage = logs.getLogMessage(LogType.AUTOMOD_ACTION, {
|
||||
const logMessage = await logs.getLogMessage(LogType.AUTOMOD_ACTION, {
|
||||
rule: ruleName,
|
||||
user: safeUser,
|
||||
users: safeUsers,
|
||||
|
|
|
@ -12,6 +12,7 @@ import { AddRolesAction } from "./addRoles";
|
|||
import { RemoveRolesAction } from "./removeRoles";
|
||||
import { SetAntiraidLevelAction } from "./setAntiraidLevel";
|
||||
import { ReplyAction } from "./reply";
|
||||
import { ChangeCounterAction } from "./changeCounter";
|
||||
|
||||
export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
|
||||
clean: CleanAction,
|
||||
|
@ -26,6 +27,7 @@ export const availableActions: Record<string, AutomodActionBlueprint<any>> = {
|
|||
remove_roles: RemoveRolesAction,
|
||||
set_antiraid_level: SetAntiraidLevelAction,
|
||||
reply: ReplyAction,
|
||||
change_counter: ChangeCounterAction,
|
||||
};
|
||||
|
||||
export const AvailableActions = t.type({
|
||||
|
@ -41,4 +43,5 @@ export const AvailableActions = t.type({
|
|||
remove_roles: RemoveRolesAction.configType,
|
||||
set_antiraid_level: SetAntiraidLevelAction.configType,
|
||||
reply: ReplyAction.configType,
|
||||
change_counter: ChangeCounterAction.configType,
|
||||
});
|
||||
|
|
27
backend/src/plugins/Automod/actions/changeCounter.ts
Normal file
27
backend/src/plugins/Automod/actions/changeCounter.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodAction } from "../helpers";
|
||||
import { CountersPlugin } from "../../Counters/CountersPlugin";
|
||||
|
||||
export const ChangeCounterAction = automodAction({
|
||||
configType: t.type({
|
||||
name: t.string,
|
||||
change: t.string,
|
||||
}),
|
||||
|
||||
defaultConfig: {},
|
||||
|
||||
async apply({ pluginData, contexts, actionConfig, matchResult }) {
|
||||
const change = parseInt(actionConfig.change, 10);
|
||||
if (Number.isNaN(change)) {
|
||||
throw new Error("Invalid change number");
|
||||
}
|
||||
|
||||
const countersPlugin = pluginData.getPlugin(CountersPlugin);
|
||||
countersPlugin.changeCounterValue(
|
||||
actionConfig.name,
|
||||
contexts[0].message?.channel_id || null,
|
||||
contexts[0].user?.id || null,
|
||||
change,
|
||||
);
|
||||
},
|
||||
});
|
|
@ -22,6 +22,8 @@ export const MuteAction = automodAction({
|
|||
duration: tNullable(tDelayString),
|
||||
notify: tNullable(t.string),
|
||||
notifyChannel: tNullable(t.string),
|
||||
remove_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
|
||||
restore_roles_on_mute: tNullable(t.union([t.boolean, t.array(t.string)])),
|
||||
}),
|
||||
|
||||
defaultConfig: {
|
||||
|
@ -32,6 +34,8 @@ export const MuteAction = automodAction({
|
|||
const duration = actionConfig.duration ? convertDelayStringToMS(actionConfig.duration)! : undefined;
|
||||
const reason = actionConfig.reason || "Muted automatically";
|
||||
const contactMethods = actionConfig.notify ? resolveActionContactMethods(pluginData, actionConfig) : undefined;
|
||||
const rolesToRemove = actionConfig.remove_roles_on_mute;
|
||||
const rolesToRestore = actionConfig.restore_roles_on_mute;
|
||||
|
||||
const caseArgs = {
|
||||
modId: pluginData.client.user.id,
|
||||
|
@ -43,7 +47,7 @@ export const MuteAction = automodAction({
|
|||
const mutes = pluginData.getPlugin(MutesPlugin);
|
||||
for (const userId of userIdsToMute) {
|
||||
try {
|
||||
await mutes.muteUser(userId, duration, reason, { contactMethods, caseArgs });
|
||||
await mutes.muteUser(userId, duration, reason, { contactMethods, caseArgs }, rolesToRemove, rolesToRestore);
|
||||
} catch (e) {
|
||||
if (e instanceof RecoverablePluginError && e.code === ERRORS.NO_MUTE_ROLE_IN_CONFIG) {
|
||||
pluginData.getPlugin(LogsPlugin).log(LogType.BOT_ALERT, {
|
||||
|
|
|
@ -66,11 +66,15 @@ export const RemoveRolesAction = automodAction({
|
|||
return;
|
||||
}
|
||||
|
||||
const memberRolesLock = await pluginData.locks.acquire(`member-roles-${member.id}`);
|
||||
|
||||
const rolesArr = Array.from(memberRoles.values());
|
||||
await member.edit({
|
||||
roles: rolesArr,
|
||||
});
|
||||
member.roles = rolesArr; // Make sure we know of the new roles internally as well
|
||||
|
||||
memberRolesLock.unlock();
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { AutomodContext, AutomodPluginType } from "../types";
|
||||
import { runAutomod } from "../functions/runAutomod";
|
||||
import { resolveMember, resolveUser, UnknownUser } from "../../../utils";
|
||||
|
||||
export async function runAutomodOnCounterTrigger(
|
||||
pluginData: GuildPluginData<AutomodPluginType>,
|
||||
counterName: string,
|
||||
condition: string,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
reverse: boolean,
|
||||
) {
|
||||
const user = userId ? await resolveUser(pluginData.client, userId) : undefined;
|
||||
|
||||
const member = (userId && (await resolveMember(pluginData.client, pluginData.guild, userId))) || undefined;
|
||||
|
||||
const context: AutomodContext = {
|
||||
timestamp: Date.now(),
|
||||
counterTrigger: {
|
||||
name: counterName,
|
||||
condition,
|
||||
channelId,
|
||||
userId,
|
||||
reverse,
|
||||
},
|
||||
user: user instanceof UnknownUser ? undefined : user,
|
||||
member,
|
||||
};
|
||||
|
||||
pluginData.state.queue.add(async () => {
|
||||
await runAutomod(pluginData, context);
|
||||
});
|
||||
}
|
27
backend/src/plugins/Automod/events/runAutomodOnModAction.ts
Normal file
27
backend/src/plugins/Automod/events/runAutomodOnModAction.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { AutomodContext, AutomodPluginType } from "../types";
|
||||
import { runAutomod } from "../functions/runAutomod";
|
||||
import { resolveUser, UnknownUser } from "../../../utils";
|
||||
import { ModActionType } from "../../ModActions/types";
|
||||
|
||||
export async function runAutomodOnModAction(
|
||||
pluginData: GuildPluginData<AutomodPluginType>,
|
||||
modAction: ModActionType,
|
||||
userId: string,
|
||||
reason?: string,
|
||||
) {
|
||||
const user = await resolveUser(pluginData.client, userId);
|
||||
|
||||
const context: AutomodContext = {
|
||||
timestamp: Date.now(),
|
||||
user: user instanceof UnknownUser ? undefined : user,
|
||||
modAction: {
|
||||
type: modAction,
|
||||
reason,
|
||||
},
|
||||
};
|
||||
|
||||
pluginData.state.queue.add(async () => {
|
||||
await runAutomod(pluginData, context);
|
||||
});
|
||||
}
|
|
@ -24,7 +24,7 @@ export async function runAutomod(pluginData: GuildPluginData<AutomodPluginType>,
|
|||
|
||||
for (const [ruleName, rule] of Object.entries(config.rules)) {
|
||||
if (rule.enabled === false) continue;
|
||||
if (!rule.affects_bots && (!user || user.bot)) continue;
|
||||
if (!rule.affects_bots && (!user || user.bot) && !context.counterTrigger) continue;
|
||||
|
||||
if (rule.cooldown && checkAndUpdateCooldown(pluginData, rule, context)) {
|
||||
return;
|
||||
|
|
|
@ -17,6 +17,14 @@ import { MemberJoinTrigger } from "./memberJoin";
|
|||
import { RoleAddedTrigger } from "./roleAdded";
|
||||
import { RoleRemovedTrigger } from "./roleRemoved";
|
||||
import { StickerSpamTrigger } from "./stickerSpam";
|
||||
import { CounterTrigger } from "./counter";
|
||||
import { NoteTrigger } from "./note";
|
||||
import { WarnTrigger } from "./warn";
|
||||
import { MuteTrigger } from "./mute";
|
||||
import { UnmuteTrigger } from "./unmute";
|
||||
import { KickTrigger } from "./kick";
|
||||
import { BanTrigger } from "./ban";
|
||||
import { UnbanTrigger } from "./unban";
|
||||
|
||||
export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>> = {
|
||||
match_words: MatchWordsTrigger,
|
||||
|
@ -37,6 +45,16 @@ export const availableTriggers: Record<string, AutomodTriggerBlueprint<any, any>
|
|||
character_spam: CharacterSpamTrigger,
|
||||
member_join_spam: MemberJoinSpamTrigger,
|
||||
sticker_spam: StickerSpamTrigger,
|
||||
|
||||
counter: CounterTrigger,
|
||||
|
||||
note: NoteTrigger,
|
||||
warn: WarnTrigger,
|
||||
mute: MuteTrigger,
|
||||
unmute: UnmuteTrigger,
|
||||
kick: KickTrigger,
|
||||
ban: BanTrigger,
|
||||
unban: UnbanTrigger,
|
||||
};
|
||||
|
||||
export const AvailableTriggers = t.type({
|
||||
|
@ -58,4 +76,14 @@ export const AvailableTriggers = t.type({
|
|||
character_spam: CharacterSpamTrigger.configType,
|
||||
member_join_spam: MemberJoinSpamTrigger.configType,
|
||||
sticker_spam: StickerSpamTrigger.configType,
|
||||
|
||||
counter: CounterTrigger.configType,
|
||||
|
||||
note: NoteTrigger.configType,
|
||||
warn: WarnTrigger.configType,
|
||||
mute: MuteTrigger.configType,
|
||||
unmute: UnmuteTrigger.configType,
|
||||
kick: KickTrigger.configType,
|
||||
ban: BanTrigger.configType,
|
||||
unban: UnbanTrigger.configType,
|
||||
});
|
||||
|
|
24
backend/src/plugins/Automod/triggers/ban.ts
Normal file
24
backend/src/plugins/Automod/triggers/ban.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface BanTriggerResultType {}
|
||||
|
||||
export const BanTrigger = automodTrigger<BanTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ context }) {
|
||||
if (context.modAction?.type !== "ban") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult }) {
|
||||
return `User was banned`;
|
||||
},
|
||||
});
|
46
backend/src/plugins/Automod/triggers/counter.ts
Normal file
46
backend/src/plugins/Automod/triggers/counter.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
import { consumeIgnoredRoleChange } from "../functions/ignoredRoleChanges";
|
||||
import { CountersPlugin } from "../../Counters/CountersPlugin";
|
||||
import { tNullable } from "../../../utils";
|
||||
|
||||
// tslint:disable-next-line
|
||||
interface CounterTriggerResult {}
|
||||
|
||||
export const CounterTrigger = automodTrigger<CounterTriggerResult>()({
|
||||
configType: t.type({
|
||||
name: t.string,
|
||||
condition: t.string,
|
||||
reverse: tNullable(t.boolean),
|
||||
}),
|
||||
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ triggerConfig, context, pluginData }) {
|
||||
if (!context.counterTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.counterTrigger.name !== triggerConfig.name) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.counterTrigger.condition !== triggerConfig.condition) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reverse = triggerConfig.reverse ?? false;
|
||||
if (context.counterTrigger.reverse !== reverse) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult, pluginData, contexts, triggerConfig }) {
|
||||
// TODO: Show user, channel, reverse
|
||||
return `Matched counter \`${triggerConfig.name} ${triggerConfig.condition}\``;
|
||||
},
|
||||
});
|
24
backend/src/plugins/Automod/triggers/kick.ts
Normal file
24
backend/src/plugins/Automod/triggers/kick.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface KickTriggerResultType {}
|
||||
|
||||
export const KickTrigger = automodTrigger<KickTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ context }) {
|
||||
if (context.modAction?.type !== "kick") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult }) {
|
||||
return `User was kicked`;
|
||||
},
|
||||
});
|
24
backend/src/plugins/Automod/triggers/mute.ts
Normal file
24
backend/src/plugins/Automod/triggers/mute.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface MuteTriggerResultType {}
|
||||
|
||||
export const MuteTrigger = automodTrigger<MuteTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ context }) {
|
||||
if (context.modAction?.type !== "mute") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult }) {
|
||||
return `User was muted`;
|
||||
},
|
||||
});
|
24
backend/src/plugins/Automod/triggers/note.ts
Normal file
24
backend/src/plugins/Automod/triggers/note.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface NoteTriggerResultType {}
|
||||
|
||||
export const NoteTrigger = automodTrigger<NoteTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ context }) {
|
||||
if (context.modAction?.type !== "note") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult }) {
|
||||
return `Note was added on user`;
|
||||
},
|
||||
});
|
24
backend/src/plugins/Automod/triggers/unban.ts
Normal file
24
backend/src/plugins/Automod/triggers/unban.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface UnbanTriggerResultType {}
|
||||
|
||||
export const UnbanTrigger = automodTrigger<UnbanTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ context }) {
|
||||
if (context.modAction?.type !== "unban") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult }) {
|
||||
return `User was unbanned`;
|
||||
},
|
||||
});
|
24
backend/src/plugins/Automod/triggers/unmute.ts
Normal file
24
backend/src/plugins/Automod/triggers/unmute.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface UnmuteTriggerResultType {}
|
||||
|
||||
export const UnmuteTrigger = automodTrigger<UnmuteTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ context }) {
|
||||
if (context.modAction?.type !== "unmute") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult }) {
|
||||
return `User was unmuted`;
|
||||
},
|
||||
});
|
24
backend/src/plugins/Automod/triggers/warn.ts
Normal file
24
backend/src/plugins/Automod/triggers/warn.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import * as t from "io-ts";
|
||||
import { automodTrigger } from "../helpers";
|
||||
|
||||
// tslint:disable-next-line:no-empty-interface
|
||||
interface WarnTriggerResultType {}
|
||||
|
||||
export const WarnTrigger = automodTrigger<WarnTriggerResultType>()({
|
||||
configType: t.type({}),
|
||||
defaultConfig: {},
|
||||
|
||||
async match({ context }) {
|
||||
if (context.modAction?.type !== "warn") {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
extra: {},
|
||||
};
|
||||
},
|
||||
|
||||
renderMatchInformation({ matchResult }) {
|
||||
return `User was warned`;
|
||||
},
|
||||
});
|
|
@ -13,6 +13,9 @@ import { GuildArchives } from "../../data/GuildArchives";
|
|||
import { RecentActionType } from "./constants";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import { RegExpRunner } from "../../RegExpRunner";
|
||||
import { CounterEvents } from "../Counters/types";
|
||||
import { ModActionsEvents, ModActionType } from "../ModActions/types";
|
||||
import { MutesEvents } from "../Mutes/types";
|
||||
|
||||
export const Rule = t.type({
|
||||
enabled: t.boolean,
|
||||
|
@ -86,6 +89,12 @@ export interface AutomodPluginType extends BasePluginType {
|
|||
|
||||
onMessageCreateFn: any;
|
||||
onMessageUpdateFn: any;
|
||||
|
||||
onCounterTrigger: CounterEvents["trigger"];
|
||||
onCounterReverseTrigger: CounterEvents["reverseTrigger"];
|
||||
|
||||
modActionsListeners: Map<keyof ModActionsEvents, any>;
|
||||
mutesListeners: Map<keyof MutesEvents, any>;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -93,6 +102,13 @@ export interface AutomodContext {
|
|||
timestamp: number;
|
||||
actioned?: boolean;
|
||||
|
||||
counterTrigger?: {
|
||||
name: string;
|
||||
condition: string;
|
||||
channelId: string | null;
|
||||
userId: string | null;
|
||||
reverse: boolean;
|
||||
};
|
||||
user?: User;
|
||||
message?: SavedMessage;
|
||||
member?: Member;
|
||||
|
@ -101,6 +117,10 @@ export interface AutomodContext {
|
|||
added?: string[];
|
||||
removed?: string[];
|
||||
};
|
||||
modAction?: {
|
||||
type: ModActionType;
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RecentAction {
|
||||
|
|
|
@ -15,6 +15,8 @@ import { AddDashboardUserCmd } from "./commands/AddDashboardUserCmd";
|
|||
import { RemoveDashboardUserCmd } from "./commands/RemoveDashboardUserCmd";
|
||||
import { Configs } from "../../data/Configs";
|
||||
import { ApiPermissionAssignments } from "../../data/ApiPermissionAssignments";
|
||||
import { ListDashboardUsersCmd } from "./commands/ListDashboardUsersCmd";
|
||||
import { ListDashboardPermsCmd } from "./commands/ListDashboardPermsCmd";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
|
@ -37,6 +39,8 @@ export const BotControlPlugin = zeppelinGlobalPlugin<BotControlPluginType>()("bo
|
|||
DisallowServerCmd,
|
||||
AddDashboardUserCmd,
|
||||
RemoveDashboardUserCmd,
|
||||
ListDashboardUsersCmd,
|
||||
ListDashboardPermsCmd,
|
||||
],
|
||||
|
||||
onLoad(pluginData) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { botControlCmd } from "../types";
|
|||
import { isOwnerPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { isSnowflake } from "../../../utils";
|
||||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
|
||||
export const AllowServerCmd = botControlCmd({
|
||||
trigger: ["allow_server", "allowserver", "add_server", "addserver"],
|
||||
|
@ -12,6 +13,7 @@ export const AllowServerCmd = botControlCmd({
|
|||
|
||||
signature: {
|
||||
guildId: ct.string(),
|
||||
userId: ct.string({ required: false }),
|
||||
},
|
||||
|
||||
async run({ pluginData, message: msg, args }) {
|
||||
|
@ -26,8 +28,18 @@ export const AllowServerCmd = botControlCmd({
|
|||
return;
|
||||
}
|
||||
|
||||
if (args.userId && !isSnowflake(args.userId)) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Invalid user ID!");
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginData.state.allowedGuilds.add(args.guildId);
|
||||
await pluginData.state.configs.saveNewRevision(`guild-${args.guildId}`, "plugins: {}", msg.author.id);
|
||||
|
||||
if (args.userId) {
|
||||
await pluginData.state.apiPermissionAssignments.addUser(args.guildId, args.userId, [ApiPermissions.EditConfig]);
|
||||
}
|
||||
|
||||
sendSuccessMessage(pluginData, msg.channel, "Server is now allowed to use Zeppelin!");
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import { botControlCmd } from "../types";
|
||||
import { isOwnerPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { createChunkedMessage, resolveUser } from "../../../utils";
|
||||
import { AllowedGuild } from "../../../data/entities/AllowedGuild";
|
||||
import { ApiPermissionAssignment } from "../../../data/entities/ApiPermissionAssignment";
|
||||
|
||||
export const ListDashboardPermsCmd = botControlCmd({
|
||||
trigger: ["list_dashboard_permissions", "list_dashboard_perms", "list_dash_permissionss", "list_dash_perms"],
|
||||
permission: null,
|
||||
config: {
|
||||
preFilters: [isOwnerPreFilter],
|
||||
},
|
||||
|
||||
signature: {
|
||||
guildId: ct.string({ option: true, shortcut: "g" }),
|
||||
user: ct.resolvedUser({ option: true, shortcut: "u" }),
|
||||
},
|
||||
|
||||
async run({ pluginData, message: msg, args }) {
|
||||
if (!args.user && !args.guildId) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Must specify at least guildId, user, or both.");
|
||||
return;
|
||||
}
|
||||
|
||||
let guild: AllowedGuild | undefined;
|
||||
if (args.guildId) {
|
||||
guild = await pluginData.state.allowedGuilds.find(args.guildId);
|
||||
if (!guild) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let existingUserAssignment: ApiPermissionAssignment[];
|
||||
if (args.user) {
|
||||
existingUserAssignment = await pluginData.state.apiPermissionAssignments.getByUserId(args.user.id);
|
||||
if (existingUserAssignment.length === 0) {
|
||||
sendErrorMessage(pluginData, msg.channel, "The user has no assigned permissions.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let finalMessage = "";
|
||||
|
||||
// If we have user, always display which guilds they have permissions in (or only specified guild permissions)
|
||||
if (args.user) {
|
||||
for (const assignment of existingUserAssignment!) {
|
||||
if (guild != null && assignment.guild_id !== args.guildId) continue;
|
||||
finalMessage += `The user has the following permissions on server \`${
|
||||
assignment.guild_id
|
||||
}\`:\n${assignment.permissions.join("\n")}\n\n`;
|
||||
}
|
||||
|
||||
if (finalMessage === "") {
|
||||
sendErrorMessage(pluginData, msg.channel, "The user has no assigned permissions on the specified server.");
|
||||
return;
|
||||
}
|
||||
// Else display all users that have permissions on the specified guild
|
||||
} else if (guild) {
|
||||
const existingGuildAssignment = await pluginData.state.apiPermissionAssignments.getByGuildId(guild.id);
|
||||
if (existingGuildAssignment.length === 0) {
|
||||
sendErrorMessage(pluginData, msg.channel, "The server has no assigned permissions.");
|
||||
return;
|
||||
}
|
||||
|
||||
finalMessage += `The server \`${guild.id}\` has the following assigned permissions:\n`; // Double \n for consistency with AddDashboardUserCmd
|
||||
for (const assignment of existingGuildAssignment) {
|
||||
const user = await resolveUser(pluginData.client, assignment.target_id);
|
||||
finalMessage += `\n**${user.username}#${user.discriminator}**, \`${
|
||||
assignment.target_id
|
||||
}\`: ${assignment.permissions.join(", ")}`;
|
||||
}
|
||||
}
|
||||
|
||||
await sendSuccessMessage(pluginData, msg.channel, finalMessage.trim(), {});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import { botControlCmd } from "../types";
|
||||
import { isOwnerPreFilter, sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { ApiPermissions } from "@shared/apiPermissions";
|
||||
import { resolveUser } from "../../../utils";
|
||||
|
||||
export const ListDashboardUsersCmd = botControlCmd({
|
||||
trigger: ["list_dashboard_users"],
|
||||
permission: null,
|
||||
config: {
|
||||
preFilters: [isOwnerPreFilter],
|
||||
},
|
||||
|
||||
signature: {
|
||||
guildId: ct.string(),
|
||||
},
|
||||
|
||||
async run({ pluginData, message: msg, args }) {
|
||||
const guild = await pluginData.state.allowedGuilds.find(args.guildId);
|
||||
if (!guild) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Server is not using Zeppelin");
|
||||
return;
|
||||
}
|
||||
|
||||
const dashboardUsers = await pluginData.state.apiPermissionAssignments.getByGuildId(guild.id);
|
||||
const users = await Promise.all(dashboardUsers.map(perm => resolveUser(pluginData.client, perm.target_id)));
|
||||
const userNameList = users.map(
|
||||
user => `<@!${user.id}> (**${user.username}#${user.discriminator}**, \`${user.id}\`)`,
|
||||
);
|
||||
|
||||
sendSuccessMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`The following users have dashboard access for **${guild.name}**:\n\n${userNameList}`,
|
||||
{},
|
||||
);
|
||||
},
|
||||
});
|
|
@ -16,6 +16,7 @@ import { humanizeDurationShort } from "../../../humanizeDurationShort";
|
|||
import { caseAbbreviations } from "../caseAbbreviations";
|
||||
import { getCaseIcon } from "./getCaseIcon";
|
||||
import { TimeAndDatePlugin } from "../../TimeAndDate/TimeAndDatePlugin";
|
||||
import { splitIntoCleanChunks, splitMessageIntoChunks } from "knub/dist/helpers";
|
||||
|
||||
const CASE_SUMMARY_REASON_MAX_LENGTH = 300;
|
||||
const INCLUDE_MORE_NOTES_THRESHOLD = 20;
|
||||
|
@ -49,9 +50,8 @@ export async function getCaseSummary(
|
|||
if (reason.length > CASE_SUMMARY_REASON_MAX_LENGTH) {
|
||||
const match = reason.slice(CASE_SUMMARY_REASON_MAX_LENGTH, 100).match(/(?:[.,!?\s]|$)/);
|
||||
const nextWhitespaceIndex = match ? CASE_SUMMARY_REASON_MAX_LENGTH + match.index! : CASE_SUMMARY_REASON_MAX_LENGTH;
|
||||
if (nextWhitespaceIndex < reason.length) {
|
||||
reason = reason.slice(0, nextWhitespaceIndex - 1) + "...";
|
||||
}
|
||||
const reasonChunks = splitMessageIntoChunks(reason, nextWhitespaceIndex);
|
||||
reason = reasonChunks[0] + "...";
|
||||
}
|
||||
|
||||
reason = disableLinkPreviews(reason);
|
||||
|
|
149
backend/src/plugins/Counters/CountersPlugin.ts
Normal file
149
backend/src/plugins/Counters/CountersPlugin.ts
Normal file
|
@ -0,0 +1,149 @@
|
|||
import { zeppelinGuildPlugin } from "../ZeppelinPluginBlueprint";
|
||||
import { ConfigSchema, CountersPluginType } from "./types";
|
||||
import { GuildCounters } from "../../data/GuildCounters";
|
||||
import { mapToPublicFn } from "../../pluginUtils";
|
||||
import { changeCounterValue } from "./functions/changeCounterValue";
|
||||
import { setCounterValue } from "./functions/setCounterValue";
|
||||
import { convertDelayStringToMS, MINUTES, SECONDS } from "../../utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { onCounterEvent } from "./functions/onCounterEvent";
|
||||
import { offCounterEvent } from "./functions/offCounterEvent";
|
||||
import { emitCounterEvent } from "./functions/emitCounterEvent";
|
||||
import { ConfigPreprocessorFn } from "knub/dist/config/configTypes";
|
||||
import { initCounterTrigger } from "./functions/initCounterTrigger";
|
||||
import { decayCounter } from "./functions/decayCounter";
|
||||
import { validateCondition } from "./functions/validateCondition";
|
||||
import { StrictValidationError } from "../../validatorUtils";
|
||||
import { PluginOptions } from "knub";
|
||||
import { ViewCounterCmd } from "./commands/ViewCounterCmd";
|
||||
import { AddCounterCmd } from "./commands/AddCounterCmd";
|
||||
import { SetCounterCmd } from "./commands/SetCounterCmd";
|
||||
|
||||
const MAX_COUNTERS = 5;
|
||||
const DECAY_APPLY_INTERVAL = 5 * MINUTES;
|
||||
|
||||
const defaultOptions: PluginOptions<CountersPluginType> = {
|
||||
config: {
|
||||
counters: {},
|
||||
can_view: false,
|
||||
can_edit: false,
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
level: ">=50",
|
||||
config: {
|
||||
can_view: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
level: ">=100",
|
||||
config: {
|
||||
can_edit: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const configPreprocessor: ConfigPreprocessorFn<CountersPluginType> = options => {
|
||||
for (const counter of Object.values(options.config?.counters || {})) {
|
||||
counter.per_user = counter.per_user ?? false;
|
||||
counter.per_channel = counter.per_channel ?? false;
|
||||
counter.initial_value = counter.initial_value ?? 0;
|
||||
}
|
||||
|
||||
if (Object.values(options.config?.counters || {}).length > MAX_COUNTERS) {
|
||||
throw new StrictValidationError([`You can only have at most ${MAX_COUNTERS} active counters`]);
|
||||
}
|
||||
|
||||
return options;
|
||||
};
|
||||
|
||||
/**
|
||||
* The Counters plugin keeps track of simple integer values that are tied to a user, channel, both, or neither — "counters".
|
||||
* These values can be changed using the functions in the plugin's public interface.
|
||||
* These values can also be set to automatically decay over time.
|
||||
*
|
||||
* Triggers can be registered that check for a specific condition, e.g. "when this counter is over 100".
|
||||
* Triggers are checked against every time a counter's value changes, and will emit an event when triggered.
|
||||
* A single trigger can only trigger once per user/channel/in general, depending on how specific the counter is (e.g. a per-user trigger can only trigger once per user).
|
||||
* After being triggered, a trigger is "reset" if the counter value no longer matches the trigger (e.g. drops to 100 or below in the above example). After this, that trigger can be triggered again.
|
||||
*/
|
||||
export const CountersPlugin = zeppelinGuildPlugin<CountersPluginType>()("counters", {
|
||||
configSchema: ConfigSchema,
|
||||
defaultOptions,
|
||||
configPreprocessor,
|
||||
|
||||
public: {
|
||||
// Change a counter's value by a relative amount, e.g. +5
|
||||
changeCounterValue: mapToPublicFn(changeCounterValue),
|
||||
// Set a counter's value to an absolute value
|
||||
setCounterValue: mapToPublicFn(setCounterValue),
|
||||
|
||||
// Initialize a trigger. Once initialized, events will be fired when this trigger is triggered.
|
||||
initCounterTrigger: mapToPublicFn(initCounterTrigger),
|
||||
|
||||
// Validate a trigger's condition string
|
||||
validateCondition: mapToPublicFn(validateCondition),
|
||||
|
||||
onCounterEvent: mapToPublicFn(onCounterEvent),
|
||||
offCounterEvent: mapToPublicFn(offCounterEvent),
|
||||
},
|
||||
|
||||
// prettier-ignore
|
||||
commands: [
|
||||
ViewCounterCmd,
|
||||
AddCounterCmd,
|
||||
SetCounterCmd,
|
||||
],
|
||||
|
||||
async onLoad(pluginData) {
|
||||
pluginData.state.counters = new GuildCounters(pluginData.guild.id);
|
||||
pluginData.state.events = new EventEmitter();
|
||||
|
||||
// Initialize and store the IDs of each of the counters internally
|
||||
pluginData.state.counterIds = {};
|
||||
const config = pluginData.config.get();
|
||||
for (const [counterName, counter] of Object.entries(config.counters)) {
|
||||
const dbCounter = await pluginData.state.counters.findOrCreateCounter(
|
||||
counterName,
|
||||
counter.per_channel,
|
||||
counter.per_user,
|
||||
);
|
||||
pluginData.state.counterIds[counterName] = dbCounter.id;
|
||||
}
|
||||
|
||||
// Mark old/unused counters to be deleted later
|
||||
await pluginData.state.counters.markUnusedCountersToBeDeleted([...Object.values(pluginData.state.counterIds)]);
|
||||
|
||||
// Start decay timers
|
||||
pluginData.state.decayTimers = [];
|
||||
for (const [counterName, counter] of Object.entries(config.counters)) {
|
||||
if (!counter.decay) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decay = counter.decay;
|
||||
const decayPeriodMs = convertDelayStringToMS(decay.every)!;
|
||||
pluginData.state.decayTimers.push(
|
||||
setInterval(() => {
|
||||
decayCounter(pluginData, counterName, decayPeriodMs, decay.amount);
|
||||
}, DECAY_APPLY_INTERVAL),
|
||||
);
|
||||
}
|
||||
|
||||
// Initially set the counter trigger map to just an empty map
|
||||
// The actual triggers are added by other plugins via initCounterTrigger()
|
||||
pluginData.state.counterTriggersByCounterId = new Map();
|
||||
|
||||
// Mark all triggers to be deleted later. This is cancelled/reset when a plugin adds the trigger again via initCounterTrigger().
|
||||
await pluginData.state.counters.markAllTriggersTobeDeleted();
|
||||
},
|
||||
|
||||
onUnload(pluginData) {
|
||||
for (const interval of pluginData.state.decayTimers) {
|
||||
clearInterval(interval);
|
||||
}
|
||||
|
||||
pluginData.state.events.removeAllListeners();
|
||||
},
|
||||
});
|
141
backend/src/plugins/Counters/commands/AddCounterCmd.ts
Normal file
141
backend/src/plugins/Counters/commands/AddCounterCmd.ts
Normal file
|
@ -0,0 +1,141 @@
|
|||
import { guildCommand } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { sendErrorMessage } from "../../../pluginUtils";
|
||||
import { resolveChannel, waitForReply } from "knub/dist/helpers";
|
||||
import { TextChannel, User } from "eris";
|
||||
import { resolveUser, UnknownUser } from "../../../utils";
|
||||
import { changeCounterValue } from "../functions/changeCounterValue";
|
||||
|
||||
export const AddCounterCmd = guildCommand<CountersPluginType>()({
|
||||
trigger: ["counters add", "counter add", "addcounter"],
|
||||
permission: "can_edit",
|
||||
|
||||
signature: [
|
||||
{
|
||||
counterName: ct.string(),
|
||||
amount: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
user: ct.resolvedUser(),
|
||||
amount: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
channel: ct.textChannel(),
|
||||
amount: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
channel: ct.textChannel(),
|
||||
user: ct.resolvedUser(),
|
||||
amount: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
user: ct.resolvedUser(),
|
||||
channel: ct.textChannel(),
|
||||
amount: ct.number(),
|
||||
},
|
||||
],
|
||||
|
||||
async run({ pluginData, message, args }) {
|
||||
const config = pluginData.config.getForMessage(message);
|
||||
const counter = config.counters[args.counterName];
|
||||
const counterId = pluginData.state.counterIds[args.counterName];
|
||||
if (!counter || !counterId) {
|
||||
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (counter.can_edit === false) {
|
||||
sendErrorMessage(pluginData, message.channel, `Missing permissions to edit this counter's value`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.channel && !counter.per_channel) {
|
||||
sendErrorMessage(pluginData, message.channel, `This counter is not per-channel`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.user && !counter.per_user) {
|
||||
sendErrorMessage(pluginData, message.channel, `This counter is not per-user`);
|
||||
return;
|
||||
}
|
||||
|
||||
let channel = args.channel;
|
||||
if (!channel && counter.per_channel) {
|
||||
message.channel.createMessage(`Which channel's counter value would you like to add to?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialChannel = resolveChannel(pluginData.guild, reply.content);
|
||||
if (!potentialChannel || !(potentialChannel instanceof TextChannel)) {
|
||||
sendErrorMessage(pluginData, message.channel, "Channel is not a text channel, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
channel = potentialChannel;
|
||||
}
|
||||
|
||||
let user = args.user;
|
||||
if (!user && counter.per_user) {
|
||||
message.channel.createMessage(`Which user's counter value would you like to add to?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialUser = await resolveUser(pluginData.client, reply.content);
|
||||
if (!potentialUser || potentialUser instanceof UnknownUser) {
|
||||
sendErrorMessage(pluginData, message.channel, "Unknown user, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
user = potentialUser;
|
||||
}
|
||||
|
||||
let amount = args.amount;
|
||||
if (!amount) {
|
||||
message.channel.createMessage("How much would you like to add to the counter's value?");
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialAmount = parseInt(reply.content, 10);
|
||||
if (!potentialAmount) {
|
||||
sendErrorMessage(pluginData, message.channel, "Not a number, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
amount = potentialAmount;
|
||||
}
|
||||
|
||||
await changeCounterValue(pluginData, args.counterName, channel?.id ?? null, user?.id ?? null, amount);
|
||||
const newValue = await pluginData.state.counters.getCurrentValue(counterId, channel?.id ?? null, user?.id ?? null);
|
||||
const counterName = counter.name || args.counterName;
|
||||
|
||||
if (channel && user) {
|
||||
message.channel.createMessage(
|
||||
`Added ${amount} to **${counterName}** for <@!${user.id}> in <#${channel.id}>. The value is now ${newValue}.`,
|
||||
);
|
||||
} else if (channel) {
|
||||
message.channel.createMessage(
|
||||
`Added ${amount} to **${counterName}** in <#${channel.id}>. The value is now ${newValue}.`,
|
||||
);
|
||||
} else if (user) {
|
||||
message.channel.createMessage(
|
||||
`Added ${amount} to **${counterName}** for <@!${user.id}>. The value is now ${newValue}.`,
|
||||
);
|
||||
} else {
|
||||
message.channel.createMessage(`Added ${amount} to **${counterName}**. The value is now ${newValue}.`);
|
||||
}
|
||||
},
|
||||
});
|
140
backend/src/plugins/Counters/commands/SetCounterCmd.ts
Normal file
140
backend/src/plugins/Counters/commands/SetCounterCmd.ts
Normal file
|
@ -0,0 +1,140 @@
|
|||
import { guildCommand } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { sendErrorMessage } from "../../../pluginUtils";
|
||||
import { resolveChannel, waitForReply } from "knub/dist/helpers";
|
||||
import { TextChannel, User } from "eris";
|
||||
import { resolveUser, UnknownUser } from "../../../utils";
|
||||
import { changeCounterValue } from "../functions/changeCounterValue";
|
||||
import { setCounterValue } from "../functions/setCounterValue";
|
||||
|
||||
export const SetCounterCmd = guildCommand<CountersPluginType>()({
|
||||
trigger: ["counters set", "counter set", "setcounter"],
|
||||
permission: "can_edit",
|
||||
|
||||
signature: [
|
||||
{
|
||||
counterName: ct.string(),
|
||||
value: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
user: ct.resolvedUser(),
|
||||
value: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
channel: ct.textChannel(),
|
||||
value: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
channel: ct.textChannel(),
|
||||
user: ct.resolvedUser(),
|
||||
value: ct.number(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
user: ct.resolvedUser(),
|
||||
channel: ct.textChannel(),
|
||||
value: ct.number(),
|
||||
},
|
||||
],
|
||||
|
||||
async run({ pluginData, message, args }) {
|
||||
const config = pluginData.config.getForMessage(message);
|
||||
const counter = config.counters[args.counterName];
|
||||
const counterId = pluginData.state.counterIds[args.counterName];
|
||||
if (!counter || !counterId) {
|
||||
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (counter.can_edit === false) {
|
||||
sendErrorMessage(pluginData, message.channel, `Missing permissions to edit this counter's value`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.channel && !counter.per_channel) {
|
||||
sendErrorMessage(pluginData, message.channel, `This counter is not per-channel`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.user && !counter.per_user) {
|
||||
sendErrorMessage(pluginData, message.channel, `This counter is not per-user`);
|
||||
return;
|
||||
}
|
||||
|
||||
let channel = args.channel;
|
||||
if (!channel && counter.per_channel) {
|
||||
message.channel.createMessage(`Which channel's counter value would you like to add to?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialChannel = resolveChannel(pluginData.guild, reply.content);
|
||||
if (!potentialChannel || !(potentialChannel instanceof TextChannel)) {
|
||||
sendErrorMessage(pluginData, message.channel, "Channel is not a text channel, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
channel = potentialChannel;
|
||||
}
|
||||
|
||||
let user = args.user;
|
||||
if (!user && counter.per_user) {
|
||||
message.channel.createMessage(`Which user's counter value would you like to add to?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialUser = await resolveUser(pluginData.client, reply.content);
|
||||
if (!potentialUser || potentialUser instanceof UnknownUser) {
|
||||
sendErrorMessage(pluginData, message.channel, "Unknown user, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
user = potentialUser;
|
||||
}
|
||||
|
||||
let value = args.value;
|
||||
if (!value) {
|
||||
message.channel.createMessage("How much would you like to add to the counter's value?");
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialValue = parseInt(reply.content, 10);
|
||||
if (!potentialValue) {
|
||||
sendErrorMessage(pluginData, message.channel, "Not a number, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
value = potentialValue;
|
||||
}
|
||||
|
||||
if (value < 0) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cannot set counter value below 0");
|
||||
return;
|
||||
}
|
||||
|
||||
await setCounterValue(pluginData, args.counterName, channel?.id ?? null, user?.id ?? null, value);
|
||||
const counterName = counter.name || args.counterName;
|
||||
|
||||
if (channel && user) {
|
||||
message.channel.createMessage(`Set **${counterName}** for <@!${user.id}> in <#${channel.id}> to ${value}`);
|
||||
} else if (channel) {
|
||||
message.channel.createMessage(`Set **${counterName}** in <#${channel.id}> to ${value}`);
|
||||
} else if (user) {
|
||||
message.channel.createMessage(`Set **${counterName}** for <@!${user.id}> to ${value}`);
|
||||
} else {
|
||||
message.channel.createMessage(`Set **${counterName}** to ${value}`);
|
||||
}
|
||||
},
|
||||
});
|
111
backend/src/plugins/Counters/commands/ViewCounterCmd.ts
Normal file
111
backend/src/plugins/Counters/commands/ViewCounterCmd.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { guildCommand } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { sendErrorMessage } from "../../../pluginUtils";
|
||||
import { resolveChannel, waitForReply } from "knub/dist/helpers";
|
||||
import { TextChannel, User } from "eris";
|
||||
import { resolveUser, UnknownUser } from "../../../utils";
|
||||
|
||||
export const ViewCounterCmd = guildCommand<CountersPluginType>()({
|
||||
trigger: ["counters view", "counter view", "viewcounter", "counter"],
|
||||
permission: "can_view",
|
||||
|
||||
signature: [
|
||||
{
|
||||
counterName: ct.string(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
user: ct.resolvedUser(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
channel: ct.textChannel(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
channel: ct.textChannel(),
|
||||
user: ct.resolvedUser(),
|
||||
},
|
||||
{
|
||||
counterName: ct.string(),
|
||||
user: ct.resolvedUser(),
|
||||
channel: ct.textChannel(),
|
||||
},
|
||||
],
|
||||
|
||||
async run({ pluginData, message, args }) {
|
||||
const config = pluginData.config.getForMessage(message);
|
||||
const counter = config.counters[args.counterName];
|
||||
const counterId = pluginData.state.counterIds[args.counterName];
|
||||
if (!counter || !counterId) {
|
||||
sendErrorMessage(pluginData, message.channel, `Unknown counter: ${args.counterName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (counter.can_view === false) {
|
||||
sendErrorMessage(pluginData, message.channel, `Missing permissions to view this counter's value`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.channel && !counter.per_channel) {
|
||||
sendErrorMessage(pluginData, message.channel, `This counter is not per-channel`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.user && !counter.per_user) {
|
||||
sendErrorMessage(pluginData, message.channel, `This counter is not per-user`);
|
||||
return;
|
||||
}
|
||||
|
||||
let channel = args.channel;
|
||||
if (!channel && counter.per_channel) {
|
||||
message.channel.createMessage(`Which channel's counter value would you like to view?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialChannel = resolveChannel(pluginData.guild, reply.content);
|
||||
if (!potentialChannel || !(potentialChannel instanceof TextChannel)) {
|
||||
sendErrorMessage(pluginData, message.channel, "Channel is not a text channel, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
channel = potentialChannel;
|
||||
}
|
||||
|
||||
let user = args.user;
|
||||
if (!user && counter.per_user) {
|
||||
message.channel.createMessage(`Which user's counter value would you like to view?`);
|
||||
const reply = await waitForReply(pluginData.client, message.channel, message.author.id);
|
||||
if (!reply || !reply.content) {
|
||||
sendErrorMessage(pluginData, message.channel, "Cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
const potentialUser = await resolveUser(pluginData.client, reply.content);
|
||||
if (!potentialUser || potentialUser instanceof UnknownUser) {
|
||||
sendErrorMessage(pluginData, message.channel, "Unknown user, cancelling");
|
||||
return;
|
||||
}
|
||||
|
||||
user = potentialUser;
|
||||
}
|
||||
|
||||
const value = await pluginData.state.counters.getCurrentValue(counterId, channel?.id ?? null, user?.id ?? null);
|
||||
const finalValue = value ?? counter.initial_value;
|
||||
const counterName = counter.name || args.counterName;
|
||||
|
||||
if (channel && user) {
|
||||
message.channel.createMessage(`**${counterName}** for <@!${user.id}> in <#${channel.id}> is ${finalValue}`);
|
||||
} else if (channel) {
|
||||
message.channel.createMessage(`**${counterName}** in <#${channel.id}> is ${finalValue}`);
|
||||
} else if (user) {
|
||||
message.channel.createMessage(`**${counterName}** for <@!${user.id}> is ${finalValue}`);
|
||||
} else {
|
||||
message.channel.createMessage(`**${counterName}** is ${finalValue}`);
|
||||
}
|
||||
},
|
||||
});
|
48
backend/src/plugins/Counters/functions/changeCounterValue.ts
Normal file
48
backend/src/plugins/Counters/functions/changeCounterValue.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { checkCounterTrigger } from "./checkCounterTrigger";
|
||||
import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
|
||||
|
||||
export async function changeCounterValue(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
change: number,
|
||||
) {
|
||||
const config = pluginData.config.get();
|
||||
const counter = config.counters[counterName];
|
||||
if (!counter) {
|
||||
throw new Error(`Unknown counter: ${counterName}`);
|
||||
}
|
||||
|
||||
if (counter.per_channel && !channelId) {
|
||||
throw new Error(`Counter is per channel but no channel ID was supplied`);
|
||||
}
|
||||
|
||||
if (counter.per_user && !userId) {
|
||||
throw new Error(`Counter is per user but no user ID was supplied`);
|
||||
}
|
||||
|
||||
channelId = counter.per_channel ? channelId : null;
|
||||
userId = counter.per_user ? userId : null;
|
||||
|
||||
const counterId = pluginData.state.counterIds[counterName];
|
||||
const lock = await pluginData.locks.acquire(counterId.toString());
|
||||
|
||||
await pluginData.state.counters.changeCounterValue(counterId, channelId, userId, change);
|
||||
|
||||
// Check for trigger matches, if any, when the counter value changes
|
||||
const triggers = pluginData.state.counterTriggersByCounterId.get(counterId);
|
||||
if (triggers) {
|
||||
const triggersArr = Array.from(triggers.values());
|
||||
await Promise.all(
|
||||
triggersArr.map(trigger => checkCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
|
||||
);
|
||||
await Promise.all(
|
||||
triggersArr.map(trigger => checkReverseCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
|
||||
);
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { buildConditionString } from "../../../data/GuildCounters";
|
||||
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
|
||||
import { emitCounterEvent } from "./emitCounterEvent";
|
||||
|
||||
export async function checkAllValuesForReverseTrigger(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
counterTrigger: CounterTrigger,
|
||||
) {
|
||||
const triggeredContexts = await pluginData.state.counters.checkAllValuesForReverseTrigger(counterTrigger);
|
||||
for (const context of triggeredContexts) {
|
||||
emitCounterEvent(
|
||||
pluginData,
|
||||
"reverseTrigger",
|
||||
counterName,
|
||||
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
|
||||
context.channelId,
|
||||
context.userId,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { buildConditionString } from "../../../data/GuildCounters";
|
||||
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
|
||||
import { emitCounterEvent } from "./emitCounterEvent";
|
||||
|
||||
export async function checkAllValuesForTrigger(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
counterTrigger: CounterTrigger,
|
||||
) {
|
||||
const triggeredContexts = await pluginData.state.counters.checkAllValuesForTrigger(counterTrigger);
|
||||
for (const context of triggeredContexts) {
|
||||
emitCounterEvent(
|
||||
pluginData,
|
||||
"trigger",
|
||||
counterName,
|
||||
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
|
||||
context.channelId,
|
||||
context.userId,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { buildConditionString } from "../../../data/GuildCounters";
|
||||
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
|
||||
import { emitCounterEvent } from "./emitCounterEvent";
|
||||
|
||||
export async function checkCounterTrigger(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
counterTrigger: CounterTrigger,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
) {
|
||||
const triggered = await pluginData.state.counters.checkForTrigger(counterTrigger, channelId, userId);
|
||||
if (triggered) {
|
||||
await emitCounterEvent(
|
||||
pluginData,
|
||||
"trigger",
|
||||
counterName,
|
||||
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
|
||||
channelId,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { buildConditionString } from "../../../data/GuildCounters";
|
||||
import { CounterTrigger } from "../../../data/entities/CounterTrigger";
|
||||
import { emitCounterEvent } from "./emitCounterEvent";
|
||||
|
||||
export async function checkReverseCounterTrigger(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
counterTrigger: CounterTrigger,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
) {
|
||||
const triggered = await pluginData.state.counters.checkForReverseTrigger(counterTrigger, channelId, userId);
|
||||
if (triggered) {
|
||||
await emitCounterEvent(
|
||||
pluginData,
|
||||
"reverseTrigger",
|
||||
counterName,
|
||||
buildConditionString(counterTrigger.comparison_op, counterTrigger.comparison_value),
|
||||
channelId,
|
||||
userId,
|
||||
);
|
||||
}
|
||||
}
|
32
backend/src/plugins/Counters/functions/decayCounter.ts
Normal file
32
backend/src/plugins/Counters/functions/decayCounter.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { checkAllValuesForTrigger } from "./checkAllValuesForTrigger";
|
||||
import { checkAllValuesForReverseTrigger } from "./checkAllValuesForReverseTrigger";
|
||||
|
||||
export async function decayCounter(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
decayPeriodMS: number,
|
||||
decayAmount: number,
|
||||
) {
|
||||
const config = pluginData.config.get();
|
||||
const counter = config.counters[counterName];
|
||||
if (!counter) {
|
||||
throw new Error(`Unknown counter: ${counterName}`);
|
||||
}
|
||||
|
||||
const counterId = pluginData.state.counterIds[counterName];
|
||||
const lock = await pluginData.locks.acquire(counterId.toString());
|
||||
|
||||
await pluginData.state.counters.decay(counterId, decayPeriodMS, decayAmount);
|
||||
|
||||
// Check for trigger matches, if any, when the counter value changes
|
||||
const triggers = pluginData.state.counterTriggersByCounterId.get(counterId);
|
||||
if (triggers) {
|
||||
const triggersArr = Array.from(triggers.values());
|
||||
await Promise.all(triggersArr.map(trigger => checkAllValuesForTrigger(pluginData, counterName, trigger)));
|
||||
await Promise.all(triggersArr.map(trigger => checkAllValuesForReverseTrigger(pluginData, counterName, trigger)));
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
}
|
10
backend/src/plugins/Counters/functions/emitCounterEvent.ts
Normal file
10
backend/src/plugins/Counters/functions/emitCounterEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { CounterEvents, CountersPluginType } from "../types";
|
||||
import { GuildPluginData } from "knub";
|
||||
|
||||
export function emitCounterEvent<TEvent extends keyof CounterEvents>(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
event: TEvent,
|
||||
...rest: Parameters<CounterEvents[TEvent]>
|
||||
) {
|
||||
return pluginData.state.events.emit(event, ...rest);
|
||||
}
|
31
backend/src/plugins/Counters/functions/initCounterTrigger.ts
Normal file
31
backend/src/plugins/Counters/functions/initCounterTrigger.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { parseCondition } from "../../../data/GuildCounters";
|
||||
|
||||
/**
|
||||
* Initialize a counter trigger.
|
||||
* After a counter trigger has been initialized, it will be checked against whenever the counter's values change.
|
||||
* If the trigger is triggered, an event is emitted.
|
||||
*/
|
||||
export async function initCounterTrigger(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
condition: string,
|
||||
) {
|
||||
const counterId = pluginData.state.counterIds[counterName];
|
||||
if (!counterId) {
|
||||
throw new Error(`Unknown counter: ${counterName}`);
|
||||
}
|
||||
|
||||
const parsedComparison = parseCondition(condition);
|
||||
if (!parsedComparison) {
|
||||
throw new Error(`Invalid comparison string: ${condition}`);
|
||||
}
|
||||
|
||||
const [comparisonOp, comparisonValue] = parsedComparison;
|
||||
const counterTrigger = await pluginData.state.counters.initCounterTrigger(counterId, comparisonOp, comparisonValue);
|
||||
if (!pluginData.state.counterTriggersByCounterId.has(counterId)) {
|
||||
pluginData.state.counterTriggersByCounterId.set(counterId, new Map());
|
||||
}
|
||||
pluginData.state.counterTriggersByCounterId.get(counterId)!.set(counterTrigger.id, counterTrigger);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import { CounterEventEmitter, CountersPluginType } from "../types";
|
||||
import { GuildPluginData } from "knub";
|
||||
|
||||
export function offCounterEvent(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
...rest: Parameters<CounterEventEmitter["off"]>
|
||||
) {
|
||||
return pluginData.state.events.off(...rest);
|
||||
}
|
10
backend/src/plugins/Counters/functions/onCounterEvent.ts
Normal file
10
backend/src/plugins/Counters/functions/onCounterEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { CounterEvents, CountersPluginType } from "../types";
|
||||
import { GuildPluginData } from "knub";
|
||||
|
||||
export function onCounterEvent<TEvent extends keyof CounterEvents>(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
event: TEvent,
|
||||
listener: CounterEvents[TEvent],
|
||||
) {
|
||||
return pluginData.state.events.on(event, listener);
|
||||
}
|
45
backend/src/plugins/Counters/functions/setCounterValue.ts
Normal file
45
backend/src/plugins/Counters/functions/setCounterValue.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { checkCounterTrigger } from "./checkCounterTrigger";
|
||||
import { checkReverseCounterTrigger } from "./checkReverseCounterTrigger";
|
||||
|
||||
export async function setCounterValue(
|
||||
pluginData: GuildPluginData<CountersPluginType>,
|
||||
counterName: string,
|
||||
channelId: string | null,
|
||||
userId: string | null,
|
||||
value: number,
|
||||
) {
|
||||
const config = pluginData.config.get();
|
||||
const counter = config.counters[counterName];
|
||||
if (!counter) {
|
||||
throw new Error(`Unknown counter: ${counterName}`);
|
||||
}
|
||||
|
||||
if (counter.per_channel && !channelId) {
|
||||
throw new Error(`Counter is per channel but no channel ID was supplied`);
|
||||
}
|
||||
|
||||
if (counter.per_user && !userId) {
|
||||
throw new Error(`Counter is per user but no user ID was supplied`);
|
||||
}
|
||||
|
||||
const counterId = pluginData.state.counterIds[counterName];
|
||||
const lock = await pluginData.locks.acquire(counterId.toString());
|
||||
|
||||
await pluginData.state.counters.setCounterValue(counterId, channelId, userId, value);
|
||||
|
||||
// Check for trigger matches, if any, when the counter value changes
|
||||
const triggers = pluginData.state.counterTriggersByCounterId.get(counterId);
|
||||
if (triggers) {
|
||||
const triggersArr = Array.from(triggers.values());
|
||||
await Promise.all(
|
||||
triggersArr.map(trigger => checkCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
|
||||
);
|
||||
await Promise.all(
|
||||
triggersArr.map(trigger => checkReverseCounterTrigger(pluginData, counterName, trigger, channelId, userId)),
|
||||
);
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { CountersPluginType } from "../types";
|
||||
import { parseCondition } from "../../../data/GuildCounters";
|
||||
|
||||
export function validateCondition(pluginData: GuildPluginData<CountersPluginType>, condition: string) {
|
||||
const parsed = parseCondition(condition);
|
||||
return parsed != null;
|
||||
}
|
51
backend/src/plugins/Counters/types.ts
Normal file
51
backend/src/plugins/Counters/types.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
import * as t from "io-ts";
|
||||
import { BasePluginType } from "knub";
|
||||
import { GuildCounters } from "../../data/GuildCounters";
|
||||
import { tDelayString, tNullable } from "../../utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { CounterTrigger } from "../../data/entities/CounterTrigger";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
|
||||
export const Counter = t.type({
|
||||
name: tNullable(t.string),
|
||||
per_channel: t.boolean,
|
||||
per_user: t.boolean,
|
||||
initial_value: t.number,
|
||||
decay: tNullable(
|
||||
t.type({
|
||||
amount: t.number,
|
||||
every: tDelayString,
|
||||
}),
|
||||
),
|
||||
can_view: tNullable(t.boolean),
|
||||
can_edit: tNullable(t.boolean),
|
||||
});
|
||||
export type TCounter = t.TypeOf<typeof Counter>;
|
||||
|
||||
export const ConfigSchema = t.type({
|
||||
counters: t.record(t.string, Counter),
|
||||
can_view: t.boolean,
|
||||
can_edit: t.boolean,
|
||||
});
|
||||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
||||
export interface CounterEvents {
|
||||
trigger: (name: string, condition: string, channelId: string | null, userId: string | null) => void;
|
||||
reverseTrigger: (name: string, condition: string, channelId: string | null, userId: string | null) => void;
|
||||
}
|
||||
|
||||
export interface CounterEventEmitter extends EventEmitter {
|
||||
on<U extends keyof CounterEvents>(event: U, listener: CounterEvents[U]): this;
|
||||
emit<U extends keyof CounterEvents>(event: U, ...args: Parameters<CounterEvents[U]>): boolean;
|
||||
}
|
||||
|
||||
export interface CountersPluginType extends BasePluginType {
|
||||
config: TConfigSchema;
|
||||
state: {
|
||||
counters: GuildCounters;
|
||||
counterIds: Record<string, number>;
|
||||
decayTimers: Timeout[];
|
||||
events: CounterEventEmitter;
|
||||
counterTriggersByCounterId: Map<number, Map<number, CounterTrigger>>;
|
||||
};
|
||||
}
|
|
@ -31,7 +31,8 @@ const defaultOptions: PluginOptions<LogsPluginType> = {
|
|||
timestamp: FORMAT_NO_TIMESTAMP, // Legacy/deprecated, use timestamp_format below instead
|
||||
...DefaultLogMessages,
|
||||
},
|
||||
ping_user: true,
|
||||
ping_user: true, // Legacy/deprecated, if below is false mentions wont actually ping. In case you really want the old behavior, set below to true
|
||||
allow_user_mentions: false,
|
||||
timestamp_format: "YYYY-MM-DD HH:mm:ss z",
|
||||
include_embed_timestamp: true,
|
||||
},
|
||||
|
@ -40,7 +41,7 @@ const defaultOptions: PluginOptions<LogsPluginType> = {
|
|||
{
|
||||
level: ">=50",
|
||||
config: {
|
||||
ping_user: false,
|
||||
ping_user: false, // Legacy/deprecated, read comment on global ping_user option
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -38,7 +38,8 @@ export const ConfigSchema = t.type({
|
|||
timestamp: t.string, // Legacy/deprecated
|
||||
}),
|
||||
]),
|
||||
ping_user: t.boolean,
|
||||
ping_user: t.boolean, // Legacy/deprecated, if below is false mentions wont actually ping
|
||||
allow_user_mentions: t.boolean,
|
||||
timestamp_format: t.string,
|
||||
include_embed_timestamp: t.boolean,
|
||||
});
|
||||
|
|
|
@ -61,7 +61,12 @@ export async function getLogMessage(
|
|||
|
||||
const memberConfig = pluginData.config.getMatchingConfig({ member, userId: user.id }) || ({} as any);
|
||||
|
||||
// Revert to old behavior (verbose name w/o ping if allow_user_mentions is enabled (for whatever reason))
|
||||
if (config.allow_user_mentions) {
|
||||
mentions.push(memberConfig.ping_user ? verboseUserMention(user) : verboseUserName(user));
|
||||
} else {
|
||||
mentions.push(verboseUserMention(user));
|
||||
}
|
||||
}
|
||||
|
||||
return mentions.join(", ");
|
||||
|
|
|
@ -62,7 +62,7 @@ export async function log(pluginData: GuildPluginData<LogsPluginType>, type: Log
|
|||
type === LogType.CENSOR ||
|
||||
type === LogType.CLEAN
|
||||
) {
|
||||
if (data.channel.parent_id && opts.excluded_categories.includes(data.channel.parent_id)) {
|
||||
if (data.channel.parentID && opts.excluded_categories.includes(data.channel.parentID)) {
|
||||
continue logChannelLoop;
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +103,7 @@ export async function log(pluginData: GuildPluginData<LogsPluginType>, type: Log
|
|||
// Default to batched unless explicitly disabled
|
||||
const batched = opts.batched ?? true;
|
||||
const batchTime = opts.batch_time ?? 1000;
|
||||
const cfg = pluginData.config.get();
|
||||
|
||||
if (batched) {
|
||||
// If we're batching log messages, gather all log messages within the set batch_time into a single message
|
||||
|
@ -111,14 +112,14 @@ export async function log(pluginData: GuildPluginData<LogsPluginType>, type: Log
|
|||
setTimeout(async () => {
|
||||
const batchedMessage = pluginData.state.batches.get(channel.id)!.join("\n");
|
||||
pluginData.state.batches.delete(channel.id);
|
||||
createChunkedMessage(channel, batchedMessage).catch(noop);
|
||||
createChunkedMessage(channel, batchedMessage, { users: cfg.allow_user_mentions }).catch(noop);
|
||||
}, batchTime);
|
||||
}
|
||||
|
||||
pluginData.state.batches.get(channel.id)!.push(message);
|
||||
} else {
|
||||
// If we're not batching log messages, just send them immediately
|
||||
await createChunkedMessage(channel, message).catch(noop);
|
||||
await createChunkedMessage(channel, message, { users: cfg.allow_user_mentions }).catch(noop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { SoftbanCmd } from "./commands/SoftbanCommand";
|
|||
import { BanCmd } from "./commands/BanCmd";
|
||||
import { UnbanCmd } from "./commands/UnbanCmd";
|
||||
import { ForcebanCmd } from "./commands/ForcebanCmd";
|
||||
import { MassunbanCmd } from "./commands/MassUnbanCmd";
|
||||
import { MassbanCmd } from "./commands/MassBanCmd";
|
||||
import { AddCaseCmd } from "./commands/AddCaseCmd";
|
||||
import { CaseCmd } from "./commands/CaseCmd";
|
||||
|
@ -36,6 +37,12 @@ import { MassmuteCmd } from "./commands/MassmuteCmd";
|
|||
import { trimPluginDescription } from "../../utils";
|
||||
import { DeleteCaseCmd } from "./commands/DeleteCaseCmd";
|
||||
import { TimeAndDatePlugin } from "../TimeAndDate/TimeAndDatePlugin";
|
||||
import { GuildTempbans } from "../../data/GuildTempbans";
|
||||
import { outdatedTempbansLoop } from "./functions/outdatedTempbansLoop";
|
||||
import { EventEmitter } from "events";
|
||||
import { mapToPublicFn } from "../../pluginUtils";
|
||||
import { onModActionsEvent } from "./functions/onModActionsEvent";
|
||||
import { offModActionsEvent } from "./functions/offModActionsEvent";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
|
@ -49,6 +56,7 @@ const defaultOptions = {
|
|||
warn_message: "You have received a warning on the {guildName} server: {reason}",
|
||||
kick_message: "You have been kicked from the {guildName} server. Reason given: {reason}",
|
||||
ban_message: "You have been banned from the {guildName} server. Reason given: {reason}",
|
||||
tempban_message: "You have been banned from the {guildName} server for {banTime}. Reason given: {reason}",
|
||||
alert_on_rejoin: false,
|
||||
alert_channel: null,
|
||||
warn_notify_enabled: false,
|
||||
|
@ -64,6 +72,7 @@ const defaultOptions = {
|
|||
can_ban: false,
|
||||
can_view: false,
|
||||
can_addcase: false,
|
||||
can_massunban: false,
|
||||
can_massban: false,
|
||||
can_massmute: false,
|
||||
can_hidecase: false,
|
||||
|
@ -87,6 +96,7 @@ const defaultOptions = {
|
|||
{
|
||||
level: ">=100",
|
||||
config: {
|
||||
can_massunban: true,
|
||||
can_massban: true,
|
||||
can_massmute: true,
|
||||
can_hidecase: true,
|
||||
|
@ -131,6 +141,7 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()("mod
|
|||
ForcebanCmd,
|
||||
MassbanCmd,
|
||||
MassmuteCmd,
|
||||
MassunbanCmd,
|
||||
AddCaseCmd,
|
||||
CaseCmd,
|
||||
CasesUserCmd,
|
||||
|
@ -158,6 +169,12 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()("mod
|
|||
banUserId(pluginData, userId, reason, banOptions);
|
||||
};
|
||||
},
|
||||
|
||||
on: mapToPublicFn(onModActionsEvent),
|
||||
off: mapToPublicFn(offModActionsEvent),
|
||||
getEventEmitter(pluginData) {
|
||||
return () => pluginData.state.events;
|
||||
},
|
||||
},
|
||||
|
||||
onLoad(pluginData) {
|
||||
|
@ -165,8 +182,20 @@ export const ModActionsPlugin = zeppelinGuildPlugin<ModActionsPluginType>()("mod
|
|||
|
||||
state.mutes = GuildMutes.getGuildInstance(guild.id);
|
||||
state.cases = GuildCases.getGuildInstance(guild.id);
|
||||
state.tempbans = GuildTempbans.getGuildInstance(guild.id);
|
||||
state.serverLogs = new GuildLogs(guild.id);
|
||||
|
||||
state.unloaded = false;
|
||||
state.outdatedTempbansTimeout = null;
|
||||
state.ignoredEvents = [];
|
||||
|
||||
state.events = new EventEmitter();
|
||||
|
||||
outdatedTempbansLoop(pluginData);
|
||||
},
|
||||
|
||||
onUnload(pluginData) {
|
||||
pluginData.state.unloaded = true;
|
||||
pluginData.state.events.removeAllListeners();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -30,7 +30,8 @@ export const AddCaseCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user exists as a guild member, make sure we can act on them first
|
||||
|
|
|
@ -1,14 +1,16 @@
|
|||
import { modActionsCmd, IgnoredEventType } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { canActOn, sendErrorMessage, hasPermission, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { resolveUser, resolveMember } from "../../../utils";
|
||||
import { resolveUser, resolveMember, stripObjectToScalars, noop } from "../../../utils";
|
||||
import { isBanned } from "../functions/isBanned";
|
||||
import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs";
|
||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
||||
import { banUserId } from "../functions/banUserId";
|
||||
import { ignoreEvent } from "../functions/ignoreEvent";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { getMemberLevel, waitForReaction } from "knub/dist/helpers";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import { CasesPlugin } from "src/plugins/Cases/CasesPlugin";
|
||||
import { CaseTypes } from "src/data/CaseTypes";
|
||||
import { LogType } from "src/data/LogType";
|
||||
|
||||
const opts = {
|
||||
mod: ct.member({ option: true }),
|
||||
|
@ -20,9 +22,16 @@ const opts = {
|
|||
export const BanCmd = modActionsCmd({
|
||||
trigger: "ban",
|
||||
permission: "can_ban",
|
||||
description: "Ban the specified member",
|
||||
description: "Ban or Tempban the specified member",
|
||||
|
||||
signature: [
|
||||
{
|
||||
user: ct.string(),
|
||||
time: ct.delay(),
|
||||
reason: ct.string({ required: false, catchAll: true }),
|
||||
|
||||
...opts,
|
||||
},
|
||||
{
|
||||
user: ct.string(),
|
||||
reason: ct.string({ required: false, catchAll: true }),
|
||||
|
@ -34,26 +43,93 @@ export const BanCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
const time = args["time"] ? args["time"] : null;
|
||||
|
||||
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
const memberToBan = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
mod = args.mod;
|
||||
}
|
||||
|
||||
// acquire a lock because of the needed user-inputs below (if banned/not on server)
|
||||
const lock = await pluginData.locks.acquire(`ban-${user.id}`);
|
||||
let forceban = false;
|
||||
if (!memberToBan) {
|
||||
const existingTempban = await pluginData.state.tempbans.findExistingTempbanForUserId(user.id);
|
||||
const banned = await isBanned(pluginData, user.id);
|
||||
|
||||
if (!memberToBan) {
|
||||
if (banned) {
|
||||
sendErrorMessage(pluginData, msg.channel, `User is already banned`);
|
||||
// Abort if trying to ban user indefinitely if they are already banned indefinitely
|
||||
if (!existingTempban && !time) {
|
||||
sendErrorMessage(pluginData, msg.channel, `User is already banned indefinitely.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask the mod if we should update the existing ban
|
||||
const alreadyBannedMsg = await msg.channel.createMessage("User is already banned, update ban?");
|
||||
const reply = await waitForReaction(pluginData.client, alreadyBannedMsg, ["✅", "❌"], msg.author.id);
|
||||
|
||||
alreadyBannedMsg.delete().catch(noop);
|
||||
if (!reply || reply.name === "❌") {
|
||||
sendErrorMessage(pluginData, msg.channel, "User already banned, update cancelled by moderator");
|
||||
lock.unlock();
|
||||
return;
|
||||
} else {
|
||||
// Update or add new tempban / remove old tempban
|
||||
if (time && time > 0) {
|
||||
if (existingTempban) {
|
||||
pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id);
|
||||
} else {
|
||||
pluginData.state.tempbans.addTempban(user.id, time, mod.id);
|
||||
}
|
||||
} else if (existingTempban) {
|
||||
pluginData.state.tempbans.clear(user.id);
|
||||
}
|
||||
|
||||
// Create a new case for the updated ban since we never stored the old case id and log the action
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
modId: mod.id,
|
||||
type: CaseTypes.Ban,
|
||||
userId: user.id,
|
||||
reason,
|
||||
noteDetails: [`Ban updated to ${time ? humanizeDuration(time) : "indefinite"}`],
|
||||
});
|
||||
const logtype = time ? LogType.MEMBER_TIMED_BAN : LogType.MEMBER_BAN;
|
||||
pluginData.state.serverLogs.log(logtype, {
|
||||
mod: stripObjectToScalars(mod.user),
|
||||
user: stripObjectToScalars(user),
|
||||
caseNumber: createdCase.case_number,
|
||||
reason,
|
||||
banTime: time ? humanizeDuration(time) : null,
|
||||
});
|
||||
|
||||
sendSuccessMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`Ban updated to ${time ? "expire in " + humanizeDuration(time) + " from now" : "indefinite"}`,
|
||||
);
|
||||
lock.unlock();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Ask the mod if we should upgrade to a forceban as the user is not on the server
|
||||
const notOnServerMsg = await msg.channel.createMessage("User not found on the server, forceban instead?");
|
||||
const reply = await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id);
|
||||
|
||||
notOnServerMsg.delete();
|
||||
notOnServerMsg.delete().catch(noop);
|
||||
if (!reply || reply.name === "❌") {
|
||||
sendErrorMessage(pluginData, msg.channel, "User not on server, ban cancelled by moderator");
|
||||
lock.unlock();
|
||||
return;
|
||||
} else {
|
||||
forceban = true;
|
||||
|
@ -70,53 +146,62 @@ export const BanCmd = modActionsCmd({
|
|||
msg.channel,
|
||||
`Cannot ban: target permission level is equal or higher to yours, ${targetLevel} >= ${ourLevel}`,
|
||||
);
|
||||
lock.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||
let mod = msg.member;
|
||||
if (args.mod) {
|
||||
if (!hasPermission(pluginData, "can_act_as_other", { message: msg, channelId: msg.channel.id })) {
|
||||
sendErrorMessage(pluginData, msg.channel, "No permission for -mod");
|
||||
return;
|
||||
}
|
||||
|
||||
mod = args.mod;
|
||||
}
|
||||
|
||||
let contactMethods;
|
||||
try {
|
||||
contactMethods = readContactMethodsFromArgs(args);
|
||||
} catch (e) {
|
||||
sendErrorMessage(pluginData, msg.channel, e.message);
|
||||
lock.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteMessageDays = args["delete-days"] ?? pluginData.config.getForMessage(msg).ban_delete_message_days;
|
||||
const reason = formatReasonWithAttachments(args.reason, msg.attachments);
|
||||
const banResult = await banUserId(pluginData, user.id, reason, {
|
||||
const banResult = await banUserId(
|
||||
pluginData,
|
||||
user.id,
|
||||
reason,
|
||||
{
|
||||
contactMethods,
|
||||
caseArgs: {
|
||||
modId: mod.id,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
|
||||
},
|
||||
deleteMessageDays,
|
||||
});
|
||||
},
|
||||
time,
|
||||
);
|
||||
|
||||
if (banResult.status === "failed") {
|
||||
sendErrorMessage(pluginData, msg.channel, `Failed to ban member: ${banResult.error}`);
|
||||
lock.unlock();
|
||||
return;
|
||||
}
|
||||
|
||||
let forTime = "";
|
||||
if (time && time > 0) {
|
||||
if (existingTempban) {
|
||||
pluginData.state.tempbans.updateExpiryTime(user.id, time, mod.id);
|
||||
} else {
|
||||
pluginData.state.tempbans.addTempban(user.id, time, mod.id);
|
||||
}
|
||||
|
||||
forTime = `for ${humanizeDuration(time)} `;
|
||||
}
|
||||
|
||||
// Confirm the action to the moderator
|
||||
let response = "";
|
||||
if (!forceban) {
|
||||
response = `Banned **${user.username}#${user.discriminator}** (Case #${banResult.case.case_number})`;
|
||||
response = `Banned **${user.username}#${user.discriminator}** ${forTime}(Case #${banResult.case.case_number})`;
|
||||
if (banResult.notifyResult.text) response += ` (${banResult.notifyResult.text})`;
|
||||
} else {
|
||||
response = `Member forcebanned (Case #${banResult.case.case_number})`;
|
||||
response = `Member forcebanned ${forTime}(Case #${banResult.case.case_number})`;
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
sendSuccessMessage(pluginData, msg.channel, response);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -37,7 +37,8 @@ export const CasesUserCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const cases = await pluginData.state.cases.with("notes").getByUserId(user.id);
|
||||
|
|
|
@ -32,7 +32,8 @@ export const ForcebanCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user exists as a guild member, make sure we can act on them first
|
||||
|
@ -66,6 +67,7 @@ export const ForcebanCmd = modActionsCmd({
|
|||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_BAN, user.id);
|
||||
|
||||
try {
|
||||
// FIXME: Use banUserId()?
|
||||
await pluginData.guild.banMember(user.id, 1, reason != null ? encodeURIComponent(reason) : undefined);
|
||||
} catch (e) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Failed to forceban member");
|
||||
|
@ -92,5 +94,7 @@ export const ForcebanCmd = modActionsCmd({
|
|||
caseNumber: createdCase.case_number,
|
||||
reason,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("ban", user.id, reason);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -34,7 +34,8 @@ export const ForcemuteCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const memberToMute = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
|
|
|
@ -32,7 +32,8 @@ export const ForceUnmuteCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if they're muted in the first place
|
||||
|
|
|
@ -75,6 +75,8 @@ export const MassbanCmd = modActionsCmd({
|
|||
reason: `Mass ban: ${banReason}`,
|
||||
postInCaseLogOverride: false,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("ban", userId, banReason);
|
||||
} catch (e) {
|
||||
failedBans.push(userId);
|
||||
}
|
||||
|
|
125
backend/src/plugins/ModActions/commands/MassUnbanCmd.ts
Normal file
125
backend/src/plugins/ModActions/commands/MassUnbanCmd.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
import { modActionsCmd, IgnoredEventType } from "../types";
|
||||
import { commandTypeHelpers as ct } from "../../../commandTypes";
|
||||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { stripObjectToScalars } from "../../../utils";
|
||||
import { isBanned } from "../functions/isBanned";
|
||||
import { formatReasonWithAttachments } from "../functions/formatReasonWithAttachments";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { TextChannel } from "eris";
|
||||
import { waitForReply } from "knub/dist/helpers";
|
||||
import { ignoreEvent } from "../functions/ignoreEvent";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
|
||||
export const MassunbanCmd = modActionsCmd({
|
||||
trigger: "massunban",
|
||||
permission: "can_massunban",
|
||||
description: "Mass-unban a list of user IDs",
|
||||
|
||||
signature: [
|
||||
{
|
||||
userIds: ct.string({ rest: true }),
|
||||
},
|
||||
],
|
||||
|
||||
async run({ pluginData, message: msg, args }) {
|
||||
// Limit to 100 users at once (arbitrary?)
|
||||
if (args.userIds.length > 100) {
|
||||
sendErrorMessage(pluginData, msg.channel, `Can only mass-unban max 100 users at once`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for unban reason (cleaner this way instead of trying to cram it into the args)
|
||||
msg.channel.createMessage("Unban reason? `cancel` to cancel");
|
||||
const unbanReasonReply = await waitForReply(pluginData.client, msg.channel as TextChannel, msg.author.id);
|
||||
if (!unbanReasonReply || !unbanReasonReply.content || unbanReasonReply.content.toLowerCase().trim() === "cancel") {
|
||||
sendErrorMessage(pluginData, msg.channel, "Cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
const unbanReason = formatReasonWithAttachments(unbanReasonReply.content, msg.attachments);
|
||||
|
||||
// Ignore automatic unban cases and logs for these users
|
||||
// We'll create our own cases below and post a single "mass unbanned" log instead
|
||||
args.userIds.forEach(userId => {
|
||||
// Use longer timeouts since this can take a while
|
||||
ignoreEvent(pluginData, IgnoredEventType.Unban, userId, 120 * 1000);
|
||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, userId, 120 * 1000);
|
||||
});
|
||||
|
||||
// Show a loading indicator since this can take a while
|
||||
const loadingMsg = await msg.channel.createMessage("Unbanning...");
|
||||
|
||||
// Unban each user and count failed unbans (if any)
|
||||
const failedUnbans: Array<{ userId: string; reason: UnbanFailReasons }> = [];
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
for (const userId of args.userIds) {
|
||||
if (!(await isBanned(pluginData, userId))) {
|
||||
failedUnbans.push({ userId, reason: UnbanFailReasons.NOT_BANNED });
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await pluginData.guild.unbanMember(userId, unbanReason != null ? encodeURIComponent(unbanReason) : undefined);
|
||||
|
||||
await casesPlugin.createCase({
|
||||
userId,
|
||||
modId: msg.author.id,
|
||||
type: CaseTypes.Unban,
|
||||
reason: `Mass unban: ${unbanReason}`,
|
||||
postInCaseLogOverride: false,
|
||||
});
|
||||
} catch (e) {
|
||||
failedUnbans.push({ userId, reason: UnbanFailReasons.UNBAN_FAILED });
|
||||
}
|
||||
}
|
||||
|
||||
// Clear loading indicator
|
||||
loadingMsg.delete();
|
||||
|
||||
const successfulUnbanCount = args.userIds.length - failedUnbans.length;
|
||||
if (successfulUnbanCount === 0) {
|
||||
// All unbans failed - don't create a log entry and notify the user
|
||||
sendErrorMessage(pluginData, msg.channel, "All unbans failed. Make sure the IDs are valid and banned.");
|
||||
} else {
|
||||
// Some or all unbans were successful. Create a log entry for the mass unban and notify the user.
|
||||
pluginData.state.serverLogs.log(LogType.MASSUNBAN, {
|
||||
mod: stripObjectToScalars(msg.author),
|
||||
count: successfulUnbanCount,
|
||||
reason: unbanReason,
|
||||
});
|
||||
|
||||
if (failedUnbans.length) {
|
||||
const notBanned = failedUnbans.filter(x => x.reason === UnbanFailReasons.NOT_BANNED);
|
||||
const unbanFailed = failedUnbans.filter(x => x.reason === UnbanFailReasons.UNBAN_FAILED);
|
||||
|
||||
let failedMsg = "";
|
||||
if (notBanned.length > 0) {
|
||||
failedMsg += `${notBanned.length}x ${UnbanFailReasons.NOT_BANNED}:`;
|
||||
notBanned.forEach(fail => {
|
||||
failedMsg += " " + fail.userId;
|
||||
});
|
||||
}
|
||||
if (unbanFailed.length > 0) {
|
||||
failedMsg += `\n${unbanFailed.length}x ${UnbanFailReasons.UNBAN_FAILED}:`;
|
||||
unbanFailed.forEach(fail => {
|
||||
failedMsg += " " + fail.userId;
|
||||
});
|
||||
}
|
||||
|
||||
sendSuccessMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`Unbanned ${successfulUnbanCount} users, ${failedUnbans.length} failed:\n${failedMsg}`,
|
||||
);
|
||||
} else {
|
||||
sendSuccessMessage(pluginData, msg.channel, `Unbanned ${successfulUnbanCount} users successfully`);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
enum UnbanFailReasons {
|
||||
NOT_BANNED = "Not banned",
|
||||
UNBAN_FAILED = "Unban failed",
|
||||
}
|
|
@ -44,7 +44,8 @@ export const MuteCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const memberToMute = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
|
|
|
@ -15,13 +15,19 @@ export const NoteCmd = modActionsCmd({
|
|||
|
||||
signature: {
|
||||
user: ct.string(),
|
||||
note: ct.string({ catchAll: true }),
|
||||
note: ct.string({ required: false, catchAll: true }),
|
||||
},
|
||||
|
||||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.note && msg.attachments.length === 0) {
|
||||
sendErrorMessage(pluginData, msg.channel, "Text or attachment required");
|
||||
return;
|
||||
}
|
||||
|
||||
const userName = `${user.username}#${user.discriminator}`;
|
||||
|
@ -43,5 +49,7 @@ export const NoteCmd = modActionsCmd({
|
|||
});
|
||||
|
||||
sendSuccessMessage(pluginData, msg.channel, `Note added on **${userName}** (Case #${createdCase.case_number})`);
|
||||
|
||||
pluginData.state.events.emit("note", user.id, reason);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -29,7 +29,8 @@ export const UnbanCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
// The moderator who did the action is the message author or, if used, the specified -mod
|
||||
|
@ -63,6 +64,8 @@ export const UnbanCmd = modActionsCmd({
|
|||
reason,
|
||||
ppId: mod.id !== msg.author.id ? msg.author.id : undefined,
|
||||
});
|
||||
// Delete the tempban, if one exists
|
||||
pluginData.state.tempbans.clear(user.id);
|
||||
|
||||
// Confirm the action
|
||||
sendSuccessMessage(pluginData, msg.channel, `Member unbanned (Case #${createdCase.case_number})`);
|
||||
|
|
|
@ -34,7 +34,8 @@ export const UnmuteCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const memberToUnmute = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
|
|
|
@ -30,7 +30,8 @@ export const WarnCmd = modActionsCmd({
|
|||
async run({ pluginData, message: msg, args }) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const memberToWarn = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
|
@ -111,5 +112,7 @@ export const WarnCmd = modActionsCmd({
|
|||
msg.channel,
|
||||
`Warned **${memberToWarn.user.username}#${memberToWarn.user.discriminator}** (Case #${warnResult.case.case_number})${messageResultText}`,
|
||||
);
|
||||
|
||||
pluginData.state.events.emit("warn", user.id, reason);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -69,5 +69,7 @@ export const CreateBanCaseOnManualBanEvt = modActionsEvt(
|
|||
caseNumber: createdCase?.case_number ?? 0,
|
||||
reason,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("ban", user.id, reason);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -62,6 +62,8 @@ export const CreateKickCaseOnManualKickEvt = modActionsEvt(
|
|||
mod: mod ? stripObjectToScalars(mod) : null,
|
||||
caseNumber: createdCase?.case_number ?? 0,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("kick", member.id, kickAuditLogEntry.reason || undefined);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -66,5 +66,7 @@ export const CreateUnbanCaseOnManualUnbanEvt = modActionsEvt(
|
|||
userId: user.id,
|
||||
caseNumber: createdCase?.case_number ?? 0,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("unban", user.id);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -25,7 +25,8 @@ export async function actualKickMemberCmd(
|
|||
) {
|
||||
const user = await resolveUser(pluginData.client, args.user);
|
||||
if (!user.id) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
sendErrorMessage(pluginData, msg.channel, `User not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
const memberToKick = await resolveMember(pluginData.client, pluginData.guild, user.id);
|
||||
|
|
|
@ -16,6 +16,7 @@ import { ignoreEvent } from "./ignoreEvent";
|
|||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { logger } from "../../../logger";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
|
||||
/**
|
||||
* Ban the specified user id, whether or not they're actually on the server at the time. Generates a case.
|
||||
|
@ -25,6 +26,7 @@ export async function banUserId(
|
|||
userId: string,
|
||||
reason?: string,
|
||||
banOptions: BanOptions = {},
|
||||
banTime?: number,
|
||||
): Promise<BanResult> {
|
||||
const config = pluginData.config.get();
|
||||
const user = await resolveUser(pluginData.client, userId);
|
||||
|
@ -43,7 +45,7 @@ export async function banUserId(
|
|||
: getDefaultContactMethods(pluginData, "ban");
|
||||
|
||||
if (contactMethods.length) {
|
||||
if (config.ban_message) {
|
||||
if (!banTime && config.ban_message) {
|
||||
const banMessage = await renderTemplate(config.ban_message, {
|
||||
guildName: pluginData.guild.name,
|
||||
reason,
|
||||
|
@ -52,9 +54,20 @@ export async function banUserId(
|
|||
: {},
|
||||
});
|
||||
|
||||
notifyResult = await notifyUser(user, banMessage, contactMethods);
|
||||
} else if (banTime && config.tempban_message) {
|
||||
const banMessage = await renderTemplate(config.tempban_message, {
|
||||
guildName: pluginData.guild.name,
|
||||
reason,
|
||||
moderator: banOptions.caseArgs?.modId
|
||||
? stripObjectToScalars(await resolveUser(pluginData.client, banOptions.caseArgs.modId))
|
||||
: {},
|
||||
banTime: humanizeDuration(banTime),
|
||||
});
|
||||
|
||||
notifyResult = await notifyUser(user, banMessage, contactMethods);
|
||||
} else {
|
||||
notifyResult = createUserNotificationError("No ban message specified in config");
|
||||
notifyResult = createUserNotificationError("No ban/tempban message specified in config");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -87,24 +100,35 @@ export async function banUserId(
|
|||
// Create a case for this action
|
||||
const modId = banOptions.caseArgs?.modId || pluginData.client.user.id;
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
|
||||
const noteDetails: string[] = [];
|
||||
const timeUntilUnban = banTime ? humanizeDuration(banTime) : "indefinite";
|
||||
const timeDetails = `Banned ${banTime ? `for ${timeUntilUnban}` : "indefinitely"}`;
|
||||
if (notifyResult.text) noteDetails.push(ucfirst(notifyResult.text));
|
||||
noteDetails.push(timeDetails);
|
||||
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
...(banOptions.caseArgs || {}),
|
||||
userId,
|
||||
modId,
|
||||
type: CaseTypes.Ban,
|
||||
reason,
|
||||
noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [],
|
||||
noteDetails,
|
||||
});
|
||||
|
||||
// Log the action
|
||||
const mod = await resolveUser(pluginData.client, modId);
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_BAN, {
|
||||
const logtype = banTime ? LogType.MEMBER_TIMED_BAN : LogType.MEMBER_BAN;
|
||||
pluginData.state.serverLogs.log(logtype, {
|
||||
mod: stripObjectToScalars(mod),
|
||||
user: stripObjectToScalars(user),
|
||||
caseNumber: createdCase.case_number,
|
||||
reason,
|
||||
banTime: banTime ? humanizeDuration(banTime) : null,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("ban", user.id, reason);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
case: createdCase,
|
||||
|
|
|
@ -85,6 +85,8 @@ export async function kickMember(
|
|||
reason,
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("kick", member.id, reason);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
case: createdCase,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { ModActionsEvents, ModActionsPluginType } from "../types";
|
||||
|
||||
export function offModActionsEvent<TEvent extends keyof ModActionsEvents>(
|
||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||
event: TEvent,
|
||||
listener: ModActionsEvents[TEvent],
|
||||
) {
|
||||
return pluginData.state.events.off(event, listener);
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { ModActionsEvents, ModActionsPluginType } from "../types";
|
||||
|
||||
export function onModActionsEvent<TEvent extends keyof ModActionsEvents>(
|
||||
pluginData: GuildPluginData<ModActionsPluginType>,
|
||||
event: TEvent,
|
||||
listener: ModActionsEvents[TEvent],
|
||||
) {
|
||||
return pluginData.state.events.on(event, listener);
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { resolveUser, SECONDS, stripObjectToScalars } from "../../../utils";
|
||||
import { GuildPluginData } from "knub";
|
||||
import { IgnoredEventType, ModActionsPluginType } from "../types";
|
||||
import { LogType } from "src/data/LogType";
|
||||
import { formatReasonWithAttachments } from "./formatReasonWithAttachments";
|
||||
import { ignoreEvent } from "./ignoreEvent";
|
||||
import { isBanned } from "./isBanned";
|
||||
import { logger } from "src/logger";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import moment from "moment-timezone";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
|
||||
const TEMPBAN_LOOP_TIME = 60 * SECONDS;
|
||||
|
||||
export async function outdatedTempbansLoop(pluginData: GuildPluginData<ModActionsPluginType>) {
|
||||
const outdatedTempbans = await pluginData.state.tempbans.getExpiredTempbans();
|
||||
|
||||
for (const tempban of outdatedTempbans) {
|
||||
if (!(await isBanned(pluginData, tempban.user_id))) {
|
||||
pluginData.state.tempbans.clear(tempban.user_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
pluginData.state.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, tempban.user_id);
|
||||
const reason = formatReasonWithAttachments(
|
||||
`Tempban timed out.
|
||||
Tempbanned at: \`${tempban.created_at} UTC\``,
|
||||
[],
|
||||
);
|
||||
try {
|
||||
ignoreEvent(pluginData, IgnoredEventType.Unban, tempban.user_id);
|
||||
await pluginData.guild.unbanMember(tempban.user_id, reason != null ? encodeURIComponent(reason) : undefined);
|
||||
} catch (e) {
|
||||
pluginData.state.serverLogs.log(LogType.BOT_ALERT, {
|
||||
body: `Encountered an error trying to automatically unban ${tempban.user_id} after tempban timeout`,
|
||||
});
|
||||
logger.warn(`Error automatically unbanning ${tempban.user_id} (tempban timeout): ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create case and delete tempban
|
||||
const casesPlugin = pluginData.getPlugin(CasesPlugin);
|
||||
const createdCase = await casesPlugin.createCase({
|
||||
userId: tempban.user_id,
|
||||
modId: tempban.mod_id,
|
||||
type: CaseTypes.Unban,
|
||||
reason,
|
||||
ppId: undefined,
|
||||
});
|
||||
pluginData.state.tempbans.clear(tempban.user_id);
|
||||
|
||||
// Log the unban
|
||||
const banTime = moment(tempban.created_at).diff(moment(tempban.expires_at));
|
||||
pluginData.state.serverLogs.log(LogType.MEMBER_TIMED_UNBAN, {
|
||||
mod: stripObjectToScalars(await resolveUser(pluginData.client, tempban.mod_id)),
|
||||
userId: tempban.user_id,
|
||||
caseNumber: createdCase.case_number,
|
||||
reason,
|
||||
banTime: humanizeDuration(banTime),
|
||||
});
|
||||
}
|
||||
|
||||
if (!pluginData.state.unloaded) {
|
||||
pluginData.state.outdatedTempbansTimeout = setTimeout(() => outdatedTempbansLoop(pluginData), TEMPBAN_LOOP_TIME);
|
||||
}
|
||||
}
|
|
@ -7,6 +7,9 @@ import { GuildLogs } from "../../data/GuildLogs";
|
|||
import { Case } from "../../data/entities/Case";
|
||||
import { CaseArgs } from "../Cases/types";
|
||||
import { TextChannel } from "eris";
|
||||
import { GuildTempbans } from "../../data/GuildTempbans";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export const ConfigSchema = t.type({
|
||||
dm_on_warn: t.boolean,
|
||||
|
@ -19,6 +22,7 @@ export const ConfigSchema = t.type({
|
|||
warn_message: tNullable(t.string),
|
||||
kick_message: tNullable(t.string),
|
||||
ban_message: tNullable(t.string),
|
||||
tempban_message: tNullable(t.string),
|
||||
alert_on_rejoin: t.boolean,
|
||||
alert_channel: tNullable(t.string),
|
||||
warn_notify_enabled: t.boolean,
|
||||
|
@ -32,6 +36,7 @@ export const ConfigSchema = t.type({
|
|||
can_ban: t.boolean,
|
||||
can_view: t.boolean,
|
||||
can_addcase: t.boolean,
|
||||
can_massunban: t.boolean,
|
||||
can_massban: t.boolean,
|
||||
can_massmute: t.boolean,
|
||||
can_hidecase: t.boolean,
|
||||
|
@ -41,14 +46,33 @@ export const ConfigSchema = t.type({
|
|||
});
|
||||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
||||
export interface ModActionsEvents {
|
||||
note: (userId: string, reason?: string) => void;
|
||||
warn: (userId: string, reason?: string) => void;
|
||||
kick: (userId: string, reason?: string) => void;
|
||||
ban: (userId: string, reason?: string) => void;
|
||||
unban: (userId: string, reason?: string) => void;
|
||||
// mute/unmute are in the Mutes plugin
|
||||
}
|
||||
|
||||
export interface ModActionsEventEmitter extends EventEmitter {
|
||||
on<U extends keyof ModActionsEvents>(event: U, listener: ModActionsEvents[U]): this;
|
||||
emit<U extends keyof ModActionsEvents>(event: U, ...args: Parameters<ModActionsEvents[U]>): boolean;
|
||||
}
|
||||
|
||||
export interface ModActionsPluginType extends BasePluginType {
|
||||
config: TConfigSchema;
|
||||
state: {
|
||||
mutes: GuildMutes;
|
||||
cases: GuildCases;
|
||||
tempbans: GuildTempbans;
|
||||
serverLogs: GuildLogs;
|
||||
|
||||
unloaded: boolean;
|
||||
outdatedTempbansTimeout: Timeout | null;
|
||||
ignoredEvents: IIgnoredEvent[];
|
||||
|
||||
events: ModActionsEventEmitter;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -115,5 +139,7 @@ export interface BanOptions {
|
|||
deleteMessageDays?: number;
|
||||
}
|
||||
|
||||
export type ModActionType = "note" | "warn" | "mute" | "unmute" | "kick" | "ban" | "unban";
|
||||
|
||||
export const modActionsCmd = guildCommand<ModActionsPluginType>();
|
||||
export const modActionsEvt = guildEventListener<ModActionsPluginType>();
|
||||
|
|
|
@ -13,11 +13,13 @@ import { ClearMutesWithoutRoleCmd } from "./commands/ClearMutesWithoutRoleCmd";
|
|||
import { ClearMutesCmd } from "./commands/ClearMutesCmd";
|
||||
import { muteUser } from "./functions/muteUser";
|
||||
import { unmuteUser } from "./functions/unmuteUser";
|
||||
import { CaseArgs } from "../Cases/types";
|
||||
import { Member } from "eris";
|
||||
import { ClearActiveMuteOnMemberBanEvt } from "./events/ClearActiveMuteOnMemberBanEvt";
|
||||
import { ReapplyActiveMuteOnJoinEvt } from "./events/ReapplyActiveMuteOnJoinEvt";
|
||||
import { mapToPublicFn } from "../../pluginUtils";
|
||||
import { EventEmitter } from "events";
|
||||
import { onMutesEvent } from "./functions/onMutesEvent";
|
||||
import { offMutesEvent } from "./functions/offMutesEvent";
|
||||
|
||||
const defaultOptions = {
|
||||
config: {
|
||||
|
@ -32,6 +34,8 @@ const defaultOptions = {
|
|||
mute_message: "You have been muted on the {guildName} server. Reason given: {reason}",
|
||||
timed_mute_message: "You have been muted on the {guildName} server for {time}. Reason given: {reason}",
|
||||
update_mute_message: "Your mute on the {guildName} server has been updated to {time}.",
|
||||
remove_roles_on_mute: false,
|
||||
restore_roles_on_mute: false,
|
||||
|
||||
can_view_list: false,
|
||||
can_cleanup: false,
|
||||
|
@ -91,6 +95,12 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()("mutes", {
|
|||
return muteRole ? member.roles.includes(muteRole) : false;
|
||||
};
|
||||
},
|
||||
|
||||
on: mapToPublicFn(onMutesEvent),
|
||||
off: mapToPublicFn(offMutesEvent),
|
||||
getEventEmitter(pluginData) {
|
||||
return () => pluginData.state.events;
|
||||
},
|
||||
},
|
||||
|
||||
onLoad(pluginData) {
|
||||
|
@ -99,6 +109,8 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()("mutes", {
|
|||
pluginData.state.serverLogs = new GuildLogs(pluginData.guild.id);
|
||||
pluginData.state.archives = GuildArchives.getGuildInstance(pluginData.guild.id);
|
||||
|
||||
pluginData.state.events = new EventEmitter();
|
||||
|
||||
// Check for expired mutes every 5s
|
||||
const firstCheckTime = Math.max(Date.now(), FIRST_CHECK_TIME) + FIRST_CHECK_INCREMENT;
|
||||
FIRST_CHECK_TIME = firstCheckTime;
|
||||
|
@ -114,5 +126,6 @@ export const MutesPlugin = zeppelinGuildPlugin<MutesPluginType>()("mutes", {
|
|||
|
||||
onUnload(pluginData) {
|
||||
clearInterval(pluginData.state.muteClearIntervalId);
|
||||
pluginData.state.events.removeAllListeners();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import { GuildPluginData } from "knub";
|
|||
import { MutesPluginType } from "../types";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { resolveMember, stripObjectToScalars, UnknownUser } from "../../../utils";
|
||||
import { MemberOptions } from "eris";
|
||||
|
||||
export async function clearExpiredMutes(pluginData: GuildPluginData<MutesPluginType>) {
|
||||
const expiredMutes = await pluginData.state.mutes.getExpiredMutes();
|
||||
|
@ -14,6 +15,14 @@ export async function clearExpiredMutes(pluginData: GuildPluginData<MutesPluginT
|
|||
if (muteRole) {
|
||||
await member.removeRole(muteRole);
|
||||
}
|
||||
if (mute.roles_to_restore) {
|
||||
const memberOptions: MemberOptions = {};
|
||||
const guildRoles = pluginData.guild.roles;
|
||||
memberOptions.roles = Array.from(
|
||||
new Set([...mute.roles_to_restore, ...member.roles.filter(x => x !== muteRole && guildRoles.has(x))]),
|
||||
);
|
||||
member.edit(memberOptions);
|
||||
}
|
||||
} catch (e) {
|
||||
pluginData.state.serverLogs.log(LogType.BOT_ALERT, {
|
||||
body: `Failed to remove mute role from {userMention(member)}`,
|
||||
|
@ -29,5 +38,7 @@ export async function clearExpiredMutes(pluginData: GuildPluginData<MutesPluginT
|
|||
? stripObjectToScalars(member, ["user", "roles"])
|
||||
: { id: mute.user_id, user: new UnknownUser({ id: mute.user_id }) },
|
||||
});
|
||||
|
||||
pluginData.state.events.emit("unmute", mute.user_id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,11 +12,13 @@ import {
|
|||
UserNotificationMethod,
|
||||
} from "../../../utils";
|
||||
import { renderTemplate } from "../../../templateFormatter";
|
||||
import { TextChannel, User } from "eris";
|
||||
import { MemberOptions, TextChannel, User } from "eris";
|
||||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { Case } from "../../../data/entities/Case";
|
||||
import { sendErrorMessage } from "src/pluginUtils";
|
||||
import { LogsPlugin } from "src/plugins/Logs/LogsPlugin";
|
||||
|
||||
export async function muteUser(
|
||||
pluginData: GuildPluginData<MutesPluginType>,
|
||||
|
@ -24,6 +26,8 @@ export async function muteUser(
|
|||
muteTime?: number,
|
||||
reason?: string,
|
||||
muteOptions: MuteOptions = {},
|
||||
removeRolesOnMuteOverride: boolean | string[] | null = null,
|
||||
restoreRolesOnMuteOverride: boolean | string[] | null = null,
|
||||
) {
|
||||
const lock = await pluginData.locks.acquire(`mute-${userId}`);
|
||||
|
||||
|
@ -50,10 +54,69 @@ export async function muteUser(
|
|||
const member = await resolveMember(pluginData.client, pluginData.guild, user.id, true); // Grab the fresh member so we don't have stale role info
|
||||
const config = pluginData.config.getMatchingConfig({ member, userId });
|
||||
|
||||
let rolesToRestore: string[] = [];
|
||||
if (member) {
|
||||
const logs = pluginData.getPlugin(LogsPlugin);
|
||||
// remove and store any roles to be removed/restored
|
||||
const currentUserRoles = member.roles;
|
||||
const memberOptions: MemberOptions = {};
|
||||
const removeRoles = removeRolesOnMuteOverride ?? config.remove_roles_on_mute;
|
||||
const restoreRoles = restoreRolesOnMuteOverride ?? config.restore_roles_on_mute;
|
||||
|
||||
// remove roles
|
||||
if (!Array.isArray(removeRoles)) {
|
||||
if (removeRoles) {
|
||||
// exclude managed roles from being removed
|
||||
const managedRoles = pluginData.guild.roles.filter(x => x.managed).map(y => y.id);
|
||||
memberOptions.roles = managedRoles.filter(x => member.roles.includes(x));
|
||||
await member.edit(memberOptions);
|
||||
}
|
||||
} else {
|
||||
memberOptions.roles = currentUserRoles.filter(x => !(<string[]>removeRoles).includes(x));
|
||||
await member.edit(memberOptions);
|
||||
}
|
||||
|
||||
// set roles to be restored
|
||||
if (!Array.isArray(restoreRoles)) {
|
||||
if (restoreRoles) {
|
||||
rolesToRestore = currentUserRoles;
|
||||
}
|
||||
} else {
|
||||
rolesToRestore = currentUserRoles.filter(x => (<string[]>restoreRoles).includes(x));
|
||||
}
|
||||
|
||||
// Apply mute role if it's missing
|
||||
if (!member.roles.includes(muteRole)) {
|
||||
try {
|
||||
await member.addRole(muteRole);
|
||||
} catch (e) {
|
||||
const actualMuteRole = pluginData.guild.roles.find(x => x.id === muteRole);
|
||||
if (!actualMuteRole) {
|
||||
lock.unlock();
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Cannot mute users, specified mute role Id is invalid`,
|
||||
});
|
||||
throw new RecoverablePluginError(ERRORS.INVALID_MUTE_ROLE_ID);
|
||||
}
|
||||
|
||||
const zep = await resolveMember(pluginData.client, pluginData.guild, pluginData.client.user.id);
|
||||
const zepRoles = pluginData.guild.roles.filter(x => zep!.roles.includes(x.id));
|
||||
// If we have roles and one of them is above the muted role, throw generic error
|
||||
if (zepRoles.length >= 0 && zepRoles.some(zepRole => zepRole.position > actualMuteRole.position)) {
|
||||
lock.unlock();
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Cannot mute user ${member.id}: ${e}`,
|
||||
});
|
||||
throw e;
|
||||
} else {
|
||||
// Otherwise, throw error that mute role is above zeps roles
|
||||
lock.unlock();
|
||||
logs.log(LogType.BOT_ALERT, {
|
||||
body: `Cannot mute users, specified mute role is above Zeppelin in the role hierarchy`,
|
||||
});
|
||||
throw new RecoverablePluginError(ERRORS.MUTE_ROLE_ABOVE_ZEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If enabled, move the user to the mute voice channel (e.g. afk - just to apply the voice perms from the mute role)
|
||||
|
@ -71,9 +134,12 @@ export async function muteUser(
|
|||
let notifyResult: UserNotificationResult = { method: null, success: true };
|
||||
|
||||
if (existingMute) {
|
||||
await pluginData.state.mutes.updateExpiryTime(user.id, muteTime);
|
||||
if (existingMute.roles_to_restore?.length || rolesToRestore?.length) {
|
||||
rolesToRestore = Array.from(new Set([...existingMute.roles_to_restore, ...rolesToRestore]));
|
||||
}
|
||||
await pluginData.state.mutes.updateExpiryTime(user.id, muteTime, rolesToRestore);
|
||||
} else {
|
||||
await pluginData.state.mutes.addMute(user.id, muteTime);
|
||||
await pluginData.state.mutes.addMute(user.id, muteTime, rolesToRestore);
|
||||
}
|
||||
|
||||
const template = existingMute
|
||||
|
@ -180,6 +246,8 @@ export async function muteUser(
|
|||
|
||||
lock.unlock();
|
||||
|
||||
pluginData.state.events.emit("mute", user.id, reason);
|
||||
|
||||
return {
|
||||
case: theCase,
|
||||
notifyResult,
|
||||
|
|
10
backend/src/plugins/Mutes/functions/offMutesEvent.ts
Normal file
10
backend/src/plugins/Mutes/functions/offMutesEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { MutesEvents, MutesPluginType } from "../types";
|
||||
|
||||
export function offMutesEvent<TEvent extends keyof MutesEvents>(
|
||||
pluginData: GuildPluginData<MutesPluginType>,
|
||||
event: TEvent,
|
||||
listener: MutesEvents[TEvent],
|
||||
) {
|
||||
return pluginData.state.events.off(event, listener);
|
||||
}
|
10
backend/src/plugins/Mutes/functions/onMutesEvent.ts
Normal file
10
backend/src/plugins/Mutes/functions/onMutesEvent.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { GuildPluginData } from "knub";
|
||||
import { MutesEvents, MutesPluginType } from "../types";
|
||||
|
||||
export function onMutesEvent<TEvent extends keyof MutesEvents>(
|
||||
pluginData: GuildPluginData<MutesPluginType>,
|
||||
event: TEvent,
|
||||
listener: MutesEvents[TEvent],
|
||||
) {
|
||||
return pluginData.state.events.on(event, listener);
|
||||
}
|
|
@ -7,7 +7,7 @@ import humanizeDuration from "humanize-duration";
|
|||
import { CasesPlugin } from "../../Cases/CasesPlugin";
|
||||
import { CaseTypes } from "../../../data/CaseTypes";
|
||||
import { LogType } from "../../../data/LogType";
|
||||
import { WithRequiredProps } from "../../../utils/typeUtils";
|
||||
import { MemberOptions } from "eris";
|
||||
|
||||
export async function unmuteUser(
|
||||
pluginData: GuildPluginData<MutesPluginType>,
|
||||
|
@ -36,7 +36,16 @@ export async function unmuteUser(
|
|||
if (muteRole && member.roles.includes(muteRole)) {
|
||||
await member.removeRole(muteRole);
|
||||
}
|
||||
if (existingMute?.roles_to_restore) {
|
||||
const memberOptions: MemberOptions = {};
|
||||
const guildRoles = pluginData.guild.roles;
|
||||
memberOptions.roles = Array.from(
|
||||
new Set([...existingMute.roles_to_restore, ...member.roles.filter(x => x !== muteRole && guildRoles.has(x))]),
|
||||
);
|
||||
member.edit(memberOptions);
|
||||
}
|
||||
} else {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn(
|
||||
`Member ${userId} not found in guild ${pluginData.guild.name} (${pluginData.guild.id}) when attempting to unmute`,
|
||||
);
|
||||
|
@ -87,6 +96,12 @@ export async function unmuteUser(
|
|||
});
|
||||
}
|
||||
|
||||
if (!unmuteTime) {
|
||||
// If the member was unmuted, not just scheduled to be unmuted, fire the unmute event as well
|
||||
// Scheduled unmutes have their event fired in clearExpiredMutes()
|
||||
pluginData.state.events.emit("unmute", user.id, caseArgs.reason);
|
||||
}
|
||||
|
||||
return {
|
||||
case: createdCase,
|
||||
};
|
||||
|
|
|
@ -10,6 +10,7 @@ import { GuildArchives } from "../../data/GuildArchives";
|
|||
import { GuildMutes } from "../../data/GuildMutes";
|
||||
import { CaseArgs } from "../Cases/types";
|
||||
import Timeout = NodeJS.Timeout;
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
export const ConfigSchema = t.type({
|
||||
mute_role: tNullable(t.string),
|
||||
|
@ -23,12 +24,24 @@ export const ConfigSchema = t.type({
|
|||
mute_message: tNullable(t.string),
|
||||
timed_mute_message: tNullable(t.string),
|
||||
update_mute_message: tNullable(t.string),
|
||||
remove_roles_on_mute: t.union([t.boolean, t.array(t.string)]),
|
||||
restore_roles_on_mute: t.union([t.boolean, t.array(t.string)]),
|
||||
|
||||
can_view_list: t.boolean,
|
||||
can_cleanup: t.boolean,
|
||||
});
|
||||
export type TConfigSchema = t.TypeOf<typeof ConfigSchema>;
|
||||
|
||||
export interface MutesEvents {
|
||||
mute: (userId: string, reason?: string) => void;
|
||||
unmute: (userId: string, reason?: string) => void;
|
||||
}
|
||||
|
||||
export interface MutesEventEmitter extends EventEmitter {
|
||||
on<U extends keyof MutesEvents>(event: U, listener: MutesEvents[U]): this;
|
||||
emit<U extends keyof MutesEvents>(event: U, ...args: Parameters<MutesEvents[U]>): boolean;
|
||||
}
|
||||
|
||||
export interface MutesPluginType extends BasePluginType {
|
||||
config: TConfigSchema;
|
||||
state: {
|
||||
|
@ -38,6 +51,8 @@ export interface MutesPluginType extends BasePluginType {
|
|||
archives: GuildArchives;
|
||||
|
||||
muteClearIntervalId: Timeout;
|
||||
|
||||
events: MutesEventEmitter;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,8 @@ export const NamesCmd = nameHistoryCmd({
|
|||
const usernames = await pluginData.state.usernameHistory.getByUserId(args.userId);
|
||||
|
||||
if (nicknames.length === 0 && usernames.length === 0) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "No name history found");
|
||||
sendErrorMessage(pluginData, msg.channel, "No name history found");
|
||||
return;
|
||||
}
|
||||
|
||||
const nicknameRows = nicknames.map(
|
||||
|
|
|
@ -16,7 +16,8 @@ export const ScheduledPostsDeleteCmd = postCmd({
|
|||
scheduledPosts.sort(sorter("post_at"));
|
||||
const post = scheduledPosts[args.num - 1];
|
||||
if (!post) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Scheduled post not found");
|
||||
sendErrorMessage(pluginData, msg.channel, "Scheduled post not found");
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginData.state.scheduledPosts.delete(post.id);
|
||||
|
|
|
@ -18,7 +18,8 @@ export const ScheduledPostsShowCmd = postCmd({
|
|||
scheduledPosts.sort(sorter("post_at"));
|
||||
const post = scheduledPosts[args.num - 1];
|
||||
if (!post) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Scheduled post not found");
|
||||
sendErrorMessage(pluginData, msg.channel, "Scheduled post not found");
|
||||
return;
|
||||
}
|
||||
|
||||
postMessage(pluginData, msg.channel as TextChannel, post.content, post.attachments, post.enable_mentions);
|
||||
|
|
|
@ -39,14 +39,12 @@ export async function actualPostCmd(
|
|||
|
||||
if (opts.repeat) {
|
||||
if (opts.repeat < MIN_REPEAT_TIME) {
|
||||
return sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
`Minimum time for -repeat is ${humanizeDuration(MIN_REPEAT_TIME)}`,
|
||||
);
|
||||
sendErrorMessage(pluginData, msg.channel, `Minimum time for -repeat is ${humanizeDuration(MIN_REPEAT_TIME)}`);
|
||||
return;
|
||||
}
|
||||
if (opts.repeat > MAX_REPEAT_TIME) {
|
||||
return sendErrorMessage(pluginData, msg.channel, `Max time for -repeat is ${humanizeDuration(MAX_REPEAT_TIME)}`);
|
||||
sendErrorMessage(pluginData, msg.channel, `Max time for -repeat is ${humanizeDuration(MAX_REPEAT_TIME)}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +54,8 @@ export async function actualPostCmd(
|
|||
// Schedule the post to be posted later
|
||||
postAt = await parseScheduleTime(pluginData, msg.author.id, opts.schedule);
|
||||
if (!postAt) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Invalid schedule time");
|
||||
sendErrorMessage(pluginData, msg.channel, "Invalid schedule time");
|
||||
return;
|
||||
}
|
||||
} else if (opts.repeat) {
|
||||
postAt = moment.utc().add(opts.repeat, "ms");
|
||||
|
@ -72,35 +71,37 @@ export async function actualPostCmd(
|
|||
|
||||
// Invalid time
|
||||
if (!repeatUntil) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Invalid time specified for -repeat-until");
|
||||
sendErrorMessage(pluginData, msg.channel, "Invalid time specified for -repeat-until");
|
||||
return;
|
||||
}
|
||||
if (repeatUntil.isBefore(moment.utc())) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "You can't set -repeat-until in the past");
|
||||
sendErrorMessage(pluginData, msg.channel, "You can't set -repeat-until in the past");
|
||||
return;
|
||||
}
|
||||
if (repeatUntil.isAfter(MAX_REPEAT_UNTIL)) {
|
||||
return sendErrorMessage(
|
||||
sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
"Unfortunately, -repeat-until can only be at most 100 years into the future. Maybe 99 years would be enough?",
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else if (opts["repeat-times"]) {
|
||||
repeatTimes = opts["repeat-times"];
|
||||
if (repeatTimes <= 0) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "-repeat-times must be 1 or more");
|
||||
sendErrorMessage(pluginData, msg.channel, "-repeat-times must be 1 or more");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (repeatUntil && repeatTimes) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "You can only use one of -repeat-until or -repeat-times at once");
|
||||
sendErrorMessage(pluginData, msg.channel, "You can only use one of -repeat-until or -repeat-times at once");
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.repeat && !repeatUntil && !repeatTimes) {
|
||||
return sendErrorMessage(
|
||||
pluginData,
|
||||
msg.channel,
|
||||
"You must specify -repeat-until or -repeat-times for repeated messages",
|
||||
);
|
||||
sendErrorMessage(pluginData, msg.channel, "You must specify -repeat-until or -repeat-times for repeated messages");
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.repeat) {
|
||||
|
@ -114,7 +115,8 @@ export async function actualPostCmd(
|
|||
// Save schedule/repeat information in DB
|
||||
if (postAt) {
|
||||
if (postAt < moment.utc()) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Post can't be scheduled to be posted in the past");
|
||||
sendErrorMessage(pluginData, msg.channel, "Post can't be scheduled to be posted in the past");
|
||||
return;
|
||||
}
|
||||
|
||||
await pluginData.state.scheduledPosts.create({
|
||||
|
|
|
@ -3,7 +3,7 @@ import { commandTypeHelpers as ct } from "../../../commandTypes";
|
|||
import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils";
|
||||
import { TextChannel } from "eris";
|
||||
import { RecoverablePluginError, ERRORS } from "../../../RecoverablePluginError";
|
||||
import { canUseEmoji, isDiscordRESTError, isValidEmoji, noop } from "../../../utils";
|
||||
import { canUseEmoji, isDiscordRESTError, isValidEmoji, noop, trimPluginDescription } from "../../../utils";
|
||||
import { applyReactionRoleReactionsToMessage } from "../util/applyReactionRoleReactionsToMessage";
|
||||
import { canReadChannel } from "../../../utils/canReadChannel";
|
||||
|
||||
|
@ -12,6 +12,19 @@ const CLEAR_ROLES_EMOJI = "❌";
|
|||
export const InitReactionRolesCmd = reactionRolesCmd({
|
||||
trigger: "reaction_roles",
|
||||
permission: "can_manage",
|
||||
description: trimPluginDescription(`
|
||||
This command allows you to add reaction roles to a given message.
|
||||
The basic usage is as follows:
|
||||
|
||||
!reaction_roles 800865377520582687
|
||||
👍 = 556110793058287637
|
||||
👎 = 558037973581430785
|
||||
|
||||
A reactionRolePair is any emoji the bot can use, an equal sign and the role id it should correspond to.
|
||||
Every pair needs to be in its own line for the command to work properly.
|
||||
If the message you specify is not found, use \`!save_messages_to_db <channelId> <messageId>\`
|
||||
to manually add it to the stored messages database permanently.
|
||||
`),
|
||||
|
||||
signature: {
|
||||
message: ct.messageTarget(),
|
||||
|
|
|
@ -17,17 +17,20 @@ export const AddRoleCmd = rolesCmd({
|
|||
|
||||
async run({ message: msg, args, pluginData }) {
|
||||
if (!canActOn(pluginData, msg.member, args.member, true)) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Cannot add roles to this user: insufficient permissions");
|
||||
sendErrorMessage(pluginData, msg.channel, "Cannot add roles to this user: insufficient permissions");
|
||||
return;
|
||||
}
|
||||
|
||||
const roleId = await resolveRoleId(pluginData.client, pluginData.guild.id, args.role);
|
||||
if (!roleId) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Invalid role id");
|
||||
sendErrorMessage(pluginData, msg.channel, "Invalid role id");
|
||||
return;
|
||||
}
|
||||
|
||||
const config = pluginData.config.getForMessage(msg);
|
||||
if (!config.assignable_roles.includes(roleId)) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "You cannot assign that role");
|
||||
sendErrorMessage(pluginData, msg.channel, "You cannot assign that role");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanity check: make sure the role is configured properly
|
||||
|
@ -36,11 +39,13 @@ export const AddRoleCmd = rolesCmd({
|
|||
pluginData.state.logs.log(LogType.BOT_ALERT, {
|
||||
body: `Unknown role configured for 'roles' plugin: ${roleId}`,
|
||||
});
|
||||
return sendErrorMessage(pluginData, msg.channel, "You cannot assign that role");
|
||||
sendErrorMessage(pluginData, msg.channel, "You cannot assign that role");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.member.roles.includes(roleId)) {
|
||||
return sendErrorMessage(pluginData, msg.channel, "Member already has that role");
|
||||
sendErrorMessage(pluginData, msg.channel, "Member already has that role");
|
||||
return;
|
||||
}
|
||||
|
||||
pluginData.state.logs.ignoreLog(LogType.MEMBER_ROLE_ADD, args.member.id);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue