From 58f35c87da169c901566a00694cfa014eb577d92 Mon Sep 17 00:00:00 2001 From: Dragory Date: Sun, 24 Dec 2017 22:04:08 +0200 Subject: [PATCH] Start work on moving data to an SQLite database. Add a migrator for legacy data. --- config.example.json | 3 +- db/.gitignore | 1 + knexfile.js | 2 + package-lock.json | 1463 ++++++++++++++++++++++++++++++++- package.json | 5 +- src/blocked.js | 42 - src/config.js | 35 +- src/{ => data}/attachments.js | 6 +- src/data/blocked.js | 49 ++ src/data/snippets.js | 69 ++ src/data/threads.js | 213 +++++ src/index.js | 449 +--------- src/knex.js | 2 + src/{ => legacy}/jsonDb.js | 7 +- src/legacy/legacyMigrator.js | 184 +++++ src/logs.js | 163 ---- src/main.js | 430 ++++++++++ src/snippets.js | 59 -- src/threads.js | 144 ---- src/utils.js | 4 +- src/webserver.js | 3 +- 21 files changed, 2460 insertions(+), 873 deletions(-) create mode 100644 knexfile.js delete mode 100644 src/blocked.js rename src/{ => data}/attachments.js (94%) create mode 100644 src/data/blocked.js create mode 100644 src/data/snippets.js create mode 100644 src/data/threads.js create mode 100644 src/knex.js rename src/{ => legacy}/jsonDb.js (91%) create mode 100644 src/legacy/legacyMigrator.js delete mode 100644 src/logs.js create mode 100644 src/main.js delete mode 100644 src/snippets.js delete mode 100644 src/threads.js diff --git a/config.example.json b/config.example.json index 9acc301..de631c9 100644 --- a/config.example.json +++ b/config.example.json @@ -1,7 +1,8 @@ { "token": "your bot token", - "mailGuildId": "id of the modmail inbox guild", + "mailGuildId": "id of the modmail inbox server", "mainGuildId": "id of the main server where users will DM the bot", + "logChannelId": "id of the channel on the inbox server where notifications of new logs etc. will be posted", "status": "Message me for help!", "responseMessage": "Thank you for your message! Our mod team will reply to you here as soon as possible." diff --git a/db/.gitignore b/db/.gitignore index 120f485..d743e61 100644 --- a/db/.gitignore +++ b/db/.gitignore @@ -1,2 +1,3 @@ * !/.gitignore +!/migrations diff --git a/knexfile.js b/knexfile.js new file mode 100644 index 0000000..cf568f4 --- /dev/null +++ b/knexfile.js @@ -0,0 +1,2 @@ +const config = require('./src/config'); +module.exports = config.knex; diff --git a/package-lock.json b/package-lock.json index 415c2a3..b37c9c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,29 @@ "sprintf-js": "1.0.3" } }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "requires": { + "arr-flatten": "1.1.0" + } + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=" + }, + "array-slice": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.0.0.tgz", + "integrity": "sha1-5zA08A3MH0CHYAj9IP6ud71LfC8=" + }, "array-union": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", @@ -78,6 +101,11 @@ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", "dev": true }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=" + }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", @@ -100,12 +128,26 @@ "js-tokens": "3.0.2" } }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.5.1", + "regenerator-runtime": "0.11.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -116,6 +158,16 @@ "concat-map": "0.0.1" } }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "requires": { + "expand-range": "1.8.2", + "preserve": "0.2.0", + "repeat-element": "1.1.2" + } + }, "builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", @@ -213,6 +265,24 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color-convert": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", + "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "commander": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.12.1.tgz", + "integrity": "sha512-PCNLExLlI5HiPdaJs4pMXwOTHkSCpNQ1QJH9ykZLKtKEyKu3p9HgmH5l97vM8c0IUz6d54l+xEu2GG9yuYrFzA==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -230,11 +300,15 @@ "typedarray": "0.0.6" } }, + "core-js": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.5.1.tgz", + "integrity": "sha1-rmh03GaTd4m4B1T/VCjfZoGcpQs=" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-error-class": { "version": "3.0.2", @@ -298,6 +372,14 @@ "rimraf": "2.6.1" } }, + "detect-file": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-0.1.0.tgz", + "integrity": "sha1-STXe39lIhkjgBrASlWbpOGcR6mM=", + "requires": { + "fs-exists-sync": "0.1.0" + } + }, "dns-packet": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.1.1.tgz", @@ -421,8 +503,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escope": { "version": "3.6.0", @@ -556,6 +637,43 @@ "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", "dev": true }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "requires": { + "is-posix-bracket": "0.1.1" + } + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "requires": { + "fill-range": "2.2.3" + } + }, + "expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "requires": { + "os-homedir": "1.0.2" + } + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=" + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "requires": { + "is-extglob": "1.0.0" + } + }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", @@ -582,6 +700,23 @@ "object-assign": "4.1.1" } }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=" + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "requires": { + "is-number": "2.1.0", + "isobject": "2.1.0", + "randomatic": "1.1.7", + "repeat-element": "1.1.2", + "repeat-string": "1.6.1" + } + }, "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", @@ -590,6 +725,44 @@ "locate-path": "2.0.0" } }, + "findup-sync": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.4.3.tgz", + "integrity": "sha1-QAQ5Kee8YK3wt/SCfExudaDeyhI=", + "requires": { + "detect-file": "0.1.0", + "is-glob": "2.0.1", + "micromatch": "2.3.11", + "resolve-dir": "0.1.1" + } + }, + "fined": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", + "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", + "requires": { + "expand-tilde": "2.0.2", + "is-plain-object": "2.0.4", + "object.defaults": "1.1.0", + "object.pick": "1.3.0", + "parse-filepath": "1.0.1" + }, + "dependencies": { + "expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", + "requires": { + "homedir-polyfill": "1.0.1" + } + } + } + }, + "flagged-respawn": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-0.3.2.tgz", + "integrity": "sha1-/xke3c1wiKZ1smEP/8l2vpuAdLU=" + }, "flat-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", @@ -602,6 +775,24 @@ "write": "0.2.1" } }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "requires": { + "for-in": "1.0.2" + } + }, + "fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -623,6 +814,11 @@ "is-property": "1.0.2" } }, + "generic-pool": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.2.0.tgz", + "integrity": "sha512-JjcXDHT84icN/kFaF5+rNd1trZsgJFVqTSgM9dv6eayxSIQKMq0ilBJ+5pvf0SgimacMlZEsav4oL+4dUE4E2g==" + }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", @@ -647,6 +843,43 @@ "path-is-absolute": "1.0.1" } }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "requires": { + "glob-parent": "2.0.0", + "is-glob": "2.0.1" + } + }, + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "requires": { + "is-glob": "2.0.1" + } + }, + "global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "requires": { + "global-prefix": "0.1.5", + "is-windows": "0.2.0" + } + }, + "global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "requires": { + "homedir-polyfill": "1.0.1", + "ini": "1.3.5", + "is-windows": "0.2.0", + "which": "1.3.0" + } + }, "globals": { "version": "9.18.0", "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", @@ -699,6 +932,19 @@ "ansi-regex": "2.1.1" } }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + }, + "homedir-polyfill": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz", + "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=", + "requires": { + "parse-passwd": "1.0.0" + } + }, "hosted-git-info": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.5.0.tgz", @@ -734,8 +980,12 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { "version": "0.12.0", @@ -792,11 +1042,25 @@ "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" }, + "is-absolute": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-0.2.6.tgz", + "integrity": "sha1-IN5p89uULvLYe5wto28XIjWxtes=", + "requires": { + "is-relative": "0.2.1", + "is-windows": "0.2.0" + } + }, "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "is-builtin-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", @@ -805,6 +1069,29 @@ "builtin-modules": "1.1.1" } }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=" + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "requires": { + "is-primitive": "2.0.0" + } + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" + }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", @@ -813,6 +1100,14 @@ "number-is-nan": "1.0.1" } }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "requires": { + "is-extglob": "1.0.0" + } + }, "is-ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-2.0.0.tgz", @@ -833,6 +1128,14 @@ "xtend": "4.0.1" } }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "requires": { + "kind-of": "3.2.2" + } + }, "is-path-cwd": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", @@ -857,6 +1160,31 @@ "path-is-inside": "1.0.2" } }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=" + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=" + }, "is-property": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", @@ -868,6 +1196,14 @@ "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz", "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=" }, + "is-relative": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-0.2.1.tgz", + "integrity": "sha1-0n9MfVFtF1+2ENuEu+7yPDvJeqU=", + "requires": { + "is-unc-path": "0.1.2" + } + }, "is-resolvable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", @@ -887,17 +1223,37 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" }, + "is-unc-path": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-0.1.2.tgz", + "integrity": "sha1-arBTpyVzwQJQ/0FqOBTDUXivObk=", + "requires": { + "unc-path-regex": "0.1.2" + } + }, + "is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=" + }, "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + }, "js-tokens": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", @@ -935,6 +1291,85 @@ "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", "dev": true }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + }, + "knex": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.14.2.tgz", + "integrity": "sha1-87P+HqBxGvhCdwEAdoryG/y8uuo=", + "requires": { + "babel-runtime": "6.26.0", + "bluebird": "3.5.1", + "chalk": "2.3.0", + "commander": "2.12.1", + "debug": "3.1.0", + "generic-pool": "3.2.0", + "inherits": "2.0.3", + "interpret": "1.0.4", + "liftoff": "2.3.0", + "lodash": "4.17.4", + "minimist": "1.2.0", + "mkdirp": "0.5.1", + "pg-connection-string": "2.0.0", + "readable-stream": "2.3.3", + "safe-buffer": "5.0.1", + "tildify": "1.2.0", + "uuid": "3.1.0", + "v8flags": "3.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz", + "integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==", + "requires": { + "color-convert": "1.9.1" + } + }, + "chalk": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", + "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "requires": { + "ansi-styles": "3.2.0", + "escape-string-regexp": "1.0.5", + "supports-color": "4.5.0" + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "interpret": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.4.tgz", + "integrity": "sha1-ggzdWIuGj/sZGoCVBtbJyPISsbA=" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "supports-color": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", + "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "requires": { + "has-flag": "2.0.0" + } + } + } + }, "lcid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", @@ -953,6 +1388,22 @@ "type-check": "0.3.2" } }, + "liftoff": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.3.0.tgz", + "integrity": "sha1-qY8v9nGD2Lp8+soQVIvX/wVQs4U=", + "requires": { + "extend": "3.0.1", + "findup-sync": "0.4.3", + "fined": "1.1.0", + "flagged-respawn": "0.3.2", + "lodash.isplainobject": "4.0.6", + "lodash.isstring": "4.0.1", + "lodash.mapvalues": "4.6.0", + "rechoir": "0.6.2", + "resolve": "1.4.0" + } + }, "load-json-file": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", @@ -976,8 +1427,22 @@ "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.mapvalues": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.mapvalues/-/lodash.mapvalues-4.6.0.tgz", + "integrity": "sha1-G6+lAF3p3W9PJmaMMMo3IwzJaJw=" }, "lowercase-keys": { "version": "1.0.0", @@ -993,6 +1458,11 @@ "yallist": "2.1.2" } }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, "mem": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", @@ -1001,6 +1471,26 @@ "mimic-fn": "1.1.0" } }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "requires": { + "arr-diff": "2.0.0", + "array-unique": "0.2.1", + "braces": "1.8.5", + "expand-brackets": "0.1.5", + "extglob": "0.3.2", + "filename-regex": "2.0.1", + "is-extglob": "1.0.0", + "is-glob": "2.0.1", + "kind-of": "3.2.2", + "normalize-path": "2.1.1", + "object.omit": "2.0.1", + "parse-glob": "3.0.4", + "regex-cache": "0.4.4" + } + }, "mime": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/mime/-/mime-1.3.6.tgz", @@ -1023,14 +1513,12 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, "requires": { "minimist": "0.0.8" } @@ -1043,8 +1531,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "mute-stream": { "version": "0.0.5", @@ -1052,6 +1539,11 @@ "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", "dev": true }, + "nan": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", + "integrity": "sha1-2Vv3IeyHfgjbJ27T/G63j5CDrUY=" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -1069,6 +1561,14 @@ "validate-npm-package-license": "3.0.1" } }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "requires": { + "remove-trailing-separator": "1.1.0" + } + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", @@ -1088,6 +1588,56 @@ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, + "object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "requires": { + "array-each": "1.0.1", + "array-slice": "1.0.0", + "for-own": "1.0.0", + "isobject": "3.0.1" + }, + "dependencies": { + "for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", + "requires": { + "for-in": "1.0.2" + } + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "requires": { + "for-own": "0.1.5", + "is-extendable": "0.1.1" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "3.0.1" + }, + "dependencies": { + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + } + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1126,8 +1676,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "2.1.0", @@ -1157,6 +1706,27 @@ "p-limit": "1.1.0" } }, + "parse-filepath": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.1.tgz", + "integrity": "sha1-FZ1hVdQ5BNFsEO9piRHaHpGWm3M=", + "requires": { + "is-absolute": "0.2.6", + "map-cache": "0.2.2", + "path-root": "0.1.1" + } + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "requires": { + "glob-base": "0.3.0", + "is-dotfile": "1.0.3", + "is-extglob": "1.0.0", + "is-glob": "2.0.1" + } + }, "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", @@ -1165,6 +1735,11 @@ "error-ex": "1.3.1" } }, + "parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=" + }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", @@ -1190,8 +1765,20 @@ "path-parse": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", - "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", - "dev": true + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=" + }, + "path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "requires": { + "path-root-regex": "0.1.2" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=" }, "path-type": { "version": "2.0.0", @@ -1201,6 +1788,11 @@ "pify": "2.3.0" } }, + "pg-connection-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.0.0.tgz", + "integrity": "sha1-Pu/lmX4G2Ugh5NUC5CtqHHP434I=" + }, "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", @@ -1238,11 +1830,15 @@ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=" + }, "process-nextick-args": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", - "dev": true + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, "progress": { "version": "1.1.8", @@ -1266,6 +1862,43 @@ "pify": "2.3.0" } }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "requires": { + "is-number": "3.0.0", + "kind-of": "4.0.0" + }, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "3.2.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "1.1.6" + } + } + } + }, "read-pkg": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", @@ -1289,7 +1922,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", - "dev": true, "requires": { "core-util-is": "1.0.2", "inherits": "2.0.3", @@ -1303,8 +1935,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" } } }, @@ -1323,11 +1954,38 @@ "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, "requires": { "resolve": "1.4.0" } }, + "regenerator-runtime": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz", + "integrity": "sha512-/aA0kLeRb5N9K0d4fw7ooEbI+xDe+DKD499EQqygGqeS8N3xto15p09uY2xj7ixP81sNPXvRLnAQIqdVStgb1A==" + }, + "regex-cache": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", + "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", + "requires": { + "is-equal-shallow": "0.1.3" + } + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -1352,11 +2010,19 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", - "dev": true, "requires": { "path-parse": "1.0.5" } }, + "resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "requires": { + "expand-tilde": "1.2.2", + "global-modules": "0.2.3" + } + }, "resolve-from": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", @@ -1471,6 +2137,718 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "sqlite3": { + "version": "3.1.13", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-3.1.13.tgz", + "integrity": "sha512-JxXKPJnkZ6NuHRojq+g2WXWBt3M1G9sjZaYiHEWSTGijDM3cwju/0T2XbWqMXFmPqDgw+iB7zKQvnns4bvzXlw==", + "requires": { + "nan": "2.7.0", + "node-pre-gyp": "0.6.38" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true + }, + "ajv": { + "version": "4.11.8", + "bundled": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "bundled": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.3" + } + }, + "asn1": { + "version": "0.2.3", + "bundled": true + }, + "assert-plus": { + "version": "0.2.0", + "bundled": true + }, + "asynckit": { + "version": "0.4.0", + "bundled": true + }, + "aws-sign2": { + "version": "0.6.0", + "bundled": true + }, + "aws4": { + "version": "1.6.0", + "bundled": true + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "bundled": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "block-stream": { + "version": "0.0.9", + "bundled": true, + "requires": { + "inherits": "2.0.3" + } + }, + "boom": { + "version": "2.10.1", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.8", + "bundled": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "caseless": { + "version": "0.12.0", + "bundled": true + }, + "co": { + "version": "4.6.0", + "bundled": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true + }, + "combined-stream": { + "version": "1.0.5", + "bundled": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "bundled": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true + }, + "cryptiles": { + "version": "2.0.5", + "bundled": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "debug": { + "version": "2.6.9", + "bundled": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.4.2", + "bundled": true + }, + "delayed-stream": { + "version": "1.0.0", + "bundled": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "extend": { + "version": "3.0.1", + "bundled": true + }, + "extsprintf": { + "version": "1.3.0", + "bundled": true + }, + "forever-agent": { + "version": "0.6.1", + "bundled": true + }, + "form-data": { + "version": "2.1.4", + "bundled": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true + }, + "fstream": { + "version": "1.0.11", + "bundled": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "bundled": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "requires": { + "aproba": "1.2.0", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + } + }, + "getpass": { + "version": "0.1.7", + "bundled": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "glob": { + "version": "7.1.2", + "bundled": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "bundled": true + }, + "har-schema": { + "version": "1.0.5", + "bundled": true + }, + "har-validator": { + "version": "4.2.1", + "bundled": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true + }, + "hawk": { + "version": "3.1.3", + "bundled": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "bundled": true + }, + "http-signature": { + "version": "1.1.1", + "bundled": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true + }, + "ini": { + "version": "1.3.4", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "bundled": true + }, + "isarray": { + "version": "1.0.0", + "bundled": true + }, + "isstream": { + "version": "0.1.2", + "bundled": true + }, + "jsbn": { + "version": "0.1.1", + "bundled": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "bundled": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "bundled": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "bundled": true + }, + "jsonify": { + "version": "0.0.0", + "bundled": true + }, + "jsprim": { + "version": "1.4.1", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "mime-db": { + "version": "1.30.0", + "bundled": true + }, + "mime-types": { + "version": "2.1.17", + "bundled": true, + "requires": { + "mime-db": "1.30.0" + } + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "requires": { + "brace-expansion": "1.1.8" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "bundled": true + }, + "node-pre-gyp": { + "version": "0.6.38", + "bundled": true, + "requires": { + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.2", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.4.1", + "tar": "2.2.1", + "tar-pack": "3.4.0" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "requires": { + "abbrev": "1.1.1", + "osenv": "0.1.4" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true + }, + "oauth-sign": { + "version": "0.8.2", + "bundled": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true + }, + "osenv": { + "version": "0.1.4", + "bundled": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true + }, + "performance-now": { + "version": "0.2.0", + "bundled": true + }, + "process-nextick-args": { + "version": "1.0.7", + "bundled": true + }, + "punycode": { + "version": "1.4.1", + "bundled": true + }, + "qs": { + "version": "6.4.0", + "bundled": true + }, + "rc": { + "version": "1.2.1", + "bundled": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true + } + } + }, + "readable-stream": { + "version": "2.3.3", + "bundled": true, + "requires": { + "core-util-is": "1.0.2", + "inherits": "2.0.3", + "isarray": "1.0.0", + "process-nextick-args": "1.0.7", + "safe-buffer": "5.1.1", + "string_decoder": "1.0.3", + "util-deprecate": "1.0.2" + } + }, + "request": { + "version": "2.81.0", + "bundled": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "rimraf": { + "version": "2.6.2", + "bundled": true, + "requires": { + "glob": "7.1.2" + } + }, + "safe-buffer": { + "version": "5.1.1", + "bundled": true + }, + "semver": { + "version": "5.4.1", + "bundled": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true + }, + "sntp": { + "version": "1.0.9", + "bundled": true, + "requires": { + "hoek": "2.16.3" + } + }, + "sshpk": { + "version": "1.13.1", + "bundled": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "string_decoder": { + "version": "1.0.3", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "stringstream": { + "version": "0.0.5", + "bundled": true + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "requires": { + "ansi-regex": "2.1.1" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true + }, + "tar": { + "version": "2.2.1", + "bundled": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "bundled": true, + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.3", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, + "tough-cookie": { + "version": "2.3.3", + "bundled": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "bundled": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "bundled": true, + "optional": true + }, + "uid-number": { + "version": "0.0.6", + "bundled": true + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true + }, + "uuid": { + "version": "3.1.0", + "bundled": true + }, + "verror": { + "version": "1.10.0", + "bundled": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "bundled": true + } + } + }, + "wide-align": { + "version": "1.1.2", + "bundled": true, + "requires": { + "string-width": "1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true + } + } + }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", @@ -1504,7 +2882,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "dev": true, "requires": { "safe-buffer": "5.1.1" }, @@ -1512,8 +2889,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" } } }, @@ -1581,6 +2957,14 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "tildify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tildify/-/tildify-1.2.0.tgz", + "integrity": "sha1-3OwD9V3Km3qj5bBPIYF+tW5jWIo=", + "requires": { + "os-homedir": "1.0.2" + } + }, "timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", @@ -1626,6 +3010,11 @@ "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.0.tgz", "integrity": "sha1-sHoualQagV/Go0zNRTO67DB8qGQ=" }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, "unzip-response": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", @@ -1651,8 +3040,20 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + }, + "v8flags": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.0.1.tgz", + "integrity": "sha1-3Oj8N5wX2fLJ6e142JzgAFKxt2s=", + "requires": { + "homedir-polyfill": "1.0.1" + } }, "validate-npm-package-license": { "version": "3.0.1", diff --git a/package.json b/package.json index 3a1dcd1..dbab6c8 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,13 @@ "dependencies": { "eris": "^0.7.2", "humanize-duration": "^3.10.0", + "knex": "^0.14.2", "mime": "^1.3.4", "moment": "^2.17.1", "public-ip": "^2.0.1", - "transliteration": "^1.6.2" + "sqlite3": "^3.1.13", + "transliteration": "^1.6.2", + "uuid": "^3.1.0" }, "devDependencies": { "eslint": "^3.9.1" diff --git a/src/blocked.js b/src/blocked.js deleted file mode 100644 index ddb9f68..0000000 --- a/src/blocked.js +++ /dev/null @@ -1,42 +0,0 @@ -const jsonDb = require('./jsonDb'); - -/** - * Checks whether userId is blocked - * @param {String} userId - * @returns {Promise} - */ -function isBlocked(userId) { - return jsonDb.get('blocked', []).then(blocked => { - return blocked.indexOf(userId) !== -1; - }); -} - -/** - * Blocks the given userId - * @param {String} userId - * @returns {Promise} - */ -function block(userId) { - return jsonDb.get('blocked', []).then(blocked => { - blocked.push(userId); - return jsonDb.save('blocked', blocked); - }); -} - -/** - * Unblocks the given userId - * @param {String} userId - * @returns {Promise} - */ -function unblock(userId) { - return jsonDb.get('blocked', []).then(blocked => { - blocked.splice(blocked.indexOf(userId), 1); - return jsonDb.save('blocked', blocked); - }); -} - -module.exports = { - isBlocked, - block, - unblock, -}; diff --git a/src/config.js b/src/config.js index 06b23a9..1dac399 100644 --- a/src/config.js +++ b/src/config.js @@ -1,3 +1,5 @@ +const path = require('path'); + let userConfig; try { @@ -31,7 +33,12 @@ const defaultConfig = { "greetingAttachment": null, "port": 8890, - "url": null + "url": null, + + "dbDir": path.join(__dirname, '..', 'db'), + "knex": null, + + "logDir": path.join(__dirname, '..', 'logs'), }; const finalConfig = Object.assign({}, defaultConfig); @@ -44,8 +51,28 @@ for (const [prop, value] of Object.entries(userConfig)) { finalConfig[prop] = value; } -if (! finalConfig.token) throw new Error('Missing token!'); -if (! finalConfig.mailGuildId) throw new Error('Missing mailGuildId (inbox server id)!'); -if (! finalConfig.mainGuildId) throw new Error('Missing mainGuildId!'); +if (! finalConfig['knex']) { + finalConfig['knex'] = { + client: 'sqlite', + connection: { + filename: path.join(finalConfig.dbDir, 'data.sqlite') + }, + useNullAsDefault: true + }; +} + +Object.assign(finalConfig['knex'], { + migrations: { + directory: path.join(finalConfig.dbDir, 'migrations') + } +}); + +const required = ['token', 'mailGuildId', 'mainGuildId', 'logChannelId']; +for (const opt of required) { + if (! finalConfig[opt]) { + console.error(`Missing required config.json value: ${opt}`); + process.exit(1); + } +} module.exports = finalConfig; diff --git a/src/attachments.js b/src/data/attachments.js similarity index 94% rename from src/attachments.js rename to src/data/attachments.js index 029bdaf..0f97f7c 100644 --- a/src/attachments.js +++ b/src/data/attachments.js @@ -1,9 +1,9 @@ const Eris = require('eris'); const fs = require('fs'); const https = require('https'); -const config = require('./config'); +const config = require('../config'); -const getUtils = () => require('./utils'); +const getUtils = () => require('../utils'); const attachmentDir = config.attachmentDir || `${__dirname}/../attachments`; @@ -36,7 +36,7 @@ function saveAttachment(attachment, tries = 0) { https.get(attachment.url, (res) => { res.pipe(writeStream); writeStream.on('finish', () => { - writeStream.close() + writeStream.closeByChannelId() resolve(); }); }).on('error', (err) => { diff --git a/src/data/blocked.js b/src/data/blocked.js new file mode 100644 index 0000000..8efeffb --- /dev/null +++ b/src/data/blocked.js @@ -0,0 +1,49 @@ +const moment = require('moment'); +const knex = require('../knex'); + +/** + * Checks whether userId is blocked + * @param {String} userId + * @returns {Promise} + */ +async function isBlocked(userId) { + const row = await knex('blocked_users') + .where('user_id', userId) + .first(); + + return !!row; +} + +/** + * Blocks the given userId + * @param {String} userId + * @returns {Promise} + */ +async function block(userId, userName = '', blockedBy = 0) { + if (await isBlocked(userId)) return; + + return knex('blocked_users') + .insert({ + user_id: userId, + user_name: userName, + blocked_by: blockedBy, + blocked_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + }); +} + +/** + * Unblocks the given userId + * @param {String} userId + * @returns {Promise} + */ +async function unblock(userId) { + return knex('blocked_users') + .where('user_id', userId) + .delete(); +} + +module.exports = { + isBlocked, + block, + unblock, +}; diff --git a/src/data/snippets.js b/src/data/snippets.js new file mode 100644 index 0000000..85bccef --- /dev/null +++ b/src/data/snippets.js @@ -0,0 +1,69 @@ +const moment = require('moment'); +const knex = require('../knex'); + +/** + * @property {String} trigger + * @property {String} body + * @property {Number} is_anonymous + * @property {String} created_by + * @property {String} created_at + */ +class Snippet { + constructor(props) { + Object.assign(this, props); + } +} + +/** + * @param {String} trigger + * @returns {Promise} + */ +async function getSnippet(trigger) { + const snippet = await knex('snippets') + .where('trigger', trigger) + .first(); + + return (snippet ? new Snippet(snippet) : null); +} + +/** + * @param {String} trigger + * @param {String} body + * @param {Boolean} isAnonymous + * @returns {Promise} + */ +async function addSnippet(trigger, body, isAnonymous = false, createdBy = 0) { + if (await getSnippet(trigger)) return; + + return knex('snippets').insert({ + trigger, + body, + is_anonymous: isAnonymous ? 1 : 0, + created_by: createdBy, + created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + }); +} + +/** + * @param {String} trigger + * @returns {Promise} + */ +async function deleteSnippet(trigger) { + return knex('snippets') + .where('trigger', trigger) + .delete(); +} + +async function getAllSnippets() { + const snippets = await knex('snippets') + .select(); + + return snippets.map(s => new Snippet(s)); +} + +module.exports = { + get: getSnippet, + add: addSnippet, + del: deleteSnippet, + all: getAllSnippets, +}; diff --git a/src/data/threads.js b/src/data/threads.js new file mode 100644 index 0000000..589b3cb --- /dev/null +++ b/src/data/threads.js @@ -0,0 +1,213 @@ +const Eris = require('eris'); +const transliterate = require('transliteration'); +const moment = require('moment'); +const uuid = require('uuid'); + +const bot = require('../bot'); +const knex = require('../knex'); +const config = require('../config'); + +const getUtils = () => require('../utils'); + +// If the following messages would be used to start a thread, ignore it instead +// This is to prevent accidental threads from e.g. irrelevant replies after the thread was already closed +// or replies to the greeting message +const accidentalThreadMessages = [ + 'ok', + 'okay', + 'thanks', + 'ty', + 'k', + 'thank you', + 'thanx', + 'thnx', + 'thx', + 'tnx', + 'ok thank you', + 'ok thanks', + 'ok ty', + 'ok thanx', + 'ok thnx', + 'ok thx', + 'ok no problem', + 'ok np', + 'okay thank you', + 'okay thanks', + 'okay ty', + 'okay thanx', + 'okay thnx', + 'okay thx', + 'okay no problem', + 'okay np', + 'okey thank you', + 'okey thanks', + 'okey ty', + 'okey thanx', + 'okey thnx', + 'okey thx', + 'okey no problem', + 'okey np', + 'cheers' +]; + +const THREAD_STATUS = { + OPEN: 1, + CLOSED: 2 +}; + +const THREAD_MESSAGE_TYPE = { + SYSTEM: 1, + CHAT: 2, + FROM_USER: 3, + TO_USER: 4, + LEGACY: 5 +}; + +/** + * @property {Number} id + * @property {Number} status + * @property {String} user_id + * @property {String} user_name + * @property {String} channel_id + * @property {String} created_at + * @property {Boolean} _wasCreated + */ +class Thread { + constructor(props) { + Object.assign(this, {_wasCreated: false}, props); + } +} + +/** + * Returns information about the modmail thread channel for the given user. We can't return channel objects + * directly since they're not always available immediately after creation. + * @param {Eris.User} user + * @param {Boolean} allowCreate + * @returns {Promise} + */ +async function getOpenThreadForUser(user, allowCreate = true, originalMessage = null) { + // Attempt to find an open thread for this user + const thread = await knex('threads') + .where('user_id', user.id) + .where('status', THREAD_STATUS.OPEN) + .select(); + + if (thread) { + return new Thread(thread); + } + + // If no open thread was found, and we're not allowed to create one, just return null + if (! allowCreate) { + return null; + } + + // No open thread was found, and we *are* allowed to create a new one, so let's do that + + // If the message's content matches any of the values in accidentalThreadMessages, + // and config.ignoreAccidentalThreads is enabled, ignore this thread + if (config.ignoreAccidentalThreads && originalMessage && originalMessage.cleanContent) { + const cleaned = originalMessage.cleanContent.replace(/[^a-z\s]/gi, '').toLowerCase().trim(); + if (accidentalThreadMessages.includes(cleaned)) { + console.log('[NOTE] Skipping thread creation for message:', originalMessage.cleanContent); + return null; + } + } + + // Use the user's name+discrim for the thread channel's name + // Channel names are particularly picky about what characters they allow, so we gotta do some clean-up + let cleanName = transliterate.slugify(user.username); + if (cleanName === '') cleanName = 'unknown'; + cleanName = cleanName.slice(0, 95); // Make sure the discrim fits + + const channelName = `${cleanName}-${user.discriminator}`; + + console.log(`[NOTE] Creating new thread channel ${channelName}`); + + // Attempt to create the inbox channel for this thread + let createdChannel; + try { + createdChannel = await getUtils().getInboxGuild().createChannel(channelName); + if (config.newThreadCategoryId) { + // If a category id for new threads is specified, move the newly created channel there + bot.editChannel(createdChannel.id, {parentID: config.newThreadCategoryId}); + } + } catch (err) { + console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`); + throw err; + } + + // Save the new thread in the database + const newThreadId = await create({ + status: THREAD_STATUS.OPEN, + user_id: user.id, + user_name: `${user.username}#${user.discriminator}`, + channel_id: createdChannel.id, + created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + }); + + const newThreadObj = new Thread(newThread); + newThreadObj._wasCreated = true; + + return newThreadObj; +} + +/** + * Creates a new thread row in the database + * @param {Object} data + * @returns {Promise} The ID of the created thread + */ +async function create(data) { + const threadId = uuid.v4(); + const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); + const finalData = Object.assign({created_at: now, is_legacy: 0}, data, {id: threadId}); + + await knex('threads').insert(newThread); + + return threadId; +} + +async function addThreadMessage(threadId, messageType, user, body) { + return knex('thread_messages').insert({ + thread_id: threadId, + message_type: messageType, + user_id: (user ? user.id : 0), + user_name: (user ? `${user.username}#${user.discriminator}` : ''), + body, + created_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') + }); +} + +/** + * @param {String} channelId + * @returns {Promise} + */ +async function getByChannelId(channelId) { + const thread = await knex('threads') + .where('channel_id', channelId) + .first(); + + return (thread ? new Thread(thread) : null); +} + +/** + * Deletes the modmail thread for the given channel id + * @param {String} channelId + * @returns {Promise} + */ +async function closeByChannelId(channelId) { + await knex('threads') + .where('channel_id', channelId) + .update({ + status: THREAD_STATUS.CLOSED + }); +} + +module.exports = { + getOpenThreadForUser, + getByChannelId, + closeByChannelId, + create, + + THREAD_STATUS, + THREAD_MESSAGE_TYPE, +}; diff --git a/src/index.js b/src/index.js index e6358ac..58cc42c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,21 +1,9 @@ -const fs = require('fs'); -const Eris = require('eris'); -const moment = require('moment'); -const humanizeDuration = require('humanize-duration'); - +const path = require('path'); const config = require('./config'); -const bot = require('./bot'); -const Queue = require('./queue'); const utils = require('./utils'); -const blocked = require('./blocked'); -const threads = require('./threads'); -const logs = require('./logs'); -const attachments = require('./attachments'); -const snippets = require('./snippets'); -const webserver = require('./webserver'); -const greeting = require('./greeting'); - -const messageQueue = new Queue(); +const main = require('./main'); +const knex = require('./knex'); +const legacyMigrator = require('./legacy/legacyMigrator'); // Force crash on unhandled rejections (use something like forever/pm2 to restart) process.on('unhandledRejection', err => { @@ -29,412 +17,35 @@ process.on('unhandledRejection', err => { process.exit(1); }); -// Once the bot has connected, set the status/"playing" message -bot.on('ready', () => { - bot.editStatus(null, {name: config.status || 'Message me for help'}); - console.log('Bot started, listening to DMs'); -}); +(async function() { + // Make sure the database is up to date + await knex.migrate.latest(); -// If the alwaysReply option is set to true, send all messages in modmail threads as replies, unless they start with a command prefix -if (config.alwaysReply) { - bot.on('messageCreate', msg => { - if (! utils.messageIsOnInboxServer(msg)) return; - if (! utils.isStaff(msg)) return; - if (msg.author.bot) return; - if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return; + // Migrate legacy data if we need to + if (await legacyMigrator.shouldMigrate()) { + console.log('=== MIGRATING LEGACY DATA ==='); + console.log('Do not close the bot!'); + console.log(''); - reply(msg, msg.content.trim(), config.alwaysReplyAnon || false); - }); -} + await legacyMigrator.migrate(); -// "Bot was mentioned in #general-discussion" -bot.on('messageCreate', async msg => { - if (! utils.messageIsOnMainServer(msg)) return; - if (! msg.mentions.some(user => user.id === bot.user.id)) return; + const relativeDbDir = (path.isAbsolute(config.dbDir) ? config.dbDir : path.resolve(process.cwd(), config.dbDir)); + const relativeLogDir = (path.isAbsolute(config.logDir) ? config.logDir : path.resolve(process.cwd(), config.logDir)); - // If the person who mentioned the modmail bot is also on the modmail server, ignore them - if (utils.getInboxGuild().members.get(msg.author.id)) return; - - // If the person who mentioned the bot is blocked, ignore them - if (await blocked.isBlocked(msg.author.id)) return; - - bot.createMessage(utils.getLogChannel(bot).id, { - content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`, - disableEveryone: false, - }); -}); - -// When we get a private message, forward the contents to the corresponding modmail thread -bot.on('messageCreate', async msg => { - if (! (msg.channel instanceof Eris.PrivateChannel)) return; - if (msg.author.id === bot.user.id) return; - - if (await blocked.isBlocked(msg.author.id)) return; - - // Download and save copies of attachments in the background - const attachmentSavePromise = attachments.saveAttachmentsInMessage(msg); - - let threadCreationFailed = false; - - // Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created - messageQueue.add(async () => { - let thread; - - // Find the corresponding modmail thread - try { - thread = await threads.getForUser(msg.author, true, msg); - } catch (e) { - console.error(e); - utils.postError(` -Modmail thread for ${msg.author.username}#${msg.author.discriminator} (${msg.author.id}) could not be created: -\`\`\`${e.message}\`\`\` - -Here's what their message contained: -\`\`\`${msg.cleanContent}\`\`\``); - return; - } - - if (! thread) { - // If there's no thread returned, this message was probably ignored (e.g. due to a common word) - // TODO: Move that logic here instead? - return; - } - - if (thread._wasCreated) { - const mainGuild = utils.getMainGuild(); - const member = (mainGuild ? mainGuild.members.get(msg.author.id) : null); - if (! member) console.log(`[INFO] Member ${msg.author.id} not found in main guild ${config.mainGuildId}`); - - let mainGuildNickname = null; - if (member && member.nick) mainGuildNickname = member.nick; - else if (member && member.user) mainGuildNickname = member.user.username; - else if (member == null) mainGuildNickname = 'NOT ON SERVER'; - - if (mainGuildNickname == null) mainGuildNickname = 'UNKNOWN'; - - const userLogs = await logs.getLogsByUserId(msg.author.id); - const accountAge = humanizeDuration(Date.now() - msg.author.createdAt, {largest: 2}); - const infoHeader = `ACCOUNT AGE **${accountAge}**, ID **${msg.author.id}**, NICKNAME **${mainGuildNickname}**, LOGS **${userLogs.length}**\n-------------------------------`; - - await bot.createMessage(thread.channelId, infoHeader); - - // Ping mods of the new thread - await bot.createMessage(thread.channelId, { - content: `@here New modmail thread (${msg.author.username}#${msg.author.discriminator})`, - disableEveryone: false, - }); - - // Send an automatic reply to the user informing them of the successfully created modmail thread - msg.channel.createMessage(config.responseMessage).catch(err => { - utils.postError(`There is an issue sending messages to ${msg.author.username}#${msg.author.discriminator} (${msg.author.id}); consider messaging manually`); - }); - } - - const timestamp = utils.getTimestamp(); - const attachmentsPendingStr = '\n\n*Attachments pending...*'; - - let content = msg.content; - if (msg.attachments.length > 0) content += attachmentsPendingStr; - - const createdMsg = await bot.createMessage(thread.channelId, `[${timestamp}] « **${msg.author.username}#${msg.author.discriminator}:** ${content}`); - - if (msg.attachments.length > 0) { - await attachmentSavePromise; - const formattedAttachments = await Promise.all(msg.attachments.map(utils.formatAttachment)); - const attachmentMsg = `\n\n` + formattedAttachments.reduce((str, formatted) => str + `\n\n${formatted}`); - createdMsg.edit(createdMsg.content.replace(attachmentsPendingStr, attachmentMsg)); - } - }); -}); - -// Edits in DMs -bot.on('messageUpdate', async (msg, oldMessage) => { - if (! (msg.channel instanceof Eris.PrivateChannel)) return; - if (msg.author.id === bot.user.id) return; - - if (await blocked.isBlocked(msg.author.id)) return; - - let oldContent = oldMessage.content; - const newContent = msg.content; - - // Old message content doesn't persist between bot restarts - if (oldContent == null) oldContent = '*Unavailable due to bot restart*'; - - // Ignore bogus edit events with no changes - if (newContent.trim() === oldContent.trim()) return; - - const thread = await threads.getForUser(msg.author); - if (! thread) return; - - const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`); - bot.createMessage(thread.channelId, editMessage); -}); - -/** - * Sends a reply to the modmail thread where `msg` was posted. - * @param {Eris.Message} msg - * @param {string} text - * @param {bool} anonymous - * @returns {Promise} - */ -async function reply(msg, text, anonymous = false) { - const thread = await threads.getByChannelId(msg.channel.id); - if (! thread) return; - - await attachments.saveAttachmentsInMessage(msg); - - const dmChannel = await bot.getDMChannel(thread.userId); - - let modUsername, logModUsername; - const mainRole = utils.getMainRole(msg.member); - - if (anonymous) { - modUsername = (mainRole ? mainRole.name : 'Moderator'); - logModUsername = `(Anonymous) (${msg.author.username}) ${mainRole ? mainRole.name : 'Moderator'}`; - } else { - const name = (config.useNicknames ? msg.member.nick || msg.author.username : msg.author.username); - modUsername = (mainRole ? `(${mainRole.name}) ${name}` : name); - logModUsername = modUsername; + console.log(''); + console.log('=== LEGACY DATA MIGRATION FINISHED ==='); + console.log(''); + console.log('IMPORTANT: After the bot starts, please verify that all logs, threads, blocked users, and snippets are still working correctly.'); + console.log(`Once you've done that, feel free to delete the following legacy files/directories:`); + console.log(''); + console.log('FILE: ' + path.resolve(relativeDbDir, 'threads.json')); + console.log('FILE: ' + path.resolve(relativeDbDir, 'blocked.json')); + console.log('FILE: ' + path.resolve(relativeDbDir, 'snippets.json')); + console.log('DIRECTORY: ' + relativeLogDir); + console.log(''); + console.log('Starting the bot...'); } - let content = `**${modUsername}:** ${text}`; - let logContent = `**${logModUsername}:** ${text}`; - - async function sendMessage(file, attachmentUrl) { - try { - await dmChannel.createMessage(content, file); - } catch (e) { - if (e.resp && e.resp.statusCode === 403) { - msg.channel.createMessage(`Could not send reply; the user has likely left the server or blocked the bot`); - } else if (e.resp) { - msg.channel.createMessage(`Could not send reply; error code ${e.resp.statusCode}`); - } else { - msg.channel.createMessage(`Could not send reply: ${e.toString()}`); - } - } - - if (attachmentUrl) { - content += `\n\n**Attachment:** ${attachmentUrl}`; - logContent += `\n\n**Attachment:** ${attachmentUrl}`; - } - - // Show the message in the modmail thread as well - msg.channel.createMessage(`[${utils.getTimestamp()}] » ${logContent}`); - msg.delete(); - }; - - if (msg.attachments.length > 0) { - // If the reply has an attachment, relay it as is - fs.readFile(attachments.getPath(msg.attachments[0].id), async (err, data) => { - const file = {file: data, name: msg.attachments[0].filename}; - - const attachmentUrl = await attachments.getUrl(msg.attachments[0].id, msg.attachments[0].filename); - sendMessage(file, attachmentUrl); - }); - } else { - // Otherwise just send the message regularly - sendMessage(); - } -} - -// Mods can reply to modmail threads using !r or !reply -// These messages get relayed back to the DM thread between the bot and the user -utils.addInboxCommand('reply', (msg, args) => { - const text = args.join(' ').trim(); - reply(msg, text, false); -}); - -bot.registerCommandAlias('r', 'reply'); - -// Anonymous replies only show the role, not the username -utils.addInboxCommand('anonreply', (msg, args) => { - const text = args.join(' ').trim(); - reply(msg, text, true); -}); - -bot.registerCommandAlias('ar', 'anonreply'); - -// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel. -utils.addInboxCommand('close', async (msg, args, thread) => { - if (! thread) return; - - await msg.channel.createMessage('Saving logs and closing channel...'); - - const logMessages = await msg.channel.getMessages(10000); - const log = logMessages.reverse().map(msg => { - const date = moment.utc(msg.timestamp, 'x').format('YYYY-MM-DD HH:mm:ss'); - return `[${date}] ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`; - }).join('\n') + '\n'; - - const logFilename = await logs.getNewLogFile(thread.userId); - await logs.saveLogFile(logFilename, log); - - const logUrl = await logs.getLogFileUrl(logFilename); - const closeMessage = `Modmail thread with ${thread.username} (${thread.userId}) was closed by ${msg.author.username} -Logs: <${logUrl}>`; - - bot.createMessage(utils.getLogChannel(bot).id, closeMessage); - await threads.close(thread.channelId); - msg.channel.delete(); -}); - -utils.addInboxCommand('block', (msg, args, thread) => { - async function block(userId) { - await blocked.block(userId); - msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`); - } - - if (args.length > 0) { - // User mention/id as argument - const userId = utils.getUserMention(args.join(' ')); - if (! userId) return; - block(userId); - } else if (thread) { - // Calling !block without args in a modmail thread blocks the user of that thread - block(thread.userId); - } -}); - -utils.addInboxCommand('unblock', (msg, args, thread) => { - async function unblock(userId) { - await blocked.unblock(userId); - msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`); - } - - if (args.length > 0) { - // User mention/id as argument - const userId = utils.getUserMention(args.join(' ')); - if (! userId) return; - unblock(userId); - } else if (thread) { - // Calling !unblock without args in a modmail thread unblocks the user of that thread - unblock(thread.userId); - } -}); - -utils.addInboxCommand('logs', (msg, args, thread) => { - async function getLogs(userId) { - const infos = await logs.getLogsWithUrlByUserId(userId); - let message = `**Log files for <@${userId}>:**\n`; - - message += infos.map(info => { - const formattedDate = moment.utc(info.date, 'YYYY-MM-DD HH:mm:ss').format('MMM Do [at] HH:mm [UTC]'); - return `\`${formattedDate}\`: <${info.url}>`; - }).join('\n'); - - // Send the list of logs in chunks of 15 lines per message - const lines = message.split('\n'); - const chunks = utils.chunk(lines, 15); - - let root = Promise.resolve(); - chunks.forEach(lines => { - root = root.then(() => msg.channel.createMessage(lines.join('\n'))); - }); - } - - if (args.length > 0) { - // User mention/id as argument - const userId = utils.getUserMention(args.join(' ')); - if (! userId) return; - getLogs(userId); - } else if (thread) { - // Calling !logs without args in a modmail thread returns the logs of the user of that thread - getLogs(thread.userId); - } -}); - -// Snippets -bot.on('messageCreate', async msg => { - if (! utils.messageIsOnInboxServer(msg)) return; - if (! utils.isStaff(msg.member)) return; - - if (msg.author.bot) return; - if (! msg.content) return; - if (! msg.content.startsWith(config.snippetPrefix)) return; - - const shortcut = msg.content.replace(config.snippetPrefix, '').toLowerCase(); - const snippet = await snippets.get(shortcut); - if (! snippet) return; - - reply(msg, snippet.text, snippet.isAnonymous); -}); - -// Show or add a snippet -utils.addInboxCommand('snippet', async (msg, args) => { - const shortcut = args[0]; - if (! shortcut) return - - const text = args.slice(1).join(' ').trim(); - const snippet = await snippets.get(shortcut); - - if (snippet) { - if (text) { - // If the snippet exists and we're trying to create a new one, inform the user the snippet already exists - msg.channel.createMessage(`Snippet "${shortcut}" already exists! You can edit or delete it with ${prefix}edit_snippet and ${prefix}delete_snippet respectively.`); - } else { - // If the snippet exists and we're NOT trying to create a new one, show info about the existing snippet - msg.channel.createMessage(`\`${config.snippetPrefix}${shortcut}\` replies ${snippet.isAnonymous ? 'anonymously ' : ''}with:\n${snippet.text}`); - } - } else { - if (text) { - // If the snippet doesn't exist and the user wants to create it, create it - await snippets.add(shortcut, text, false); - msg.channel.createMessage(`Snippet "${shortcut}" created!`); - } else { - // If the snippet doesn't exist and the user isn't trying to create it, inform them how to create it - msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist! You can create it with \`${prefix}snippet ${shortcut} text\``); - } - } -}); - -bot.registerCommandAlias('s', 'snippet'); - -utils.addInboxCommand('delete_snippet', async (msg, args) => { - const shortcut = args[0]; - if (! shortcut) return; - - const snippet = await snippets.get(shortcut); - if (! snippet) { - msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist!`); - return; - } - - await snippets.del(shortcut); - msg.channel.createMessage(`Snippet "${shortcut}" deleted!`); -}); - -bot.registerCommandAlias('ds', 'delete_snippet'); - -utils.addInboxCommand('edit_snippet', async (msg, args) => { - const shortcut = args[0]; - if (! shortcut) return; - - const text = args.slice(1).join(' ').trim(); - if (! text) return; - - const snippet = await snippets.get(shortcut); - if (! snippet) { - msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist!`); - return; - } - - await snippets.del(shortcut); - await snippets.add(shortcut, text, snippet.isAnonymous); - - msg.channel.createMessage(`Snippet "${shortcut}" edited!`); -}); - -bot.registerCommandAlias('es', 'edit_snippet'); - -utils.addInboxCommand('snippets', async msg => { - const allSnippets = await snippets.all(); - const shortcuts = Object.keys(allSnippets); - shortcuts.sort(); - - msg.channel.createMessage(`Available snippets (prefix ${config.snippetPrefix}):\n${shortcuts.join(', ')}`); -}); - -// Start the bot! -bot.connect(); -webserver.run(); -greeting.init(bot); + // Start the bot + // main.start(); +})(); diff --git a/src/knex.js b/src/knex.js new file mode 100644 index 0000000..b6b6346 --- /dev/null +++ b/src/knex.js @@ -0,0 +1,2 @@ +const config = require('./config'); +module.exports = require('knex')(config.knex); diff --git a/src/jsonDb.js b/src/legacy/jsonDb.js similarity index 91% rename from src/jsonDb.js rename to src/legacy/jsonDb.js index bc42182..d2e8ca1 100644 --- a/src/jsonDb.js +++ b/src/legacy/jsonDb.js @@ -1,11 +1,14 @@ const fs = require('fs'); const path = require('path'); -const config = require('./config'); +const config = require('../config'); -const dbDir = config.dbDir || `${__dirname}/../db`; +const dbDir = config.dbDir; const databases = {}; +/** + * @deprecated Only used for migrating legacy data + */ class JSONDB { constructor(path, def = {}, useCloneByDefault = false) { this.path = path; diff --git a/src/legacy/legacyMigrator.js b/src/legacy/legacyMigrator.js new file mode 100644 index 0000000..368a1ef --- /dev/null +++ b/src/legacy/legacyMigrator.js @@ -0,0 +1,184 @@ +const fs = require('fs'); +const path = require('path'); +const promisify = require('util').promisify; +const moment = require('moment'); +const uuid = require('uuid'); + +const knex = require('../knex'); +const config = require('../config'); +const jsonDb = require('./jsonDb'); +const threads = require('../data/threads'); + +const readDir = promisify(fs.readdir); +const readFile = promisify(fs.readFile); +const access = promisify(fs.access); +const writeFile = promisify(fs.writeFile); + +async function migrate() { + console.log('Migrating open threads...'); + await migrateOpenThreads(); + + console.log('Migrating logs...'); + await migrateLogs(); + + console.log('Migrating blocked users...'); + await migrateBlockedUsers(); + + console.log('Migrating snippets...'); + await migrateSnippets(); + + await writeFile(path.join(config.dbDir, '.migrated_legacy'), ''); +} + +async function shouldMigrate() { + // If there is a file marking a finished migration, assume we don't need to migrate + const migrationFile = path.join(config.dbDir, '.migrated_legacy'); + try { + await access(migrationFile); + return false; + } catch (e) {} + + // If there are any old threads, we need to migrate + const oldThreads = await jsonDb.get('threads', []); + if (oldThreads.length) { + return true; + } + + // If there are any old blocked users, we need to migrate + const blockedUsers = await jsonDb.get('blocked', []); + if (blockedUsers.length) { + return true; + } + + // If there are any old snippets, we need to migrate + const snippets = await jsonDb.get('snippets', {}); + if (Object.keys(snippets).length) { + return true; + } + + // If the log file dir exists, we need to migrate + try { + await access(config.logDir); + return true; + } catch(e) {} + + return false; +} + +async function migrateOpenThreads() { + const oldThreads = await jsonDb.get('threads', []); + const promises = oldThreads.map(async oldThread => { + const existingOpenThread = await knex('threads') + .where('channel_id', oldThread.channelId) + .first(); + + if (existingOpenThread) return; + + const newThread = { + status: threads.THREAD_STATUS.OPEN, + user_id: oldThread.userId, + user_name: oldThread.username, + channel_id: oldThread.channelId, + is_legacy: 1 + }; + + return threads.create(newThread); + }); + + return Promise.all(promises); +} + +async function migrateLogs() { + const logDir = config.logDir || `${__dirname}/../../logs`; + const logFiles = await readDir(logDir); + + const promises = logFiles.map(async logFile => { + if (! logFile.endsWith('.txt')) return; + + const [rawDate, userId, threadId] = logFile.slice(0, -4).split('__'); + const date = `${rawDate.slice(0, 10)} ${rawDate.slice(11).replace('-', ':')}`; + + const fullPath = path.join(logDir, logFile); + const contents = await readFile(fullPath, {encoding: 'utf8'}); + + const newThread = { + id: threadId, + status: threads.THREAD_STATUS.CLOSED, + user_id: userId, + user_name: '', + channel_id: null, + is_legacy: 1, + created_at: date + }; + + return knex.transaction(async trx => { + const existingThread = await trx('threads') + .where('id', newThread.id) + .first(); + + if (existingThread) return; + + await trx('threads').insert(newThread); + + await trx('thread_messages').insert({ + thread_id: newThread.id, + message_type: threads.THREAD_MESSAGE_TYPE.LEGACY, + user_id: userId, + user_name: '', + body: contents, + created_at: date + }); + }); + }); + + return Promise.all(promises); +} + +async function migrateBlockedUsers() { + const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); + const blockedUsers = await jsonDb.get('blocked', []); + const promises = blockedUsers.map(async userId => { + const existingBlockedUser = await knex('blocked_users') + .where('user_id', userId) + .first(); + + if (existingBlockedUser) return; + + return knex('blocked_users').insert({ + user_id: userId, + user_name: '', + blocked_by: 0, + blocked_at: now + }); + }); + + return Promise.all(promises); +} + +async function migrateSnippets() { + const now = moment.utc().format('YYYY-MM-DD HH:mm:ss'); + const snippets = await jsonDb.get('snippets', {}); + + const promises = Object.entries(snippets).map(async ([trigger, data]) => { + const existingSnippet = await knex('snippets') + .where('trigger', trigger) + .first(); + + if (existingSnippet) return; + + return knex('snippets').insert({ + trigger, + body: data.text, + is_anonymous: data.isAnonymous ? 1 : 0, + created_by: null, + created_at: now + }); + }); + + return Promise.all(promises); +} + +module.exports = { + migrate, + shouldMigrate, +}; diff --git a/src/logs.js b/src/logs.js deleted file mode 100644 index b6a3012..0000000 --- a/src/logs.js +++ /dev/null @@ -1,163 +0,0 @@ -const fs = require('fs'); -const crypto = require('crypto'); -const moment = require('moment'); -const config = require('./config'); - -const getUtils = () => require('./utils'); - -const logDir = config.logDir || `${__dirname}/../logs`; -const logFileFormatRegex = /^([0-9\-]+?)__([0-9]+?)__([0-9a-f]+?)\.txt$/; - -/** - * @typedef {Object} LogFileInfo - * @property {String} filename - * @property {String} date - * @property {String} userId - * @property {String} token - * @property {String=} url - */ - -/** - * Returns information about the given logfile - * @param {String} logFilename - * @returns {LogFileInfo} - */ -function getLogFileInfo(logFilename) { - const match = logFilename.match(logFileFormatRegex); - if (! match) return null; - - const date = moment.utc(match[1], 'YYYY-MM-DD-HH-mm-ss').format('YYYY-MM-DD HH:mm:ss'); - - return { - filename: logFilename, - date: date, - userId: match[2], - token: match[3], - }; -} - -/** - * Returns the filesystem path to the given logfile - * @param {String} logFilename - * @returns {String} - */ -function getLogFilePath(logFilename) { - return `${logDir}/${logFilename}`; -} - -/** - * Returns the self-hosted URL to the given logfile - * @param {String} logFilename - * @returns {String} - */ -function getLogFileUrl(logFilename) { - const info = getLogFileInfo(logFilename); - return getUtils().getSelfUrl(`logs/${info.token}`); -} - -/** - * Returns a new, unique log file name for the given userId - * @param {String} userId - * @returns {Promise} - */ -function getNewLogFile(userId) { - return new Promise(resolve => { - crypto.randomBytes(16, (err, buf) => { - const token = buf.toString('hex'); - const date = moment.utc().format('YYYY-MM-DD-HH-mm-ss'); - - resolve(`${date}__${userId}__${token}.txt`); - }); - }); -} - -/** - * Finds a log file name by its token - * @param {String} token - * @returns {Promise} - */ -function findLogFile(token) { - return new Promise(resolve => { - fs.readdir(logDir, (err, files) => { - for (const file of files) { - if (file.endsWith(`__${token}.txt`)) { - resolve(file); - return; - } - } - - resolve(null); - }); - }); -} - -/** - * Returns all log file infos for the given userId - * @param {String} userId - * @returns {Promise} - */ -function getLogsByUserId(userId) { - return new Promise((resolve, reject) => { - fs.readdir(logDir, (err, files) => { - if (err) return reject(err); - - const logfileInfos = files - .map(file => getLogFileInfo(file)) - .filter(info => info && info.userId === userId); - - resolve(logfileInfos); - }); - }); -} - -/** - * Returns all log file infos with URLs for the given userId - * @param {String} userId - * @returns {Promise} - */ -function getLogsWithUrlByUserId(userId) { - return getLogsByUserId(userId).then(infos => { - const urlPromises = infos.map(info => { - return getLogFileUrl(info.filename).then(url => { - info.url = url; - return info; - }); - }); - - return Promise.all(urlPromises).then(infos => { - // Sort logs by date, in descending order - infos.sort((a, b) => { - if (a.date > b.date) return -1; - if (a.date < b.date) return 1; - return 0; - }); - - return infos; - }); - }); -} - -/** - * @param {String} logFilename - * @param {String} content - * @returns {Promise} - */ -function saveLogFile(logFilename, content) { - return new Promise((resolve, reject) => { - fs.writeFile(getLogFilePath(logFilename), content, {encoding: 'utf8'}, err => { - if (err) return reject(err); - resolve(); - }); - }); -} - -module.exports = { - getLogFileInfo, - getLogFilePath, - getNewLogFile, - findLogFile, - getLogsByUserId, - getLogsWithUrlByUserId, - saveLogFile, - getLogFileUrl, -}; diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..2d9de98 --- /dev/null +++ b/src/main.js @@ -0,0 +1,430 @@ +const fs = require('fs'); +const Eris = require('eris'); +const moment = require('moment'); +const humanizeDuration = require('humanize-duration'); + +const config = require('./config'); +const bot = require('./bot'); +const Queue = require('./queue'); +const utils = require('./utils'); +const blocked = require('./data/blocked'); +const threads = require('./data/threads'); +const attachments = require('./data/attachments'); +const snippets = require('./data/snippets'); +const webserver = require('./webserver'); +const greeting = require('./greeting'); + +const messageQueue = new Queue(); + +// Once the bot has connected, set the status/"playing" message +bot.on('ready', () => { + bot.editStatus(null, {name: config.status}); + console.log('Bot started, listening to DMs'); +}); + +// If the alwaysReply option is set to true, send all messages in modmail threads as replies, unless they start with a command prefix +if (config.alwaysReply) { + bot.on('messageCreate', msg => { + if (! utils.messageIsOnInboxServer(msg)) return; + if (! utils.isStaff(msg)) return; + if (msg.author.bot) return; + if (msg.content.startsWith(config.prefix) || msg.content.startsWith(config.snippetPrefix)) return; + + reply(msg, msg.content.trim(), config.alwaysReplyAnon || false); + }); +} + +// "Bot was mentioned in #general-discussion" +bot.on('messageCreate', async msg => { + if (! utils.messageIsOnMainServer(msg)) return; + if (! msg.mentions.some(user => user.id === bot.user.id)) return; + + // If the person who mentioned the modmail bot is also on the modmail server, ignore them + if (utils.getInboxGuild().members.get(msg.author.id)) return; + + // If the person who mentioned the bot is blocked, ignore them + if (await blocked.isBlocked(msg.author.id)) return; + + bot.createMessage(utils.getLogChannel(bot).id, { + content: `@here Bot mentioned in ${msg.channel.mention} by **${msg.author.username}#${msg.author.discriminator}**: "${msg.cleanContent}"`, + disableEveryone: false, + }); +}); + +// When we get a private message, forward the contents to the corresponding modmail thread +bot.on('messageCreate', async msg => { + if (! (msg.channel instanceof Eris.PrivateChannel)) return; + if (msg.author.id === bot.user.id) return; + + if (await blocked.isBlocked(msg.author.id)) return; + + // Download and save copies of attachments in the background + const attachmentSavePromise = attachments.saveAttachmentsInMessage(msg); + + let threadCreationFailed = false; + + // Private message handling is queued so e.g. multiple message in quick succession don't result in multiple channels being created + messageQueue.add(async () => { + let thread; + + // Find the corresponding modmail thread + try { + thread = await threads.getOpenThreadForUser(msg.author, true, msg); + } catch (e) { + console.error(e); + utils.postError(` +Modmail thread for ${msg.author.username}#${msg.author.discriminator} (${msg.author.id}) could not be created: +\`\`\`${e.message}\`\`\` + +Here's what their message contained: +\`\`\`${msg.cleanContent}\`\`\``); + return; + } + + if (! thread) { + // If there's no thread returned, this message was probably ignored (e.g. due to a common word) + // TODO: Move that logic here instead? + return; + } + + if (thread._wasCreated) { + const mainGuild = utils.getMainGuild(); + const member = (mainGuild ? mainGuild.members.get(msg.author.id) : null); + if (! member) console.log(`[INFO] Member ${msg.author.id} not found in main guild ${config.mainGuildId}`); + + let mainGuildNickname = null; + if (member && member.nick) mainGuildNickname = member.nick; + else if (member && member.user) mainGuildNickname = member.user.username; + else if (member == null) mainGuildNickname = 'NOT ON SERVER'; + + if (mainGuildNickname == null) mainGuildNickname = 'UNKNOWN'; + + const userLogs = await logs.getLogsByUserId(msg.author.id); + const accountAge = humanizeDuration(Date.now() - msg.author.createdAt, {largest: 2}); + const infoHeader = `ACCOUNT AGE **${accountAge}**, ID **${msg.author.id}**, NICKNAME **${mainGuildNickname}**, LOGS **${userLogs.length}**\n-------------------------------`; + + await bot.createMessage(thread.channelId, infoHeader); + + // Ping mods of the new thread + await bot.createMessage(thread.channelId, { + content: `@here New modmail thread (${msg.author.username}#${msg.author.discriminator})`, + disableEveryone: false, + }); + + // Send an automatic reply to the user informing them of the successfully created modmail thread + msg.channel.createMessage(config.responseMessage).catch(err => { + utils.postError(`There is an issue sending messages to ${msg.author.username}#${msg.author.discriminator} (${msg.author.id}); consider messaging manually`); + }); + } + + const timestamp = utils.getTimestamp(); + const attachmentsPendingStr = '\n\n*Attachments pending...*'; + + let content = msg.content; + if (msg.attachments.length > 0) content += attachmentsPendingStr; + + const createdMsg = await bot.createMessage(thread.channelId, `[${timestamp}] « **${msg.author.username}#${msg.author.discriminator}:** ${content}`); + + if (msg.attachments.length > 0) { + await attachmentSavePromise; + const formattedAttachments = await Promise.all(msg.attachments.map(utils.formatAttachment)); + const attachmentMsg = `\n\n` + formattedAttachments.reduce((str, formatted) => str + `\n\n${formatted}`); + createdMsg.edit(createdMsg.content.replace(attachmentsPendingStr, attachmentMsg)); + } + }); +}); + +// Edits in DMs +bot.on('messageUpdate', async (msg, oldMessage) => { + if (! (msg.channel instanceof Eris.PrivateChannel)) return; + if (msg.author.id === bot.user.id) return; + + if (await blocked.isBlocked(msg.author.id)) return; + + let oldContent = oldMessage.content; + const newContent = msg.content; + + // Old message content doesn't persist between bot restarts + if (oldContent == null) oldContent = '*Unavailable due to bot restart*'; + + // Ignore bogus edit events with no changes + if (newContent.trim() === oldContent.trim()) return; + + const thread = await threads.getOpenThreadForUser(msg.author); + if (! thread) return; + + const editMessage = utils.disableLinkPreviews(`**The user edited their message:**\n\`B:\` ${oldContent}\n\`A:\` ${newContent}`); + bot.createMessage(thread.channelId, editMessage); +}); + +/** + * Sends a reply to the modmail thread where `msg` was posted. + * @param {Eris.Message} msg + * @param {string} text + * @param {bool} anonymous + * @returns {Promise} + */ +async function reply(msg, text, anonymous = false) { + const thread = await threads.getByChannelId(msg.channel.id); + if (! thread) return; + + await attachments.saveAttachmentsInMessage(msg); + + const dmChannel = await bot.getDMChannel(thread.userId); + + let modUsername, logModUsername; + const mainRole = utils.getMainRole(msg.member); + + if (anonymous) { + modUsername = (mainRole ? mainRole.name : 'Moderator'); + logModUsername = `(Anonymous) (${msg.author.username}) ${mainRole ? mainRole.name : 'Moderator'}`; + } else { + const name = (config.useNicknames ? msg.member.nick || msg.author.username : msg.author.username); + modUsername = (mainRole ? `(${mainRole.name}) ${name}` : name); + logModUsername = modUsername; + } + + let content = `**${modUsername}:** ${text}`; + let logContent = `**${logModUsername}:** ${text}`; + + async function sendMessage(file, attachmentUrl) { + try { + await dmChannel.createMessage(content, file); + } catch (e) { + if (e.resp && e.resp.statusCode === 403) { + msg.channel.createMessage(`Could not send reply; the user has likely left the server or blocked the bot`); + } else if (e.resp) { + msg.channel.createMessage(`Could not send reply; error code ${e.resp.statusCode}`); + } else { + msg.channel.createMessage(`Could not send reply: ${e.toString()}`); + } + } + + if (attachmentUrl) { + content += `\n\n**Attachment:** ${attachmentUrl}`; + logContent += `\n\n**Attachment:** ${attachmentUrl}`; + } + + // Show the message in the modmail thread as well + msg.channel.createMessage(`[${utils.getTimestamp()}] » ${logContent}`); + msg.delete(); + }; + + if (msg.attachments.length > 0) { + // If the reply has an attachment, relay it as is + fs.readFile(attachments.getPath(msg.attachments[0].id), async (err, data) => { + const file = {file: data, name: msg.attachments[0].filename}; + + const attachmentUrl = await attachments.getUrl(msg.attachments[0].id, msg.attachments[0].filename); + sendMessage(file, attachmentUrl); + }); + } else { + // Otherwise just send the message regularly + sendMessage(); + } +} + +// Mods can reply to modmail threads using !r or !reply +// These messages get relayed back to the DM thread between the bot and the user +utils.addInboxCommand('reply', (msg, args) => { + const text = args.join(' ').trim(); + reply(msg, text, false); +}); + +bot.registerCommandAlias('r', 'reply'); + +// Anonymous replies only show the role, not the username +utils.addInboxCommand('anonreply', (msg, args) => { + const text = args.join(' ').trim(); + reply(msg, text, true); +}); + +bot.registerCommandAlias('ar', 'anonreply'); + +// Close a thread. Closing a thread saves a log of the channel's contents and then deletes the channel. +utils.addInboxCommand('close', async (msg, args, thread) => { + if (! thread) return; + + await msg.channel.createMessage('Saving logs and closing channel...'); + + const logMessages = await msg.channel.getMessages(10000); + const log = logMessages.reverse().map(msg => { + const date = moment.utc(msg.timestamp, 'x').format('YYYY-MM-DD HH:mm:ss'); + return `[${date}] ${msg.author.username}#${msg.author.discriminator}: ${msg.content}`; + }).join('\n') + '\n'; + + const logFilename = await logs.getNewLogFile(thread.userId); + await logs.saveLogFile(logFilename, log); + + const logUrl = await logs.getLogFileUrl(logFilename); + const closeMessage = `Modmail thread with ${thread.username} (${thread.userId}) was closed by ${msg.author.username} +Logs: <${logUrl}>`; + + bot.createMessage(utils.getLogChannel(bot).id, closeMessage); + await threads.closeByChannelId(thread.channelId); + msg.channel.delete(); +}); + +utils.addInboxCommand('block', (msg, args, thread) => { + async function block(userId) { + await blocked.block(userId); + msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`); + } + + if (args.length > 0) { + // User mention/id as argument + const userId = utils.getUserMention(args.join(' ')); + if (! userId) return; + block(userId); + } else if (thread) { + // Calling !block without args in a modmail thread blocks the user of that thread + block(thread.userId); + } +}); + +utils.addInboxCommand('unblock', (msg, args, thread) => { + async function unblock(userId) { + await blocked.unblock(userId); + msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`); + } + + if (args.length > 0) { + // User mention/id as argument + const userId = utils.getUserMention(args.join(' ')); + if (! userId) return; + unblock(userId); + } else if (thread) { + // Calling !unblock without args in a modmail thread unblocks the user of that thread + unblock(thread.userId); + } +}); + +utils.addInboxCommand('logs', (msg, args, thread) => { + async function getLogs(userId) { + const infos = await logs.getLogsWithUrlByUserId(userId); + let message = `**Log files for <@${userId}>:**\n`; + + message += infos.map(info => { + const formattedDate = moment.utc(info.date, 'YYYY-MM-DD HH:mm:ss').format('MMM Do [at] HH:mm [UTC]'); + return `\`${formattedDate}\`: <${info.url}>`; + }).join('\n'); + + // Send the list of logs in chunks of 15 lines per message + const lines = message.split('\n'); + const chunks = utils.chunk(lines, 15); + + let root = Promise.resolve(); + chunks.forEach(lines => { + root = root.then(() => msg.channel.createMessage(lines.join('\n'))); + }); + } + + if (args.length > 0) { + // User mention/id as argument + const userId = utils.getUserMention(args.join(' ')); + if (! userId) return; + getLogs(userId); + } else if (thread) { + // Calling !logs without args in a modmail thread returns the logs of the user of that thread + getLogs(thread.userId); + } +}); + +// Snippets +bot.on('messageCreate', async msg => { + if (! utils.messageIsOnInboxServer(msg)) return; + if (! utils.isStaff(msg.member)) return; + + if (msg.author.bot) return; + if (! msg.content) return; + if (! msg.content.startsWith(config.snippetPrefix)) return; + + const shortcut = msg.content.replace(config.snippetPrefix, '').toLowerCase(); + const snippet = await snippets.get(shortcut); + if (! snippet) return; + + reply(msg, snippet.text, snippet.isAnonymous); +}); + +// Show or add a snippet +utils.addInboxCommand('snippet', async (msg, args) => { + const shortcut = args[0]; + if (! shortcut) return + + const text = args.slice(1).join(' ').trim(); + const snippet = await snippets.get(shortcut); + + if (snippet) { + if (text) { + // If the snippet exists and we're trying to create a new one, inform the user the snippet already exists + msg.channel.createMessage(`Snippet "${shortcut}" already exists! You can edit or delete it with ${prefix}edit_snippet and ${prefix}delete_snippet respectively.`); + } else { + // If the snippet exists and we're NOT trying to create a new one, show info about the existing snippet + msg.channel.createMessage(`\`${config.snippetPrefix}${shortcut}\` replies ${snippet.isAnonymous ? 'anonymously ' : ''}with:\n${snippet.text}`); + } + } else { + if (text) { + // If the snippet doesn't exist and the user wants to create it, create it + await snippets.add(shortcut, text, false); + msg.channel.createMessage(`Snippet "${shortcut}" created!`); + } else { + // If the snippet doesn't exist and the user isn't trying to create it, inform them how to create it + msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist! You can create it with \`${prefix}snippet ${shortcut} text\``); + } + } +}); + +bot.registerCommandAlias('s', 'snippet'); + +utils.addInboxCommand('delete_snippet', async (msg, args) => { + const shortcut = args[0]; + if (! shortcut) return; + + const snippet = await snippets.get(shortcut); + if (! snippet) { + msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist!`); + return; + } + + await snippets.del(shortcut); + msg.channel.createMessage(`Snippet "${shortcut}" deleted!`); +}); + +bot.registerCommandAlias('ds', 'delete_snippet'); + +utils.addInboxCommand('edit_snippet', async (msg, args) => { + const shortcut = args[0]; + if (! shortcut) return; + + const text = args.slice(1).join(' ').trim(); + if (! text) return; + + const snippet = await snippets.get(shortcut); + if (! snippet) { + msg.channel.createMessage(`Snippet "${shortcut}" doesn't exist!`); + return; + } + + await snippets.del(shortcut); + await snippets.add(shortcut, text, snippet.isAnonymous); + + msg.channel.createMessage(`Snippet "${shortcut}" edited!`); +}); + +bot.registerCommandAlias('es', 'edit_snippet'); + +utils.addInboxCommand('snippets', async msg => { + const allSnippets = await snippets.all(); + const shortcuts = Object.keys(allSnippets); + shortcuts.sort(); + + msg.channel.createMessage(`Available snippets (prefix ${config.snippetPrefix}):\n${shortcuts.join(', ')}`); +}); + +module.exports = { + start() { + bot.connect(); + webserver.run(); + greeting.init(bot); + } +}; diff --git a/src/snippets.js b/src/snippets.js deleted file mode 100644 index b638224..0000000 --- a/src/snippets.js +++ /dev/null @@ -1,59 +0,0 @@ -const jsonDb = require('./jsonDb'); - -/** - * @typedef {Object} Snippet - * @property {String} text - * @property {Boolean} isAnonymous - */ - -/** - * Returns the expanded text for the given snippet shortcut - * @param {String} shortcut - * @returns {Promise} - */ -function getSnippet(shortcut) { - return jsonDb.get('snippets', {}).then(snippets => { - return snippets[shortcut] || null; - }); -} - -/** - * Adds a snippet - * @param {String} shortcut - * @param {String} text - * @param {Boolean} isAnonymous - * @returns {Promise} - */ -function addSnippet(shortcut, text, isAnonymous = false) { - return jsonDb.get('snippets', {}).then(snippets => { - snippets[shortcut] = { - text, - isAnonymous, - }; - - jsonDb.save('snippets', snippets); - }); -} - -/** - * Deletes a snippet - * @param {String} shortcut - * @returns {Promise} - */ -function deleteSnippet(shortcut) { - return jsonDb.get('snippets', {}).then(snippets => { - delete snippets[shortcut]; - jsonDb.save('snippets', snippets); - }); -} - -function getAllSnippets() { - return jsonDb.get('snippets', {}); -} - -module.exports = { - get: getSnippet, - add: addSnippet, - del: deleteSnippet, - all: getAllSnippets, -}; diff --git a/src/threads.js b/src/threads.js deleted file mode 100644 index d0d961c..0000000 --- a/src/threads.js +++ /dev/null @@ -1,144 +0,0 @@ -const Eris = require('eris'); -const bot = require('./bot'); -const transliterate = require('transliteration'); -const jsonDb = require('./jsonDb'); -const config = require('./config'); - -const getUtils = () => require('./utils'); - -// If the following messages would be used to start a thread, ignore it instead -// This is to prevent accidental threads from e.g. irrelevant replies after the thread was already closed -// or replies to the greeting message -const accidentalThreadMessages = [ - 'ok', - 'okay', - 'thanks', - 'ty', - 'k', - 'thank you', - 'thanx', - 'thnx', - 'thx', - 'tnx', - 'ok thank you', - 'ok thanks', - 'ok ty', - 'ok thanx', - 'ok thnx', - 'ok thx', - 'ok no problem', - 'ok np', - 'okay thank you', - 'okay thanks', - 'okay ty', - 'okay thanx', - 'okay thnx', - 'okay thx', - 'okay no problem', - 'okay np', - 'okey thank you', - 'okey thanks', - 'okey ty', - 'okey thanx', - 'okey thnx', - 'okey thx', - 'okey no problem', - 'okey np', - 'cheers' -]; - -/** - * @typedef {Object} ModMailThread - * @property {String} channelId - * @property {String} userId - * @property {String} username - * @property {Boolean} _wasCreated - */ - -/** - * Returns information about the modmail thread channel for the given user. We can't return channel objects - * directly since they're not always available immediately after creation. - * @param {Eris.User} user - * @param {Boolean} allowCreate - * @returns {Promise} - */ -function getForUser(user, allowCreate = true, originalMessage = null) { - return jsonDb.get('threads', []).then(threads => { - const thread = threads.find(t => t.userId === user.id); - if (thread) return thread; - - // If we didn't find an existing modmail thread, attempt creating one - if (! allowCreate) return null; - - // Channel names are particularly picky about what characters they allow... - let cleanName = transliterate.slugify(user.username); - if (cleanName === '') cleanName = 'unknown'; - cleanName = cleanName.slice(0, 95); // Make sure the discrim fits - - const channelName = `${cleanName}-${user.discriminator}`; - - if (originalMessage && originalMessage.cleanContent && config.ignoreAccidentalThreads) { - const cleaned = originalMessage.cleanContent.replace(/[^a-z\s]/gi, '').toLowerCase().trim(); - if (accidentalThreadMessages.includes(cleaned)) { - console.log('[NOTE] Skipping thread creation for message:', originalMessage.cleanContent); - return null; - } - } - - console.log(`[NOTE] Creating new thread channel ${channelName}`); - return getUtils().getInboxGuild().createChannel(`${channelName}`) - .then(channel => { - const thread = { - channelId: channel.id, - userId: user.id, - username: `${user.username}#${user.discriminator}`, - }; - - if (config.newThreadCategoryId) { - // If a category id is specified, move the newly created channel there - bot.editChannel(channel.id, {parentID: config.newThreadCategoryId}); - } - - return jsonDb.get('threads', []).then(threads => { - threads.push(thread); - jsonDb.save('threads', threads); - - return Object.assign({}, thread, {_wasCreated: true}); - }); - }, err => { - console.error(`Error creating modmail channel for ${user.username}#${user.discriminator}!`); - throw err; - }); - }); -} - -/** - * @param {String} channelId - * @returns {Promise} - */ -function getByChannelId(channelId) { - return jsonDb.get('threads', []).then(threads => { - return threads.find(t => t.channelId === channelId); - }); -} - -/** - * Deletes the modmail thread for the given channel id - * @param {String} channelId - * @returns {Promise} - */ -function close(channelId) { - return jsonDb.get('threads', []).then(threads => { - const thread = threads.find(t => t.channelId === channelId); - if (! thread) return; - - threads.splice(threads.indexOf(thread), 1); - return jsonDb.save('threads', threads); - }); -} - -module.exports = { - getForUser, - getByChannelId, - close, -}; diff --git a/src/utils.js b/src/utils.js index d2911dc..249c1be 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,8 +2,8 @@ const Eris = require('eris'); const bot = require('./bot'); const moment = require('moment'); const publicIp = require('public-ip'); -const threads = require('./threads'); -const attachments = require('./attachments'); +const threads = require('./data/threads'); +const attachments = require('./data/attachments'); const config = require('./config'); class BotError extends Error {} diff --git a/src/webserver.js b/src/webserver.js index fc1acdd..a6d4108 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -3,8 +3,7 @@ const mime = require('mime'); const url = require('url'); const fs = require('fs'); const config = require('./config'); -const logs = require('./logs'); -const attachments = require('./attachments'); +const attachments = require('./data/attachments'); const port = config.port || 8890;