Merge branch 'develop' into release

This commit is contained in:
SabreCat 2017-05-08 23:14:45 +00:00
commit f1de7c02e9
58 changed files with 1587 additions and 208 deletions

View file

@ -97,5 +97,9 @@
"COMMUNITY_MANAGER_EMAIL" : "leslie@habitica.com",
"TECH_ASSISTANCE_EMAIL" : "admin@habitica.com",
"PRESS_ENQUIRY_EMAIL" : "leslie@habitica.com"
},
"LOGGLY" : {
"TOKEN" : "example-token",
"SUBDOMAIN" : "exmaple-subdomain"
}
}

276
npm-shrinkwrap.json generated
View file

@ -15,7 +15,7 @@
"abbrev": {
"version": "1.1.0",
"from": "abbrev@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz"
"resolved": "http://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz"
},
"accepts": {
"version": "1.3.3",
@ -25,12 +25,12 @@
"acorn": {
"version": "4.0.11",
"from": "acorn@>=4.0.3 <5.0.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.11.tgz"
"resolved": "http://registry.npmjs.org/acorn/-/acorn-4.0.11.tgz"
},
"acorn-dynamic-import": {
"version": "2.0.2",
"from": "acorn-dynamic-import@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz"
"resolved": "http://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-2.0.2.tgz"
},
"acorn-globals": {
"version": "1.0.9",
@ -288,12 +288,12 @@
"apidoc": {
"version": "0.17.5",
"from": "apidoc@>=0.17.5 <0.18.0",
"resolved": "https://registry.npmjs.org/apidoc/-/apidoc-0.17.5.tgz"
"resolved": "http://registry.npmjs.org/apidoc/-/apidoc-0.17.5.tgz"
},
"apidoc-core": {
"version": "0.8.2",
"from": "apidoc-core@>=0.8.1 <0.9.0",
"resolved": "https://registry.npmjs.org/apidoc-core/-/apidoc-core-0.8.2.tgz",
"resolved": "http://registry.npmjs.org/apidoc-core/-/apidoc-core-0.8.2.tgz",
"dependencies": {
"glob": {
"version": "7.1.1",
@ -321,7 +321,7 @@
"aproba": {
"version": "1.1.1",
"from": "aproba@>=1.0.3 <2.0.0",
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz"
"resolved": "http://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz"
},
"archive-type": {
"version": "3.2.0",
@ -461,7 +461,7 @@
"astw": {
"version": "2.2.0",
"from": "astw@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/astw/-/astw-2.2.0.tgz"
"resolved": "http://registry.npmjs.org/astw/-/astw-2.2.0.tgz"
},
"async": {
"version": "1.5.2",
@ -482,7 +482,7 @@
"async-foreach": {
"version": "0.1.3",
"from": "async-foreach@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz"
"resolved": "http://registry.npmjs.org/async-foreach/-/async-foreach-0.1.3.tgz"
},
"asynckit": {
"version": "0.4.0",
@ -539,7 +539,7 @@
"aws4": {
"version": "1.6.0",
"from": "aws4@>=1.2.1 <2.0.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz"
"resolved": "http://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz"
},
"axios": {
"version": "0.15.3",
@ -569,12 +569,12 @@
"babel-helper-define-map": {
"version": "6.23.0",
"from": "babel-helper-define-map@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-helper-define-map/-/babel-helper-define-map-6.23.0.tgz"
},
"babel-helper-function-name": {
"version": "6.23.0",
"from": "babel-helper-function-name@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-helper-function-name/-/babel-helper-function-name-6.23.0.tgz"
},
"babel-helper-get-function-arity": {
"version": "6.22.0",
@ -589,7 +589,7 @@
"babel-helper-optimise-call-expression": {
"version": "6.23.0",
"from": "babel-helper-optimise-call-expression@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.23.0.tgz"
},
"babel-helper-regex": {
"version": "6.22.0",
@ -604,12 +604,12 @@
"babel-helper-replace-supers": {
"version": "6.23.0",
"from": "babel-helper-replace-supers@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-helper-replace-supers/-/babel-helper-replace-supers-6.23.0.tgz"
},
"babel-helpers": {
"version": "6.23.0",
"from": "babel-helpers@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-helpers/-/babel-helpers-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-helpers/-/babel-helpers-6.23.0.tgz"
},
"babel-loader": {
"version": "6.4.1",
@ -619,7 +619,7 @@
"babel-messages": {
"version": "6.23.0",
"from": "babel-messages@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-messages/-/babel-messages-6.23.0.tgz"
},
"babel-plugin-check-es2015-constants": {
"version": "6.22.0",
@ -668,12 +668,12 @@
"babel-plugin-transform-es2015-block-scoping": {
"version": "6.23.0",
"from": "babel-plugin-transform-es2015-block-scoping@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.23.0.tgz"
},
"babel-plugin-transform-es2015-classes": {
"version": "6.23.0",
"from": "babel-plugin-transform-es2015-classes@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.23.0.tgz"
},
"babel-plugin-transform-es2015-computed-properties": {
"version": "6.22.0",
@ -683,7 +683,7 @@
"babel-plugin-transform-es2015-destructuring": {
"version": "6.23.0",
"from": "babel-plugin-transform-es2015-destructuring@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz"
},
"babel-plugin-transform-es2015-duplicate-keys": {
"version": "6.22.0",
@ -693,7 +693,7 @@
"babel-plugin-transform-es2015-for-of": {
"version": "6.23.0",
"from": "babel-plugin-transform-es2015-for-of@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz"
},
"babel-plugin-transform-es2015-function-name": {
"version": "6.22.0",
@ -718,7 +718,7 @@
"babel-plugin-transform-es2015-modules-systemjs": {
"version": "6.23.0",
"from": "babel-plugin-transform-es2015-modules-systemjs@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.23.0.tgz"
},
"babel-plugin-transform-es2015-modules-umd": {
"version": "6.24.0",
@ -733,7 +733,7 @@
"babel-plugin-transform-es2015-parameters": {
"version": "6.23.0",
"from": "babel-plugin-transform-es2015-parameters@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.23.0.tgz"
},
"babel-plugin-transform-es2015-shorthand-properties": {
"version": "6.22.0",
@ -758,7 +758,7 @@
"babel-plugin-transform-es2015-typeof-symbol": {
"version": "6.23.0",
"from": "babel-plugin-transform-es2015-typeof-symbol@>=6.22.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz"
},
"babel-plugin-transform-es2015-unicode-regex": {
"version": "6.22.0",
@ -768,7 +768,7 @@
"babel-plugin-transform-object-rest-spread": {
"version": "6.23.0",
"from": "babel-plugin-transform-object-rest-spread@>=6.16.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-plugin-transform-object-rest-spread/-/babel-plugin-transform-object-rest-spread-6.23.0.tgz"
},
"babel-plugin-transform-regenerator": {
"version": "6.22.0",
@ -783,7 +783,7 @@
"babel-polyfill": {
"version": "6.23.0",
"from": "babel-polyfill@>=6.6.1 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.23.0.tgz"
},
"babel-preset-es2015": {
"version": "6.24.0",
@ -798,22 +798,22 @@
"babel-runtime": {
"version": "6.23.0",
"from": "babel-runtime@>=6.11.6 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-runtime/-/babel-runtime-6.23.0.tgz"
},
"babel-template": {
"version": "6.23.0",
"from": "babel-template@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-template/-/babel-template-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-template/-/babel-template-6.23.0.tgz"
},
"babel-traverse": {
"version": "6.23.1",
"from": "babel-traverse@>=6.23.1 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-traverse/-/babel-traverse-6.23.1.tgz"
"resolved": "http://registry.npmjs.org/babel-traverse/-/babel-traverse-6.23.1.tgz"
},
"babel-types": {
"version": "6.23.0",
"from": "babel-types@>=6.23.0 <7.0.0",
"resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.23.0.tgz"
"resolved": "http://registry.npmjs.org/babel-types/-/babel-types-6.23.0.tgz"
},
"babelify": {
"version": "7.3.0",
@ -860,8 +860,8 @@
},
"basic-auth": {
"version": "1.1.0",
"from": "basic-auth@>=1.0.4 <2.0.0",
"resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz"
"from": "basic-auth@>=1.1.0 <1.2.0",
"resolved": "http://registry.npmjs.org/basic-auth/-/basic-auth-1.1.0.tgz"
},
"bcrypt": {
"version": "1.0.2",
@ -973,7 +973,7 @@
"bluebird": {
"version": "3.5.0",
"from": "bluebird@>=3.3.5 <4.0.0",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz"
"resolved": "http://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz"
},
"bn.js": {
"version": "4.11.6",
@ -983,7 +983,7 @@
"body-parser": {
"version": "1.17.1",
"from": "body-parser@>=1.15.0 <2.0.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.17.1.tgz",
"resolved": "http://registry.npmjs.org/body-parser/-/body-parser-1.17.1.tgz",
"dependencies": {
"debug": {
"version": "2.6.1",
@ -993,7 +993,7 @@
"qs": {
"version": "6.4.0",
"from": "qs@6.4.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz"
"resolved": "http://registry.npmjs.org/qs/-/qs-6.4.0.tgz"
}
}
},
@ -1010,7 +1010,7 @@
"bootstrap": {
"version": "4.0.0-alpha.6",
"from": "bootstrap@>=4.0.0-alpha.6 <5.0.0",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.0.0-alpha.6.tgz"
"resolved": "http://registry.npmjs.org/bootstrap/-/bootstrap-4.0.0-alpha.6.tgz"
},
"bower": {
"version": "1.3.12",
@ -1628,7 +1628,7 @@
"cached-path-relative": {
"version": "1.0.1",
"from": "cached-path-relative@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz"
"resolved": "http://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz"
},
"caller-path": {
"version": "0.1.0",
@ -1882,12 +1882,12 @@
"cli-cursor": {
"version": "2.1.0",
"from": "cli-cursor@>=2.1.0 <3.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz"
"resolved": "http://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz"
},
"cli-spinners": {
"version": "1.0.0",
"from": "cli-spinners@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-1.0.0.tgz"
"resolved": "http://registry.npmjs.org/cli-spinners/-/cli-spinners-1.0.0.tgz"
},
"cli-width": {
"version": "2.1.0",
@ -1915,7 +1915,7 @@
"clone-deep": {
"version": "0.2.4",
"from": "clone-deep@>=0.2.4 <0.3.0",
"resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz"
"resolved": "http://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz"
},
"clone-stats": {
"version": "0.0.1",
@ -2316,7 +2316,7 @@
"qs": {
"version": "6.3.2",
"from": "qs@>=6.3.0 <6.4.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
"resolved": "http://registry.npmjs.org/qs/-/qs-6.3.2.tgz",
"dev": true
},
"request": {
@ -2356,7 +2356,7 @@
"is-windows": {
"version": "1.0.0",
"from": "is-windows@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.0.tgz",
"resolved": "http://registry.npmjs.org/is-windows/-/is-windows-1.0.0.tgz",
"dev": true
}
}
@ -3006,7 +3006,7 @@
"detective": {
"version": "4.5.0",
"from": "detective@>=4.0.0 <5.0.0",
"resolved": "https://registry.npmjs.org/detective/-/detective-4.5.0.tgz"
"resolved": "http://registry.npmjs.org/detective/-/detective-4.5.0.tgz"
},
"di": {
"version": "0.0.1",
@ -3325,7 +3325,7 @@
"enhanced-resolve": {
"version": "3.1.0",
"from": "enhanced-resolve@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz"
"resolved": "http://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-3.1.0.tgz"
},
"ent": {
"version": "2.2.0",
@ -3346,7 +3346,7 @@
"error-ex": {
"version": "1.3.1",
"from": "error-ex@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz"
"resolved": "http://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz"
},
"es5-ext": {
"version": "0.10.15",
@ -3811,7 +3811,7 @@
},
"express-basic-auth": {
"version": "1.0.1",
"from": "express-basic-auth@>=1.0.1 <2.0.0",
"from": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.0.1.tgz"
},
"express-csv": {
@ -3859,7 +3859,7 @@
"extract-text-webpack-plugin": {
"version": "2.1.0",
"from": "extract-text-webpack-plugin@>=2.0.0-rc.3 <3.0.0",
"resolved": "https://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.0.tgz",
"resolved": "http://registry.npmjs.org/extract-text-webpack-plugin/-/extract-text-webpack-plugin-2.1.0.tgz",
"dependencies": {
"async": {
"version": "2.2.0",
@ -4029,7 +4029,7 @@
"filesize": {
"version": "3.5.6",
"from": "filesize@>=3.5.4 <4.0.0",
"resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.6.tgz",
"resolved": "http://registry.npmjs.org/filesize/-/filesize-3.5.6.tgz",
"dev": true
},
"fill-range": {
@ -4194,7 +4194,7 @@
"fs-extra": {
"version": "2.0.0",
"from": "fs-extra@>=2.0.0 <2.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-2.0.0.tgz"
"resolved": "http://registry.npmjs.org/fs-extra/-/fs-extra-2.0.0.tgz"
},
"fs-write-stream-atomic": {
"version": "1.0.10",
@ -4244,7 +4244,7 @@
"gauge": {
"version": "2.7.3",
"from": "gauge@>=2.7.1 <2.8.0",
"resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz"
"resolved": "http://registry.npmjs.org/gauge/-/gauge-2.7.3.tgz"
},
"gaze": {
"version": "0.5.2",
@ -4421,7 +4421,7 @@
"globby": {
"version": "6.1.0",
"from": "globby@>=6.1.0 <7.0.0",
"resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz",
"dependencies": {
"glob": {
"version": "7.1.1",
@ -5327,7 +5327,7 @@
"har-schema": {
"version": "1.0.5",
"from": "har-schema@>=1.0.5 <2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz"
"resolved": "http://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz"
},
"har-validator": {
"version": "2.0.6",
@ -5507,7 +5507,7 @@
"http-errors": {
"version": "1.6.1",
"from": "http-errors@>=1.6.1 <1.7.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz"
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.1.tgz"
},
"http-proxy": {
"version": "1.16.2",
@ -5524,7 +5524,7 @@
"http-proxy-middleware": {
"version": "0.17.4",
"from": "http-proxy-middleware@>=0.17.0 <0.18.0",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz",
"resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.17.4.tgz",
"dev": true,
"dependencies": {
"is-extglob": {
@ -5785,7 +5785,7 @@
"sax": {
"version": "1.2.2",
"from": "sax@>=1.2.1 <1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.2.tgz",
"resolved": "http://registry.npmjs.org/sax/-/sax-1.2.2.tgz",
"optional": true
},
"svgo": {
@ -5826,7 +5826,7 @@
"in-publish": {
"version": "2.0.0",
"from": "in-publish@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz"
"resolved": "http://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz"
},
"indent-string": {
"version": "2.1.0",
@ -6011,7 +6011,7 @@
"ip": {
"version": "1.1.5",
"from": "ip@>=1.1.2 <2.0.0",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz"
"resolved": "http://registry.npmjs.org/ip/-/ip-1.1.5.tgz"
},
"ip-regex": {
"version": "1.0.3",
@ -6172,12 +6172,12 @@
"is-plain-object": {
"version": "2.0.1",
"from": "is-plain-object@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.1.tgz",
"resolved": "http://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.1.tgz",
"dependencies": {
"isobject": {
"version": "1.0.2",
"from": "isobject@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-1.0.2.tgz"
"resolved": "http://registry.npmjs.org/isobject/-/isobject-1.0.2.tgz"
}
}
},
@ -6559,7 +6559,7 @@
"js-yaml": {
"version": "3.8.2",
"from": "js-yaml@>=3.1.0 <4.0.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.8.2.tgz",
"resolved": "http://registry.npmjs.org/js-yaml/-/js-yaml-3.8.2.tgz",
"dependencies": {
"esprima": {
"version": "3.1.3",
@ -6576,7 +6576,7 @@
"jsbn": {
"version": "0.1.1",
"from": "jsbn@>=0.1.0 <0.2.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"resolved": "http://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"optional": true
},
"jsesc": {
@ -6669,7 +6669,7 @@
"kareem": {
"version": "1.2.1",
"from": "kareem@1.2.1",
"resolved": "https://registry.npmjs.org/kareem/-/kareem-1.2.1.tgz"
"resolved": "http://registry.npmjs.org/kareem/-/kareem-1.2.1.tgz"
},
"karma": {
"version": "1.5.0",
@ -6892,7 +6892,7 @@
"karma-sinon-stub-promise": {
"version": "1.0.0",
"from": "karma-sinon-stub-promise@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/karma-sinon-stub-promise/-/karma-sinon-stub-promise-1.0.0.tgz",
"resolved": "http://registry.npmjs.org/karma-sinon-stub-promise/-/karma-sinon-stub-promise-1.0.0.tgz",
"dev": true
},
"karma-sourcemap-loader": {
@ -7350,7 +7350,7 @@
},
"lodash.clone": {
"version": "3.0.3",
"from": "lodash.clone@3.0.3",
"from": "lodash.clone@>=3.0.3 <4.0.0",
"resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-3.0.3.tgz",
"dev": true
},
@ -7361,7 +7361,7 @@
},
"lodash.create": {
"version": "3.1.1",
"from": "lodash.create@3.1.1",
"from": "lodash.create@>=3.1.1 <4.0.0",
"resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz"
},
"lodash.debounce": {
@ -7462,7 +7462,7 @@
"lodash.mergewith": {
"version": "4.6.0",
"from": "lodash.mergewith@>=4.6.0 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz"
"resolved": "http://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz"
},
"lodash.now": {
"version": "2.4.1",
@ -7488,7 +7488,7 @@
"lodash.tail": {
"version": "4.1.1",
"from": "lodash.tail@>=4.1.1 <5.0.0",
"resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz"
"resolved": "http://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz"
},
"lodash.template": {
"version": "3.6.2",
@ -7641,7 +7641,7 @@
"markdown-it": {
"version": "8.3.1",
"from": "markdown-it@>=8.2.2 <9.0.0",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.3.1.tgz"
"resolved": "http://registry.npmjs.org/markdown-it/-/markdown-it-8.3.1.tgz"
},
"markdown-it-emoji": {
"version": "1.3.0",
@ -7810,12 +7810,12 @@
"mixin-object": {
"version": "2.0.1",
"from": "mixin-object@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
"resolved": "http://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz",
"dependencies": {
"for-in": {
"version": "0.1.8",
"from": "for-in@>=0.1.3 <0.2.0",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz"
"resolved": "http://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz"
}
}
},
@ -7896,7 +7896,7 @@
"module-deps": {
"version": "4.1.1",
"from": "module-deps@>=4.0.2 <5.0.0",
"resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz"
"resolved": "http://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz"
},
"moment": {
"version": "2.18.1",
@ -7962,7 +7962,7 @@
"morgan": {
"version": "1.8.1",
"from": "morgan@>=1.7.0 <2.0.0",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.8.1.tgz",
"resolved": "http://registry.npmjs.org/morgan/-/morgan-1.8.1.tgz",
"dependencies": {
"debug": {
"version": "2.6.1",
@ -8048,7 +8048,7 @@
"muri": {
"version": "1.2.1",
"from": "muri@1.2.1",
"resolved": "https://registry.npmjs.org/muri/-/muri-1.2.1.tgz"
"resolved": "http://registry.npmjs.org/muri/-/muri-1.2.1.tgz"
},
"mute-stream": {
"version": "0.0.4",
@ -8214,7 +8214,7 @@
"node-forge": {
"version": "0.6.49",
"from": "node-forge@>=0.6.20 <0.7.0",
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.6.49.tgz"
"resolved": "http://registry.npmjs.org/node-forge/-/node-forge-0.6.49.tgz"
},
"node-gcm": {
"version": "0.14.5",
@ -8314,6 +8314,38 @@
}
}
},
"node-loggly-bulk": {
"version": "1.1.3",
"from": "node-loggly-bulk@>=1.1.0 <1.2.0",
"resolved": "http://registry.npmjs.org/node-loggly-bulk/-/node-loggly-bulk-1.1.3.tgz",
"dependencies": {
"bl": {
"version": "1.0.3",
"from": "bl@>=1.0.0 <1.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-1.0.3.tgz"
},
"node-uuid": {
"version": "1.4.8",
"from": "node-uuid@>=1.4.7 <1.5.0",
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.8.tgz"
},
"qs": {
"version": "5.2.1",
"from": "qs@>=5.2.0 <5.3.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-5.2.1.tgz"
},
"request": {
"version": "2.67.0",
"from": "request@>=2.67.0 <2.68.0",
"resolved": "https://registry.npmjs.org/request/-/request-2.67.0.tgz"
},
"tough-cookie": {
"version": "2.2.2",
"from": "tough-cookie@>=2.2.0 <2.3.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz"
}
}
},
"node-pre-gyp": {
"version": "0.6.32",
"from": "node-pre-gyp@0.6.32",
@ -8322,7 +8354,7 @@
"caseless": {
"version": "0.12.0",
"from": "caseless@>=0.12.0 <0.13.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz"
"resolved": "http://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz"
},
"form-data": {
"version": "2.1.2",
@ -8336,8 +8368,8 @@
},
"har-validator": {
"version": "4.2.1",
"from": "har-validator@>=4.2.1 <4.3.0",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz"
"from": "har-validator@>=4.2.0 <4.3.0",
"resolved": "http://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz"
},
"qs": {
"version": "6.4.0",
@ -8389,7 +8421,7 @@
"gaze": {
"version": "1.1.2",
"from": "gaze@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz"
"resolved": "http://registry.npmjs.org/gaze/-/gaze-1.1.2.tgz"
},
"glob": {
"version": "7.1.1",
@ -8399,7 +8431,7 @@
"globule": {
"version": "1.1.0",
"from": "globule@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/globule/-/globule-1.1.0.tgz"
"resolved": "http://registry.npmjs.org/globule/-/globule-1.1.0.tgz"
},
"har-validator": {
"version": "4.2.1",
@ -8697,7 +8729,7 @@
"object-hash": {
"version": "1.1.7",
"from": "object-hash@>=1.1.4 <2.0.0",
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-1.1.7.tgz",
"resolved": "http://registry.npmjs.org/object-hash/-/object-hash-1.1.7.tgz",
"dev": true
},
"object-inspect": {
@ -9030,12 +9062,12 @@
"passport": {
"version": "0.3.2",
"from": "passport@>=0.3.2 <0.4.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.3.2.tgz"
"resolved": "http://registry.npmjs.org/passport/-/passport-0.3.2.tgz"
},
"passport-facebook": {
"version": "2.1.1",
"from": "passport-facebook@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/passport-facebook/-/passport-facebook-2.1.1.tgz"
"resolved": "http://registry.npmjs.org/passport-facebook/-/passport-facebook-2.1.1.tgz"
},
"passport-google-oauth20": {
"version": "1.0.0",
@ -9148,7 +9180,7 @@
"performance-now": {
"version": "0.2.0",
"from": "performance-now@>=0.2.0 <0.3.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz"
"resolved": "http://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz"
},
"phantom-bridge": {
"version": "2.0.1",
@ -9178,7 +9210,7 @@
"qs": {
"version": "6.3.2",
"from": "qs@>=6.3.0 <6.4.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz"
"resolved": "http://registry.npmjs.org/qs/-/qs-6.3.2.tgz"
},
"request": {
"version": "2.79.0",
@ -9257,7 +9289,7 @@
"pkginfo": {
"version": "0.4.0",
"from": "pkginfo@>=0.4.0 <0.5.0",
"resolved": "https://registry.npmjs.org/pkginfo/-/pkginfo-0.4.0.tgz"
"resolved": "http://registry.npmjs.org/pkginfo/-/pkginfo-0.4.0.tgz"
},
"plur": {
"version": "2.1.2",
@ -9277,8 +9309,8 @@
},
"postcss": {
"version": "5.2.16",
"from": "postcss@>=5.2.16 <6.0.0",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-5.2.16.tgz",
"from": "postcss@>=5.2.15 <6.0.0",
"resolved": "http://registry.npmjs.org/postcss/-/postcss-5.2.16.tgz",
"dependencies": {
"supports-color": {
"version": "3.2.3",
@ -9295,12 +9327,12 @@
"postcss-colormin": {
"version": "2.2.2",
"from": "postcss-colormin@>=2.1.8 <3.0.0",
"resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz"
"resolved": "http://registry.npmjs.org/postcss-colormin/-/postcss-colormin-2.2.2.tgz"
},
"postcss-convert-values": {
"version": "2.6.1",
"from": "postcss-convert-values@>=2.3.4 <3.0.0",
"resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz"
"resolved": "http://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-2.6.1.tgz"
},
"postcss-discard-comments": {
"version": "2.0.4",
@ -9330,7 +9362,7 @@
"postcss-easy-import": {
"version": "2.0.0",
"from": "postcss-easy-import@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/postcss-easy-import/-/postcss-easy-import-2.0.0.tgz",
"resolved": "http://registry.npmjs.org/postcss-easy-import/-/postcss-easy-import-2.0.0.tgz",
"dependencies": {
"is-extglob": {
"version": "2.1.1",
@ -9352,22 +9384,22 @@
"postcss-import": {
"version": "9.1.0",
"from": "postcss-import@>=9.1.0 <10.0.0",
"resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-9.1.0.tgz"
"resolved": "http://registry.npmjs.org/postcss-import/-/postcss-import-9.1.0.tgz"
},
"postcss-load-config": {
"version": "1.2.0",
"from": "postcss-load-config@>=1.1.0 <2.0.0",
"resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz"
"resolved": "http://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz"
},
"postcss-load-options": {
"version": "1.2.0",
"from": "postcss-load-options@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz"
"resolved": "http://registry.npmjs.org/postcss-load-options/-/postcss-load-options-1.2.0.tgz"
},
"postcss-load-plugins": {
"version": "2.3.0",
"from": "postcss-load-plugins@>=2.3.0 <3.0.0",
"resolved": "https://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz"
"resolved": "http://registry.npmjs.org/postcss-load-plugins/-/postcss-load-plugins-2.3.0.tgz"
},
"postcss-merge-idents": {
"version": "2.1.7",
@ -9547,7 +9579,7 @@
"private": {
"version": "0.1.7",
"from": "private@>=0.1.6 <0.2.0",
"resolved": "https://registry.npmjs.org/private/-/private-0.1.7.tgz"
"resolved": "http://registry.npmjs.org/private/-/private-0.1.7.tgz"
},
"process": {
"version": "0.11.9",
@ -9646,7 +9678,7 @@
},
"proxy-agent": {
"version": "2.0.0",
"from": "proxy-agent@2.0.0",
"from": "proxy-agent@>=2.0.0",
"resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-2.0.0.tgz",
"dev": true,
"dependencies": {
@ -9681,7 +9713,7 @@
"pug": {
"version": "2.0.0-beta11",
"from": "pug@>=2.0.0-beta11 <3.0.0",
"resolved": "https://registry.npmjs.org/pug/-/pug-2.0.0-beta11.tgz"
"resolved": "http://registry.npmjs.org/pug/-/pug-2.0.0-beta11.tgz"
},
"pug-attrs": {
"version": "2.0.2",
@ -9725,7 +9757,7 @@
"pug-filters": {
"version": "2.1.1",
"from": "pug-filters@>=2.1.1 <3.0.0",
"resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-2.1.1.tgz",
"resolved": "http://registry.npmjs.org/pug-filters/-/pug-filters-2.1.1.tgz",
"dependencies": {
"asap": {
"version": "2.0.5",
@ -9774,7 +9806,7 @@
"pug-lexer": {
"version": "3.0.0",
"from": "pug-lexer@>=3.0.0 <4.0.0",
"resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-3.0.0.tgz",
"resolved": "http://registry.npmjs.org/pug-lexer/-/pug-lexer-3.0.0.tgz",
"dependencies": {
"character-parser": {
"version": "2.2.0",
@ -9786,12 +9818,12 @@
"pug-linker": {
"version": "2.0.2",
"from": "pug-linker@>=2.0.2 <3.0.0",
"resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-2.0.2.tgz"
"resolved": "http://registry.npmjs.org/pug-linker/-/pug-linker-2.0.2.tgz"
},
"pug-load": {
"version": "2.0.5",
"from": "pug-load@>=2.0.5 <3.0.0",
"resolved": "https://registry.npmjs.org/pug-load/-/pug-load-2.0.5.tgz"
"resolved": "http://registry.npmjs.org/pug-load/-/pug-load-2.0.5.tgz"
},
"pug-parser": {
"version": "2.0.2",
@ -9811,7 +9843,7 @@
"pug-walk": {
"version": "1.1.1",
"from": "pug-walk@>=1.1.1 <2.0.0",
"resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.1.tgz"
"resolved": "http://registry.npmjs.org/pug-walk/-/pug-walk-1.1.1.tgz"
},
"pump": {
"version": "0.3.5",
@ -9854,12 +9886,12 @@
"qs": {
"version": "6.2.3",
"from": "qs@>=6.2.0 <6.3.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.2.3.tgz"
"resolved": "http://registry.npmjs.org/qs/-/qs-6.2.3.tgz"
},
"query-string": {
"version": "4.3.2",
"from": "query-string@>=4.1.0 <5.0.0",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-4.3.2.tgz"
"resolved": "http://registry.npmjs.org/query-string/-/query-string-4.3.2.tgz"
},
"querystring": {
"version": "0.2.0",
@ -10262,7 +10294,7 @@
"restore-cursor": {
"version": "2.0.0",
"from": "restore-cursor@>=2.0.0 <3.0.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
"resolved": "http://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
"dependencies": {
"onetime": {
"version": "2.0.1",
@ -10345,7 +10377,7 @@
"sass-graph": {
"version": "2.1.2",
"from": "sass-graph@>=2.1.1 <3.0.0",
"resolved": "https://registry.npmjs.org/sass-graph/-/sass-graph-2.1.2.tgz",
"resolved": "http://registry.npmjs.org/sass-graph/-/sass-graph-2.1.2.tgz",
"dependencies": {
"cliui": {
"version": "3.2.0",
@ -10589,7 +10621,7 @@
"setprototypeof": {
"version": "1.0.3",
"from": "setprototypeof@1.0.3",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz"
"resolved": "http://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz"
},
"sha.js": {
"version": "2.4.8",
@ -10599,17 +10631,17 @@
"shallow-clone": {
"version": "0.1.2",
"from": "shallow-clone@>=0.1.2 <0.2.0",
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
"resolved": "http://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz",
"dependencies": {
"kind-of": {
"version": "2.0.1",
"from": "kind-of@>=2.0.1 <3.0.0",
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz"
"resolved": "http://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz"
},
"lazy-cache": {
"version": "0.2.7",
"from": "lazy-cache@>=0.2.3 <0.3.0",
"resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz"
"resolved": "http://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz"
}
}
},
@ -10942,7 +10974,7 @@
"sshpk": {
"version": "1.11.0",
"from": "sshpk@>=1.7.0 <2.0.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.11.0.tgz",
"resolved": "http://registry.npmjs.org/sshpk/-/sshpk-1.11.0.tgz",
"dependencies": {
"assert-plus": {
"version": "1.0.0",
@ -11048,7 +11080,7 @@
"stdout-stream": {
"version": "1.4.0",
"from": "stdout-stream@>=1.4.0 <2.0.0",
"resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz"
"resolved": "http://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.0.tgz"
},
"stream-browserify": {
"version": "2.0.1",
@ -11332,14 +11364,14 @@
"sax": {
"version": "1.2.2",
"from": "sax@>=1.2.1 <1.3.0",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.2.2.tgz"
"resolved": "http://registry.npmjs.org/sax/-/sax-1.2.2.tgz"
}
}
},
"syntax-error": {
"version": "1.3.0",
"from": "syntax-error@>=1.1.1 <2.0.0",
"resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz"
"resolved": "http://registry.npmjs.org/syntax-error/-/syntax-error-1.3.0.tgz"
},
"table": {
"version": "3.8.3",
@ -11463,7 +11495,7 @@
"tether": {
"version": "1.4.0",
"from": "tether@>=1.4.0 <2.0.0",
"resolved": "https://registry.npmjs.org/tether/-/tether-1.4.0.tgz"
"resolved": "http://registry.npmjs.org/tether/-/tether-1.4.0.tgz"
},
"text-table": {
"version": "0.2.0",
@ -11541,6 +11573,11 @@
}
}
},
"timespan": {
"version": "2.3.0",
"from": "timespan@>=2.3.0 <2.4.0",
"resolved": "http://registry.npmjs.org/timespan/-/timespan-2.3.0.tgz"
},
"tiny-lr-fork": {
"version": "0.0.5",
"from": "tiny-lr-fork@0.0.5",
@ -11592,7 +11629,7 @@
"toposort": {
"version": "1.0.3",
"from": "toposort@>=1.0.0 <2.0.0",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.3.tgz"
"resolved": "http://registry.npmjs.org/toposort/-/toposort-1.0.3.tgz"
},
"touch": {
"version": "0.0.2",
@ -11671,7 +11708,7 @@
"trim-right": {
"version": "1.0.1",
"from": "trim-right@>=1.0.1 <2.0.0",
"resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz"
"resolved": "http://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz"
},
"tryit": {
"version": "1.0.3",
@ -12314,7 +12351,7 @@
},
"watchpack": {
"version": "1.3.1",
"from": "watchpack@>=1.3.1 <2.0.0",
"from": "watchpack@>=1.2.0 <2.0.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.3.1.tgz",
"dependencies": {
"async": {
@ -12440,7 +12477,7 @@
"webpack-merge": {
"version": "2.6.1",
"from": "webpack-merge@>=2.6.1 <3.0.0",
"resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-2.6.1.tgz"
"resolved": "http://registry.npmjs.org/webpack-merge/-/webpack-merge-2.6.1.tgz"
},
"webpack-sources": {
"version": "0.1.5",
@ -12489,6 +12526,11 @@
}
}
},
"winston-loggly-bulk": {
"version": "1.4.2",
"from": "winston-loggly-bulk@>=1.4.2 <2.0.0",
"resolved": "http://registry.npmjs.org/winston-loggly-bulk/-/winston-loggly-bulk-1.4.2.tgz"
},
"with": {
"version": "4.0.3",
"from": "with@>=4.0.0 <4.1.0",

View file

@ -124,6 +124,7 @@
"webpack": "^2.2.1",
"webpack-merge": "^2.6.1",
"winston": "^2.1.0",
"winston-loggly-bulk": "^1.4.2",
"xml2js": "^0.4.4"
},
"private": true,

View file

@ -4,11 +4,17 @@ import {
sleep,
server,
} from '../../../../helpers/api-v3-integration.helper';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
TAVERN_ID,
} from '../../../../../website/server/models/group';
import { v4 as generateUUID } from 'uuid';
describe('POST /chat', () => {
let user, groupWithChat, userWithChatRevoked, member;
let user, groupWithChat, member, additionalMember;
let testMessage = 'Test Message';
let testBannedWordMessage = 'TEST_PLACEHOLDER_SWEAR_WORD_HERE';
before(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
@ -22,8 +28,8 @@ describe('POST /chat', () => {
user = groupLeader;
groupWithChat = group;
userWithChatRevoked = await members[0].update({'flags.chatRevoked': true});
member = members[0];
additionalMember = members[1];
});
it('Returns an error when no message is provided', async () => {
@ -62,6 +68,7 @@ describe('POST /chat', () => {
});
it('returns an error when chat privileges are revoked when sending a message to a public guild', async () => {
let userWithChatRevoked = await member.update({'flags.chatRevoked': true});
await expect(userWithChatRevoked.post(`/groups/${groupWithChat._id}/chat`, { message: testMessage})).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
@ -69,6 +76,96 @@ describe('POST /chat', () => {
});
});
context('banned word', () => {
it('returns an error when chat message contains a banned word in tavern', async () => {
await expect(user.post('/groups/habitrpg/chat', { message: testBannedWordMessage}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is part of a phrase', async () => {
let wordInPhrase = `phrase ${testBannedWordMessage} end`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('errors when word is surrounded by non alphabet characters', async () => {
let wordInPhrase = `_!${testBannedWordMessage}@_`;
await expect(user.post('/groups/habitrpg/chat', { message: wordInPhrase}))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('bannedWordUsed'),
});
});
it('does not error when bad word is suffix of a word', async () => {
let wordAsSuffix = `prefix${testBannedWordMessage}`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsSuffix});
expect(message.message.id).to.exist;
});
it('does not error when bad word is prefix of a word', async () => {
let wordAsPrefix = `${testBannedWordMessage}suffix`;
let message = await user.post('/groups/habitrpg/chat', { message: wordAsPrefix});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a party', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'Party',
type: 'party',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a public guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'public guild',
type: 'guild',
privacy: 'public',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
it('does not error when sending a chat message containing a banned word to a private guild', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
name: 'private guild',
type: 'guild',
privacy: 'private',
},
members: 1,
});
let message = await members[0].post(`/groups/${group._id}/chat`, { message: testBannedWordMessage});
expect(message.message.id).to.exist;
});
});
it('does not error when sending a message to a private guild with a user with revoked chat', async () => {
let { group, members } = await createAndPopulateGroup({
groupDetails: {
@ -173,4 +270,30 @@ describe('POST /chat', () => {
expect(message.message.id).to.exist;
expect(memberWithNotification.newMessages[`${group._id}`]).to.exist;
});
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++) {
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
await expect(additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage })).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupChatSpam'),
});
});
it('contributor should not receive spam alert', async () => {
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++) {
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
expect(result.message.id).to.exist;
}
});
});
});

View file

@ -0,0 +1,93 @@
import {
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
import { find } from 'lodash';
describe('POST /group/:groupId/remove-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonManager;
function findAssignedTask (memberTask) {
return memberTask.group.id === groupToUpdate._id;
}
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonManager = members[0];
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when manager does not exist', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonManager._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userIsNotManager'),
});
});
it('allows a leader to remove managers', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
it('removes group approval notifications from a manager that is removed', async () => {
await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
let task = await leader.post(`/tasks/group/${groupToUpdate._id}`, {
text: 'test todo',
type: 'todo',
requiresApproval: true,
});
await nonLeader.post(`/tasks/${task._id}/assign/${leader._id}`);
let memberTasks = await leader.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(leader.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/remove-manager`, {
managerId: nonLeader._id,
});
await nonLeader.sync();
expect(nonLeader.notifications.length).to.equal(0);
expect(updatedGroup.managers[nonLeader._id]).to.not.exist;
});
});

View file

@ -0,0 +1,85 @@
import {
generateUser,
createAndPopulateGroup,
translate as t,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /group/:groupId/add-manager', () => {
let leader, nonLeader, groupToUpdate;
let groupName = 'Test Public Guild';
let groupType = 'guild';
let nonMember;
context('Guilds', () => {
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'public',
},
members: 1,
});
groupToUpdate = group;
leader = groupLeader;
nonLeader = members[0];
nonMember = await generateUser();
});
it('returns an error when a non group leader tries to add member', async () => {
await expect(nonLeader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('messageGroupOnlyLeaderCanUpdate'),
});
});
it('returns an error when trying to promote a non member', async () => {
await expect(leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonMember._id,
})).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('userMustBeMember'),
});
});
it('allows a leader to add managers', async () => {
let updatedGroup = await leader.post(`/groups/${groupToUpdate._id}/add-manager`, {
managerId: nonLeader._id,
});
expect(updatedGroup.managers[nonLeader._id]).to.be.true;
});
});
context('Party', () => {
let party, partyLeader, partyNonLeader;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: 'party',
privacy: 'private',
},
members: 1,
});
party = group;
partyLeader = groupLeader;
partyNonLeader = members[0];
});
it('allows leader of party to add managers', async () => {
let updatedGroup = await partyLeader.post(`/groups/${party._id}/add-manager`, {
managerId: partyNonLeader._id,
});
expect(updatedGroup.managers[partyNonLeader._id]).to.be.true;
});
});
});

View file

@ -4,7 +4,7 @@ import {
} from '../../../../../helpers/api-integration/v3';
import { find } from 'lodash';
describe('DELETE /tasks/:id', () => {
describe('Groups DELETE /tasks/:id', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@ -48,6 +48,21 @@ describe('DELETE /tasks/:id', () => {
});
});
it('allows a manager to delete a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.del(`/tasks/${task._id}`);
await expect(user.get(`/tasks/${task._id}`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('taskNotFound'),
});
});
it('unlinks assigned user', async () => {
await user.del(`/tasks/${task._id}`);

View file

@ -55,4 +55,13 @@ describe('GET /approvals/group/:groupId', () => {
let approvals = await user.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
it('allows managers to get a list of task that need approval', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member._id,
});
let approvals = await member.get(`/approvals/group/${guild._id}`);
expect(approvals[0]._id).to.equal(syncedTask._id);
});
});

View file

@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/approve/:userId', () => {
let user, guild, member, task;
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@ -17,12 +17,13 @@ describe('POST /tasks/:id/approve/:userId', () => {
name: 'Test Guild',
type: 'guild',
},
members: 1,
members: 2,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@ -69,4 +70,74 @@ describe('POST /tasks/:id/approve/:userId', () => {
expect(syncedTask.group.approval.approvingUser).to.equal(user._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('allows a manager to approve an assigned user', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await member.sync();
expect(member.notifications.length).to.equal(2);
expect(member.notifications[0].type).to.equal('GROUP_TASK_APPROVED');
expect(member.notifications[0].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(member.notifications[1].type).to.equal('SCORED_TASK');
expect(member.notifications[1].data.message).to.equal(t('yourTaskHasBeenApproved', {taskText: task.text}));
expect(syncedTask.group.approval.approved).to.be.true;
expect(syncedTask.group.approval.approvingUser).to.equal(member2._id);
expect(syncedTask.group.approval.dateApproved).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('removes approval pending notifications from managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(0);
expect(member2.notifications.length).to.equal(0);
});
it('prevents double approval on a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
await member2.post(`/tasks/${task._id}/approve/${member._id}`);
await expect(user.post(`/tasks/${task._id}/approve/${member._id}`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('canOnlyApproveTaskOnce'),
});
});
});

View file

@ -5,7 +5,7 @@ import {
import { find } from 'lodash';
describe('POST /tasks/:id/score/:direction', () => {
let user, guild, member, task;
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
return memberTask.group.id === guild._id;
@ -17,12 +17,13 @@ describe('POST /tasks/:id/score/:direction', () => {
name: 'Test Guild',
type: 'guild',
},
members: 1,
members: 2,
});
guild = group;
user = groupLeader;
member = members[0];
member2 = members[1];
task = await user.post(`/tasks/group/${guild._id}`, {
text: 'test todo',
@ -56,6 +57,7 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}, 'cs')); // This test only works if we have the notification translated
expect(user.notifications[0].data.groupId).to.equal(guild._id);
@ -63,6 +65,42 @@ describe('POST /tasks/:id/score/:direction', () => {
expect(updatedTask.group.approval.requestedDate).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type
});
it('sends notifications to all managers', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
await expect(member.post(`/tasks/${syncedTask._id}/score/up`))
.to.eventually.be.rejected.and.to.eql({
code: 401,
error: 'NotAuthorized',
message: t('taskApprovalHasBeenRequested'),
});
let updatedTask = await member.get(`/tasks/${syncedTask._id}`);
await user.sync();
await member2.sync();
expect(user.notifications.length).to.equal(1);
expect(user.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(user.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(user.notifications[0].data.groupId).to.equal(guild._id);
expect(member2.notifications.length).to.equal(1);
expect(member2.notifications[0].type).to.equal('GROUP_TASK_APPROVAL');
expect(member2.notifications[0].data.message).to.equal(t('userHasRequestedTaskApproval', {
user: member.auth.local.username,
taskName: updatedTask.text,
taskId: updatedTask._id,
}));
expect(member2.notifications[0].data.groupId).to.equal(guild._id);
});
it('errors when approval has already been requested', async () => {
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);

View file

@ -1,16 +1,29 @@
import {
generateUser,
generateGroup,
createAndPopulateGroup,
translate as t,
} from '../../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /tasks/group/:groupid', () => {
let user, guild;
let user, guild, manager;
let groupName = 'Test Public Guild';
let groupType = 'guild';
beforeEach(async () => {
user = await generateUser({balance: 1});
guild = await generateGroup(user, {type: 'guild'});
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: {
name: groupName,
type: groupType,
privacy: 'private',
},
members: 1,
});
guild = group;
user = groupLeader;
manager = members[0];
});
it('returns error when group is not found', async () => {
@ -116,4 +129,27 @@ describe('POST /tasks/group/:groupid', () => {
expect(task.everyX).to.eql(5);
expect(new Date(task.startDate)).to.eql(now);
});
it('allows a manager to add a group task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: manager._id,
});
let task = await manager.post(`/tasks/group/${guild._id}`, {
text: 'test habit',
type: 'habit',
up: false,
down: true,
notes: 1976,
});
let groupTask = await manager.get(`/tasks/group/${guild._id}`);
expect(groupTask[0].group.id).to.equal(guild._id);
expect(task.text).to.eql('test habit');
expect(task.notes).to.eql('1976');
expect(task.type).to.eql('habit');
expect(task.up).to.eql(false);
expect(task.down).to.eql(true);
});
});

View file

@ -6,7 +6,7 @@ import {
import { v4 as generateUUID } from 'uuid';
import { find } from 'lodash';
describe('POST /tasks/:taskId', () => {
describe('POST /tasks/:taskId/assign/:memberId', () => {
let user, guild, member, member2, task;
function findAssignedTask (memberTask) {
@ -130,4 +130,19 @@ describe('POST /tasks/:taskId', () => {
expect(member1SyncedTask).to.exist;
expect(member2SyncedTask).to.exist;
});
it('allows a manager to assign tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/assign/${member._id}`);
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.contain(member._id);
expect(syncedTask).to.exist;
});
});

View file

@ -114,4 +114,19 @@ describe('POST /tasks/:taskId/unassign/:memberId', () => {
expect(groupTask[0].group.assignedUsers).to.contain(member2._id);
expect(member2SyncedTask).to.exist;
});
it('allows a manager to unassign a user from a task', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.post(`/tasks/${task._id}/unassign/${member._id}`);
let groupTask = await member2.get(`/tasks/group/${guild._id}`);
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(groupTask[0].group.assignedUsers).to.not.contain(member._id);
expect(syncedTask).to.not.exist;
});
});

View file

@ -89,4 +89,25 @@ describe('PUT /tasks/:id', () => {
expect(member2SyncedTask.up).to.eql(false);
expect(member2SyncedTask.down).to.eql(false);
});
it('updates the linked tasks', async () => {
await user.post(`/groups/${guild._id}/add-manager`, {
managerId: member2._id,
});
await member2.put(`/tasks/${task._id}`, {
text: 'some new text',
up: false,
down: false,
notes: 'some new notes',
});
let memberTasks = await member.get('/tasks/user');
let syncedTask = find(memberTasks, findAssignedTask);
expect(syncedTask.text).to.eql('some new text');
expect(syncedTask.up).to.eql(false);
expect(syncedTask.down).to.eql(false);
});
});

View file

@ -603,6 +603,64 @@ describe('Purchasing a subscription for group', () => {
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with a Google subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.GOOGLE_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.GOOGLE_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('does not modify a user with an iOS subscription', async () => {
plan.customerId = 'random';
plan.paymentMethod = api.constants.IOS_PAYMENT_METHOD;
let recipient = new User();
recipient.profile.name = 'recipient';
recipient.purchased.plan = plan;
recipient.guilds.push(group._id);
await recipient.save();
user.guilds.push(group._id);
await user.save();
data.groupId = group._id;
await api.createSubscription(data);
let updatedUser = await User.findById(recipient._id).exec();
expect(updatedUser.purchased.plan.planId).to.eql('basic_3mo');
expect(updatedUser.purchased.plan.customerId).to.eql('random');
expect(updatedUser.purchased.plan.dateUpdated).to.exist;
expect(updatedUser.purchased.plan.gemsBought).to.eql(0);
expect(updatedUser.purchased.plan.paymentMethod).to.eql(api.constants.IOS_PAYMENT_METHOD);
expect(updatedUser.purchased.plan.extraMonths).to.eql(0);
expect(updatedUser.purchased.plan.dateTerminated).to.eql(null);
expect(updatedUser.purchased.plan.lastBillingDate).to.exist;
expect(updatedUser.purchased.plan.dateCreated).to.exist;
});
it('updates a user with a cancelled but active group subscription', async () => {
plan.key = 'basic_earned';
plan.customerId = api.constants.GROUP_PLAN_CUSTOMER_ID;

View file

@ -2,7 +2,13 @@ import moment from 'moment';
import { v4 as generateUUID } from 'uuid';
import validator from 'validator';
import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group, INVITES_LIMIT } from '../../../../../website/server/models/group';
import {
SPAM_MESSAGE_LIMIT,
SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
SPAM_WINDOW_LENGTH,
INVITES_LIMIT,
model as Group,
} from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { quests as questScrolls } from '../../../../../website/common/script/content';
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
@ -595,6 +601,99 @@ describe('Group Model', () => {
});
});
describe('#checkChatSpam', () => {
let testUser, testTime, tavern;
let testUserID = '1';
beforeEach(async () => {
testTime = Date.now();
tavern = new Group({
name: 'test tavern',
type: 'guild',
privacy: 'public',
});
tavern._id = TAVERN_ID;
testUser = {
_id: testUserID,
};
});
function generateTestMessage (overrides = {}) {
return Object.assign({}, {
text: 'test message',
uuid: testUserID,
timestamp: testTime,
}, overrides);
}
it('group that is not the tavern returns false, while tavern returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
party.chat.push(generateTestMessage());
}
expect(party.checkChatSpam(testUser)).to.eql(false);
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
it('high enough contributor returns false', async () => {
let highContributor = testUser;
highContributor.contributor = {
level: SPAM_MIN_EXEMPT_CONTRIB_LEVEL,
};
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(highContributor)).to.eql(false);
});
it('chat with no messages returns false', async () => {
expect(tavern.chat.length).to.eql(0);
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has not reached limit but another one has returns false', async () => {
let otherUserID = '2';
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage({uuid: otherUserID}));
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user messages is less than the limit returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has reached the message limit outside of window returns false', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT - 1; i++) {
tavern.chat.push(generateTestMessage());
}
let earlierTimestamp = testTime - SPAM_WINDOW_LENGTH - 1;
tavern.chat.push(generateTestMessage({timestamp: earlierTimestamp}));
expect(tavern.checkChatSpam(testUser)).to.eql(false);
});
it('user has posted too many messages in limit returns true', async () => {
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
tavern.chat.push(generateTestMessage());
}
expect(tavern.checkChatSpam(testUser)).to.eql(true);
});
});
describe('#leaveGroup', () => {
it('removes user from group quest', async () => {
party.quest.members = {

View file

@ -334,4 +334,34 @@ describe('Settings Controller', function () {
});
});
});
context('Fixing character values', function () {
describe('#restore', function () {
var blankRestoreValues = {
stats: {
hp: 0,
exp: 0,
gp: 0,
lvl: 0,
mp: 0,
},
achievements: {
streak: 0,
},
};
it('doesn\'t update character values when level is less than 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restore();
expect(User.set).to.not.be.called;
});
it('updates character values when level is at least 1', function () {
scope.restoreValues = blankRestoreValues;
scope.restoreValues.stats.lvl = 1;
scope.restore();
expect(User.set).to.be.called;
});
});
});
});

4
vagrant_scripts/install_node.sh Executable file → Normal file
View file

@ -16,7 +16,7 @@ nvm use
nvm alias default current
echo Update npm...
npm install -g npm@3
npm install -g npm@4
echo Installing global modules...
npm install -g gulp bower grunt-cli mocha
npm install -g gulp bower grunt-cli mocha node-pre-gyp

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View file

@ -215,3 +215,6 @@ group-members-autocomplete
background #ff6633
color #fff
cursor pointer
.add-manager-button, .remove-manager-button
margin-left: 1rem;

View file

@ -160,8 +160,10 @@ habitrpg.controller('GroupTasksCtrl', ['$scope', 'Shared', 'Tasks', 'User', '$ro
$scope.checkGroupAccess = function (group) {
if (!group || !group.leader) return true;
if (User.user._id !== group.leader._id) return false;
return true;
var userId = User.user._id;
var leader = group.leader._id === userId;
var isManager = Boolean(group.managers[userId]);
return leader || isManager;
};
/*

View file

@ -1,7 +1,7 @@
'use strict';
habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'ApiUrl', 'Notification', 'Members', '$rootScope', 'Analytics',
function($scope, Groups, Chat, User, $http, ApiUrl, Notification, Members, $rootScope, Analytics){
function($scope, Groups, Chat, User, $http, ApiUrl, Notification, Members, $rootScope, Analytics) {
$scope.message = {content:''};
$scope._sending = false;
@ -23,7 +23,7 @@ habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'A
return message.highlight;
}
$scope.postChat = function(group, message){
$scope.postChat = function(group, message) {
if (_.isEmpty(message) || $scope._sending) return;
$scope._sending = true;
@ -55,8 +55,12 @@ habitrpg.controller('ChatCtrl', ['$scope', 'Groups', 'Chat', 'User', '$http', 'A
} else {
Analytics.track({'hitType':'event','eventCategory':'behavior','eventAction':'group chat','groupType':group.type,'privacy':group.privacy});
}
}, function(err){
})
.catch(function (err) {
$scope._sending = false;
if (err.data.message === env.t('bannedWordUsed')) {
alert(err.data.message);
}
});
}

View file

@ -128,5 +128,37 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', '
.then(function (response) {
$rootScope.openModal('private-message', {controller: 'MemberModalCtrl'});
});
};
$scope.memberProfileName = function (memberId) {
var member = _.find($scope.groupCopy.members, function (member) { return member._id === memberId; });
return member.profile.name;
};
$scope.addManager = function () {
Groups.Group.addManager($scope.groupCopy._id, $scope.groupCopy._newManager)
.then(function (response) {
$scope.groupCopy._newManager = '';
$scope.groupCopy.managers = response.data.data.managers;
});
};
$scope.removeManager = function (memberId) {
Groups.Group.removeManager($scope.groupCopy._id, memberId)
.then(function (response) {
$scope.groupCopy._newManager = '';
$scope.groupCopy.managers = response.data.data.managers;
});
};
$scope.isManager = function (memberId, group) {
return Boolean(group.managers[memberId]);
}
$scope.userCanApprove = function (userId, group) {
if (!group) return false;
var leader = group.leader._id === userId;
var userIsManager = !!group.managers[userId];
return leader || userIsManager;
};
}]);

View file

@ -1,16 +1,45 @@
"use strict";
habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall',
function($scope, $rootScope, User, Notification, ApiUrl, Hall) {
habitrpg.controller("HallHeroesCtrl", ['$scope', '$rootScope', 'User', 'Notification', 'ApiUrl', 'Hall', 'Content',
function($scope, $rootScope, User, Notification, ApiUrl, Hall, Content) {
$scope.hero = undefined;
$scope.currentHeroIndex = undefined;
$scope.heroes = [];
$scope.allItemPaths = getAllItemPaths();
Hall.getHeroes()
.then(function (response) {
$scope.heroes = response.data.data;
});
function _getFormattedItemReference (pathPrefix, keys, values) {
var finishedString = '\n'.concat('path: ', pathPrefix, ', ', 'value: {', values, '}\n');
_.each(keys, function (key) {
finishedString = finishedString.concat('\t', pathPrefix, '.', key, '\n');
});
return finishedString;
}
function getAllItemPaths () {
var quests = _getFormattedItemReference('items.quests', _.keys(Content.quests), 'Numeric Quantity');
var mounts = _getFormattedItemReference('items.mounts', _.keys(Content.mountInfo), 'Boolean');
var food = _getFormattedItemReference('items.food', _.keys(Content.food), 'Numeric Quantity');
var eggs = _getFormattedItemReference('items.eggs', _.keys(Content.eggs), 'Numeric Quantity');
var hatchingPotions = _getFormattedItemReference('items.hatchingPotions', _.keys(Content.hatchingPotions), 'Numeric Quantity');
var pets = _getFormattedItemReference('items.pets', _.keys(Content.petInfo), '-1: Owns Mount, 0: Not Owned, 1-49: Progress to mount');
var special = _getFormattedItemReference('items.special', _.keys(Content.special), 'Numeric Quantity');
var gear = _getFormattedItemReference('items.gear.owned', _.keys(Content.gear.flat), 'Boolean');
var equippedGear = '\nEquipped Gear:\n\titems.gear.{equipped/costume}.{head/headAccessory/eyewear/armor/body/back/shield/weapon}.{gearKey}\n';
var equippedPet = '\nEquipped Pet:\n\titems.currentPet.{petKey}\n';
var equippedMount = '\nEquipped Mount:\n\titems.currentMount.{mountKey}\n';
var data = quests.concat(mounts, food, eggs, hatchingPotions, pets, special, gear, equippedGear, equippedPet, equippedMount);
return data;
}
$scope.loadHero = function(uuid, heroIndex) {
$scope.currentHeroIndex = heroIndex;
Hall.getHero(uuid)

View file

@ -116,10 +116,10 @@ angular.module('habitrpg')
return selectNotificationValue(false, false, false, false, false, true, false, false);
};
$scope.viewGroupApprovalNotification = function (notification, $index) {
$scope.viewGroupApprovalNotification = function (notification, $index, navigate) {
User.readNotification(notification.id);
User.user.groupNotifications.splice($index, 1);
$state.go("options.social.guilds.detail", {gid: notification.data.groupId});
if (navigate) $state.go("options.social.guilds.detail", {gid: notification.data.groupId});
};
$scope.groupApprovalNotificationIcon = function (notification) {

View file

@ -89,14 +89,20 @@ habitrpg.controller('NotificationCtrl',
var notificationsToRead = [];
var scoreTaskNotification;
User.user.groupNotifications = []; // Flush group notifictions
after.forEach(function (notification) {
if (lastShownNotifications.indexOf(notification.id) !== -1) {
return;
}
lastShownNotifications.push(notification.id);
if (lastShownNotifications.length > 10) {
lastShownNotifications.splice(0, 9);
// Some notifications are not marked read here, so we need to fix this system
// to handle notifications differently
if (['GROUP_TASK_APPROVED', 'GROUP_TASK_APPROVAL'].indexOf(notification.type) === -1) {
lastShownNotifications.push(notification.id);
if (lastShownNotifications.length > 10) {
lastShownNotifications.splice(0, 9);
}
}
var markAsRead = true;

View file

@ -2,8 +2,8 @@
// Make user and settings available for everyone through root scope.
habitrpg.controller('SettingsCtrl',
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$timeout', 'Content', 'Notification', 'Shared', 'Social', '$compile',
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $timeout, Content, Notification, Shared, Social, $compile) {
['$scope', 'User', '$rootScope', '$http', 'ApiUrl', 'Guide', '$location', '$modalStack', '$timeout', 'Content', 'Notification', 'Shared', 'Social', '$compile',
function($scope, User, $rootScope, $http, ApiUrl, Guide, $location, $modalStack, $timeout, Content, Notification, Shared, Social, $compile) {
var RELEASE_ANIMAL_TYPES = {
pets: 'releasePets',
mounts: 'releaseMounts',
@ -164,14 +164,21 @@ habitrpg.controller('SettingsCtrl',
$scope.restore = function(){
var stats = $scope.restoreValues.stats,
achievements = $scope.restoreValues.achievements;
if (stats.lvl < 1) {
Notification.error(env.t('invalidLevel'), true);
return;
}
User.set({
"stats.hp": stats.hp,
"stats.exp": stats.exp,
"stats.gp": stats.gp,
"stats.lvl": stats.lvl,
"stats.mp": stats.mp,
"achievements.streak": achievements.streak
'stats.hp': stats.hp,
'stats.exp': stats.exp,
'stats.gp': stats.gp,
'stats.lvl': stats.lvl,
'stats.mp': stats.mp,
'achievements.streak': achievements.streak
});
$modalStack.dismissAll();
}
$scope.reset = function(){

View file

@ -108,6 +108,26 @@ angular.module('habitrpg')
});
};
Group.addManager = function(gid, memberId) {
return $http({
method: "POST",
url: groupApiURLPrefix + '/' + gid + '/add-manager/',
data: {
managerId: memberId,
},
});
};
Group.removeManager = function(gid, memberId) {
return $http({
method: "POST",
url: groupApiURLPrefix + '/' + gid + '/remove-manager/',
data: {
managerId: memberId,
},
});
};
$rootScope.$on('syncPartyRequest', function (event, options) {
if (options.type === 'user_update') {
var index = _.findIndex(data.party.members, function(user) { return user._id === options.user._id; });
@ -221,7 +241,7 @@ angular.module('habitrpg')
function inviteOrStartParty (group) {
Analytics.track({'hitType':'event','eventCategory':'button','eventAction':'click','eventLabel':'Invite Friends'});
var sendInviteText = window.env.t('sendInvitations');
if (group.type !== 'party' && group.type !== 'guild') {
$location.path("/options/groups/party");

View file

@ -21,6 +21,7 @@
"communityGuidelines": "Community Guidelines",
"communityGuidelinesRead1": "Please read our",
"communityGuidelinesRead2": "before chatting.",
"bannedWordUsed": "Oops! Looks like this post contains a swearword, religious oath, or reference to an addictive substance or adult topic. Habitica has users from all backgrounds, so we keep our chat very clean. Feel free to edit your message so you can post it!",
"party": "Party",
"createAParty": "Create A Party",
"updatedParty": "Party settings updated.",
@ -271,5 +272,13 @@
"confirmCancelGroupPlan": "Are you sure you want to cancel the group plan and remove its benefits from all members, including their free subscriptions?",
"canceledGroupPlan": "Canceled Group Plan",
"groupPlanCanceled": "Group Plan will become inactive on",
"purchasedGroupPlanPlanExtraMonths": "You have <%= months %> months of extra group plan credit."
"purchasedGroupPlanPlanExtraMonths": "You have <%= months %> months of extra group plan credit.",
"addManagers": "Add Managers",
"addManager": "Add Manager",
"removeManager": "Remove",
"userMustBeMember": "User must be a member",
"userIsNotManager": "User is not manager",
"canOnlyApproveTaskOnce": "This task has already been approved.",
"leaderMarker": " - Leader",
"managerMarker": " - Manager"
}

View file

@ -56,9 +56,11 @@
"messageGroupChatFlagAlreadyReported": "You have already reported this message",
"messageGroupChatNotFound": "Message not found!",
"messageGroupChatAdminClearFlagCount": "Only an admin can clear the flag count!",
"messageGroupChatSpam": "Whoops, looks like you're posting too many messages! Please wait a minute and try again. The Tavern chat only holds 200 messages at a time, so Habitica encourages posting longer, more thoughtful messages and consolidating replies. Can't wait to hear what you have to say. :)",
"messageUserOperationProtected": "path `<%= operation %>` was not saved, as it's a protected path.",
"messageUserOperationNotFound": "<%= operation %> operation not found",
"messageNotificationNotFound": "Notification not found.",
"notificationsRequired": "Notification ids are required."
}

View file

@ -42,6 +42,7 @@
"food": "Food and Saddles",
"noFood": "You don't have any food or saddles.",
"dropsExplanation": "Get these items faster with Gems if you don't want to wait for them to drop when completing a task. <a href=\"http://habitica.wikia.com/wiki/Drops\">Learn more about the drop system.</a>",
"dropsExplanationEggs": "Spend Gems to get eggs more quickly, if you don't want to wait for standard eggs to drop, or to repeat Quests to earn Quest eggs. <a href=\"http://habitica.wikia.com/wiki/Drops\">Learn more about the drop system.</a>",
"premiumPotionNoDropExplanation": "Magic Hatching Potions cannot be used on eggs received from Quests. The only way to get Magic Hatching Potions is by buying them below, not from random drops.",
"beastMasterProgress": "Beast Master Progress",
"stableBeastMasterProgress": "Beast Master Progress: <%= number %> Pets Found",

View file

@ -26,6 +26,7 @@
"showBaileyPop": "Bring Bailey the Town Crier out of hiding so you can review past news.",
"fixVal": "Fix Character Values",
"fixValPop": "Manually change values like Health, Level, and Gold.",
"invalidLevel": "Invalid value: Level must be 1 or greater.",
"enableClass": "Enable Class System",
"enableClassPop": "You opted out of the class system initially. Would you like now to opt-in?",
"classTourPop": "Show the tour for using the class system.",

View file

@ -166,5 +166,6 @@
"groupTasksByChallenge": "Group tasks by challenge title",
"taskNotes": "Task Notes",
"monthlyRepeatHelpContent": "This task will be due every X months",
"yearlyRepeatHelpContent": "This task will be due every X years"
"yearlyRepeatHelpContent": "This task will be due every X years",
"resets": "Resets"
}

View file

@ -1235,4 +1235,4 @@
"eyewearMystery301703Notes": "完美适配美妙的化妆舞会或者静悄悄地穿过那些特别讲究穿着的人群。没有属性加成。3017年三月捐赠者礼品。",
"eyewearArmoirePlagueDoctorMaskText": "瘟疫医生面具",
"eyewearArmoirePlagueDoctorMaskNotes": "防治延迟瘟疫的医生袋的地道面具。没有属性加成。魔法衣橱: 瘟疫医生系列 (3件的第2件)。"
}
}

View file

@ -112,7 +112,7 @@ let specialAchievs = {
textKey: 'contribText',
},
npc: {
icon: 'achievement-ultimate-warrior',
icon: 'achievement-npc',
titleKey: 'npcAchievementName',
textKey: 'npcAchievementText',
},

View file

@ -19,4 +19,5 @@ module.exports = prefill({
spookySkins: {setPrice: 5, availableUntil: '2016-01-01', text: t('spookySkins')},
supernaturalSkins: {setPrice: 5, availableUntil: '2016-11-01', text: t('supernaturalSkins')},
splashySkins: {setPrice: 5, availableUntil: '2016-01-01', text: t('splashySkins')},
winterySkins: {setPrice: 5, availableFrom: '2015-12-18', availableUntil: '2016-02-02', text: t('winterySkins')},
});

View file

@ -64,5 +64,14 @@ module.exports = prefill({
'shark': {price: 2, set: sets.splashySkins},
'tropicalwater': {price: 2, set: sets.splashySkins},
'aurora': {price: 2, set: sets.winterySkins},
'dapper': {price: 2, set: sets.winterySkins},
'festive': {price: 2, set: sets.winterySkins},
'holly': {price: 2, set: sets.winterySkins},
'polar': {price: 2, set: sets.winterySkins},
'sugar': {price: 2, set: sets.winterySkins},
'snowy': {price: 2, set: sets.winterySkins},
'winterstar': {price: 2, set: sets.winterySkins},
/* eslint-enable quote-props */
});

View file

@ -28,6 +28,7 @@ function hasQuestAchievementFunction (key) {
let drops = {
Wolf: {
text: t('dropEggWolfText'),
mountText: t('dropEggWolfMountText'),
adjective: t('dropEggWolfAdjective'),
},
TigerCub: {
@ -47,18 +48,22 @@ let drops = {
},
Fox: {
text: t('dropEggFoxText'),
mountText: t('dropEggFoxMountText'),
adjective: t('dropEggFoxAdjective'),
},
FlyingPig: {
text: t('dropEggFlyingPigText'),
mountText: t('dropEggFlyingPigMountText'),
adjective: t('dropEggFlyingPigAdjective'),
},
Dragon: {
text: t('dropEggDragonText'),
mountText: t('dropEggDragonMountText'),
adjective: t('dropEggDragonAdjective'),
},
Cactus: {
text: t('dropEggCactusText'),
mountText: t('dropEggCactusMountText'),
adjective: t('dropEggCactusAdjective'),
},
BearCub: {
@ -71,66 +76,78 @@ let drops = {
let quests = {
Gryphon: {
text: t('questEggGryphonText'),
mountText: t('questEggGryphonMountText'),
adjective: t('questEggGryphonAdjective'),
canBuy: hasQuestAchievementFunction('gryphon'),
},
Hedgehog: {
text: t('questEggHedgehogText'),
mountText: t('questEggHedgehogMountText'),
adjective: t('questEggHedgehogAdjective'),
canBuy: hasQuestAchievementFunction('hedgehog'),
},
Deer: {
text: t('questEggDeerText'),
mountText: t('questEggDeerMountText'),
adjective: t('questEggDeerAdjective'),
canBuy: hasQuestAchievementFunction('ghost_stag'),
},
Egg: {
text: t('questEggEggText'),
adjective: t('questEggEggAdjective'),
mountText: t('questEggEggMountText'),
adjective: t('questEggEggAdjective'),
},
Rat: {
text: t('questEggRatText'),
mountText: t('questEggRatMountText'),
adjective: t('questEggRatAdjective'),
canBuy: hasQuestAchievementFunction('rat'),
},
Octopus: {
text: t('questEggOctopusText'),
mountText: t('questEggOctopusMountText'),
adjective: t('questEggOctopusAdjective'),
canBuy: hasQuestAchievementFunction('octopus'),
},
Seahorse: {
text: t('questEggSeahorseText'),
mountText: t('questEggSeahorseMountText'),
adjective: t('questEggSeahorseAdjective'),
canBuy: hasQuestAchievementFunction('dilatory_derby'),
},
Parrot: {
text: t('questEggParrotText'),
mountText: t('questEggParrotMountText'),
adjective: t('questEggParrotAdjective'),
canBuy: hasQuestAchievementFunction('harpy'),
},
Rooster: {
text: t('questEggRoosterText'),
mountText: t('questEggRoosterMountText'),
adjective: t('questEggRoosterAdjective'),
canBuy: hasQuestAchievementFunction('rooster'),
},
Spider: {
text: t('questEggSpiderText'),
mountText: t('questEggSpiderMountText'),
adjective: t('questEggSpiderAdjective'),
canBuy: hasQuestAchievementFunction('spider'),
},
Owl: {
text: t('questEggOwlText'),
mountText: t('questEggOwlMountText'),
adjective: t('questEggOwlAdjective'),
canBuy: hasQuestAchievementFunction('owl'),
},
Penguin: {
text: t('questEggPenguinText'),
mountText: t('questEggPenguinMountText'),
adjective: t('questEggPenguinAdjective'),
canBuy: hasQuestAchievementFunction('penguin'),
},
TRex: {
text: t('questEggTRexText'),
mountText: t('questEggTRexMountText'),
adjective: t('questEggTRexAdjective'),
canBuy (user) {
let questAchievements = user.achievements.quests;
@ -143,51 +160,61 @@ let quests = {
},
Rock: {
text: t('questEggRockText'),
mountText: t('questEggRockMountText'),
adjective: t('questEggRockAdjective'),
canBuy: hasQuestAchievementFunction('rock'),
},
Bunny: {
text: t('questEggBunnyText'),
mountText: t('questEggBunnyMountText'),
adjective: t('questEggBunnyAdjective'),
canBuy: hasQuestAchievementFunction('bunny'),
},
Slime: {
text: t('questEggSlimeText'),
mountText: t('questEggSlimeMountText'),
adjective: t('questEggSlimeAdjective'),
canBuy: hasQuestAchievementFunction('slime'),
},
Sheep: {
text: t('questEggSheepText'),
mountText: t('questEggSheepMountText'),
adjective: t('questEggSheepAdjective'),
canBuy: hasQuestAchievementFunction('sheep'),
},
Cuttlefish: {
text: t('questEggCuttlefishText'),
mountText: t('questEggCuttlefishMountText'),
adjective: t('questEggCuttlefishAdjective'),
canBuy: hasQuestAchievementFunction('kraken'),
},
Whale: {
text: t('questEggWhaleText'),
mountText: t('questEggWhaleMountText'),
adjective: t('questEggWhaleAdjective'),
canBuy: hasQuestAchievementFunction('whale'),
},
Cheetah: {
text: t('questEggCheetahText'),
mountText: t('questEggCheetahMountText'),
adjective: t('questEggCheetahAdjective'),
canBuy: hasQuestAchievementFunction('cheetah'),
},
Horse: {
text: t('questEggHorseText'),
mountText: t('questEggHorseMountText'),
adjective: t('questEggHorseAdjective'),
canBuy: hasQuestAchievementFunction('horse'),
},
Frog: {
text: t('questEggFrogText'),
mountText: t('questEggFrogMountText'),
adjective: t('questEggFrogAdjective'),
canBuy: hasQuestAchievementFunction('frog'),
},
Snake: {
text: t('questEggSnakeText'),
mountText: t('questEggSnakeMountText'),
adjective: t('questEggSnakeAdjective'),
canBuy: hasQuestAchievementFunction('snake'),
},
@ -199,81 +226,97 @@ let quests = {
},
Sabretooth: {
text: t('questEggSabretoothText'),
mountText: t('questEggSabretoothMountText'),
adjective: t('questEggSabretoothAdjective'),
canBuy: hasQuestAchievementFunction('sabretooth'),
},
Monkey: {
text: t('questEggMonkeyText'),
mountText: t('questEggMonkeyMountText'),
adjective: t('questEggMonkeyAdjective'),
canBuy: hasQuestAchievementFunction('monkey'),
},
Snail: {
text: t('questEggSnailText'),
mountText: t('questEggSnailMountText'),
adjective: t('questEggSnailAdjective'),
canBuy: hasQuestAchievementFunction('snail'),
},
Falcon: {
text: t('questEggFalconText'),
mountText: t('questEggFalconMountText'),
adjective: t('questEggFalconAdjective'),
canBuy: hasQuestAchievementFunction('falcon'),
},
Treeling: {
text: t('questEggTreelingText'),
mountText: t('questEggTreelingMountText'),
adjective: t('questEggTreelingAdjective'),
canBuy: hasQuestAchievementFunction('treeling'),
},
Axolotl: {
text: t('questEggAxolotlText'),
mountText: t('questEggAxolotlMountText'),
adjective: t('questEggAxolotlAdjective'),
canBuy: hasQuestAchievementFunction('axolotl'),
},
Turtle: {
text: t('questEggTurtleText'),
mountText: t('questEggTurtleMountText'),
adjective: t('questEggTurtleAdjective'),
canBuy: hasQuestAchievementFunction('turtle'),
},
Armadillo: {
text: t('questEggArmadilloText'),
mountText: t('questEggArmadilloMountText'),
adjective: t('questEggArmadilloAdjective'),
canBuy: hasQuestAchievementFunction('armadillo'),
},
Cow: {
text: t('questEggCowText'),
mountText: t('questEggCowMountText'),
adjective: t('questEggCowAdjective'),
canBuy: hasQuestAchievementFunction('cow'),
},
Beetle: {
text: t('questEggBeetleText'),
mountText: t('questEggBeetleMountText'),
adjective: t('questEggBeetleAdjective'),
canBuy: hasQuestAchievementFunction('beetle'),
},
Ferret: {
text: t('questEggFerretText'),
mountText: t('questEggFerretMountText'),
adjective: t('questEggFerretAdjective'),
canBuy: hasQuestAchievementFunction('ferret'),
},
Sloth: {
text: t('questEggSlothText'),
mountText: t('questEggSlothMountText'),
adjective: t('questEggSlothAdjective'),
canBuy: hasQuestAchievementFunction('sloth'),
},
Triceratops: {
text: t('questEggTriceratopsText'),
mountText: t('questEggTriceratopsMountText'),
adjective: t('questEggTriceratopsAdjective'),
canBuy: hasQuestAchievementFunction('triceratops'),
},
GuineaPig: {
text: t('questEggGuineaPigText'),
mountText: t('questEggGuineaPigMountText'),
adjective: t('questEggGuineaPigAdjective'),
canBuy: hasQuestAchievementFunction('guineapig'),
},
Peacock: {
text: t('questEggPeacockText'),
mountText: t('questEggPeacockMountText'),
adjective: t('questEggPeacockAdjective'),
canBuy: hasQuestAchievementFunction('peacock'),
},
Butterfly: {
text: t('questEggButterflyText'),
mountText: t('questEggButterflyMountText'),
adjective: t('questEggButterflyAdjective'),
canBuy: hasQuestAchievementFunction('butterfly'),
},

View file

@ -585,6 +585,8 @@ module.exports = function getLoginIncentives (api) {
},
},
};
// When the final check-in prize is added here, change checkinReceivedAllRewardsMessage in website/common/locales/en/loginIncentives.json
// to say "You have received the final Check-In prize!". Confirm the message with Lemoness first.
// Add reference link to next reward and add filler days so we have a map to reference the next reward from any day
// We could also, use a list, but then we would be cloning each of the rewards.

View file

@ -25,7 +25,7 @@ shops.getMarketCategories = function getMarket (user, language) {
let eggsCategory = {
identifier: 'eggs',
text: i18n.t('eggs', language),
notes: i18n.t('dropsExplanation', language),
notes: i18n.t('dropsExplanationEggs', language),
};
eggsCategory.items = sortBy(values(content.questEggs)

View file

@ -2,6 +2,7 @@ import { authWithHeaders } from '../../middlewares/auth';
import { model as Group } from '../../models/group';
import { model as User } from '../../models/user';
import {
BadRequest,
NotFound,
NotAuthorized,
} from '../../libs/errors';
@ -12,6 +13,8 @@ import slack from '../../libs/slack';
import pusher from '../../libs/pusher';
import nconf from 'nconf';
import Bluebird from 'bluebird';
import bannedWords from '../../libs/bannedWords';
import { TAVERN_ID } from '../../models/group';
const FLAG_REPORT_EMAILS = nconf.get('FLAG_REPORT_EMAIL').split(',').map((email) => {
return { email, canSend: true };
@ -82,6 +85,28 @@ api.getChat = {
},
};
// @TODO: Probably move this to a library
function matchExact (r, str) {
let match = str.match(r);
return match !== null && match[0] !== null;
}
let bannedWordRegexs = [];
for (let i = 0; i < bannedWords.length; i += 1) {
let word = bannedWords[i];
let regEx = new RegExp(`\\b([^a-z]+)?${word.toLowerCase()}([^a-z]+)?\\b`);
bannedWordRegexs.push(regEx);
}
function textContainsBannedWords (message) {
for (let i = 0; i < bannedWordRegexs.length; i += 1) {
let regEx = bannedWordRegexs[i];
if (matchExact(regEx, message.toLowerCase())) return true;
}
return false;
}
/**
* @api {post} /api/v3/groups/:groupId/chat Post chat message to a group
* @apiName PostChat
@ -121,9 +146,17 @@ api.postChat = {
throw new NotFound('Your chat privileges have been revoked.');
}
if (group._id === TAVERN_ID && textContainsBannedWords(req.body.message)) {
throw new BadRequest(res.t('bannedWordUsed'));
}
let lastClientMsg = req.query.previousMsg;
chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false;
if (group.checkChatSpam(user)) {
throw new NotAuthorized(res.t('messageGroupChatSpam'));
}
let newChatMessage = group.sendChat(req.body.message, user);
let toSave = [group.save()];

View file

@ -1095,4 +1095,108 @@ api.inviteToGroup = {
},
};
/**
* @api {post} /api/v3/groups/:groupId/add-manager Add a manager to a group
* @apiName AddGroupManager
* @apiGroup Group
*
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
*
* @apiParamExample {String} party:
* /api/v3/groups/party/add-manager
*
* @apiBody (Body) {UUID} managerId The user _id of the member to promote to manager
*
* @apiSuccess {Object} data An empty object
*
* @apiError (400) {NotAuthorized} managerId req.body.managerId is required
* @apiUse groupIdRequired
*/
api.addGroupManager = {
method: 'POST',
url: '/groups/:groupId/add-manager',
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let managerId = req.body.managerId;
req.checkParams('groupId', apiMessages('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
req.checkBody('managerId', apiMessages('managerIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let newManager = await User.findById(managerId, 'guilds party').exec();
let groupFields = basicGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: groupFields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
let isMember = group.isMember(newManager);
if (!isMember) throw new NotAuthorized(res.t('userMustBeMember'));
group.managers[managerId] = true;
group.markModified('managers');
await group.save();
res.respond(200, group);
},
};
/**
* @api {post} /api/v3/groups/:groupId/remove-manager Remove a manager from a group
* @apiName RemoveGroupManager
* @apiGroup Group
*
* @apiParam (Path) {UUID} groupId The group _id ('party' for the user party and 'habitrpg' for tavern are accepted)
*
* @apiParamExample {String} party:
* /api/v3/groups/party/add-manager
*
* @apiBody (Body) {UUID} managerId The user _id of the member to remove
*
* @apiSuccess {Object} group The group
*
* @apiError (400) {NotAuthorized} managerId req.body.managerId is required
* @apiUse groupIdRequired
*/
api.removeGroupManager = {
method: 'POST',
url: '/groups/:groupId/remove-manager',
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
let managerId = req.body.managerId;
req.checkParams('groupId', apiMessages('groupIdRequired')).notEmpty(); // .isUUID(); can't be used because it would block 'habitrpg' or 'party'
req.checkBody('managerId', apiMessages('managerIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let groupFields = basicGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: groupFields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
if (!group.managers[managerId]) throw new NotAuthorized(res.t('userIsNotManager'));
delete group.managers[managerId];
group.markModified('managers');
await group.save();
let manager = await User.findById(managerId, 'notifications').exec();
let newNotifications = manager.notifications.filter((notification) => {
return notification.type !== 'GROUP_TASK_APPROVAL';
});
manager.notifications = newNotifications;
manager.markModified('notifications');
await manager.save();
res.respond(200, group);
},
};
module.exports = api;

View file

@ -25,6 +25,13 @@ import logger from '../../libs/logger';
const MAX_SCORE_NOTES_LENGTH = 256;
function canNotEditTasks (group, user, assignedUserId) {
let isNotGroupLeader = group.leader !== user._id;
let isManager = Boolean(group.managers[user._id]);
let userIsAssigningToSelf = Boolean(assignedUserId && user._id === assignedUserId);
return isNotGroupLeader && !isManager && !userIsAssigningToSelf;
}
/**
* @apiDefine TaskNotFound
* @apiError (404) {NotFound} TaskNotFound The specified task could not be found.
@ -413,9 +420,10 @@ api.updateTask = {
throw new NotFound(res.t('taskNotFound'));
} else if (task.group.id && !task.userId) {
// @TODO: Abstract this access snippet
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
group = await Group.getGroup({user, groupId: task.group.id, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
@ -530,18 +538,29 @@ api.scoreTask = {
task.group.approval.requested = true;
task.group.approval.requestedDate = new Date();
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
let groupLeader = await User.findById(group.leader).exec(); // Use this method so we can get access to notifications
let fields = requiredGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId: task.group.id, fields});
groupLeader.addNotification('GROUP_TASK_APPROVAL', {
message: res.t('userHasRequestedTaskApproval', {
user: user.profile.name,
taskName: task.text,
}, groupLeader.preferences.language),
groupId: group._id,
// @TODO: we can use the User.pushNotification function because we need to ensure notifications are translated
let managerIds = Object.keys(group.managers);
managerIds.push(group.leader);
let managers = await User.find({_id: managerIds}, 'notifications preferences').exec(); // Use this method so we can get access to notifications
let managerPromises = [];
managers.forEach((manager) => {
manager.addNotification('GROUP_TASK_APPROVAL', {
message: res.t('userHasRequestedTaskApproval', {
user: user.profile.name,
taskName: task.text,
}, manager.preferences.language),
groupId: group._id,
taskId: task._id,
});
managerPromises.push(manager.save());
});
await Bluebird.all([groupLeader.save(), task.save()]);
managerPromises.push(task.save());
await Bluebird.all(managerPromises);
throw new NotAuthorized(res.t('taskApprovalHasBeenRequested'));
}
@ -694,9 +713,9 @@ api.addChecklistItem = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (task.group.id && !task.userId) {
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
let fields = requiredGroupFields.concat(' managers');
group = await Group.getGroup({user, groupId: task.group.id, fields});
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
@ -803,9 +822,10 @@ api.updateChecklistItem = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (task.group.id && !task.userId) {
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
group = await Group.getGroup({user, groupId: task.group.id, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
@ -867,9 +887,10 @@ api.removeChecklistItem = {
if (!task) {
throw new NotFound(res.t('taskNotFound'));
} else if (task.group.id && !task.userId) {
group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
group = await Group.getGroup({user, groupId: task.group.id, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
@ -1185,9 +1206,10 @@ api.deleteTask = {
throw new NotFound(res.t('taskNotFound'));
} else if (task.group.id && !task.userId) {
// @TODO: Abstract this access snippet
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId: task.group.id, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
await group.removeTask(task);
} else if (task.challenge.id && !task.userId) { // If the task belongs to a challenge make sure the user has rights
challenge = await Challenge.findOne({_id: task.challenge.id}).exec();

View file

@ -1,3 +1,4 @@
import findIndex from 'lodash/findIndex';
import { authWithHeaders } from '../../../middlewares/auth';
import Bluebird from 'bluebird';
import * as Tasks from '../../../models/task';
@ -16,6 +17,14 @@ import {
let requiredGroupFields = '_id leader tasksOrder name';
let types = Tasks.tasksTypes.map(type => `${type}s`);
function canNotEditTasks (group, user, assignedUserId) {
let isNotGroupLeader = group.leader !== user._id;
let isManager = Boolean(group.managers[user._id]);
let userIsAssigningToSelf = Boolean(assignedUserId && user._id === assignedUserId);
return isNotGroupLeader && !isManager && !userIsAssigningToSelf;
}
let api = {};
/**
@ -40,10 +49,11 @@ api.createGroupTasks = {
let user = res.locals.user;
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId: req.params.groupId, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
let tasks = await createTasks(req, res, {user, group});
@ -171,11 +181,11 @@ api.assignTask = {
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
}
let groupFields = `${requiredGroupFields} chat`;
let groupFields = `${requiredGroupFields} chat managers`;
let group = await Group.getGroup({user, groupId: task.group.id, fields: groupFields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id && user._id !== assignedUserId) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
// User is claiming the task
if (user._id === assignedUserId) {
@ -229,10 +239,11 @@ api.unassignTask = {
throw new NotAuthorized(res.t('onlyGroupTasksCanBeAssigned'));
}
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId: task.group.id, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
await group.unlinkTask(task, assignedUser);
@ -277,10 +288,12 @@ api.approveTask = {
throw new NotFound(res.t('taskNotFound'));
}
let group = await Group.getGroup({user, groupId: task.group.id, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId: task.group.id, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (task.group.approval.approved === true) throw new NotAuthorized(res.t('canOnlyApproveTaskOnce'));
task.group.approval.dateApproved = new Date();
task.group.approval.approvingUser = user._id;
@ -296,7 +309,25 @@ api.approveTask = {
scoreTask: task,
});
await Bluebird.all([assignedUser.save(), task.save()]);
let managerIds = Object.keys(group.managers);
managerIds.push(group.leader);
let managers = await User.find({_id: managerIds}, 'notifications').exec(); // Use this method so we can get access to notifications
let managerPromises = [];
managers.forEach((manager) => {
let notificationIndex = findIndex(manager.notifications, function findNotification (notification) {
return notification.data.taskId === task._id;
});
if (notificationIndex !== -1) {
manager.notifications.splice(notificationIndex, 1);
managerPromises.push(manager.save());
}
});
managerPromises.push(task.save());
managerPromises.push(assignedUser.save());
await Bluebird.all(managerPromises);
res.respond(200, task);
},
@ -325,10 +356,11 @@ api.getGroupApprovals = {
let user = res.locals.user;
let groupId = req.params.groupId;
let group = await Group.getGroup({user, groupId, fields: requiredGroupFields});
let fields = requiredGroupFields.concat(' managers');
let group = await Group.getGroup({user, groupId, fields});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
if (canNotEditTasks(group, user)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
let approvals = await Tasks.Task.find({
'group.id': groupId,

View file

@ -8,6 +8,8 @@ const messages = {
guildsOnlyPaginate: 'Only public guilds support pagination.',
guildsPaginateBooleanString: 'req.query.paginate must be a boolean string.',
guildsPageInteger: 'req.query.page must be an integer greater than or equal to 0.',
groupIdRequired: 'req.params.groupId must contain a groupId.',
managerIdRequired: 'req.body.managerId must contain a user ID.',
};
export default function (msgKey, vars = {}) {
@ -18,4 +20,4 @@ export default function (msgKey, vars = {}) {
// TODO cache the result of template() ? More memory usage, faster output
return _.template(message)(clonedVars);
}
}

View file

@ -0,0 +1,172 @@
/* eslint-disable no-multiple-empty-lines */
// CONTENT WARNING:
//
// This file contains slurs, swear words, religious oaths, and words related
// to addictive substance and adult topics.
// Do not read this file if you do not want to be exposed to those words.
//
// The words are stored in an array called `bannedWords` which is then
// exported with `module.exports = bannedWords;`
// This file does not contain any other code.
//
//
// EDIT WARNING:
//
// If you think this file needs to be changed please do NOT create a
// pull request or issue in GitHub!
// Email admin@habitica.com to discuss the change you want made.
//
// All updates to this file must be done through a direct commit to limit
// the words visibility in GitHub to protect our coders, socialites, and
// wiki editors who look through PRs for information.
//
// When adding words that contain asterisks, put two backslashes before them.
//
// Do NOT enter any words with $ signs. E.g., "i$$" cannot be on the list
// because it would cause the word "I" to be blocked. Escaping $ signs with
// backslashes doesn't work.
// Do not include the following words:
// 'ale' because it's Polish for 'but'.
// 'Jesus' because it's a legitimate modern given name.
// 'Christ' because it's a legitimate modern surname.
let bannedWords = [
'TEST_PLACEHOLDER_SWEAR_WORD_HERE',
'hell',
'hellish',
'damn',
'goddamn',
'damnit',
'dammit',
'damned',
'omg',
'god',
'g\\*d',
'shit',
'shitty',
'shitting',
'shithole',
'shitface',
'shitfaced',
'sh\\*t',
'sh\\*tty',
'sh\\*tting',
'fuck',
'fucking',
'fucked',
'fuckwit',
'fucker',
'f\\*ck',
'motherfucker',
'motherfucking',
'muthafucka',
'fu',
'wtf',
'dafuq',
'af',
'ass',
'arse',
'asshole',
'badass',
'arsehole',
'badarse',
'lmao',
'lmfao',
'bitch',
'bitchy',
'bitches',
'bitching',
'b\\*tch',
'nigger',
'niggers',
'nigga',
'niggas',
'fag',
'fags',
'faggot',
'faggots',
'whore',
'whores',
'slut',
'sluts',
'cunt',
'cunts',
'dyke',
'dykes',
'spic',
'spics',
'tranny',
'trannies',
'tard',
'tards',
'retard',
'retards',
'bastard',
'bastards',
'rape',
'raped',
'raping',
'blowjob',
'rimjob',
'handjob',
'cunnilingus',
'fellatio',
'sodomy',
'milf',
'cocksucker',
'cocksucking',
'fap',
'nofap',
'fapping',
'alcohol',
'beer',
'beers',
'mead',
'sherry',
'champagne',
'rum',
'brandy',
'whisky',
'whiskey',
'lager',
'gin',
'wine',
'wines',
'liquor',
'liquors',
'heroin',
'cocaine',
];
module.exports = bannedWords;

View file

@ -1,5 +1,6 @@
// Logger utility
import winston from 'winston';
import 'winston-loggly-bulk';
import nconf from 'nconf';
import _ from 'lodash';
import {
@ -20,6 +21,12 @@ if (IS_PROD) {
colorize: false,
prettyPrint: false,
});
logger.add(winston.transports.Loggly, {
inputToken: nconf.get('LOGGLY:TOKEN'),
subdomain: nconf.get('LOGGLY:SUBDOMAIN'),
tags: ['Winston-NodeJS'],
json: true,
});
}
} else if (!IS_TEST || IS_TEST && ENABLE_LOGS_IN_TEST) { // Do not log anything when testing unless specified
logger

View file

@ -1,4 +1,5 @@
import _ from 'lodash';
import nconf from 'nconf';
import analytics from './analyticsService';
import {
getUserInfo,
@ -18,12 +19,16 @@ import {
} from './errors';
import slack from './slack';
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS:TECH_ASSISTANCE_EMAIL');
let api = {};
api.constants = {
UNLIMITED_CUSTOMER_ID: 'habitrpg', // Users with the customerId have an unlimted free subscription
GROUP_PLAN_CUSTOMER_ID: 'group-plan',
GROUP_PLAN_PAYMENT_METHOD: 'Group Plan',
GOOGLE_PAYMENT_METHOD: 'Google',
IOS_PAYMENT_METHOD: 'Apple',
};
function revealMysteryItems (user) {
@ -77,6 +82,7 @@ api.addSubscriptionToGroupUsers = async function addSubscriptionToGroupUsers (gr
*/
api.addSubToGroupUser = async function addSubToGroupUser (member, group) {
let customerIdsToIgnore = [this.constants.GROUP_PLAN_CUSTOMER_ID, this.constants.UNLIMITED_CUSTOMER_ID];
let paymentMethodsToIgnore = [this.constants.GOOGLE_PAYMENT_METHOD, this.constants.IOS_PAYMENT_METHOD];
let data = {
user: {},
@ -109,7 +115,21 @@ api.addSubToGroupUser = async function addSubToGroupUser (member, group) {
if (member.isSubscribed()) {
let memberPlan = member.purchased.plan;
let customerHasCancelledGroupPlan = memberPlan.customerId === this.constants.GROUP_PLAN_CUSTOMER_ID && !member.hasNotCancelled();
if (customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1 && !customerHasCancelledGroupPlan) return;
let ignorePaymentPlan = paymentMethodsToIgnore.indexOf(memberPlan.paymentMethod) !== -1;
let ignoreCustomerId = customerIdsToIgnore.indexOf(memberPlan.customerId) !== -1;
if (ignorePaymentPlan) {
txnEmail(TECH_ASSISTANCE_EMAIL, 'admin-user-subscription-details', [
{name: 'PROFILE_NAME', content: member.profile.name},
{name: 'UUID', content: member._id},
{name: 'EMAIL', content: getUserInfo(member, ['email']).email},
{name: 'PAYMENT_METHOD', content: memberPlan.paymentMethod},
{name: 'PURCHASED_PLAN', content: JSON.stringify(memberPlan)},
{name: 'ACTION_NEEDED', content: 'User has joined group plan. Tell them to cancel subscription then give them free sub.'},
]);
}
if ((ignorePaymentPlan || ignoreCustomerId) && !customerHasCancelledGroupPlan) return;
if (member.hasNotCancelled()) await member.cancelSubscription();

View file

@ -46,6 +46,16 @@ const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
const MAX_UPDATE_RETRIES = 5;
/*
# Spam constants to limit people from sending too many messages too quickly
# SPAM_MESSAGE_LIMIT - The amount of messages that can be sent in a time window
# SPAM_WINDOW_LENGTH - The window length for spam protection in milliseconds
# SPAM_MIN_EXEMPT_CONTRIB_LEVEL - Anyone at or above this level is exempt
*/
export const SPAM_MESSAGE_LIMIT = 2;
export const SPAM_WINDOW_LENGTH = 60000; // 1 minute
export const SPAM_MIN_EXEMPT_CONTRIB_LEVEL = 4;
export let schema = new Schema({
name: {type: String, required: true},
description: String,
@ -105,13 +115,16 @@ export let schema = new Schema({
return {};
}},
},
managers: {type: Schema.Types.Mixed, default: () => {
return {};
}},
}, {
strict: true,
minimize: false, // So empty objects are returned
});
schema.plugin(baseModel, {
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased'],
noSet: ['_id', 'balance', 'quest', 'memberCount', 'chat', 'challengeCount', 'tasksOrder', 'purchased', 'managers'],
private: ['purchased.plan'],
toJSONTransform (plainObj, originalDoc) {
if (plainObj.purchased) plainObj.purchased.active = originalDoc.isSubscribed();
@ -1199,6 +1212,31 @@ schema.methods.removeTask = async function groupRemoveTask (task) {
}, {multi: true}).exec();
};
// Returns true if the user has reached the spam message limit
schema.methods.checkChatSpam = function groupCheckChatSpam (user) {
if (this._id !== TAVERN_ID) {
return false;
} else if (user.contributor && user.contributor.level >= SPAM_MIN_EXEMPT_CONTRIB_LEVEL) {
return false;
}
let currentTime = Date.now();
let userMessages = 0;
for (let i = 0; i < this.chat.length; i++) {
let message = this.chat[i];
if (message.uuid === user._id && currentTime - message.timestamp <= SPAM_WINDOW_LENGTH) {
userMessages++;
if (userMessages >= SPAM_MESSAGE_LIMIT) {
return true;
}
} else if (currentTime - message.timestamp > SPAM_WINDOW_LENGTH) {
break;
}
}
return false;
};
schema.methods.isSubscribed = function isSubscribed () {
let now = new Date();
let plan = this.purchased.plan;

View file

@ -495,7 +495,7 @@ let schema = new Schema({
mp: {type: Number, default: 10},
exp: {type: Number, default: 0},
gp: {type: Number, default: 0},
lvl: {type: Number, default: 1},
lvl: {type: Number, default: 1, min: 1},
// Class System
class: {type: String, enum: ['warrior', 'rogue', 'wizard', 'healer'], default: 'warrior', required: true},

View file

@ -24,4 +24,3 @@ form.chat-form(ng-if='user.flags.communityGuidelinesAccepted' ng-submit='postCha
label
input(type='checkbox', ng-model='user.preferences.reverseChatOrder', ng-change='set({"preferences.reverseChatOrder": user.preferences.reverseChatOrder?true: false})')
span=env.t('reverseChatOrder')

View file

@ -14,7 +14,7 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
a(ng-click="groupPanel = 'chat'")=env.t('groupHomeTitle')
li(ng-show='group.purchased.active')
a(ng-click="groupPanel = 'tasks'")=env.t('groupTasksTitle')
li(ng-show='group.purchased.active && group.leader._id === user._id')
li(ng-show='group.purchased.active && userCanApprove(user._id, group)')
a(ng-click="groupPanel = 'approvals'")=env.t('approvalsTitle')
li
a(ng-click="groupPanel = 'subscription'", ng-show='group.leader._id === user._id && group.purchased.plan.customerId')=env.t('paymentDetails')
@ -65,6 +65,17 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
h4=env.t('assignLeader')
select#group-leader-selection(ng-model='groupCopy._newLeader', ng-options='member.profile.name for member in group.members')
div(ng-if='group.purchased.active')
h4=env.t('addManagers')
.form-group
select#group-leader-selection(ng-model='groupCopy._newManager')
option(ng-repeat='member in group.members', ng-if='member._id !== group.leader.id', ng-value='member._id') {{member.profile.name}}
button.btn.btn-primary.add-manager-button(ng-click='addManager()')=env.t('addManager')
ul
li(ng-repeat='(managerId, value) in groupCopy.managers')
| {{memberProfileName(managerId)}}
button.btn.btn-warning.remove-manager-button(ng-click='removeManager(managerId)')=env.t('removeManager')
div(ng-show='!group._editing')
img.img-rendering-auto.pull-right(ng-show='group.logo', ng-src='{{group.logo}}')
markdown(text='group.description')
@ -105,6 +116,10 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
| {{member.profile.name}}
span(ng-click='clickMember(member._id, true)' ng-if='group.type === "party"')
| (#[strong {{member.stats.hp.toFixed(1)}}] #{env.t('hp')}) {{member.id === user.id ? ' ' + env.t('you') : ''}}
span(ng-if='group.leader._id === member.id')
| {{env.t('leaderMarker')}}
span(ng-show='isManager(member._id, group)')
| {{env.t('managerMarker')}}
.pull-right(ng-if='group.type === "party"')
span.text-success {{member.online ? '&#9679 ' + env.t('online') : ''}}
tr(ng-if='::group.memberCount > group.members.length')
@ -178,6 +193,6 @@ a.pull-right.gem-wallet(ng-if='group.type!="party"', popover-trigger='mouseenter
group-tasks(ng-show="groupPanel == 'tasks'")
group-approvals(ng-show="groupPanel == 'approvals'", ng-if="group.leader._id === user._id", group="group")
group-approvals(ng-show="groupPanel == 'approvals'", ng-if="userCanApprove(user._id, group)", group="group")
+groupSubscription

View file

@ -1,5 +1,5 @@
script(type='text/ng-template', id='partials/groups.tasks.actions.html')
div(ng-if="group.leader._id === user._id", class="col-md-12")
div(ng-if="group.leader._id === user._id || group.managers[user._id]", class="col-md-12")
strong=env.t('assignTask')
group-members-autocomplete(ng-model="assignedMembers")

View file

@ -50,12 +50,15 @@ script(type='text/ng-template', id='partials/options.social.hall.heroes.html')
h4 Update Item
.form-group.well
input.form-control(type='text',placeholder='Path (eg, items.pets.BearCub-Base)',ng-model='hero.itemPath')
small.muted Enter the <strong>item path</strong>. E.g., <code>items.pets.BearCub-Zombie</code> or <code>items.gear.owned.head_special_0</code> or <code>items.gear.equipped.head</code>. <a href='/api/v3/content' target='_blank'>See all paths here.</a> When in doubt, ask Tyler.
small.muted Enter the <strong>item path</strong>. E.g., <code>items.pets.BearCub-Zombie</code> or <code>items.gear.owned.head_special_0</code> or <code>items.gear.equipped.head</code>. You can find all the item paths below.
br
input.form-control(type='text',placeholder='Value (eg, 5)',ng-model='hero.itemVal')
small.muted Enter the <strong>item value</strong>. E.g., <code>5</code> or <code>false</code> or <code>head_warrior_3</code> (respectively from above examples).
h4 Current Items
pre {{::toJson(hero.items,true)}}
small.muted Enter the <strong>item value</strong>. E.g., <code>5</code> or <code>false</code> or <code>head_warrior_3</code>. All values are listed in the All Item Paths section below.
accordion
accordion-group(heading='All Item Paths')
pre {{::allItemPaths}}
accordion-group(heading='Current Items')
pre {{::toJson(hero.items,true)}}
accordion-group(heading='Auth')
h4 Auth
pre {{::toJson(hero.auth)}}

View file

@ -223,10 +223,16 @@ nav.toolbar(ng-controller='MenuCtrl')
a(ng-click='clearMessages(k)', popover=env.t('clear'),popover-placement='right',popover-trigger='mouseenter',popover-append-to-body='true')
span.glyphicon.glyphicon-remove-circle
li(ng-repeat='notification in user.groupNotifications')
a(ng-click='viewGroupApprovalNotification(notification, $index)', data-close-menu)
a(ng-click='viewGroupApprovalNotification(notification, $index, true)', data-close-menu)
span(class="{{::groupApprovalNotificationIcon(notification)}}")
span
| {{notification.data.message}}
a(ng-click='viewGroupApprovalNotification(notification, $index)',
popover=env.t('clear'),
popover-placement='right',
popover-trigger='mouseenter',
popover-append-to-body='true')
span.glyphicon.glyphicon-remove-circle
ul.toolbar-controls
li.toolbar-controls-button

View file

@ -55,7 +55,7 @@ script(type='text/ng-template', id='modals/restore.html')
a.btn.btn-sm.btn-warning(ng-controller='FooterCtrl', ng-click='addMissedDay(1)')=env.t('triggerDay')
.modal-footer
button.btn.btn-default(ng-click='$close()')=env.t('discardChanges')
button.btn.btn-primary(ng-click='restore(); $close();')=env.t('saveAndClose')
button.btn.btn-primary(ng-click='restore()')=env.t('saveAndClose')
script(type='text/ng-template', id='modals/delete.html')
.modal-header

View file

@ -1,8 +1,7 @@
fieldset.option-group.counter_period(ng-if='task.type === "habit" && canEdit(task) && task._edit._advanced')
fieldset.option-group.counter_period(ng-if='task.type === "habit" && task._edit._advanced')
.form-group
legend.option-title=env.t('counterPeriod')
select.form-control(ng-model='task._edit.frequency', ng-disabled='!canEdit(task)')
option(value='daily')=env.t('counterPeriodDay')
option(value='weekly')=env.t('counterPeriodWeek')
option(value='monthly')=env.t('counterPeriodMonth')

View file

@ -2,11 +2,11 @@
// Counter
span(ng-if='showDoubleTaskCounter(task, obj)')
span(tooltip=env.t('habitCounterUp')) +{{task.counterUp}}|
span(tooltip=env.t('habitCounterDown')) -{{task.counterDown}}&nbsp;
span(tooltip='{{env.t("habitCounterUp")}} ({{env.t("resets")}} {{env.t(task.frequency)}})') +{{task.counterUp}}|
span(tooltip='{{env.t("habitCounterDown")}} ({{env.t("resets")}} {{env.t(task.frequency)}})') -{{task.counterDown}}&nbsp;
span(ng-if='showSingleTaskCounter(task, obj)')
span(tooltip=env.t('habitCounter')) {{task.up ? task.counterUp : task.counterDown}}&nbsp;
span(tooltip='{{env.t("habitCounter")}} ({{env.t("resets")}} {{env.t(task.frequency)}})') {{task.up ? task.counterUp : task.counterDown}}&nbsp;
// Due Date
span(ng-if='task.type=="todo" && task.date')