diff --git a/README.md b/README.md index 85dc8c0edb..22b1589b9b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ -Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) +Habitica [![Build Status](https://travis-ci.org/HabitRPG/habitica.svg?branch=develop)](https://travis-ci.org/HabitRPG/habitica) [![Code Climate](https://codeclimate.com/github/HabitRPG/habitrpg.svg)](https://codeclimate.com/github/HabitRPG/habitrpg) [![Coverage Status](https://coveralls.io/repos/github/HabitRPG/habitica/badge.svg?branch=develop)](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [![Bountysource](https://api.bountysource.com/badge/tracker?tracker_id=68393)](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) [![Open Source Helpers](https://www.codetriage.com/habitrpg/habitica/badges/users.svg)](https://www.codetriage.com/habitrpg/habitica) =============== +[![Greenkeeper badge](https://badges.greenkeeper.io/HabitRPG/habitica.svg)](https://greenkeeper.io/) + [Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor. We need more programmers! Your assistance will be greatly appreciated. diff --git a/config.json.example b/config.json.example index 1aa62936b7..efaa9298f6 100644 --- a/config.json.example +++ b/config.json.example @@ -105,5 +105,12 @@ "LOGGLY" : { "TOKEN" : "example-token", "SUBDOMAIN" : "exmaple-subdomain" + }, + "KAFKA": { + "GROUP_ID": "", + "CLOUDKARAFKA_BROKERS": "", + "CLOUDKARAFKA_USERNAME": "", + "CLOUDKARAFKA_PASSWORD": "", + "CLOUDKARAFKA_TOPIC_PREFIX": "" } } diff --git a/package-lock.json b/package-lock.json index ad8419aa6a..afa3473bd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,60 +5,88 @@ "requires": true, "dependencies": { "@babel/code-frame": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.36.tgz", - "integrity": "sha512-sW77BFwJ48YvQp3Gzz5xtAUiXuYOL2aMJKDwiaY3OcvdqBFurtYfOpSa4QrNyDxmOGRFSYzUpabU2m9QrlWE7w==", + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0-beta.40.tgz", + "integrity": "sha512-eVXQSbu/RimU6OKcK2/gDJVTFcxXJI4sHbIqw2mhwMZeQ2as/8AhS9DGkEDoHMBBNJZ5B0US63lF56x+KDcxiA==", "requires": { - "chalk": "2.3.0", + "@babel/highlight": "7.0.0-beta.40" + } + }, + "@babel/generator": { + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0-beta.40.tgz", + "integrity": "sha512-c91BQcXyTq/5aFV4afgOionxZS1dxWt8OghEx5Q52SKssdGRFSiMKnk9tGkev1pYULPJBqjSDZU2Pcuc58ffZw==", + "requires": { + "@babel/types": "7.0.0-beta.40", + "jsesc": "2.5.1", + "lodash": "4.17.5", + "source-map": "0.5.7", + "trim-right": "1.0.1" + }, + "dependencies": { + "jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha1-5CGiqOINawgZ3yiQj3glJrlt0f4=" + } + } + }, + "@babel/helper-function-name": { + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.40.tgz", + "integrity": "sha512-cK9BVLtOfisSISTTHXKGvBc2OBh65tjEk4PgXhsSnnH0i8RP2v+5RCxoSlh2y/i+l2fxQqKqv++Qo5RMiwmRCA==", + "requires": { + "@babel/helper-get-function-arity": "7.0.0-beta.40", + "@babel/template": "7.0.0-beta.40", + "@babel/types": "7.0.0-beta.40" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.40.tgz", + "integrity": "sha512-MwquaPznI4cUoZEgHC/XGkddOXtqKqD4DvZDOyJK2LR9Qi6TbMbAhc6IaFoRX7CRTFCmtGeu8gdXW2dBotBBTA==", + "requires": { + "@babel/types": "7.0.0-beta.40" + } + }, + "@babel/highlight": { + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0-beta.40.tgz", + "integrity": "sha512-mOhhTrzieV6VO7odgzFGFapiwRK0ei8RZRhfzHhb6cpX3QM8XXuCLXWjN8qBB7JReDdUR80V3LFfFrGUYevhNg==", + "requires": { + "chalk": "2.3.1", "esutils": "2.0.2", "js-tokens": "3.0.2" } }, - "@babel/helper-function-name": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.36.tgz", - "integrity": "sha512-/SGPOyifPf20iTrMN+WdlY2MbKa7/o4j7B/4IAsdOusASp2icT+Wcdjf4tjJHaXNX8Pe9bpgVxLNxhRvcf8E5w==", - "requires": { - "@babel/helper-get-function-arity": "7.0.0-beta.36", - "@babel/template": "7.0.0-beta.36", - "@babel/types": "7.0.0-beta.36" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.36.tgz", - "integrity": "sha512-vPPcx2vsSoDbcyWr9S3nd0FM3B4hEXnt0p1oKpwa08GwK0fSRxa98MyaRGf8suk8frdQlG1P3mDrz5p/Rr3pbA==", - "requires": { - "@babel/types": "7.0.0-beta.36" - } - }, "@babel/template": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.36.tgz", - "integrity": "sha512-mUBi90WRyZ9iVvlWLEdeo8gn/tROyJdjKNC4W5xJTSZL+9MS89rTJSqiaJKXIkxk/YRDL/g/8snrG/O0xl33uA==", + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.0.0-beta.40.tgz", + "integrity": "sha512-RlQiVB7eL7fxsKN6JvnCCwEwEL28CBYalXSgWWULuFlEHjtMoXBqQanSie3bNyhrANJx67sb+Sd/vuGivoMwLQ==", "requires": { - "@babel/code-frame": "7.0.0-beta.36", - "@babel/types": "7.0.0-beta.36", - "babylon": "7.0.0-beta.36", + "@babel/code-frame": "7.0.0-beta.40", + "@babel/types": "7.0.0-beta.40", + "babylon": "7.0.0-beta.40", "lodash": "4.17.5" }, "dependencies": { "babylon": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.36.tgz", - "integrity": "sha512-rw4YdadGwajAMMRl6a5swhQ0JCOOFyaYCfJ0AsmNBD8uBD/r4J8mux7wBaqavvFKqUKQYWOzA1Speams4YDzsQ==" + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.40.tgz", + "integrity": "sha512-AVxF2EcxvGD5hhOuLTOLAXBb0VhwWpEX0HyHdAI2zU+AAP4qEwtQj8voz1JR3uclGai0rfcE+dCTHnNMOnimFg==" } } }, "@babel/traverse": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.36.tgz", - "integrity": "sha512-OTUb6iSKVR/98dGThRJ1BiyfwbuX10BVnkz89IpaerjTPRhDfMBfLsqmzxz5MiywUOW4M0Clta0o7rSxkfcuzw==", + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.0.0-beta.40.tgz", + "integrity": "sha512-h96SQorjvdSuxQ6hHFIuAa3oxnad1TA5bU1Zz88+XqzwmM5QM0/k2D+heXGGy/76gT5ajl7xYLKGiPA/KTyVhQ==", "requires": { - "@babel/code-frame": "7.0.0-beta.36", - "@babel/helper-function-name": "7.0.0-beta.36", - "@babel/types": "7.0.0-beta.36", - "babylon": "7.0.0-beta.36", + "@babel/code-frame": "7.0.0-beta.40", + "@babel/generator": "7.0.0-beta.40", + "@babel/helper-function-name": "7.0.0-beta.40", + "@babel/types": "7.0.0-beta.40", + "babylon": "7.0.0-beta.40", "debug": "3.1.0", "globals": "11.3.0", "invariant": "2.2.2", @@ -66,9 +94,9 @@ }, "dependencies": { "babylon": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.36.tgz", - "integrity": "sha512-rw4YdadGwajAMMRl6a5swhQ0JCOOFyaYCfJ0AsmNBD8uBD/r4J8mux7wBaqavvFKqUKQYWOzA1Speams4YDzsQ==" + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.40.tgz", + "integrity": "sha512-AVxF2EcxvGD5hhOuLTOLAXBb0VhwWpEX0HyHdAI2zU+AAP4qEwtQj8voz1JR3uclGai0rfcE+dCTHnNMOnimFg==" }, "debug": { "version": "3.1.0", @@ -86,9 +114,9 @@ } }, "@babel/types": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.36.tgz", - "integrity": "sha512-PyAORDO9um9tfnrddXgmWN9e6Sq9qxraQIt5ynqBOSXKA5qvK1kUr+Q3nSzKFdzorsiK+oqcUnAFvEoKxv9D+Q==", + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0-beta.40.tgz", + "integrity": "sha512-uXCGCzTgMZxcSUzutCPtZmXbVC+cvENgS2e0tRuhn+Y1hZnMb8IHP0Trq7Q2MB/eFmG5pKrAeTIUfQIe5kA4Tg==", "requires": { "esutils": "2.0.2", "lodash": "4.17.5", @@ -102,6 +130,28 @@ } } }, + "@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==" + }, + "@sinonjs/formatio": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-2.0.0.tgz", + "integrity": "sha512-ls6CAMA6/5gG+O/IdsBcblvnd8qcO/l1TYoNeAzp3wcISOxlPXQEus0mLcdwazEkWjaBdaJ3TaxmNgCLWwvWzg==", + "dev": true, + "requires": { + "samsam": "1.3.0" + }, + "dependencies": { + "samsam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.3.0.tgz", + "integrity": "sha512-1HwIYD/8UlOtFS3QO3w7ey+SdSDFE4HRNLZoZRYVQefrOY3l17epswImeB1ijgJFQJodIaHcwkp3r/myBjFVbg==", + "dev": true + } + } + }, "@slack/client": { "version": "3.16.0", "resolved": "https://registry.npmjs.org/@slack/client/-/client-3.16.0.tgz", @@ -234,7 +284,9 @@ "addressparser": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/addressparser/-/addressparser-1.0.1.tgz", - "integrity": "sha1-R6++GiqSYhkdtoOOT9HTm0CCF0Y=" + "integrity": "sha1-R6++GiqSYhkdtoOOT9HTm0CCF0Y=", + "dev": true, + "optional": true }, "after": { "version": "0.8.2", @@ -950,9 +1002,9 @@ } }, "aws-sdk": { - "version": "2.189.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.189.0.tgz", - "integrity": "sha1-TfZJuM+iAI/uCfNX/jqYNUZcTaQ=", + "version": "2.200.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.200.0.tgz", + "integrity": "sha1-9GDJZAhyWw64xlj93qbgv+DvWkQ=", "requires": { "buffer": "4.9.1", "events": "1.1.1", @@ -1005,9 +1057,9 @@ "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=" }, "axios": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.17.1.tgz", - "integrity": "sha1-LY4+XQvb1zJ/kbyBT1xXZg+Bgk0=", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", + "integrity": "sha1-MtU+SFHv3AoRmTts0AB4nXDAUQI=", "requires": { "follow-redirects": "1.4.1", "is-buffer": "1.1.6" @@ -1069,22 +1121,22 @@ } }, "babel-eslint": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.2.1.tgz", - "integrity": "sha512-RzdVOyWKQRUnLXhwLk+eKb4oyW+BykZSkpYwFhM4tnfzAG5OWfvG0w/uyzMp5XKEU0jN82+JefHr39bG2+KhRQ==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-8.2.2.tgz", + "integrity": "sha512-Qt2lz2egBxNYWqN9JIO2z4NOOf8i4b5JS6CFoYrOZZTDssueiV1jH/jsefyg+86SeNY3rB361/mi3kE1WK2WYQ==", "requires": { - "@babel/code-frame": "7.0.0-beta.36", - "@babel/traverse": "7.0.0-beta.36", - "@babel/types": "7.0.0-beta.36", - "babylon": "7.0.0-beta.36", + "@babel/code-frame": "7.0.0-beta.40", + "@babel/traverse": "7.0.0-beta.40", + "@babel/types": "7.0.0-beta.40", + "babylon": "7.0.0-beta.40", "eslint-scope": "3.7.1", "eslint-visitor-keys": "1.0.0" }, "dependencies": { "babylon": { - "version": "7.0.0-beta.36", - "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.36.tgz", - "integrity": "sha512-rw4YdadGwajAMMRl6a5swhQ0JCOOFyaYCfJ0AsmNBD8uBD/r4J8mux7wBaqavvFKqUKQYWOzA1Speams4YDzsQ==" + "version": "7.0.0-beta.40", + "resolved": "https://registry.npmjs.org/babylon/-/babylon-7.0.0-beta.40.tgz", + "integrity": "sha512-AVxF2EcxvGD5hhOuLTOLAXBb0VhwWpEX0HyHdAI2zU+AAP4qEwtQj8voz1JR3uclGai0rfcE+dCTHnNMOnimFg==" } } }, @@ -1893,6 +1945,11 @@ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=" }, + "bindings": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz", + "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==" + }, "bitsyntax": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/bitsyntax/-/bitsyntax-0.0.4.tgz", @@ -2439,6 +2496,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/buildmail/-/buildmail-4.0.1.tgz", "integrity": "sha1-h393OLeHKYccmhBeO4N9K+EaenI=", + "dev": true, + "optional": true, "requires": { "addressparser": "1.0.1", "libbase64": "0.1.0", @@ -2485,6 +2544,55 @@ "unset-value": "1.0.0" } }, + "cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "requires": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + }, + "dependencies": { + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "requires": { + "prepend-http": "2.0.0", + "query-string": "5.1.0", + "sort-keys": "2.0.0" + } + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "query-string": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.0.tgz", + "integrity": "sha512-F3DkxxlY0AqD/rwe4YAwjRE2HjOkKW7TxsuteyrS/Jbwrxw887PqYBL4sWUJ9D/V1hmFns0SCD6FDyvlwo9RCQ==", + "requires": { + "decode-uri-component": "0.2.0", + "object-assign": "4.1.1", + "strict-uri-encode": "1.1.0" + } + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "requires": { + "is-plain-obj": "1.1.0" + } + } + } + }, "cached-constructors-x": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/cached-constructors-x/-/cached-constructors-x-1.0.0.tgz", @@ -2688,13 +2796,13 @@ "dev": true }, "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.1.tgz", + "integrity": "sha512-QUU4ofkDoMIVO7hcx1iPTISs88wsO8jA92RQIm4JAwZvFGGAV2hSAA1NX7oVj2Ej2Q6NDTcRDjPTFrMCRZoJ6g==", "requires": { "ansi-styles": "3.2.0", "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" + "supports-color": "5.2.0" }, "dependencies": { "ansi-styles": { @@ -2706,16 +2814,16 @@ } }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "supports-color": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz", - "integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", "requires": { - "has-flag": "2.0.0" + "has-flag": "3.0.0" } } } @@ -3057,6 +3165,14 @@ } } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "1.0.0" + } + }, "clone-stats": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", @@ -3237,21 +3353,28 @@ "dev": true }, "compressible": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.12.tgz", - "integrity": "sha1-xZpcmdt2dn6YdlAOJx72OzSTvWY=", + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.13.tgz", + "integrity": "sha1-DRAgq5JLL9tNYnmHXH1tq6a6p6k=", "requires": { - "mime-db": "1.30.0" + "mime-db": "1.33.0" + }, + "dependencies": { + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" + } } }, "compression": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.1.tgz", - "integrity": "sha1-7/JgPvwuIs+G810uuTWJ+YdTc9s=", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.2.tgz", + "integrity": "sha1-qv+81qr4VLROuygDU9WtFlH1mmk=", "requires": { "accepts": "1.3.4", "bytes": "3.0.0", - "compressible": "2.0.12", + "compressible": "2.0.13", "debug": "2.6.9", "on-headers": "1.0.1", "safe-buffer": "5.1.1", @@ -3813,9 +3936,9 @@ } }, "csv-stringify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-2.0.1.tgz", - "integrity": "sha512-qoWgXJHmANfwIZFogezZjfN7KeaALrSe2zA5velb2tVEx0r7tgVuMfideB5gq12x5Z7LkvgOayXplOwlKcXnKQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-2.0.4.tgz", + "integrity": "sha512-rm8JxNYXglaDKExA2KqCKY+jx8qVBSkXm8/bgw2BIyzLq1/qni/QMSPBchAkd8RGsJhMJDQ+ZxikG/Qgvck+gQ==", "requires": { "lodash.get": "4.4.2" } @@ -4172,6 +4295,14 @@ } } }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "1.0.0" + } + }, "decompress-tar": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-3.1.0.tgz", @@ -5484,14 +5615,14 @@ } }, "eslint": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.18.0.tgz", - "integrity": "sha512-Ep2lUbztzXLg0gNUl48I1xvbQFy1QuWyh1C9PSympmln33jwOr8B3QfuEcXpPPE4uSwEzDaWhUxBN0sNQkzrBg==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.18.1.tgz", + "integrity": "sha512-gPSfpSRCHre1GLxGmO68tZNxOlL2y7xBd95VcLD+Eo4S2js31YoMum3CAQIOaxY24hqYOMksMvW38xuuWKQTgw==", "dev": true, "requires": { "ajv": "5.5.2", "babel-code-frame": "6.26.0", - "chalk": "2.3.0", + "chalk": "2.3.1", "concat-stream": "1.6.0", "cross-spawn": "5.1.0", "debug": "3.1.0", @@ -6171,38 +6302,20 @@ } }, "express-basic-auth": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.1.3.tgz", - "integrity": "sha512-+tYtERHfl46Y1sPexskNcnIgYCmEnvj4Hp8/19dfByMUQAAWKEQ/Ik1taQ+Kj9LLJP1ItuXa95Ta8e8LtyltVQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.1.4.tgz", + "integrity": "sha512-stNYRMPULJu/Tk3aFSdkyFMHDPDukens2L5FrTHJab+k/gvBdjB885W3gAZtHNrUaSmbAO8dQ9Omcot9qtbAag==", "requires": { "basic-auth": "1.1.0" } }, "express-validator": { - "version": "2.21.0", - "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-2.21.0.tgz", - "integrity": "sha1-9fwvn6npqFeGNPEOhrpaQ0a5b08=", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-5.0.1.tgz", + "integrity": "sha512-M5SgmiyzAzdqnaCAgqvQpiC7KpCBeIqcwYxivTv32ALWk+ZQG/E2DlxH5FoIOsO5m/V9xO8SGyghtz5Waxz35A==", "requires": { - "bluebird": "3.4.7", - "lodash": "4.16.6", - "validator": "5.7.0" - }, - "dependencies": { - "bluebird": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" - }, - "lodash": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.16.6.tgz", - "integrity": "sha1-0iyaxmAojzhD4Wun0rXQbMon13c=" - }, - "validator": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-5.7.0.tgz", - "integrity": "sha1-eoelgUa2laxIYHEUHAxJ1n2gXlw=" - } + "lodash": "4.17.5", + "validator": "9.4.1" } }, "extend": { @@ -6727,6 +6840,15 @@ "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "2.0.3", + "readable-stream": "2.3.3" + } + }, "fs-access": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz", @@ -8125,21 +8247,47 @@ } }, "got": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/got/-/got-8.2.0.tgz", + "integrity": "sha512-giadqJpXIwjY+ZsuWys8p2yjZGhOHiU4hiJHjS/oeCxw1u8vANQz3zPlrxW2Zw/siCXsSMI3hvzWGcnFyujyAg==", "requires": { - "create-error-class": "3.0.2", + "@sindresorhus/is": "0.7.0", + "cacheable-request": "2.1.4", + "decompress-response": "3.3.0", "duplexer3": "0.1.4", "get-stream": "3.0.0", - "is-redirect": "1.0.0", + "into-stream": "3.1.0", "is-retry-allowed": "1.1.0", - "is-stream": "1.1.0", + "isurl": "1.0.0", "lowercase-keys": "1.0.0", + "mimic-response": "1.0.0", + "p-cancelable": "0.3.0", + "p-timeout": "2.0.1", + "pify": "3.0.0", "safe-buffer": "5.1.1", "timed-out": "4.0.1", - "unzip-response": "2.0.1", - "url-parse-lax": "1.0.0" + "url-parse-lax": "3.0.0", + "url-to-options": "1.0.1" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "2.0.0" + } + } } }, "graceful-fs": { @@ -9108,6 +9256,11 @@ } } }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" + }, "http-errors": { "version": "1.6.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz", @@ -9283,6 +9436,7 @@ "version": "1.6.1", "resolved": "https://registry.npmjs.org/httpntlm/-/httpntlm-1.6.1.tgz", "integrity": "sha1-rQFScUOi6Hc8+uapb1hla7UqNLI=", + "dev": true, "requires": { "httpreq": "0.4.24", "underscore": "1.7.0" @@ -9291,14 +9445,16 @@ "underscore": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.7.0.tgz", - "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=" + "integrity": "sha1-a7rwh3UA02vjTsqlhODbn+8DUgk=", + "dev": true } } }, "httpreq": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/httpreq/-/httpreq-0.4.24.tgz", - "integrity": "sha1-QzX/2CzZaWaKOUZckprGHWOTYn8=" + "integrity": "sha1-QzX/2CzZaWaKOUZckprGHWOTYn8=", + "dev": true }, "https-browserify": { "version": "1.0.0", @@ -9790,6 +9946,15 @@ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=" }, + "into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "requires": { + "from2": "2.3.0", + "p-is-promise": "1.1.0" + } + }, "intro.js": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/intro.js/-/intro.js-2.7.0.tgz", @@ -9816,7 +9981,8 @@ "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "dev": true }, "ip-regex": { "version": "1.0.3", @@ -10131,6 +10297,11 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" }, + "is-object": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", + "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=" + }, "is-object-like-x": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/is-object-like-x/-/is-object-like-x-1.6.0.tgz", @@ -10497,6 +10668,15 @@ "handlebars": "4.0.11" } }, + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "requires": { + "has-to-string-tag-x": "1.4.1", + "is-object": "1.0.1" + } + }, "jmespath": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", @@ -10548,9 +10728,12 @@ } }, "js2xmlparser": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-1.0.0.tgz", - "integrity": "sha1-WhcPLo1kds5FQF4EgjJCUTeC/jA=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-3.0.0.tgz", + "integrity": "sha1-P7YOqgicVED5MZ9RdgzNB+JJlzM=", + "requires": { + "xmlcreate": "1.0.2" + } }, "jsbn": { "version": "0.1.1", @@ -10563,6 +10746,11 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=" }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, "json-content-demux": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/json-content-demux/-/json-content-demux-0.1.3.tgz", @@ -11271,6 +11459,14 @@ "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.2.tgz", "integrity": "sha1-rTKXxVcGneqLz+ek+kkbdcXd65E=" }, + "keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", @@ -11644,12 +11840,14 @@ "libbase64": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/libbase64/-/libbase64-0.1.0.tgz", - "integrity": "sha1-YjUag5VjrF/1vSbxL2Dpgwu3UeY=" + "integrity": "sha1-YjUag5VjrF/1vSbxL2Dpgwu3UeY=", + "dev": true }, "libmime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/libmime/-/libmime-3.0.0.tgz", "integrity": "sha1-UaGp50SOy9Ms2lRCFnW7IbwJPaY=", + "dev": true, "requires": { "iconv-lite": "0.4.15", "libbase64": "0.1.0", @@ -11659,14 +11857,16 @@ "iconv-lite": { "version": "0.4.15", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", - "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=" + "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=", + "dev": true } } }, "libqp": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/libqp/-/libqp-1.1.0.tgz", - "integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=" + "integrity": "sha1-9ebgatdLeU+1tbZpiL9yjvHe2+g=", + "dev": true }, "liftoff": { "version": "2.5.0", @@ -12141,6 +12341,22 @@ } } }, + "nodemailer": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.7.2.tgz", + "integrity": "sha1-8kLmSa7q45tsftdA73sGHEBNMPk=", + "dev": true, + "optional": true, + "requires": { + "libmime": "3.0.0", + "mailcomposer": "4.0.1", + "nodemailer-direct-transport": "3.3.2", + "nodemailer-shared": "1.1.0", + "nodemailer-smtp-pool": "2.8.2", + "nodemailer-smtp-transport": "2.7.2", + "socks": "1.1.9" + } + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -12347,6 +12563,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/mailcomposer/-/mailcomposer-4.0.1.tgz", "integrity": "sha1-DhxEsqB890DuF9wUm6AJ8Zyt/rQ=", + "dev": true, + "optional": true, "requires": { "buildmail": "4.0.1", "libmime": "3.0.0" @@ -12687,6 +12905,11 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" }, + "mimic-response": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.0.tgz", + "integrity": "sha1-3z02Uqc/3ta5sLJBRub9BSNTRY4=" + }, "minimalistic-assert": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", @@ -12760,9 +12983,9 @@ "dev": true }, "mocha": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.0.tgz", - "integrity": "sha512-ukB2dF+u4aeJjc6IGtPNnJXfeby5d4ZqySlIBT0OEyva/DrMjVm5HkQxKnHDLKEfEQBsEnwTg9HHhtPHJdTd8w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.1.tgz", + "integrity": "sha512-SpwyojlnE/WRBNGtvJSNfllfm5PqEDFxcWluSIgLeSBJtXG4DmoX2NNAeEA7rP5kK+79VgtVq8nG6HskaL1ykg==", "dev": true, "requires": { "browser-stdout": "1.3.0", @@ -12964,7 +13187,12 @@ "integrity": "sha512-Yh9y73JRljxW5QxN08Fner68eFLxM5ynNOAw2LbIB1YAGeQzZT8QFSUvkAz609Zf+IHhhaUxqZK8dG3W/+HEvg==" }, "moment-recur": { - "version": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/moment-recur/-/moment-recur-1.0.7.tgz", + "integrity": "sha1-TVCSr2SK7e1q/lwT7zjFKQHJlBk=", + "requires": { + "moment": "2.20.1" + } }, "mongodb": { "version": "2.2.34", @@ -13002,9 +13230,9 @@ } }, "mongoose": { - "version": "4.13.10", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.13.10.tgz", - "integrity": "sha512-ERmAyJf/ECyApfmkcjlbnmY0CoNzanOHA8vCIFuZwY55tW4b4gTVtklFXWLmSye8XjZjM0xHioXr5eN3Z8jtZg==", + "version": "4.13.11", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-4.13.11.tgz", + "integrity": "sha512-OgXmFc3vzXwq4zWp41XfSBDnKZLqnBc4Kh7mwwGjBE5iWH5tfkixaPK0uFtpEuzDzUvAIg33bgniyTsmc00olA==", "requires": { "async": "2.1.4", "bson": "1.0.4", @@ -13387,27 +13615,18 @@ } }, "nise": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.2.2.tgz", - "integrity": "sha512-rvxf+PSZeCKtP0DgmwMmNf1G3I6X1r4WHiP2H88PlIkOkt7mGqufdokjS8caoHBgZzVx0ee/5ytGcGHbZaUw8w==", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.2.5.tgz", + "integrity": "sha512-Es4hGuq3lpip5PckrB+Qpuma282M0UJANJ+jxAgI+0wWTL9X6MtNv+M385JgqsAE8hv6NvD3lv8CQtXgEnvlpQ==", "dev": true, "requires": { - "formatio": "1.2.0", + "@sinonjs/formatio": "2.0.0", "just-extend": "1.1.27", - "lolex": "1.6.0", + "lolex": "2.3.2", "path-to-regexp": "1.7.0", "text-encoding": "0.6.4" }, "dependencies": { - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "1.1.2" - } - }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -13415,9 +13634,9 @@ "dev": true }, "lolex": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", - "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.3.2.tgz", + "integrity": "sha512-A5pN2tkFj7H0dGIAM6MFvHKMJcPnjZsOMvR7ujCjfgW5TbV6H9vb1PgxLtHvjqNZTHsUolz+6/WEO0N1xNx2ng==", "dev": true }, "path-to-regexp": { @@ -13674,6 +13893,15 @@ } } }, + "node-rdkafka": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/node-rdkafka/-/node-rdkafka-2.2.3.tgz", + "integrity": "sha1-BdjK/brye/Ho7yuOW/Oa+1mi5wE=", + "requires": { + "bindings": "1.3.0", + "nan": "2.6.2" + } + }, "node-sass": { "version": "4.7.2", "resolved": "https://registry.npmjs.org/node-sass/-/node-sass-4.7.2.tgz", @@ -13774,23 +14002,16 @@ "integrity": "sha1-WuVUHQJGRdMqWPzdyc7s6nrjrC8=" }, "nodemailer": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-2.7.2.tgz", - "integrity": "sha1-8kLmSa7q45tsftdA73sGHEBNMPk=", - "requires": { - "libmime": "3.0.0", - "mailcomposer": "4.0.1", - "nodemailer-direct-transport": "3.3.2", - "nodemailer-shared": "1.1.0", - "nodemailer-smtp-pool": "2.8.2", - "nodemailer-smtp-transport": "2.7.2", - "socks": "1.1.9" - } + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-4.5.0.tgz", + "integrity": "sha512-bznbREPZoRt3g400RdmVYXs2mbZft9Y1DfEqE2GS0VcBwU8DiEMDQ2q4gauSltbsnXtIVHtKH+VmE80I35TxhA==" }, "nodemailer-direct-transport": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/nodemailer-direct-transport/-/nodemailer-direct-transport-3.3.2.tgz", "integrity": "sha1-6W+vuQNYVglH5WkBfZfmBzilCoY=", + "dev": true, + "optional": true, "requires": { "nodemailer-shared": "1.1.0", "smtp-connection": "2.12.0" @@ -13799,12 +14020,14 @@ "nodemailer-fetch": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz", - "integrity": "sha1-ecSQihwPXzdbc/6IjamCj23JY6Q=" + "integrity": "sha1-ecSQihwPXzdbc/6IjamCj23JY6Q=", + "dev": true }, "nodemailer-shared": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nodemailer-shared/-/nodemailer-shared-1.1.0.tgz", "integrity": "sha1-z1mU4v0mjQD1zw+nZ6CBae2wfsA=", + "dev": true, "requires": { "nodemailer-fetch": "1.6.0" } @@ -13813,6 +14036,8 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/nodemailer-smtp-pool/-/nodemailer-smtp-pool-2.8.2.tgz", "integrity": "sha1-LrlNbPhXgLG0clzoU7nL1ejajHI=", + "dev": true, + "optional": true, "requires": { "nodemailer-shared": "1.1.0", "nodemailer-wellknown": "0.1.10", @@ -13823,6 +14048,8 @@ "version": "2.7.2", "resolved": "https://registry.npmjs.org/nodemailer-smtp-transport/-/nodemailer-smtp-transport-2.7.2.tgz", "integrity": "sha1-A9ccdjFPFKx9vHvwM6am0W1n+3c=", + "dev": true, + "optional": true, "requires": { "nodemailer-shared": "1.1.0", "nodemailer-wellknown": "0.1.10", @@ -13832,7 +14059,8 @@ "nodemailer-wellknown": { "version": "0.1.10", "resolved": "https://registry.npmjs.org/nodemailer-wellknown/-/nodemailer-wellknown-0.1.10.tgz", - "integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U=" + "integrity": "sha1-WG24EB2zDLRDjrVGc3pBqtDPE9U=", + "dev": true }, "nodemon": { "version": "1.14.12", @@ -14361,45 +14589,29 @@ } }, "ora": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ora/-/ora-1.4.0.tgz", - "integrity": "sha512-iMK1DOQxzzh2MBlVsU42G80mnrvUhqsMh74phHtDlrcTZPK0pH6o7l7DRshK+0YsxDyEuaOkziVdvM3T0QTzpw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-2.0.0.tgz", + "integrity": "sha512-g+IR0nMUXq1k4nE3gkENbN4wkF0XsVZFyxznTF6CdmwQ9qeTGONGpSR9LM5//1l0TVvJoJF3MkMtJp6slUsWFg==", "requires": { - "chalk": "2.3.0", + "chalk": "2.3.1", "cli-cursor": "2.1.0", "cli-spinners": "1.1.0", - "log-symbols": "2.2.0" + "log-symbols": "2.2.0", + "strip-ansi": "4.0.0", + "wcwidth": "1.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" - } + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" }, - "chalk": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", - "integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==", + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { - "ansi-styles": "3.2.0", - "escape-string-regexp": "1.0.5", - "supports-color": "4.5.0" - } - }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=" - }, - "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" + "ansi-regex": "3.0.0" } } } @@ -14478,11 +14690,21 @@ "os-tmpdir": "1.0.2" } }, + "p-cancelable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", + "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==" + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, + "p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=" + }, "p-limit": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.2.0.tgz", @@ -14509,6 +14731,14 @@ "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=" }, + "p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "requires": { + "p-finally": "1.0.0" + } + }, "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", @@ -14569,6 +14799,24 @@ "semver": "5.5.0" }, "dependencies": { + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "requires": { + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.0", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" + } + }, "semver": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", @@ -16275,9 +16523,9 @@ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" }, "puppeteer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.0.0.tgz", - "integrity": "sha512-e00NMdUL32YhBcua9OkVXHgyDEMBWJhDXkYNv0pyKRU1Z1OrsRm5zCpppAdxAsBI+/MJBspFNfOUZuZ24qPGMQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-1.1.0.tgz", + "integrity": "sha512-Up+EiDVtc+EueFUzEFi4JqTlkXdC4pjMng0W9s/uqg2HP7+EHJiIe8OtwP7I+Jk7rI1kS5WIVpqx46aihnLuJQ==", "dev": true, "requires": { "debug": "2.6.9", @@ -16320,12 +16568,6 @@ } } }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, "progress": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", @@ -17227,6 +17469,14 @@ "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "1.0.0" + } + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -17427,9 +17677,9 @@ } }, "selenium-server": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/selenium-server/-/selenium-server-3.9.0.tgz", - "integrity": "sha512-xbcL9yt/a1U0pvwAkneTWxrq7zF/X5oCAY5CMfwYbY3BglmvlXjrPhM5rnzVveDlVOtbXkZpcDnG3Ny9YUpddA==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/selenium-server/-/selenium-server-3.9.1.tgz", + "integrity": "sha512-p+GORe/uLnUUjMkEX3zqQmZKDxJ+WG7od92RKAh0Oz+H4e9Kjgxzgwb3E8IB4nGf2ifsZkJVo/uEvqRP30y9+Q==", "dev": true }, "semver": { @@ -17675,33 +17925,24 @@ "integrity": "sha1-BcLuxXn//+FFoDCsJs/qYbmA+r4=" }, "sinon": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.2.2.tgz", - "integrity": "sha512-BEa593xl+IkIc94nKo0O0LauQC/gQy8Gyv4DkzPwF/9DweC5phr1y+42zibCpn9abfkdHxt9r8AhD0R6u9DE/Q==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-4.3.0.tgz", + "integrity": "sha512-pmf05hFgEZUS52AGJcsVjOjqAyJW2yo14cOwVYvzCyw7+inv06YXkLyW75WG6X6p951lzkoKh51L2sNbR9CDvw==", "dev": true, "requires": { + "@sinonjs/formatio": "2.0.0", "diff": "3.2.0", - "formatio": "1.2.0", "lodash.get": "4.4.2", "lolex": "2.3.2", - "nise": "1.2.2", - "supports-color": "5.1.0", + "nise": "1.2.5", + "supports-color": "5.2.0", "type-detect": "4.0.8" }, "dependencies": { - "formatio": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", - "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", - "dev": true, - "requires": { - "samsam": "1.1.2" - } - }, "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, "lolex": { @@ -17711,19 +17952,13 @@ "dev": true }, "supports-color": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.1.0.tgz", - "integrity": "sha512-Ry0AwkoKjDpVKK4sV4h6o3UJmNRbjYm2uXhwfj3J56lMVdvnUNqzQVRztOOMGQ++w1K/TjNDFvpJk0F/LoeBCQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.2.0.tgz", + "integrity": "sha512-F39vS48la4YvTZUPVeTqsjsFNrvcMwrV3RLZINsmHo+7djCvuUzSIeXOnZ5hmjef4bajL1dNccN+tg5XAliO5Q==", "dev": true, "requires": { - "has-flag": "2.0.0" + "has-flag": "3.0.0" } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true } } }, @@ -17779,12 +18014,14 @@ "smart-buffer": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-1.1.15.tgz", - "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=" + "integrity": "sha1-fxFLW2X6s+KjWqd1uxLw0cZJvxY=", + "dev": true }, "smtp-connection": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/smtp-connection/-/smtp-connection-2.12.0.tgz", "integrity": "sha1-1275EnyyPCJZ7bHoNJwujV4tdME=", + "dev": true, "requires": { "httpntlm": "1.6.1", "nodemailer-shared": "1.1.0" @@ -17966,6 +18203,7 @@ "version": "1.1.9", "resolved": "https://registry.npmjs.org/socks/-/socks-1.1.9.tgz", "integrity": "sha1-Yo1+TQSRJDVEWsC25Fk3bLPm1pE=", + "dev": true, "requires": { "ip": "1.1.5", "smart-buffer": "1.1.15" @@ -18630,9 +18868,9 @@ "integrity": "sha1-IrD6OkE4WzO+PzMVUbu4N/oM164=" }, "stripe": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-5.4.0.tgz", - "integrity": "sha512-VCDFp4oQu1uOcOLHIwRIznH8ikLJcpDsHahWN48V/QuV6y2Bm281cq5wnkjqv+LPdUpqXVp9pjlb+SfN6dnyZg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-5.5.0.tgz", + "integrity": "sha512-oy8xNoxv5/4eFsn+W/0KQvG9St7x/xM8TbOIW2prBLd2+zo+2y2V9iyqcNt6B14efcOE2YeCR1s5yw+1cqa2VQ==", "requires": { "bluebird": "3.5.1", "lodash.isplainobject": "4.0.6", @@ -18897,7 +19135,7 @@ "requires": { "ajv": "5.5.2", "ajv-keywords": "2.1.1", - "chalk": "2.3.0", + "chalk": "2.3.1", "lodash": "4.17.5", "slice-ansi": "1.0.0", "string-width": "2.1.1" @@ -19979,6 +20217,11 @@ "ip-regex": "1.0.3" } }, + "url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=" + }, "url2": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/url2/-/url2-1.0.4.tgz", @@ -20166,19 +20409,9 @@ "integrity": "sha1-fif8uzFbhB54JDQxiXZxkp4gt/Q=" }, "validator": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-4.9.0.tgz", - "integrity": "sha1-CC/84qdhSP8HqOienCukOq8S7Ew=", - "requires": { - "depd": "1.1.0" - }, - "dependencies": { - "depd": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.0.tgz", - "integrity": "sha1-4b2Cxqq2ztlluXuIsX7T5SjKGMM=" - } - } + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/validator/-/validator-9.4.1.tgz", + "integrity": "sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA==" }, "value-or-function": { "version": "3.0.0", @@ -20463,9 +20696,9 @@ "integrity": "sha512-vLLoY452L+JBpALMP5UHum9+7nzR9PeIBCghU9ZtJ1eWm6ieUI8Zb/DI3MYxH32bxkjzYV1LRjNv4qr8d+uX/w==" }, "vue-style-loader": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-3.1.2.tgz", - "integrity": "sha512-ICtVdK/p+qXWpdSs2alWtsXt9YnDoYjQe0w5616j9+/EhjoxZkbun34uWgsMFnC1MhrMMwaWiImz3K2jK1Yp2Q==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.0.2.tgz", + "integrity": "sha512-Bwf1Gf331Z5OTzMRAYQYiFpFbaCpaXQjQcSvWYsmEwSgOIVa+moXWoD8fQCNetcekbP3OSE5pyvomNKbvIUQtQ==", "requires": { "hash-sum": "1.0.2", "loader-utils": "1.1.0" @@ -20516,6 +20749,26 @@ "cheerio": "0.19.0", "got": "6.7.1", "meow": "3.7.0" + }, + "dependencies": { + "got": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "requires": { + "create-error-class": "3.0.2", + "duplexer3": "0.1.4", + "get-stream": "3.0.0", + "is-redirect": "1.0.0", + "is-retry-allowed": "1.1.0", + "is-stream": "1.1.0", + "lowercase-keys": "1.0.0", + "safe-buffer": "5.1.1", + "timed-out": "4.0.1", + "unzip-response": "2.0.1", + "url-parse-lax": "1.0.0" + } + } } }, "ware": { @@ -20659,6 +20912,14 @@ } } }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "1.0.3" + } + }, "webpack": { "version": "3.11.0", "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.11.0.tgz", @@ -21122,7 +21383,7 @@ "integrity": "sha512-B53SD4N4BHpZdUwZcj4st2QT7gVfqZtqHDruC1N+K2sciq0Rt/3F1Dx6RlylVkcrToMLTaiaeT48k9Lq4iDVDA==", "dev": true, "requires": { - "chalk": "2.3.0", + "chalk": "2.3.1", "log-symbols": "2.2.0", "loglevelnext": "1.0.3", "uuid": "3.2.1" @@ -21377,6 +21638,11 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.4.tgz", "integrity": "sha1-UZy0ymhtAFqEINNJbz8MruzKWA8=" }, + "xmlcreate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", + "integrity": "sha1-+mv3YqYKQT+z3Y9LA8WyaSONMI8=" + }, "xmldom": { "version": "0.1.19", "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.19.tgz", diff --git a/package.json b/package.json index 91fc401579..5056f7e964 100644 --- a/package.json +++ b/package.json @@ -3,20 +3,24 @@ "description": "A habit tracker app which treats your goals like a Role Playing Game.", "version": "4.28.2", "main": "./website/server/index.js", + "greenkeeper": { + "ignore": [ + "mongoose" + ] + }, "dependencies": { "@slack/client": "^3.8.1", "accepts": "^1.3.2", "amazon-payments": "^0.2.6", "amplitude": "^3.5.0", "apidoc": "^0.17.5", - "apn": "^1.7.6", "autoprefixer": "^8.0.0", - "aws-sdk": "^2.0.25", - "axios": "^0.17.1", + "aws-sdk": "^2.200.0", + "axios": "^0.18.0", "axios-progress-bar": "^1.1.8", "babel-core": "^6.0.0", + "babel-eslint": "^8.2.2", "babel-loader": "^7.1.2", - "babel-eslint": "^8.2.1", "babel-plugin-syntax-async-functions": "^6.13.0", "babel-plugin-syntax-dynamic-import": "^6.18.0", "babel-plugin-transform-async-to-module-method": "^6.8.0", @@ -31,20 +35,20 @@ "body-parser": "^1.15.0", "bootstrap": "^4.0.0", "bootstrap-vue": "^2.0.0-rc.1", - "compression": "^1.6.1", + "compression": "^1.7.2", "cookie-session": "^1.2.0", "coupon-code": "^0.4.5", "cross-env": "^5.1.3", "css-loader": "^0.28.0", - "csv-stringify": "^2.0.1", + "csv-stringify": "^2.0.4", "cwait": "^1.1.1", "domain-middleware": "~0.1.0", "express": "^4.16.2", - "express-basic-auth": "^1.0.1", - "express-validator": "^2.18.0", + "express-basic-auth": "^1.1.4", + "express-validator": "^5.0.1", "extract-text-webpack-plugin": "^3.0.2", "glob": "^7.1.2", - "got": "^6.1.1", + "got": "^8.2.0", "gulp": "^4.0.0", "gulp-babel": "^7.0.1", "gulp-imagemin": "^4.1.0", @@ -57,19 +61,20 @@ "in-app-purchase": "^1.1.6", "intro.js": "^2.6.0", "jquery": ">=3.0.0", - "js2xmlparser": "~1.0.0", + "js2xmlparser": "^3.0.0", "lodash": "^4.17.4", "merge-stream": "^1.0.0", "method-override": "^2.3.5", "moment": "^2.13.0", - "moment-recur": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626", - "mongoose": "^4.13.10", + "moment-recur": "^1.0.7", + "mongoose": "^4.13.11", "morgan": "^1.7.0", "nconf": "^0.10.0", "node-gcm": "^0.14.4", + "node-rdkafka": "^2.2.3", "node-sass": "^4.5.0", - "nodemailer": "^2.3.2", - "ora": "^1.1.0", + "nodemailer": "^4.5.0", + "ora": "^2.0.0", "pageres": "^4.1.1", "passport": "^0.4.0", "passport-facebook": "^2.0.0", @@ -82,11 +87,10 @@ "pug": "^2.0.0-rc.4", "push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9", "pusher": "^1.3.0", - "request": "^2.83.0", "rimraf": "^2.4.3", "sass-loader": "^6.0.2", "shelljs": "^0.8.1", - "stripe": "^5.4.0", + "stripe": "^5.5.0", "superagent": "^3.4.3", "svg-inline-loader": "^0.8.0", "svg-url-loader": "^2.0.2", @@ -96,13 +100,13 @@ "url-loader": "^0.6.2", "useragent": "^2.1.9", "uuid": "^3.0.1", - "validator": "^4.9.0", + "validator": "^9.4.1", "vinyl-buffer": "^1.0.1", "vue": "^2.5.2", "vue-loader": "^14.1.1", "vue-mugen-scroll": "^0.2.1", "vue-router": "^3.0.0", - "vue-style-loader": "^3.0.0", + "vue-style-loader": "^4.0.2", "vue-template-compiler": "^2.5.2", "vuedraggable": "^2.15.0", "vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec", @@ -145,12 +149,12 @@ "babel-plugin-istanbul": "^4.0.0", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", - "chalk": "^2.3.0", + "chalk": "^2.3.1", "chromedriver": "^2.27.2", "connect-history-api-fallback": "^1.1.0", "coveralls": "^3.0.0", "cross-spawn": "^6.0.4", - "eslint": "^4.17.0", + "eslint": "^4.18.1", "eslint-config-habitrpg": "^4.0.0", "eslint-friendly-formatter": "^3.0.0", "eslint-loader": "^1.3.0", @@ -173,13 +177,13 @@ "karma-spec-reporter": "0.0.32", "karma-webpack": "^2.0.2", "lcov-result-merger": "^2.0.0", - "mocha": "^5.0.0", + "mocha": "^5.0.1", "monk": "^6.0.5", "nightwatch": "^0.9.12", - "puppeteer": "^1.0.0", + "puppeteer": "^1.1.0", "require-again": "^2.0.0", - "selenium-server": "^3.0.1", - "sinon": "^4.2.2", + "selenium-server": "^3.9.1", + "sinon": "^4.3.0", "sinon-chai": "^2.8.0", "sinon-stub-promise": "^4.0.0", "webpack-bundle-analyzer": "^2.2.1", diff --git a/test/api/v3/integration/chat/POST-chat.test.js b/test/api/v3/integration/chat/POST-chat.test.js index 8892c8e845..c9b2d97384 100644 --- a/test/api/v3/integration/chat/POST-chat.test.js +++ b/test/api/v3/integration/chat/POST-chat.test.js @@ -478,8 +478,8 @@ describe('POST /chat', () => { context('Spam prevention', () => { it('Returns an error when the user has been posting too many messages', async () => { // Post as many messages are needed to reach the spam limit - for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) { // eslint-disable-line no-await-in-loop - let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); + for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) { + let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop expect(result.message.id).to.exist; } @@ -494,8 +494,8 @@ describe('POST /chat', () => { let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false}); // Post 1 more message than the spam limit to ensure they do not reach the limit - for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) { // eslint-disable-line no-await-in-loop - let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); + for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) { + let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop expect(result.message.id).to.exist; } }); diff --git a/test/api/v3/integration/tasks/GET-tasks_user.test.js b/test/api/v3/integration/tasks/GET-tasks_user.test.js index 0525f6b4b5..ea8f2a49d0 100644 --- a/test/api/v3/integration/tasks/GET-tasks_user.test.js +++ b/test/api/v3/integration/tasks/GET-tasks_user.test.js @@ -113,10 +113,10 @@ describe('GET /tasks/user', () => { await user.sync(); let initialTodoCount = user.tasksOrder.todos.length; - for (let i = 0; i < numberOfTodos; i++) { // eslint-disable-line no-await-in-loop + for (let i = 0; i < numberOfTodos; i++) { let id = todos[i]._id; - await user.post(`/tasks/${id}/score/up`); + await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop } await user.sync(); diff --git a/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js b/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js index f3c79fe868..7cdd7753d2 100644 --- a/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_clearCompletedTodos.test.js @@ -33,9 +33,9 @@ describe('POST /tasks/clearCompletedTodos', () => { let tasks = await user.get('/tasks/user?type=todos'); expect(tasks.length).to.equal(initialTodoCount + 7); - for (let task of tasks) { // eslint-disable-line no-await-in-loop + for (let task of tasks) { if (['todo 2', 'todo 3', 'todo 6'].indexOf(task.text) !== -1) { - await user.post(`/tasks/${task._id}/score/up`); + await user.post(`/tasks/${task._id}/score/up`); // eslint-disable-line no-await-in-loop } } diff --git a/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js b/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js index 9453757530..d8cd074bc7 100644 --- a/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js +++ b/test/api/v3/integration/user/POST-user_class_cast_spellId.test.js @@ -187,6 +187,22 @@ describe('POST /user/class/cast/:spellId', () => { expect(group.chat[0].uuid).to.equal('system'); }); + it('cast bulk', async () => { + let { group, groupLeader } = await createAndPopulateGroup({ + groupDetails: { type: 'party', privacy: 'private' }, + members: 1, + }); + + await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13}); + await groupLeader.post('/user/class/cast/earth', {quantity: 2}); + + await sleep(1); + await group.sync(); + + expect(group.chat[0]).to.exist; + expect(group.chat[0].uuid).to.equal('system'); + }); + it('searing brightness does not affect challenge or group tasks', async () => { let guild = await generateGroup(user); let challenge = await generateChallenge(user, guild); diff --git a/test/api/v3/integration/webhook/PUT-user_update_webhook.test.js b/test/api/v3/integration/webhook/PUT-user_update_webhook.test.js index 2e02562b5f..994eff6750 100644 --- a/test/api/v3/integration/webhook/PUT-user_update_webhook.test.js +++ b/test/api/v3/integration/webhook/PUT-user_update_webhook.test.js @@ -34,7 +34,7 @@ describe('PUT /user/webhook/:id', () => { }); it('returns an error if validation fails', async () => { - await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, { url: 'foo', enabled: true })).to.eventually.be.rejected.and.eql({ + await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, { url: 'foo_invalid', enabled: true })).to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', message: 'User validation failed', diff --git a/test/api/v3/unit/libs/email.test.js b/test/api/v3/unit/libs/email.test.js index 1597ccee73..ebfa2ac6ea 100644 --- a/test/api/v3/unit/libs/email.test.js +++ b/test/api/v3/unit/libs/email.test.js @@ -1,27 +1,11 @@ /* eslint-disable global-require */ -import request from 'request'; +import got from 'got'; import nconf from 'nconf'; import nodemailer from 'nodemailer'; -import Bluebird from 'bluebird'; import requireAgain from 'require-again'; import logger from '../../../../../website/server/libs/logger'; import { TAVERN_ID } from '../../../../../website/server/models/group'; - -function defer () { - let resolve; - let reject; - - let promise = new Bluebird((resolveParam, rejectParam) => { - resolve = resolveParam; - reject = rejectParam; - }); - - return { - resolve, - reject, - promise, - }; -} +import { defer } from '../../../../helpers/api-unit.helper'; function getUser () { return { @@ -158,7 +142,7 @@ describe('emails', () => { describe('sendTxnEmail', () => { beforeEach(() => { - sandbox.stub(request, 'post'); + sandbox.stub(got, 'post').returns(defer().promise); }); afterEach(() => { @@ -176,8 +160,9 @@ describe('emails', () => { }; sendTxnEmail(mailingInfo, emailType); - expect(request.post).to.be.calledWith(sinon.match({ - json: { + expect(got.post).to.be.calledWith('undefined/job', sinon.match({ + json: true, + body: { data: { emailType: sinon.match.same(emailType), to: sinon.match((value) => { @@ -199,7 +184,7 @@ describe('emails', () => { }; sendTxnEmail(mailingInfo, emailType); - expect(request.post).not.to.be.called; + expect(got.post).not.to.be.called; }); it('uses getUserInfo in case of user data', () => { @@ -210,8 +195,9 @@ describe('emails', () => { let mailingInfo = getUser(); sendTxnEmail(mailingInfo, emailType); - expect(request.post).to.be.calledWith(sinon.match({ - json: { + expect(got.post).to.be.calledWith('undefined/job', sinon.match({ + json: true, + body: { data: { emailType: sinon.match.same(emailType), to: sinon.match(val => val[0]._id === mailingInfo._id), @@ -232,8 +218,9 @@ describe('emails', () => { let variables = [1, 2, 3]; sendTxnEmail(mailingInfo, emailType, variables); - expect(request.post).to.be.calledWith(sinon.match({ - json: { + expect(got.post).to.be.calledWith('undefined/job', sinon.match({ + json: true, + body: { data: { variables: sinon.match((value) => { return value[0].name === 'BASE_URL'; diff --git a/test/api/v3/unit/libs/payments/amazon/subscribe.test.js b/test/api/v3/unit/libs/payments/amazon/subscribe.test.js index 930c64d06a..b0ae072880 100644 --- a/test/api/v3/unit/libs/payments/amazon/subscribe.test.js +++ b/test/api/v3/unit/libs/payments/amazon/subscribe.test.js @@ -216,6 +216,9 @@ describe('Amazon Payments - Subscribe', () => { }); it('subscribes with amazon', async () => { + user.guilds.push(groupId); + await user.save(); + await amzLib.subscribe({ billingAgreementId, sub, @@ -241,8 +244,13 @@ describe('Amazon Payments - Subscribe', () => { user = new User(); user.guilds.push(groupId); await user.save(); - group.memberCount = 2; - await group.save(); + + // Add existing users + user = new User(); + user.guilds.push(groupId); + await user.save(); + + // Set expected amount sub.key = 'group_monthly'; sub.price = 9; amount = 12; diff --git a/test/api/v3/unit/libs/payments/stripe/checkout-subscription.test.js b/test/api/v3/unit/libs/payments/stripe/checkout-subscription.test.js index 7271b1dbd3..411acbedaf 100644 --- a/test/api/v3/unit/libs/payments/stripe/checkout-subscription.test.js +++ b/test/api/v3/unit/libs/payments/stripe/checkout-subscription.test.js @@ -227,6 +227,11 @@ describe('checkout with subscription', () => { sub = data.sub; groupId = group._id; email = 'test@test.com'; + + // Add user to group + user.guilds.push(groupId); + await user.save(); + headers = {}; await stripePayments.checkout({ @@ -267,9 +272,15 @@ describe('checkout with subscription', () => { groupId = group._id; email = 'test@test.com'; headers = {}; + + // Add user to group + user.guilds.push(groupId); + await user.save(); + user = new User(); user.guilds.push(groupId); await user.save(); + group.memberCount = 2; await group.save(); diff --git a/test/api/v3/unit/libs/webhooks.test.js b/test/api/v3/unit/libs/webhooks.test.js index 49c3d0e24f..467ce9bd8a 100644 --- a/test/api/v3/unit/libs/webhooks.test.js +++ b/test/api/v3/unit/libs/webhooks.test.js @@ -1,16 +1,17 @@ -import request from 'request'; +import got from 'got'; import { WebhookSender, taskScoredWebhook, groupChatReceivedWebhook, taskActivityWebhook, } from '../../../../../website/server/libs/webhook'; +import { defer } from '../../../../helpers/api-unit.helper'; describe('webhooks', () => { let webhooks; beforeEach(() => { - sandbox.stub(request, 'post'); + sandbox.stub(got, 'post').returns(defer().promise); webhooks = [{ id: 'taskActivity', @@ -59,8 +60,9 @@ describe('webhooks', () => { sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body); expect(WebhookSender.defaultTransformData).to.be.calledOnce; - expect(request.post).to.be.calledOnce; - expect(request.post).to.be.calledWithMatch({ + expect(got.post).to.be.calledOnce; + expect(got.post).to.be.calledWithMatch('http://custom-url.com', { + json: true, body, }); }); @@ -81,8 +83,9 @@ describe('webhooks', () => { sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body); expect(WebhookSender.defaultTransformData).to.not.be.called; - expect(request.post).to.be.calledOnce; - expect(request.post).to.be.calledWithMatch({ + expect(got.post).to.be.calledOnce; + expect(got.post).to.be.calledWithMatch('http://custom-url.com', { + json: true, body: { foo: 'bar', baz: 'biz', @@ -117,7 +120,7 @@ describe('webhooks', () => { sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body); expect(WebhookSender.defaultWebhookFilter).to.not.be.called; - expect(request.post).to.not.be.called; + expect(got.post).to.not.be.called; }); it('can pass in a webhook filter function that filters on data', () => { @@ -136,10 +139,8 @@ describe('webhooks', () => { { id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }}, ], body); - expect(request.post).to.be.calledOnce; - expect(request.post).to.be.calledWithMatch({ - url: 'http://custom-url.com', - }); + expect(got.post).to.be.calledOnce; + expect(got.post).to.be.calledWithMatch('http://custom-url.com'); }); it('ignores disabled webhooks', () => { @@ -151,7 +152,7 @@ describe('webhooks', () => { sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body); - expect(request.post).to.not.be.called; + expect(got.post).to.not.be.called; }); it('ignores webhooks with invalid urls', () => { @@ -163,7 +164,7 @@ describe('webhooks', () => { sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body); - expect(request.post).to.not.be.called; + expect(got.post).to.not.be.called; }); it('ignores webhooks of other types', () => { @@ -178,9 +179,8 @@ describe('webhooks', () => { { id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'}, ], body); - expect(request.post).to.be.calledOnce; - expect(request.post).to.be.calledWithMatch({ - url: 'http://custom-url.com', + expect(got.post).to.be.calledOnce; + expect(got.post).to.be.calledWithMatch('http://custom-url.com', { body, json: true, }); @@ -198,14 +198,12 @@ describe('webhooks', () => { { id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'}, ], body); - expect(request.post).to.be.calledTwice; - expect(request.post).to.be.calledWithMatch({ - url: 'http://custom-url.com', + expect(got.post).to.be.calledTwice; + expect(got.post).to.be.calledWithMatch('http://custom-url.com', { body, json: true, }); - expect(request.post).to.be.calledWithMatch({ - url: 'http://other-url.com', + expect(got.post).to.be.calledWithMatch('http://other-url.com', { body, json: true, }); @@ -252,8 +250,9 @@ describe('webhooks', () => { it('sends task and stats data', () => { taskScoredWebhook.send(webhooks, data); - expect(request.post).to.be.calledOnce; - expect(request.post).to.be.calledWithMatch({ + expect(got.post).to.be.calledOnce; + expect(got.post).to.be.calledWithMatch(webhooks[0].url, { + json: true, body: { type: 'scored', user: { @@ -283,7 +282,7 @@ describe('webhooks', () => { taskScoredWebhook.send(webhooks, data); - expect(request.post).to.not.be.called; + expect(got.post).to.not.be.called; }); }); @@ -304,8 +303,9 @@ describe('webhooks', () => { taskActivityWebhook.send(webhooks, data); - expect(request.post).to.be.calledOnce; - expect(request.post).to.be.calledWithMatch({ + expect(got.post).to.be.calledOnce; + expect(got.post).to.be.calledWithMatch(webhooks[0].url, { + json: true, body: { type, task: data.task, @@ -319,7 +319,7 @@ describe('webhooks', () => { taskActivityWebhook.send(webhooks, data); - expect(request.post).to.not.be.called; + expect(got.post).to.not.be.called; }); }); }); @@ -340,8 +340,9 @@ describe('webhooks', () => { groupChatReceivedWebhook.send(webhooks, data); - expect(request.post).to.be.calledOnce; - expect(request.post).to.be.calledWithMatch({ + expect(got.post).to.be.calledOnce; + expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, { + json: true, body: { group: { id: 'group-id', @@ -370,7 +371,7 @@ describe('webhooks', () => { groupChatReceivedWebhook.send(webhooks, data); - expect(request.post).to.not.be.called; + expect(got.post).to.not.be.called; }); }); }); diff --git a/test/api/v3/unit/models/user.test.js b/test/api/v3/unit/models/user.test.js index 63d99193b6..14959bd823 100644 --- a/test/api/v3/unit/models/user.test.js +++ b/test/api/v3/unit/models/user.test.js @@ -352,7 +352,12 @@ describe('User Model', () => { context('manage unallocated stats points notifications', () => { it('doesn\'t add a notification if there are no points to allocate', async () => { let user = new User(); + + user.flags.classSelected = true; + user.preferences.disableClasses = false; + user.stats.class = 'warrior'; user = await user.save(); // necessary for user.isSelected to work correctly + const oldNotificationsCount = user.notifications.length; user.stats.points = 0; @@ -363,6 +368,10 @@ describe('User Model', () => { it('removes a notification if there are no more points to allocate', async () => { let user = new User(); + + user.flags.classSelected = true; + user.preferences.disableClasses = false; + user.stats.class = 'warrior'; user.stats.points = 9; user = await user.save(); // necessary for user.isSelected to work correctly @@ -377,6 +386,9 @@ describe('User Model', () => { it('adds a notification if there are points to allocate', async () => { let user = new User(); + user.flags.classSelected = true; + user.preferences.disableClasses = false; + user.stats.class = 'warrior'; user = await user.save(); // necessary for user.isSelected to work correctly const oldNotificationsCount = user.notifications.length; @@ -391,6 +403,9 @@ describe('User Model', () => { it('adds a notification if the points to allocate have changed', async () => { let user = new User(); user.stats.points = 9; + user.flags.classSelected = true; + user.preferences.disableClasses = false; + user.stats.class = 'warrior'; user = await user.save(); // necessary for user.isSelected to work correctly const oldNotificationsCount = user.notifications.length; @@ -406,6 +421,37 @@ describe('User Model', () => { expect(user.notifications[0].data.points).to.equal(11); expect(user.notifications[0].id).to.not.equal(oldNotificationsUUID); }); + + it('does not add a notification if the user has disabled classes', async () => { + let user = new User(); + user.stats.points = 9; + user.flags.classSelected = true; + user.preferences.disableClasses = true; + user.stats.class = 'warrior'; + user = await user.save(); // necessary for user.isSelected to work correctly + + const oldNotificationsCount = user.notifications.length; + + user.stats.points = 9; + user = await user.save(); + + expect(user.notifications.length).to.equal(oldNotificationsCount); + }); + + it('does not add a notification if the user has not selected a class', async () => { + let user = new User(); + user.stats.points = 9; + user.flags.classSelected = false; + user.stats.class = 'warrior'; + user = await user.save(); // necessary for user.isSelected to work correctly + + const oldNotificationsCount = user.notifications.length; + + user.stats.points = 9; + user = await user.save(); + + expect(user.notifications.length).to.equal(oldNotificationsCount); + }); }); }); diff --git a/test/helpers/api-unit.helper.js b/test/helpers/api-unit.helper.js index ebfea46dad..d32423dfb9 100644 --- a/test/helpers/api-unit.helper.js +++ b/test/helpers/api-unit.helper.js @@ -108,3 +108,19 @@ export function generateDaily (user) { return task; } + +export function defer () { + let resolve; + let reject; + + let promise = new Promise((resolveParam, rejectParam) => { + resolve = resolveParam; + reject = rejectParam; + }); + + return { + resolve, + reject, + promise, + }; +} diff --git a/website/client/components/inventory/equipment/index.vue b/website/client/components/inventory/equipment/index.vue index f676f7ccf0..6caa7f3fcd 100644 --- a/website/client/components/inventory/equipment/index.vue +++ b/website/client/components/inventory/equipment/index.vue @@ -13,8 +13,8 @@ :key="group.key", ) .custom-control.custom-checkbox - input.custom-control-input(type="checkbox", v-model="viewOptions[group.key].selected", :id="group.key") - label.custom-control-label(v-once, :for="group.key") {{ group.label }} + input.custom-control-input(type="checkbox", v-model="viewOptions[group.key].selected", :id="groupBy + group.key") + label.custom-control-label(v-once, :for="groupBy + group.key") {{ group.label }} .standard-page .clearfix diff --git a/website/client/components/userMenu/inbox.vue b/website/client/components/userMenu/inbox.vue index df63939f05..58428090c7 100644 --- a/website/client/components/userMenu/inbox.vue +++ b/website/client/components/userMenu/inbox.vue @@ -160,6 +160,7 @@ import Vue from 'vue'; import moment from 'moment'; import filter from 'lodash/filter'; import sortBy from 'lodash/sortBy'; +import groupBy from 'lodash/groupBy'; import { mapState } from 'client/libs/store'; import styleHelper from 'client/mixins/styleHelper'; @@ -220,45 +221,53 @@ export default { computed: { ...mapState({user: 'user.data'}), conversations () { - let conversations = {}; - for (let messageId in this.user.inbox.messages) { - let message = this.user.inbox.messages[messageId]; - let userId = message.uuid; + const inboxGroup = groupBy(this.user.inbox.messages, 'uuid'); - if (!conversations[userId]) { - conversations[userId] = { - name: message.user, - key: userId, - messages: [], - }; + // Create conversation objects + const convos = []; + for (let key in inboxGroup) { + const convoSorted = sortBy(inboxGroup[key], [(o) => { + return o.timestamp; + }]); + + // Fix poor inbox chat models + const newChatModels = convoSorted.map(chat => { + let newChat = Object.assign({}, chat); + if (newChat.sent) { + newChat.toUUID = newChat.uuid; + newChat.toUser = newChat.user; + newChat.uuid = this.user._id; + newChat.user = this.user.profile.name; + newChat.contributor = this.user.contributor; + newChat.backer = this.user.backer; + } + return newChat; + }); + + const recentMessage = newChatModels[newChatModels.length - 1]; + + // Special case where we have placeholder message because conversations are just grouped messages for now + if (!recentMessage.text) { + newChatModels.splice(newChatModels.length - 1, 1); } - let newMessage = { - text: message.text, - timestamp: message.timestamp, - user: message.user, - uuid: message.uuid, - id: message.id, - contributor: message.contributor, + const convoModel = { + name: recentMessage.toUser ? recentMessage.toUser : recentMessage.user, // Handles case where from user sent the only message or the to user sent the only message + key: recentMessage.toUUID ? recentMessage.toUUID : recentMessage.uuid, + messages: newChatModels, + lastMessageText: recentMessage.text, + date: recentMessage.timestamp, }; - if (message.sent) { - newMessage.user = this.user.profile.name; - newMessage.uuid = this.user._id; - newMessage.contributor = this.user.contributor; - } - - if (newMessage.text) conversations[userId].messages.push(newMessage); - conversations[userId].lastMessageText = message.text; - conversations[userId].date = message.timestamp; + convos.push(convoModel); } - conversations = sortBy(conversations, [(o) => { + // Sort models by most recent + const conversations = sortBy(convos, [(o) => { return moment(o.date).toDate(); }]); - conversations = conversations.reverse(); - return conversations; + return conversations.reverse(); }, filtersConversations () { if (!this.search) return this.conversations; diff --git a/website/client/store/actions/user.js b/website/client/store/actions/user.js index 07d7d599a6..ecbd5683a5 100644 --- a/website/client/store/actions/user.js +++ b/website/client/store/actions/user.js @@ -21,7 +21,7 @@ export function fetch (store, options = {}) { // eslint-disable-line no-shadow export async function set (store, changes) { const user = store.state.user.data; - for (let key in changes) { // eslint-disable-line no-await-in-loop + for (let key in changes) { if (key === 'tags') { // Keep challenge and group tags const oldTags = user.tags.filter(t => { @@ -31,7 +31,7 @@ export async function set (store, changes) { user.tags = changes[key].concat(oldTags); // Remove deleted tags from tasks - const userTasksByType = (await store.dispatch('tasks:fetchUserTasks')).data; + const userTasksByType = (await store.dispatch('tasks:fetchUserTasks')).data; // eslint-disable-line no-await-in-loop Object.keys(userTasksByType).forEach(taskType => { userTasksByType[taskType].forEach(task => { @@ -119,7 +119,8 @@ export function openMysteryItem () { return axios.post('/api/v3/user/open-mystery-item'); } -export function newStuffLater () { +export function newStuffLater (store) { + store.state.user.data.flags.newStuff = false; return axios.post('/api/v3/news/tell-me-later'); } diff --git a/website/common/locales/en/loadingScreenTips.json b/website/common/locales/en/loadingScreenTips.json index ec50ad2ee1..cbbffb7501 100644 --- a/website/common/locales/en/loadingScreenTips.json +++ b/website/common/locales/en/loadingScreenTips.json @@ -12,7 +12,7 @@ "tip10": "You can win gems by competing in Challenges. New ones are added every day!", "tip11": "Having more than four Party members increases accountability!", "tip12": "Add checklists to your To-Dos to multiply your rewards!", - "tip13": "Click “Filters” on your task page to make an unwieldy task list very manageable!", + "tip13": "Click “Tags” on your task page to make an unwieldy task list very manageable!", "tip14": "You can add headers or inspirational quotes to your list as Habits with no (+/-).", "tip15": "Complete all the Masterclasser Quest-lines to learn about Habitica’s secret lore.", "tip16": "Click the link to the Data Display Tool in the footer for valuable insights on your progress.", diff --git a/website/common/locales/en/tasks.json b/website/common/locales/en/tasks.json index 0e7b7fda6e..df45b1b9aa 100644 --- a/website/common/locales/en/tasks.json +++ b/website/common/locales/en/tasks.json @@ -149,6 +149,8 @@ "taskAliasAlreadyUsed": "Task alias already used on another task.", "taskNotFound": "Task not found.", "invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".", + "invalidTasksType": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\".", + "invalidTasksTypeExtra": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\", \"completedTodos\".", "cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.", "checklistOnlyDailyTodo": "Checklists are supported only on Dailies and To-Dos", "checklistItemNotFound": "No checklist item was found with given id.", diff --git a/website/server/controllers/api-v3/auth.js b/website/server/controllers/api-v3/auth.js index ddb4ccbfcc..542e77b081 100644 --- a/website/server/controllers/api-v3/auth.js +++ b/website/server/controllers/api-v3/auth.js @@ -247,7 +247,7 @@ api.loginLocal = { let username = req.body.username; let password = req.body.password; - if (validator.isEmail(username)) { + if (validator.isEmail(String(username))) { login = {'auth.local.email': username.toLowerCase()}; // Emails are stored lowercase } else { login = {'auth.local.username': username}; @@ -410,7 +410,7 @@ api.pusherAuth = { } resourceId = resourceId.join('-'); // the split at the beginning had split resourceId too - if (!validator.isUUID(resourceId)) { + if (!validator.isUUID(String(resourceId))) { throw new BadRequest('Invalid Pusher resource id, must be a UUID.'); } diff --git a/website/server/controllers/api-v3/tasks.js b/website/server/controllers/api-v3/tasks.js index 0e46bb10e7..8b9672afd0 100644 --- a/website/server/controllers/api-v3/tasks.js +++ b/website/server/controllers/api-v3/tasks.js @@ -287,7 +287,7 @@ api.getUserTasks = { async handler (req, res) { let types = Tasks.tasksTypes.map(type => `${type}s`); types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future - req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types); + req.checkQuery('type', res.t('invalidTasksTypeExtra')).optional().isIn(types); let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; @@ -325,7 +325,7 @@ api.getChallengeTasks = { async handler (req, res) { req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID(); let types = Tasks.tasksTypes.map(type => `${type}s`); - req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types); + req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types); let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; diff --git a/website/server/controllers/api-v3/tasks/groups.js b/website/server/controllers/api-v3/tasks/groups.js index 62ebb403ee..449a0e39b0 100644 --- a/website/server/controllers/api-v3/tasks/groups.js +++ b/website/server/controllers/api-v3/tasks/groups.js @@ -86,7 +86,7 @@ api.getGroupTasks = { middlewares: [authWithHeaders()], async handler (req, res) { req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID(); - req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types); + req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types); let validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; diff --git a/website/server/controllers/api-v3/user.js b/website/server/controllers/api-v3/user.js index 70721cfa54..ac2e82722d 100644 --- a/website/server/controllers/api-v3/user.js +++ b/website/server/controllers/api-v3/user.js @@ -1,16 +1,14 @@ import { authWithHeaders } from '../../middlewares/auth'; import common from '../../../common'; import { - NotFound, BadRequest, NotAuthorized, } from '../../libs/errors'; -import * as Tasks from '../../models/task'; import { basicFields as basicGroupFields, model as Group, } from '../../models/group'; -import { model as User } from '../../models/user'; +import * as Tasks from '../../models/task'; import Bluebird from 'bluebird'; import _ from 'lodash'; import * as passwordUtils from '../../libs/password'; @@ -18,6 +16,7 @@ import { getUserInfo, sendTxn as txnEmail, } from '../../libs/email'; +import Queue from '../../libs/queue'; import nconf from 'nconf'; import get from 'lodash/get'; @@ -434,6 +433,8 @@ api.deleteUser = { ]); } + if (feedback) Queue.sendMessage({feedback, username: user.profile.name}, user._id); + res.analytics.track('account delete', { uuid: user._id, hitType: 'event', @@ -524,224 +525,6 @@ api.getUserAnonymized = { }, }; -const partyMembersFields = 'profile.name stats achievements items.special'; - -async function castTaskSpell (res, req, targetId, user, spell) { - if (!targetId) throw new BadRequest(res.t('targetIdUUID')); - - const task = await Tasks.Task.findOne({ - _id: targetId, - userId: user._id, - }).exec(); - if (!task) throw new NotFound(res.t('taskNotFound')); - if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast')); - if (task.group.id) throw new BadRequest(res.t('groupTasksNoCast')); - - spell.cast(user, task, req); - - const results = await Bluebird.all([ - user.save(), - task.save(), - ]); - - return results; -} - -async function castMultiTaskSpell (req, user, spell) { - const tasks = await Tasks.Task.find({ - userId: user._id, - ...Tasks.taskIsGroupOrChallengeQuery, - }).exec(); - - spell.cast(user, tasks, req); - - const toSave = tasks - .filter(t => t.isModified()) - .map(t => t.save()); - toSave.unshift(user.save()); - const saved = await Bluebird.all(toSave); - - const response = { - tasks: saved, - user, - }; - - return response; -} - -async function castSelfSpell (req, user, spell) { - spell.cast(user, null, req); - await user.save(); -} - -async function castPartySpell (req, party, partyMembers, user, spell) { - if (!party) { - partyMembers = [user]; // Act as solo party - } else { - partyMembers = await User - .find({ - 'party._id': party._id, - _id: { $ne: user._id }, // add separately - }) - // .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save - // default values for non-selected fields and pre('save') will mess up thinking some values are missing - .exec(); - - partyMembers.unshift(user); - } - - spell.cast(user, partyMembers, req); - await Bluebird.all(partyMembers.map(m => m.save())); - - return partyMembers; -} - -async function castUserSpell (res, req, party, partyMembers, targetId, user, spell) { - if (!party && (!targetId || user._id === targetId)) { - partyMembers = user; - } else { - if (!targetId) throw new BadRequest(res.t('targetIdUUID')); - if (!party) throw new NotFound(res.t('partyNotFound')); - partyMembers = await User - .findOne({_id: targetId, 'party._id': party._id}) - // .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save - // default values for non-selected fields and pre('save') will mess up thinking some values are missing - .exec(); - } - - if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId})); - - spell.cast(user, partyMembers, req); - - if (partyMembers !== user) { - await Bluebird.all([ - user.save(), - partyMembers.save(), - ]); - } else { - await partyMembers.save(); // partyMembers is user - } - - return partyMembers; -} - -/** - * @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target - * @apiName UserCast - * @apiGroup User - * - - * @apiParam (Path) {String=fireball, mpheal, earth, frost, smash, defensiveStance, valorousPresence, intimidate, pickPocket, backStab, toolsOfTrade, stealth, heal, protectAura, brightness, healAll} spellId The skill to cast. - * @apiParam (Query) {UUID} targetId Query parameter, necessary if the spell is cast on a party member or task. Not used if the spell is case on the user or the user's current party. - * @apiParamExample {json} Query example: - * Cast "Pickpocket" on a task: - * https://habitica.com/api/v3/user/class/cast/pickPocket?targetId=fd427623... - * - * Cast "Tools of the Trade" on the party: - * https://habitica.com/api/v3/user/class/cast/toolsOfTrade - * - * @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned. - * - * @apiDescription Skill Key to Name Mapping - * Mage - * fireball: "Burst of Flames" - * mpheal: "Ethereal Surge" - * earth: "Earthquake" - * frost: "Chilling Frost" - * - * Warrior - * smash: "Brutal Smash" - * defensiveStance: "Defensive Stance" - * valorousPresence: "Valorous Presence" - * intimidate: "Intimidating Gaze" - * - * Rogue - * pickPocket: "Pickpocket" - * backStab: "Backstab" - * toolsOfTrade: "Tools of the Trade" - * stealth: "Stealth" - * - * Healer - * heal: "Healing Light" - * protectAura: "Protective Aura" - * brightness: "Searing Brightness" - * healAll: "Blessing" - * - * @apiError (400) {NotAuthorized} Not enough mana. - * @apiUse TaskNotFound - * @apiUse PartyNotFound - * @apiUse UserNotFound - */ -api.castSpell = { - method: 'POST', - middlewares: [authWithHeaders()], - url: '/user/class/cast/:spellId', - async handler (req, res) { - let user = res.locals.user; - let spellId = req.params.spellId; - let targetId = req.query.targetId; - - // optional because not required by all targetTypes, presence is checked later if necessary - req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID(); - - let reqValidationErrors = req.validationErrors(); - if (reqValidationErrors) throw reqValidationErrors; - - let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class; - let spell = common.content.spells[klass][spellId]; - - if (!spell) throw new NotFound(res.t('spellNotFound', {spellId})); - if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana')); - if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold')); - if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl})); - - let targetType = spell.target; - - if (targetType === 'task') { - const results = await castTaskSpell(res, req, targetId, user, spell); - res.respond(200, { - user: results[0], - task: results[1], - }); - } else if (targetType === 'self') { - await castSelfSpell(req, user, spell); - res.respond(200, { user }); - } else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary - const response = await castMultiTaskSpell(req, user, spell); - res.respond(200, response); - } else if (targetType === 'party' || targetType === 'user') { - const party = await Group.getGroup({groupId: 'party', user}); - // arrays of users when targetType is 'party' otherwise single users - let partyMembers; - - if (targetType === 'party') { - partyMembers = await castPartySpell(req, party, partyMembers, user, spell); - } else { - partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell); - } - - let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers]; - - // Only return some fields. - // See comment above on why we can't just select the necessary fields when querying - partyMembersRes = partyMembersRes.map(partyMember => { - return common.pickDeep(partyMember.toJSON(), common.$w(partyMembersFields)); - }); - - res.respond(200, { - partyMembers: partyMembersRes, - user, - }); - - if (party && !spell.silent) { - let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``; - party.sendChat(message); - await party.save(); - } - } - }, -}; - /** * @api {post} /api/v3/user/sleep Make the user start / stop sleeping (resting in the Inn) * @apiName UserSleep diff --git a/website/server/controllers/api-v3/user/spells.js b/website/server/controllers/api-v3/user/spells.js new file mode 100644 index 0000000000..35415137eb --- /dev/null +++ b/website/server/controllers/api-v3/user/spells.js @@ -0,0 +1,140 @@ +import { authWithHeaders } from '../../../middlewares/auth'; +import common from '../../../../common'; +import { + model as Group, +} from '../../../models/group'; +import { + NotAuthorized, + NotFound, +} from '../../../libs/errors'; +import { + castTaskSpell, + castMultiTaskSpell, + castSelfSpell, + castPartySpell, + castUserSpell, +} from '../../../libs/spells'; + +const partyMembersFields = 'profile.name stats achievements items.special'; + +let api = {}; + +/** + * @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target + * @apiName UserCast + * @apiGroup User + * + + * @apiParam (Path) {String=fireball, mpheal, earth, frost, smash, defensiveStance, valorousPresence, intimidate, pickPocket, backStab, toolsOfTrade, stealth, heal, protectAura, brightness, healAll} spellId The skill to cast. + * @apiParam (Query) {UUID} targetId Query parameter, necessary if the spell is cast on a party member or task. Not used if the spell is case on the user or the user's current party. + * @apiParamExample {json} Query example: + * Cast "Pickpocket" on a task: + * https://habitica.com/api/v3/user/class/cast/pickPocket?targetId=fd427623... + * + * Cast "Tools of the Trade" on the party: + * https://habitica.com/api/v3/user/class/cast/toolsOfTrade + * + * @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned. + * + * @apiDescription Skill Key to Name Mapping + * Mage + * fireball: "Burst of Flames" + * mpheal: "Ethereal Surge" + * earth: "Earthquake" + * frost: "Chilling Frost" + * + * Warrior + * smash: "Brutal Smash" + * defensiveStance: "Defensive Stance" + * valorousPresence: "Valorous Presence" + * intimidate: "Intimidating Gaze" + * + * Rogue + * pickPocket: "Pickpocket" + * backStab: "Backstab" + * toolsOfTrade: "Tools of the Trade" + * stealth: "Stealth" + * + * Healer + * heal: "Healing Light" + * protectAura: "Protective Aura" + * brightness: "Searing Brightness" + * healAll: "Blessing" + * + * @apiError (400) {NotAuthorized} Not enough mana. + * @apiUse TaskNotFound + * @apiUse PartyNotFound + * @apiUse UserNotFound + */ +api.castSpell = { + method: 'POST', + middlewares: [authWithHeaders()], + url: '/user/class/cast/:spellId', + async handler (req, res) { + let user = res.locals.user; + let spellId = req.params.spellId; + let targetId = req.query.targetId; + const quantity = req.body.quantity || 1; + + // optional because not required by all targetTypes, presence is checked later if necessary + req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID(); + + let reqValidationErrors = req.validationErrors(); + if (reqValidationErrors) throw reqValidationErrors; + + let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class; + let spell = common.content.spells[klass][spellId]; + + if (!spell) throw new NotFound(res.t('spellNotFound', {spellId})); + if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana')); + if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold')); + if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl})); + + let targetType = spell.target; + + if (targetType === 'task') { + const results = await castTaskSpell(res, req, targetId, user, spell, quantity); + res.respond(200, { + user: results[0], + task: results[1], + }); + } else if (targetType === 'self') { + await castSelfSpell(req, user, spell, quantity); + res.respond(200, { user }); + } else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary + const response = await castMultiTaskSpell(req, user, spell, quantity); + res.respond(200, response); + } else if (targetType === 'party' || targetType === 'user') { + const party = await Group.getGroup({groupId: 'party', user}); + // arrays of users when targetType is 'party' otherwise single users + let partyMembers; + + if (targetType === 'party') { + partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity); + } else { + partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell, quantity); + } + + let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers]; + + // Only return some fields. + // See comment above on why we can't just select the necessary fields when querying + partyMembersRes = partyMembersRes.map(partyMember => { + return common.pickDeep(partyMember.toJSON(), common.$w(partyMembersFields)); + }); + + res.respond(200, { + partyMembers: partyMembersRes, + user, + }); + + if (party && !spell.silent) { + let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``; + party.sendChat(message); + await party.save(); + } + } + }, +}; + +module.exports = api; diff --git a/website/server/controllers/top-level/dataexport.js b/website/server/controllers/top-level/dataexport.js index d8d63f01a3..9f075b27ab 100644 --- a/website/server/controllers/top-level/dataexport.js +++ b/website/server/controllers/top-level/dataexport.js @@ -144,7 +144,12 @@ api.exportUserDataXml = { 'Content-Type': 'text/xml', 'Content-disposition': 'attachment; filename=habitica-user-data.xml', }); - res.status(200).send(js2xml('user', userData)); + res.status(200).send(js2xml.parse('user', userData, { + cdataInvalidChars: true, + declaration: { + include: false, + }, + })); }, }; diff --git a/website/server/libs/amazonPayments.js b/website/server/libs/amazonPayments.js index f71185526f..75ccddcecc 100644 --- a/website/server/libs/amazonPayments.js +++ b/website/server/libs/amazonPayments.js @@ -270,10 +270,10 @@ api.subscribe = async function subscribe (options) { let priceOfSingleMember = 3; if (groupId) { - let groupFields = basicGroupFields.concat(' purchased'); - let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); - - amount = sub.price + (group.memberCount - leaderCount) * priceOfSingleMember; + const groupFields = basicGroupFields.concat(' purchased'); + const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); + const membersCount = await group.getMemberCount(); + amount = sub.price + (membersCount - leaderCount) * priceOfSingleMember; } await this.setBillingAgreementDetails({ diff --git a/website/server/libs/applePayments.js b/website/server/libs/applePayments.js index 4cb0f0e76f..e77f452a78 100644 --- a/website/server/libs/applePayments.js +++ b/website/server/libs/applePayments.js @@ -33,16 +33,16 @@ api.verifyGemPurchase = async function verifyGemPurchase (user, receipt, headers let correctReceipt = false; // Purchasing one item at a time (processing of await(s) below is sequential not parallel) - for (let index in purchaseDataList) { // eslint-disable-line no-await-in-loop + for (let index in purchaseDataList) { let purchaseData = purchaseDataList[index]; let token = purchaseData.transactionId; - let existingReceipt = await IapPurchaseReceipt.findOne({ + let existingReceipt = await IapPurchaseReceipt.findOne({ // eslint-disable-line no-await-in-loop _id: token, }).exec(); if (!existingReceipt) { - await IapPurchaseReceipt.create({ + await IapPurchaseReceipt.create({ // eslint-disable-line no-await-in-loop _id: token, consumed: true, userId: user._id, diff --git a/website/server/libs/bannedWords.js b/website/server/libs/bannedWords.js index 55aa88abd3..bdb78503b2 100644 --- a/website/server/libs/bannedWords.js +++ b/website/server/libs/bannedWords.js @@ -80,6 +80,10 @@ let bannedWords = [ 'oh, god', 'g\\*d', + 'bugger', + 'buggery', + 'buggering', + 'buggered', 'shit', 'shitty', 'shitting', diff --git a/website/server/libs/email.js b/website/server/libs/email.js index ed53e7e315..330f2d2ac4 100644 --- a/website/server/libs/email.js +++ b/website/server/libs/email.js @@ -1,8 +1,8 @@ -import { createTransport } from 'nodemailer'; +import nodemailer from 'nodemailer'; import nconf from 'nconf'; import { TAVERN_ID } from '../models/group'; import { encrypt } from './encryption'; -import request from 'request'; +import got from 'got'; import logger from './logger'; import common from '../../common'; @@ -16,7 +16,7 @@ const EMAIL_SERVER = { }; const BASE_URL = nconf.get('BASE_URL'); -let smtpTransporter = createTransport({ +let smtpTransporter = nodemailer.createTransport({ service: nconf.get('SMTP_SERVICE'), auth: { user: nconf.get('SMTP_USER'), @@ -150,13 +150,10 @@ export function sendTxn (mailingInfoArray, emailType, variables, personalVariabl } if (IS_PROD && mailingInfoArray.length > 0) { - request.post({ - url: `${EMAIL_SERVER.url}/job`, - auth: { - user: EMAIL_SERVER.auth.user, - pass: EMAIL_SERVER.auth.password, - }, - json: { + got.post(`${EMAIL_SERVER.url}/job`, { + auth: `${EMAIL_SERVER.auth.user}:${EMAIL_SERVER.auth.password}`, + json: true, + body: { type: 'email', data: { emailType, @@ -170,6 +167,6 @@ export function sendTxn (mailingInfoArray, emailType, variables, personalVariabl backoff: {delay: 10 * 60 * 1000, type: 'fixed'}, }, }, - }, (err) => logger.error(err)); + }).catch((err) => logger.error(err)); } } diff --git a/website/server/libs/pushNotifications.js b/website/server/libs/pushNotifications.js index e7071c03bc..7e31d76660 100644 --- a/website/server/libs/pushNotifications.js +++ b/website/server/libs/pushNotifications.js @@ -1,8 +1,7 @@ import _ from 'lodash'; import nconf from 'nconf'; -// TODO remove this lib and use directly the apn module +// @TODO remove this lib and use directly the apn module import pushNotify from 'push-notify'; -import apnLib from 'apn'; import logger from './logger'; import Bluebird from 'bluebird'; import { @@ -44,21 +43,9 @@ if (APN_ENABLED) { apn.on('transmissionError', (errorCode, notification, device) => { logger.error('APN transmissionError', errorCode, notification, device); }); - - let feedback = new apnLib.Feedback({ - key, - cert, - batchFeedback: true, - interval: 3600, // Check for feedback once an hour - }); - - feedback.on('feedback', (devices) => { - if (devices && devices.length > 0) { - logger.info('Delivery of push notifications failed for some Apple devices.', devices); - } - }); }); } + function sendNotification (user, details = {}) { if (!user) throw new Error('User is required.'); if (user.preferences.pushNotifications.unsubscribeFromAll === true) return; diff --git a/website/server/libs/queue/index.js b/website/server/libs/queue/index.js new file mode 100644 index 0000000000..90f7cfe1e8 --- /dev/null +++ b/website/server/libs/queue/index.js @@ -0,0 +1,43 @@ +import Kafka from 'node-rdkafka'; +import nconf from 'nconf'; + +const GROUP_ID = nconf.get('KAFKA:GROUP_ID'); +const CLOUDKARAFKA_BROKERS = nconf.get('KAFKA:CLOUDKARAFKA_BROKERS'); +const CLOUDKARAFKA_USERNAME = nconf.get('KAFKA:CLOUDKARAFKA_USERNAME'); +const CLOUDKARAFKA_PASSWORD = nconf.get('KAFKA:CLOUDKARAFKA_PASSWORD'); +const CLOUDKARAFKA_TOPIC_PREFIX = nconf.get('KAFKA:CLOUDKARAFKA_TOPIC_PREFIX'); + +const kafkaConf = { + 'group.id': GROUP_ID, + 'metadata.broker.list': CLOUDKARAFKA_BROKERS ? CLOUDKARAFKA_BROKERS.split(',') : '', + 'socket.keepalive.enable': true, + 'security.protocol': 'SASL_SSL', + 'sasl.mechanisms': 'SCRAM-SHA-256', + 'sasl.username': CLOUDKARAFKA_USERNAME, + 'sasl.password': CLOUDKARAFKA_PASSWORD, + debug: 'generic,broker,security', +}; + +const prefix = CLOUDKARAFKA_TOPIC_PREFIX; +const topic = `${prefix}-default`; +const producer = new Kafka.Producer(kafkaConf); + +producer.connect(); + +process.on('exit', () => { + if (producer.isConnected()) producer.disconnect(); +}); + +const api = {}; + +api.sendMessage = function sendMessage (message, key) { + if (!producer.isConnected()) return; + + try { + producer.produce(topic, -1, new Buffer(JSON.stringify(message)), key); + } catch (e) { + // @TODO: Send the to loggly? + } +}; + +module.exports = api; diff --git a/website/server/libs/spells.js b/website/server/libs/spells.js new file mode 100644 index 0000000000..ba728e3664 --- /dev/null +++ b/website/server/libs/spells.js @@ -0,0 +1,121 @@ +import Bluebird from 'bluebird'; + +import { model as User } from '../models/user'; +import * as Tasks from '../models/task'; +import { + NotFound, + BadRequest, +} from './errors'; + +// @TODO: After refactoring individual spells, move quantity to the calculations + +async function castTaskSpell (res, req, targetId, user, spell, quantity = 1) { + if (!targetId) throw new BadRequest(res.t('targetIdUUID')); + + const task = await Tasks.Task.findOne({ + _id: targetId, + userId: user._id, + }).exec(); + if (!task) throw new NotFound(res.t('taskNotFound')); + if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast')); + if (task.group.id) throw new BadRequest(res.t('groupTasksNoCast')); + + for (let i = 0; i < quantity; i += 1) { + spell.cast(user, task, req); + } + + const results = await Bluebird.all([ + user.save(), + task.save(), + ]); + + return results; +} + +async function castMultiTaskSpell (req, user, spell, quantity = 1) { + const tasks = await Tasks.Task.find({ + userId: user._id, + ...Tasks.taskIsGroupOrChallengeQuery, + }).exec(); + + for (let i = 0; i < quantity; i += 1) { + spell.cast(user, tasks, req); + } + + const toSave = tasks + .filter(t => t.isModified()) + .map(t => t.save()); + toSave.unshift(user.save()); + const saved = await Bluebird.all(toSave); + + const response = { + tasks: saved, + user, + }; + + return response; +} + +async function castSelfSpell (req, user, spell, quantity = 1) { + for (let i = 0; i < quantity; i += 1) { + spell.cast(user, null, req); + } + await user.save(); +} + +async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) { + if (!party) { + partyMembers = [user]; // Act as solo party + } else { + partyMembers = await User + .find({ + 'party._id': party._id, + _id: { $ne: user._id }, // add separately + }) + // .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save + // default values for non-selected fields and pre('save') will mess up thinking some values are missing + .exec(); + + partyMembers.unshift(user); + } + + for (let i = 0; i < quantity; i += 1) { + spell.cast(user, partyMembers, req); + } + await Bluebird.all(partyMembers.map(m => m.save())); + + return partyMembers; +} + +async function castUserSpell (res, req, party, partyMembers, targetId, user, spell, quantity = 1) { + if (!party && (!targetId || user._id === targetId)) { + partyMembers = user; + } else { + if (!targetId) throw new BadRequest(res.t('targetIdUUID')); + if (!party) throw new NotFound(res.t('partyNotFound')); + partyMembers = await User + .findOne({_id: targetId, 'party._id': party._id}) + // .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save + // default values for non-selected fields and pre('save') will mess up thinking some values are missing + .exec(); + } + + if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId})); + + for (let i = 0; i < quantity; i += 1) { + spell.cast(user, partyMembers, req); + } + + if (partyMembers !== user) { + await Bluebird.all([ + user.save(), + partyMembers.save(), + ]); + } else { + await partyMembers.save(); // partyMembers is user + } + + return partyMembers; +} + +export {castTaskSpell, castMultiTaskSpell, castSelfSpell, castPartySpell, castUserSpell}; diff --git a/website/server/libs/stripePayments.js b/website/server/libs/stripePayments.js index 63b2041ca3..7cf6c0b75e 100644 --- a/website/server/libs/stripePayments.js +++ b/website/server/libs/stripePayments.js @@ -97,9 +97,10 @@ api.checkout = async function checkout (options, stripeInc) { if (groupId) { customerObject.quantity = sub.quantity; - let groupFields = basicGroupFields.concat(' purchased'); - let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); - customerObject.quantity = group.memberCount + sub.quantity - 1; + const groupFields = basicGroupFields.concat(' purchased'); + const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields}); + const membersCount = await group.getMemberCount(); + customerObject.quantity = membersCount + sub.quantity - 1; } response = await stripeApi.customers.create(customerObject); diff --git a/website/server/libs/webhook.js b/website/server/libs/webhook.js index bbe2ba8f45..4265abf9ba 100644 --- a/website/server/libs/webhook.js +++ b/website/server/libs/webhook.js @@ -1,21 +1,21 @@ -import { post } from 'request'; +import got from 'got'; import { isURL } from 'validator'; import logger from './logger'; +import nconf from 'nconf'; + +const IS_PRODUCTION = nconf.get('IS_PROD'); function sendWebhook (url, body) { - post({ - url, + got.post(url, { body, json: true, - }, (err) => { - if (err) { - logger.error(err); - } - }); + }).catch(err => logger.error(err)); } function isValidWebhook (hook) { - return hook.enabled && isURL(hook.url); + return hook.enabled && isURL(hook.url, { + require_tld: IS_PRODUCTION ? true : false, // eslint-disable-line camelcase + }); } export class WebhookSender { diff --git a/website/server/models/group.js b/website/server/models/group.js index 1870a9c766..67f3fe39a7 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -442,6 +442,16 @@ schema.methods.isMember = function isGroupMember (user) { } }; +schema.methods.getMemberCount = async function getMemberCount () { + let query = { guilds: this._id }; + + if (this.type === 'party') { + query = { 'party._id': this._id }; + } + + return await User.count(query).exec(); +}; + export function chatDefaults (msg, user) { let message = { id: shared.uuid(), diff --git a/website/server/models/task.js b/website/server/models/task.js index ee3bdc538e..866d3b5c68 100644 --- a/website/server/models/task.js +++ b/website/server/models/task.js @@ -144,7 +144,7 @@ TaskSchema.statics.findByIdOrAlias = async function findByIdOrAlias (identifier, let query = _.cloneDeep(additionalQueries); - if (validator.isUUID(identifier)) { + if (validator.isUUID(String(identifier))) { query._id = identifier; } else { query.userId = userId; diff --git a/website/server/models/user/hooks.js b/website/server/models/user/hooks.js index 022137ef43..3667fb5860 100644 --- a/website/server/models/user/hooks.js +++ b/website/server/models/user/hooks.js @@ -253,8 +253,9 @@ schema.pre('save', true, function preSaveUser (next, done) { } // Manage unallocated stats points notifications - if (this.isSelected('stats') && this.isSelected('notifications')) { + if (this.isSelected('stats') && this.isSelected('notifications') && this.isSelected('flags') && this.isSelected('preferences')) { const pointsToAllocate = this.stats.points; + const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses; // Sometimes there can be more than 1 notification const existingNotifications = this.notifications.filter(notification => { @@ -271,7 +272,7 @@ schema.pre('save', true, function preSaveUser (next, done) { let notificationsToRemove = outdatedNotification ? existingNotificationsLength : existingNotificationsLength - 1; // If there are points to allocate and the notification is outdated, add a new notifications - if (pointsToAllocate > 0 && outdatedNotification) { + if (pointsToAllocate > 0 && !classNotEnabled && outdatedNotification) { this.addNotification('UNALLOCATED_STATS_POINTS', { points: pointsToAllocate }); } diff --git a/website/server/models/webhook.js b/website/server/models/webhook.js index 19522bd491..7398592080 100644 --- a/website/server/models/webhook.js +++ b/website/server/models/webhook.js @@ -5,7 +5,9 @@ import shared from '../../common'; import {v4 as uuid} from 'uuid'; import _ from 'lodash'; import { BadRequest } from '../libs/errors'; +import nconf from 'nconf'; +const IS_PRODUCTION = nconf.get('IS_PROD'); const Schema = mongoose.Schema; const TASK_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({ @@ -36,7 +38,11 @@ export let schema = new Schema({ url: { type: String, required: true, - validate: [validator.isURL, shared.i18n.t('invalidUrl')], + validate: [(v) => { + return validator.isURL(v, { + require_tld: IS_PRODUCTION ? true : false, // eslint-disable-line camelcase + }); + }, shared.i18n.t('invalidUrl')], }, enabled: { type: Boolean, required: true, default: true }, options: { @@ -72,7 +78,7 @@ schema.methods.formatOptions = function formatOptions (res) { } else if (this.type === 'groupChatReceived') { this.options = _.pick(this.options, 'groupId'); - if (!validator.isUUID(this.options.groupId)) { + if (!validator.isUUID(String(this.options.groupId))) { throw new BadRequest(res.t('groupIdRequired')); } }