mirror of
https://github.com/HabitRPG/habitica.git
synced 2026-05-13 11:31:23 -05:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 47c156d9b1 | |||
| e6418e4356 | |||
| 3c23989e99 | |||
| 2d1f341256 | |||
| 44adfd611a | |||
| ab50c41287 | |||
| c43abe82fe | |||
| ac0b4a324f | |||
| efa0a325a2 | |||
| bc970d33ac | |||
| 09e432cf32 | |||
| 40aa2e214d | |||
| 9f563b741d | |||
| 9db541f4c3 | |||
| ce4a20e3d8 | |||
| cc7683a871 | |||
| 31b2781333 | |||
| d37d3bc5ac | |||
| ef3a28791e | |||
| c3c2607bca | |||
| 7a6d64f158 | |||
| 836e63246d |
+1
-1
Submodule habitica-images updated: 980a2ee92e...32a4678c6b
Generated
+268
-26
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"version": "5.46.3",
|
||||
"version": "5.46.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habitica",
|
||||
"version": "5.46.3",
|
||||
"version": "5.46.4",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -24,6 +24,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"bullmq": "^5.71.1",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
@@ -48,6 +49,7 @@
|
||||
"heapdump": "^0.3.15",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
@@ -71,7 +73,6 @@
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"redis": "^3.1.2",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
@@ -2824,6 +2825,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
|
||||
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw=="
|
||||
},
|
||||
"node_modules/@ioredis/commands": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz",
|
||||
"integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
|
||||
@@ -3060,6 +3067,84 @@
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -6603,6 +6688,52 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.71.1",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.71.1.tgz",
|
||||
"integrity": "sha512-kOBfdcsHmO6wwmIjpersoVdYQ7jkjTgky4Yop0loc7QwSdgxliSzD69U9ijZuRrkyCJwz5p5eqxeGeQkJ0YGZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "4.9.0",
|
||||
"ioredis": "5.10.1",
|
||||
"msgpackr": "1.11.5",
|
||||
"node-abort-controller": "3.1.1",
|
||||
"semver": "7.7.4",
|
||||
"tslib": "2.8.1",
|
||||
"uuid": "11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
||||
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq/node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/bullmq/node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -7083,6 +7214,15 @@
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cluster-key-slot": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
|
||||
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/coa": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz",
|
||||
@@ -7606,6 +7746,18 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cron-parser": {
|
||||
"version": "4.9.0",
|
||||
"resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz",
|
||||
"integrity": "sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"luxon": "^3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz",
|
||||
@@ -8144,6 +8296,7 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz",
|
||||
"integrity": "sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
@@ -13453,6 +13606,39 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis": {
|
||||
"version": "5.10.1",
|
||||
"resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz",
|
||||
"integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ioredis/commands": "1.5.1",
|
||||
"cluster-key-slot": "^1.1.0",
|
||||
"debug": "^4.3.4",
|
||||
"denque": "^2.1.0",
|
||||
"lodash.defaults": "^4.2.0",
|
||||
"lodash.isarguments": "^3.1.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0",
|
||||
"standard-as-callback": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/ioredis"
|
||||
}
|
||||
},
|
||||
"node_modules/ioredis/node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/iota-array": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz",
|
||||
@@ -14642,6 +14828,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
|
||||
},
|
||||
"node_modules/lodash.defaults": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz",
|
||||
"integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.flattendeep": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
|
||||
@@ -14658,6 +14850,12 @@
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
|
||||
},
|
||||
"node_modules/lodash.isarguments": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz",
|
||||
"integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
@@ -14847,6 +15045,15 @@
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
||||
"integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A=="
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.7.2",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz",
|
||||
"integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
@@ -15865,6 +16072,37 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.5",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
|
||||
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multimatch": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz",
|
||||
@@ -16131,6 +16369,12 @@
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz",
|
||||
"integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw=="
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz",
|
||||
@@ -16202,6 +16446,21 @@
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-loggly-bulk": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/node-loggly-bulk/-/node-loggly-bulk-4.0.1.tgz",
|
||||
@@ -18315,29 +18574,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redis": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz",
|
||||
"integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==",
|
||||
"dependencies": {
|
||||
"denque": "^1.5.0",
|
||||
"redis-commands": "^1.7.0",
|
||||
"redis-errors": "^1.2.0",
|
||||
"redis-parser": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/node-redis"
|
||||
}
|
||||
},
|
||||
"node_modules/redis-commands": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz",
|
||||
"integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ=="
|
||||
},
|
||||
"node_modules/redis-errors": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz",
|
||||
@@ -19853,6 +20089,12 @@
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/standard-as-callback": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz",
|
||||
"integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/static-extend": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz",
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "5.46.3",
|
||||
"version": "5.46.4",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.22.10",
|
||||
@@ -19,6 +19,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"body-parser": "^1.20.3",
|
||||
"bootstrap": "^4.6.2",
|
||||
"bullmq": "^5.71.1",
|
||||
"compression": "^1.8.1",
|
||||
"cookie-session": "^2.1.1",
|
||||
"coupon-code": "^0.4.5",
|
||||
@@ -43,6 +44,7 @@
|
||||
"heapdump": "^0.3.15",
|
||||
"helmet": "^4.6.0",
|
||||
"in-app-purchase": "^1.11.3",
|
||||
"ioredis": "^5.10.1",
|
||||
"js2xmlparser": "^5.0.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwks-rsa": "^2.1.5",
|
||||
@@ -66,7 +68,6 @@
|
||||
"pp-ipn": "^1.1.0",
|
||||
"ps-tree": "^1.0.0",
|
||||
"rate-limiter-flexible": "^2.4.2",
|
||||
"redis": "^3.1.2",
|
||||
"remove-markdown": "^0.5.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"short-uuid": "^4.2.2",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable global-require */
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import requireAgain from 'require-again';
|
||||
import { TAVERN_ID } from '../../../../website/server/models/group';
|
||||
import { defer } from '../../../helpers/api-unit.helper';
|
||||
import worker from '../../../../website/server/libs/worker';
|
||||
|
||||
function getUser () {
|
||||
return {
|
||||
@@ -127,7 +127,7 @@ describe('emails', () => {
|
||||
let sendTxn = null;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
sandbox.stub(worker, 'sendJob').returns(defer().promise);
|
||||
|
||||
const nconfGetStub = sandbox.stub(nconf, 'get');
|
||||
nconfGetStub.withArgs('IS_PROD').returns(true);
|
||||
@@ -149,13 +149,12 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
},
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(value => Array.isArray(value) && value[0].name === mailingInfo.name, 'matches mailing info array'),
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -168,7 +167,7 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).not.to.be.called;
|
||||
expect(worker.sendJob).not.to.be.called;
|
||||
});
|
||||
|
||||
it('throws error when mail target is only a string', async () => {
|
||||
@@ -233,13 +232,12 @@ describe('emails', () => {
|
||||
const mailingInfo = getUser();
|
||||
|
||||
sendTxn(mailingInfo, emailType);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
},
|
||||
}));
|
||||
});
|
||||
@@ -253,15 +251,14 @@ describe('emails', () => {
|
||||
const variables = [];
|
||||
|
||||
sendTxn(mailingInfo, emailType, variables);
|
||||
expect(got.post).to.be.called;
|
||||
expect(got.post).to.be.calledWith('http://example.com/job', sinon.match({
|
||||
json: {
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
expect(worker.sendJob).to.be.called;
|
||||
expect(worker.sendJob).to.be.calledWith('email', sinon.match({
|
||||
identifier: emailType,
|
||||
data: {
|
||||
variables: sinon.match(value => value[0].name === 'BASE_URL', 'matches variables'),
|
||||
personalVariables: sinon.match(value => value[0].rcpt === mailingInfo.email
|
||||
&& value[0].vars[0].name === 'RECIPIENT_NAME'
|
||||
&& value[0].vars[1].name === 'RECIPIENT_UNSUB_URL', 'matches personal variables'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
|
||||
@@ -66,13 +66,15 @@ describe('Amazon Payments - Cancel Subscription', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
subscriptionBlock = common.content.subscriptionBlocks[subKey];
|
||||
subscriptionLength = subscriptionBlock.months * 30;
|
||||
|
||||
@@ -30,12 +30,14 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
amount = common.content.subscriptionBlocks[subKey].price;
|
||||
billingAgreementId = 'billingAgreementId';
|
||||
@@ -246,11 +248,6 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Add existing users
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Set expected amount
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
|
||||
@@ -128,11 +128,12 @@ describe('Purchasing a group plan for group', () => {
|
||||
expect(publicGroup.purchased.plan.planId).to.not.exist;
|
||||
data.groupId = publicGroup._id;
|
||||
|
||||
// Public Guilds are no longer even findable
|
||||
await expect(api.createSubscription(data))
|
||||
.to.eventually.be.rejected.and.to.eql({
|
||||
httpCode: 401,
|
||||
name: 'NotAuthorized',
|
||||
message: i18n.t('onlyPrivateGuildsCanUpgrade'),
|
||||
httpCode: 404,
|
||||
name: 'NotFound',
|
||||
message: i18n.t('groupNotFound'),
|
||||
});
|
||||
|
||||
const updatedGroup = await Group.findById(publicGroup._id).exec();
|
||||
|
||||
@@ -30,13 +30,15 @@ describe('paypal - subscribeCancel', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = groupCustomerId;
|
||||
group.purchased.plan.planId = subKey;
|
||||
group.purchased.plan.lastBillingDate = new Date();
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
nextBillingDate = new Date();
|
||||
|
||||
|
||||
@@ -236,7 +236,7 @@ describe('Stripe - Checkout', () => {
|
||||
const group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
const groupId = group._id;
|
||||
@@ -376,11 +376,13 @@ describe('Stripe - Checkout', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
});
|
||||
|
||||
it('throws if user is not allowed to change group plan', async () => {
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('Stripe - Subscriptions', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
groupId = group._id;
|
||||
@@ -315,12 +315,14 @@ describe('Stripe - Subscriptions', () => {
|
||||
group = generateGroup({
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'public',
|
||||
privacy: 'private',
|
||||
leader: user._id,
|
||||
});
|
||||
group.purchased.plan.customerId = 'customer-id';
|
||||
group.purchased.plan.planId = subKey;
|
||||
await group.save();
|
||||
user.guilds.push(group._id);
|
||||
await user.save();
|
||||
|
||||
groupId = group._id;
|
||||
});
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
createAndPopulateGroup,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import { TAVERN_ID } from '../../../../../website/common/script/constants';
|
||||
|
||||
describe('POST /challenges/:challengeId/join', () => {
|
||||
it('returns error when challengeId is not a valid UUID', async () => {
|
||||
@@ -27,6 +29,37 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
});
|
||||
});
|
||||
|
||||
context('public Guild', () => {
|
||||
let group;
|
||||
let groupLeader;
|
||||
let members;
|
||||
let challenge;
|
||||
before(async () => {
|
||||
({ group, groupLeader, members } = await createAndPopulateGroup({
|
||||
groupDetails: {
|
||||
name: 'test group',
|
||||
type: 'guild',
|
||||
privacy: 'private',
|
||||
},
|
||||
members: 1,
|
||||
upgradeToGroupPlan: true,
|
||||
}));
|
||||
challenge = await generateChallenge(groupLeader, group);
|
||||
// Creation API is shut down, we need to simulate an extant public group
|
||||
await Group.updateOne({ _id: group._id }, { $set: { privacy: 'public' }, $unset: { 'purchased.plan': 1 } }).exec();
|
||||
});
|
||||
|
||||
it('returns error when challengeId is in an old public Guild', async () => {
|
||||
const authorizedUser = members[0]; // eslint-disable-line prefer-destructuring
|
||||
|
||||
await expect(authorizedUser.post(`/challenges/${challenge._id}/join`)).to.eventually.be.rejected.and.eql({
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('challengeNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Joining a valid challenge', () => {
|
||||
let groupLeader;
|
||||
let group;
|
||||
@@ -66,6 +99,15 @@ describe('POST /challenges/:challengeId/join', () => {
|
||||
expect(res.name).to.equal(challenge.name);
|
||||
});
|
||||
|
||||
it('succeeds when it\'s a Tavern challenge, even if the user isn\'t a "member" of Tavern', async () => {
|
||||
const tavern = await groupLeader.get(`/groups/${TAVERN_ID}`);
|
||||
const tavernChallenge = await generateChallenge(groupLeader, tavern, { prize: 1 });
|
||||
const generalUser = await generateUser();
|
||||
|
||||
const res = await generalUser.post(`/challenges/${tavernChallenge._id}/join`);
|
||||
expect(res.name).to.equal(tavernChallenge.name);
|
||||
});
|
||||
|
||||
it('returns challenge data', async () => {
|
||||
const res = await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
|
||||
@@ -62,9 +62,9 @@ describe('GET /groups/:groupId/chat', () => {
|
||||
|
||||
it('returns error if user attempts to fetch a sunset Guild', async () => {
|
||||
await expect(user.get(`/groups/${group._id}/chat`)).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('featureRetired'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,9 +121,9 @@ describe('POST /chat/:chatId/like', () => {
|
||||
|
||||
await expect(user.post(`/groups/${groupWithChat._id}/chat/${message.message.id}/like`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('featureRetired'),
|
||||
code: 404,
|
||||
error: 'NotFound',
|
||||
message: t('groupNotFound'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -193,23 +193,6 @@ describe('POST /groups/:groupId/quests/force-start', () => {
|
||||
expect(questingGroup.quest.members[notInPartyUser._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who have been deleted from quest.members', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].del('/user', {
|
||||
password: 'password',
|
||||
});
|
||||
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
|
||||
|
||||
await sleep(0.5);
|
||||
|
||||
await questingGroup.sync();
|
||||
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.not.exist;
|
||||
});
|
||||
|
||||
it('removes users who don\'t have true value in quest.members from quest.members', async () => {
|
||||
const partyMemberThatRejects = partyMembers[1];
|
||||
const partyMemberThatIgnores = partyMembers[2];
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import {
|
||||
each,
|
||||
map,
|
||||
} from 'lodash';
|
||||
import {
|
||||
checkExistence,
|
||||
createAndPopulateGroup,
|
||||
generateGroup,
|
||||
generateUser,
|
||||
generateChallenge,
|
||||
translate as t,
|
||||
} from '../../../../helpers/api-integration/v3';
|
||||
import {
|
||||
@@ -15,6 +9,7 @@ import {
|
||||
sha1Encrypt as sha1EncryptPassword,
|
||||
} from '../../../../../website/server/libs/password';
|
||||
import * as email from '../../../../../website/server/libs/email';
|
||||
import sendJob from '../../../../../website/server/libs/worker';
|
||||
|
||||
const DELETE_CONFIRMATION = 'DELETE';
|
||||
|
||||
@@ -47,12 +42,13 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
it('sends deletion job to worker', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
|
||||
it('returns an error if excessive feedback is supplied', async () => {
|
||||
@@ -84,53 +80,6 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes the user\'s tasks', async () => {
|
||||
await user.post('/tasks/user', {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
await user.sync();
|
||||
|
||||
// gets the user's tasks ids
|
||||
const ids = [];
|
||||
each(user.tasksOrder, idsForOrder => {
|
||||
ids.push(...idsForOrder);
|
||||
});
|
||||
|
||||
expect(ids.length).to.be.above(0); // make sure the user has some task to delete
|
||||
|
||||
await user.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await Promise.all(map(ids, id => expect(checkExistence('tasks', id)).to.eventually.eql(false)));
|
||||
});
|
||||
|
||||
it('reduces memberCount in challenges user is linked to', async () => {
|
||||
const populatedGroup = await createAndPopulateGroup({
|
||||
members: 2,
|
||||
});
|
||||
|
||||
const { group } = populatedGroup;
|
||||
const authorizedUser = populatedGroup.members[1];
|
||||
|
||||
const challenge = await generateChallenge(populatedGroup.groupLeader, group);
|
||||
await populatedGroup.groupLeader.post(`/challenges/${challenge._id}/join`);
|
||||
await authorizedUser.post(`/challenges/${challenge._id}/join`);
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(2);
|
||||
|
||||
await authorizedUser.del('/user', {
|
||||
password,
|
||||
});
|
||||
|
||||
await challenge.sync();
|
||||
|
||||
expect(challenge.memberCount).to.eql(1);
|
||||
});
|
||||
|
||||
it('sends feedback to the admin email', async () => {
|
||||
sandbox.spy(email, 'sendTxn');
|
||||
|
||||
@@ -158,10 +107,10 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes the user with a legacy sha1 password', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
const textPassword = 'mySecretPassword';
|
||||
const salt = sha1MakeSalt();
|
||||
const sha1HashedPassword = sha1EncryptPassword(textPassword, salt);
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
|
||||
await user.updateOne({
|
||||
'auth.local.hashed_password': sha1HashedPassword,
|
||||
@@ -179,7 +128,8 @@ describe('DELETE /user', () => {
|
||||
await user.del('/user', {
|
||||
password: textPassword,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
|
||||
context('last member of a party', () => {
|
||||
@@ -213,11 +163,12 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
|
||||
it('deletes a Google user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -232,12 +183,13 @@ describe('DELETE /user', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('deletes a Apple user', async () => {
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(true);
|
||||
it('deletes an Apple user', async () => {
|
||||
const workerStub = sandbox.stub(sendJob, 'sendJob');
|
||||
await user.del('/user', {
|
||||
password: DELETE_CONFIRMATION,
|
||||
});
|
||||
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||
expect(workerStub).to.be.calledOnce;
|
||||
workerStub.restore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { getMatchingSwap, makeSubstitutionMap } from '../../website/common/script/content/constants/aprilFools';
|
||||
|
||||
describe('April Fools', () => {
|
||||
describe('getMatchingSwap', () => {
|
||||
it('returns Veggie for 2020', () => {
|
||||
const swap = getMatchingSwap(new Date('2020-04-01'));
|
||||
expect(swap).to.equal('Veggie');
|
||||
});
|
||||
it('returns Alien for 2026', () => {
|
||||
const swap = getMatchingSwap(new Date('2026-04-01'));
|
||||
expect(swap).to.equal('Alien');
|
||||
});
|
||||
it('Cycles through swaps correctly', () => {
|
||||
const swap = getMatchingSwap(new Date('2027-04-01'));
|
||||
expect(swap).to.equal('Veggie');
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeSubstitutionMap', () => {
|
||||
it('returns correct substitution for Veggie', () => {
|
||||
const substitutions = makeSubstitutionMap('Veggie');
|
||||
expect(substitutions.pets['Pet-Wolf-']).to.equal('Pet-Wolf-Veggie');
|
||||
expect(substitutions.pets['Pet-TigerCub-']).to.equal('Pet-TigerCub-Veggie');
|
||||
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Veggie');
|
||||
expect(substitutions.pets.default).to.equal('Pet-Dragon-Veggie');
|
||||
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Veggie');
|
||||
expect(substitutions.pets.noPetIOS).to.equal('Pet-TigerCub-Veggie');
|
||||
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Veggie');
|
||||
});
|
||||
|
||||
it('returns correct substitution for Cryptid', () => {
|
||||
const substitutions = makeSubstitutionMap('Cryptid');
|
||||
expect(substitutions.pets['Pet-Fox-']).to.equal('Pet-Fox-Cryptid');
|
||||
expect(substitutions.pets['Pet-FlyingPig-']).to.equal('Pet-FlyingPig-Cryptid');
|
||||
expect(substitutions.pets['Pet-Yarn-']).to.equal('Pet-BearCub-Cryptid');
|
||||
expect(substitutions.pets.default).to.equal('Pet-Dragon-Cryptid');
|
||||
expect(substitutions.pets.noPet).to.equal('Pet-Wolf-Cryptid');
|
||||
expect(substitutions.pets.noPetAndroid).to.equal('Pet-Cactus-Cryptid');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,57 +1,3 @@
|
||||
.quest_lostMasterclasser4 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_lostMasterclasser4.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_windup.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_solarSystem {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_solarSystem.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.quest_virtualpet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/quest_virtualpet.gif") no-repeat;
|
||||
width: 219px;
|
||||
height: 219px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert, .Pet_HatchingPotion_Veggie, .Pet_HatchingPotion_Windup,
|
||||
.Pet_HatchingPotion_VirtualPet, .Pet_HatchingPotion_Fungi, .Pet_HatchingPotion_Cryptid {
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Dessert {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Dessert.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Veggie {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Veggie.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Windup {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Windup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_VirtualPet {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_VirtualPet.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Fungi {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Fungi.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet_HatchingPotion_Cryptid {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_HatchingPotion_Cryptid.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Gems {
|
||||
display:inline-block;
|
||||
margin-right:5px;
|
||||
@@ -80,6 +26,7 @@
|
||||
margin-left: -3px;
|
||||
margin-top: -18px;
|
||||
}
|
||||
|
||||
.slim_armor_special_0, .broad_armor_special_0, .shield_special_0 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -87,7 +34,6 @@
|
||||
|
||||
/* Critical */
|
||||
.weapon_special_critical {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/weapon_special_critical.gif") no-repeat;
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
margin-left:-12px;
|
||||
@@ -98,6 +44,7 @@
|
||||
.weapon_special_1 {
|
||||
margin-left: -12px;
|
||||
}
|
||||
|
||||
.broad_armor_special_1, .slim_armor_special_1, .head_special_1 {
|
||||
width: 90px;
|
||||
height: 90px;
|
||||
@@ -106,36 +53,15 @@
|
||||
.back_special_heroicAureole {
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/back_special_heroicAureole.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeHelmet.gif") no-repeat;
|
||||
}
|
||||
.head_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalHelmet.gif") no-repeat;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
.broad_armor_special_0,.slim_armor_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-ShadeArmor.gif") no-repeat;
|
||||
}
|
||||
.broad_armor_special_1,.slim_armor_special_1 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/ContributorOnly-Equip-CrystalArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Shield-TormentedSkull.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_0 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Weapon-DarkSoulsBlade.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Pet-Wolf-Cerberus {
|
||||
width: 105px;
|
||||
height: 72px;
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-CerberusPup.gif") no-repeat;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019, .eyewear_special_ks2019, .head_special_ks2019, .shield_special_ks2019 {
|
||||
@@ -143,36 +69,17 @@
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.broad_armor_special_ks2019, .slim_armor_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonArmor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.eyewear_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonVisor.gif") no-repeat;
|
||||
}
|
||||
|
||||
.head_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonHelm.gif") no-repeat;
|
||||
}
|
||||
|
||||
.shield_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonShield.gif") no-repeat;
|
||||
}
|
||||
|
||||
.weapon_special_ks2019 {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Equip-MythicGryphonGlaive.gif") no-repeat;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.Pet-Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Pet-Gryphatrice.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
|
||||
.Pet-Gryphatrice-Jubilant {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Gryphatrice-Jubilant.gif") no-repeat;
|
||||
width: 81px;
|
||||
height: 96px;
|
||||
}
|
||||
@@ -182,39 +89,11 @@
|
||||
height: 135px;
|
||||
}
|
||||
|
||||
.Mount_Head_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Head-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Gryphon-Gryphatrice {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/BackerOnly-Mount-Body-Gryphatrice.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Head_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Head_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.Mount_Body_Dragon-Hydra {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/Mount_Body_Dragon-Hydra.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_airship, .background_clocktower, .background_steamworks {
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
|
||||
.background_airship {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_airship.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_clocktower {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_clocktower.gif") no-repeat;
|
||||
}
|
||||
|
||||
.background_steamworks {
|
||||
background: url("https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_steamworks.gif") no-repeat;
|
||||
}
|
||||
|
||||
[class*="Mount_Head_"],
|
||||
[class*="Mount_Body_"] {
|
||||
margin-top:18px; /* Sprite accommodates 105x123 box */
|
||||
|
||||
@@ -1801,6 +1801,11 @@
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_a_strange_planet {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_a_strange_planet.png');
|
||||
width: 141px;
|
||||
height: 147px;
|
||||
}
|
||||
.background_on_tree_branch {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/background_on_tree_branch.png');
|
||||
width: 141px;
|
||||
@@ -31485,8 +31490,8 @@
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
.slim_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit .png');
|
||||
.slim_armor_armoire_handstandOutfit {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/slim_armor_armoire_handstandOutfit.png');
|
||||
width: 114px;
|
||||
height: 90px;
|
||||
}
|
||||
@@ -53223,6 +53228,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-BearCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-BearCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -53713,6 +53723,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Cactus-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Cactus-Amber.png');
|
||||
width: 81px;
|
||||
@@ -54503,6 +54518,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Dragon-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Dragon-Amber.png');
|
||||
width: 81px;
|
||||
@@ -54998,6 +55018,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-FlyingPig-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-FlyingPig-Amber.png');
|
||||
width: 81px;
|
||||
@@ -55333,6 +55358,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Fox-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Fox-Amber.png');
|
||||
width: 81px;
|
||||
@@ -56113,6 +56143,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-LionCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-LionCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -56718,6 +56753,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-PandaCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-PandaCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -58113,6 +58153,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-TigerCub-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-TigerCub-Amber.png');
|
||||
width: 81px;
|
||||
@@ -58758,6 +58803,11 @@
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Alien {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Alien.png');
|
||||
width: 81px;
|
||||
height: 99px;
|
||||
}
|
||||
.Pet-Wolf-Amber {
|
||||
background-image: url('https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet-Wolf-Amber.png');
|
||||
width: 81px;
|
||||
|
||||
@@ -321,10 +321,11 @@ export default {
|
||||
return null;
|
||||
},
|
||||
petClass () {
|
||||
const foolEvent = this.currentEventList?.find(event => event.aprilFools && moment()
|
||||
.isBetween(event.start, event.end));
|
||||
if (foolEvent) {
|
||||
return this.foolPet(this.member.items.currentPet, foolEvent.aprilFools);
|
||||
const substitutionEvent = this.currentEventList?.find(event => event.spriteSubstitutions
|
||||
&& moment().isBetween(event.start, event.end));
|
||||
if (substitutionEvent && substitutionEvent.spriteSubstitutions.pets) {
|
||||
return this.foolPet(`Pet-${this.member.items.currentPet}`,
|
||||
substitutionEvent.spriteSubstitutions.pets);
|
||||
}
|
||||
if (this.member?.items.currentPet) return `Pet-${this.member.items.currentPet}`;
|
||||
return '';
|
||||
|
||||
@@ -187,7 +187,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="user.purchased.background.birthday_bash"
|
||||
v-if="user.purchased.background.birthday_bash
|
||||
|| user.purchased.background.on_a_strange_planet"
|
||||
>
|
||||
<div
|
||||
class="row justify-content-center title-row mb-3"
|
||||
|
||||
@@ -182,12 +182,10 @@ export default {
|
||||
return 'GreyedOut';
|
||||
},
|
||||
imageName () {
|
||||
const foolEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.aprilFools);
|
||||
if (this.isOwned() && foolEvent) {
|
||||
if (this.isSpecial()) return `stable_${this.foolPet(this.item.key, foolEvent.aprilFools)}`;
|
||||
const petString = `${this.item.eggKey}-${this.item.key}`;
|
||||
return `stable_${this.foolPet(petString, foolEvent.aprilFools)}`;
|
||||
const substitutionEvent = this.currentEventList?.find(event => moment()
|
||||
.isBetween(event.start, event.end) && event.spriteSubstitutions);
|
||||
if (this.isOwned() && substitutionEvent && substitutionEvent.spriteSubstitutions.pets) {
|
||||
return `stable_${this.foolPet(`Pet-${this.item.key}`, substitutionEvent.spriteSubstitutions.pets)}`;
|
||||
}
|
||||
|
||||
if (this.isOwned() || (this.mountOwned() && this.isHatchable())) {
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
v-if="addNew || availableToSelect.length > 0"
|
||||
:class="{
|
||||
'item-group': true,
|
||||
'add-new': availableToSelect.length === 0 && search !== '',
|
||||
'add-new': search !== '' && !hasExactMatch,
|
||||
'scroll': availableToSelect.length > 5
|
||||
}"
|
||||
>
|
||||
@@ -86,7 +86,7 @@
|
||||
</b-dropdown-item-button>
|
||||
|
||||
<div
|
||||
v-if="addNew"
|
||||
v-if="addNew && search !== '' && !hasExactMatch"
|
||||
class="hint"
|
||||
>
|
||||
{{ $t('pressEnterToAddTag', { tagName: search }) }}
|
||||
@@ -171,7 +171,8 @@ $itemHeight: 2rem;
|
||||
max-height: #{5*$itemHeight};
|
||||
|
||||
&.add-new {
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
height: auto;
|
||||
|
||||
.hint {
|
||||
display: block;
|
||||
@@ -245,6 +246,7 @@ export default {
|
||||
selected: this.selectedItems,
|
||||
search: '',
|
||||
textbox: null,
|
||||
itemsAdded: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -272,6 +274,16 @@ export default {
|
||||
|
||||
return filteredItems;
|
||||
},
|
||||
hasExactMatch () {
|
||||
const searchTerm = this.search.trim().toLowerCase();
|
||||
if (!searchTerm) return false;
|
||||
if (this.itemsAdded.indexOf(searchTerm) !== -1) return true;
|
||||
if (this.availableToSelect.length === 0) return false;
|
||||
if (this.availableToSelect[0].name.toLowerCase() === searchTerm) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selected () {
|
||||
@@ -310,6 +322,7 @@ export default {
|
||||
this.closeSelectPopup();
|
||||
},
|
||||
selectItem (item) {
|
||||
if (!item) return;
|
||||
this.selectedItems.push(item.id);
|
||||
this.$emit('toggle', item.id);
|
||||
this.preventHide = true;
|
||||
@@ -371,9 +384,16 @@ export default {
|
||||
handleSubmit () {
|
||||
if (!this.addNew) return;
|
||||
const { search } = this;
|
||||
this.$emit('addNew', search);
|
||||
|
||||
this.search = '';
|
||||
// If there is a existing tag
|
||||
if (this.hasExactMatch) {
|
||||
this.selectItem(this.availableToSelect[0]);
|
||||
this.search = '';
|
||||
} else {
|
||||
// Creating a new tag as there is no existing tag present
|
||||
this.$emit('addNew', search);
|
||||
this.itemsAdded.push(search.toLowerCase());
|
||||
this.search = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,56 +1,14 @@
|
||||
import includes from 'lodash/includes';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
foolPet (pet, prank) {
|
||||
const SPECIAL_PETS = [
|
||||
'Bear-Veteran',
|
||||
'BearCub-Polar',
|
||||
'Cactus-Veteran',
|
||||
'Dragon-Hydra',
|
||||
'Dragon-Veteran',
|
||||
'Fox-Veteran',
|
||||
'Gryphatrice-Jubilant',
|
||||
'Gryphon-Gryphatrice',
|
||||
'Gryphon-RoyalPurple',
|
||||
'Hippogriff-Hopeful',
|
||||
'Jackalope-RoyalPurple',
|
||||
'JackOLantern-Base',
|
||||
'JackOLantern-Ghost',
|
||||
'JackOLantern-Glow',
|
||||
'JackOLantern-RoyalPurple',
|
||||
'Lion-Veteran',
|
||||
'MagicalBee-Base',
|
||||
'Mammoth-Base',
|
||||
'MantisShrimp-Base',
|
||||
'Orca-Base',
|
||||
'Phoenix-Base',
|
||||
'Tiger-Veteran',
|
||||
'Turkey-Base',
|
||||
'Turkey-Gilded',
|
||||
'Wolf-Cerberus',
|
||||
'Wolf-Veteran',
|
||||
];
|
||||
const BASE_PETS = [
|
||||
'BearCub',
|
||||
'Cactus',
|
||||
'Dragon',
|
||||
'FlyingPig',
|
||||
'Fox',
|
||||
'LionCub',
|
||||
'PandaCub',
|
||||
'TigerCub',
|
||||
'Wolf',
|
||||
];
|
||||
if (!pet) return `Pet-TigerCub-${prank}`;
|
||||
if (SPECIAL_PETS.indexOf(pet) !== -1) {
|
||||
return `Pet-Dragon-${prank}`;
|
||||
foolPet (pet, substitutions) {
|
||||
if (!pet || pet === 'Pet-') return substitutions.noPet;
|
||||
if (substitutions[pet]) return substitutions[pet];
|
||||
for (const key in substitutions) {
|
||||
if (pet.startsWith(key)) {
|
||||
return substitutions[key];
|
||||
}
|
||||
}
|
||||
const species = pet.slice(0, pet.indexOf('-'));
|
||||
if (includes(BASE_PETS, species)) {
|
||||
return `Pet-${species}-${prank}`;
|
||||
}
|
||||
return `Pet-BearCub-${prank}`;
|
||||
return substitutions.default;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<div
|
||||
v-once
|
||||
class="feedback"
|
||||
class="feedback mt-3"
|
||||
v-html="$t('feedback')"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -85,6 +85,13 @@ const router = new VueRouter({
|
||||
props: true,
|
||||
},
|
||||
{ name: 'profile', path: '/user/profile' },
|
||||
{
|
||||
name: 'avatar',
|
||||
path: '/avatar',
|
||||
children: [
|
||||
{ name: 'backgrounds', path: 'backgrounds' },
|
||||
],
|
||||
},
|
||||
{ name: 'stats', path: '/user/stats' },
|
||||
{ name: 'achievements', path: '/user/achievements' },
|
||||
{
|
||||
@@ -410,6 +417,13 @@ router.beforeEach(async (to, from, next) => {
|
||||
router.app.$root.$emit('bv::hide::modal', 'profile');
|
||||
}
|
||||
|
||||
if (to.name === 'backgrounds') {
|
||||
store.state.avatarEditorOptions.editingUser = true;
|
||||
store.state.avatarEditorOptions.startingPage = 'backgrounds';
|
||||
router.app.$root.$emit('bv::show::modal', 'avatar-modal');
|
||||
return null;
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
|
||||
@@ -1086,6 +1086,8 @@
|
||||
"eventBackgrounds": "Event Backgrounds",
|
||||
"backgroundBirthdayBashText": "Birthday Bash",
|
||||
"backgroundBirthdayBashNotes": "Habitica's having a birthday party, and everyone's invited!",
|
||||
"backgroundOnAStrangePlanetText": "On a Strange Planet",
|
||||
"backgroundOnAStrangePlanetNotes": "Venture where no Habitican has gone before: On a Strange Planet.",
|
||||
|
||||
"monthlyBackgrounds": "Monthly Backgrounds"
|
||||
}
|
||||
|
||||
@@ -356,6 +356,7 @@
|
||||
"hatchingPotionBalloon": "Balloon",
|
||||
"hatchingPotionCryptid": "Cryptid",
|
||||
"hatchingPotionOpal": "Opal",
|
||||
"hatchingPotionAlien": "Alien",
|
||||
|
||||
"hatchingPotionNotes": "Pour this on an egg, and it will hatch as a <%= potText %> Pet.",
|
||||
"premiumPotionUnlimitedNotes": "Not usable on Quest Pet eggs.",
|
||||
|
||||
@@ -221,7 +221,7 @@
|
||||
"questTRexUndeadBoss": "Skeletal Tyrannosaur",
|
||||
"questTRexUndeadRageTitle": "Skeleton Healing",
|
||||
"questTRexUndeadRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Skeletal Tyrannosaur will heal 30% of its remaining health!",
|
||||
"questTRexUndeadRageEffect": "`Skeletal Tyrannosaur uses SKELETON HEALING!`\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!",
|
||||
"questTRexUndeadRageEffect": "Skeletal Tyrannosaur uses SKELETON HEALING!\n\nThe monster lets forth an unearthly roar, and some of its damaged bones knit back together!",
|
||||
|
||||
"questTRexDropTRexEgg": "Tyrannosaur (Egg)",
|
||||
"questTRexUnlockText": "Unlocks Tyrannosaur Eggs for purchase in the Market",
|
||||
@@ -282,7 +282,7 @@
|
||||
"questDilatoryDistress2Boss": "Water Skull Swarm",
|
||||
"questDilatoryDistress2RageTitle": "Swarm Respawn",
|
||||
"questDilatoryDistress2RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Water Skull Swarm will heal 30% of its remaining health!",
|
||||
"questDilatoryDistress2RageEffect": "`Water Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!",
|
||||
"questDilatoryDistress2RageEffect": "Water Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls pour forth from the crevasse, bolstering the swarm!",
|
||||
"questDilatoryDistress2DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questDilatoryDistress2DropCottonCandyBluePotion": "Cotton Candy Blue Hatching Potion",
|
||||
"questDilatoryDistress2DropHeadgear": "Fire Coral Circlet (Headgear)",
|
||||
@@ -398,7 +398,7 @@
|
||||
"questAxolotlUnlockText": "Unlocks Axolotl Eggs for purchase in the Market",
|
||||
"questAxolotlRageTitle": "Axolotl Regeneration",
|
||||
"questAxolotlRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Magical Axolotl will heal 30% of its remaining health!",
|
||||
"questAxolotlRageEffect": "`Magical Axolotl uses AXOLOTL REGENERATION!`\n\n`A curtain of colorful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!`",
|
||||
"questAxolotlRageEffect": "Magical Axolotl uses AXOLOTL REGENERATION!\n\nA curtain of colorful bubbles obscures the monster for a moment, and when it clears, some of its wounds have vanished!",
|
||||
|
||||
"questTurtleText": "Guide the Turtle",
|
||||
"questTurtleNotes": "Help! This giant sea turtle cannot find her way to her nesting beach. She returns there every year to lay her eggs, but this year Inkomplete Bay is filled with toxic Task Flotsam made of red Dailies and unchecked To Do's. \"She's thrashing in a panic!\" @JessicaChase says.<br><br>@UncommonCriminal nods. \"It's because her guiding senses are fogged and confused.\"<br><br>@Scarabsi grabs your arm. \"Can you help clear the Task Flotsam blocking her path? It may be hazardous, but we have to help her!\"",
|
||||
@@ -435,7 +435,7 @@
|
||||
"questTaskwoodsTerror1Boss": "Fire Skull Swarm",
|
||||
"questTaskwoodsTerror1RageTitle": "Swarm Respawn",
|
||||
"questTaskwoodsTerror1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Fire Skull Swarm will heal 30% of its remaining health!",
|
||||
"questTaskwoodsTerror1RageEffect": "`Fire Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!",
|
||||
"questTaskwoodsTerror1RageEffect": "Fire Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls swirl around you in a gout of flame!",
|
||||
"questTaskwoodsTerror1DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questTaskwoodsTerror1DropRedPotion": "Red Hatching Potion",
|
||||
"questTaskwoodsTerror1DropHeadgear": "Pyromancer's Turban (Headgear)",
|
||||
@@ -507,7 +507,7 @@
|
||||
"questStoikalmCalamity1Boss": "Earth Skull Swarm",
|
||||
"questStoikalmCalamity1RageTitle": "Swarm Respawn",
|
||||
"questStoikalmCalamity1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Earth Skull Swarm will heal 30% of its remaining health!",
|
||||
"questStoikalmCalamity1RageEffect": "`Earth Skull Swarm uses SWARM RESPAWN!`\n\nMore skulls break free from the ground, their teeth chattering in the cold!",
|
||||
"questStoikalmCalamity1RageEffect": "Earth Skull Swarm uses SWARM RESPAWN!\n\nMore skulls break free from the ground, their teeth chattering in the cold!",
|
||||
"questStoikalmCalamity1DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questStoikalmCalamity1DropDesertPotion": "Desert Hatching Potion",
|
||||
"questStoikalmCalamity1DropArmor": "Mammoth Rider Armor",
|
||||
@@ -554,7 +554,7 @@
|
||||
"questMayhemMistiflying1Boss": "Air Skull Swarm",
|
||||
"questMayhemMistiflying1RageTitle": "Swarm Respawn",
|
||||
"questMayhemMistiflying1RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Air Skull Swarm will heal 30% of its remaining health!",
|
||||
"questMayhemMistiflying1RageEffect": "`Air Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls come whirling out of the clouds!",
|
||||
"questMayhemMistiflying1RageEffect": "Air Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls come whirling out of the clouds!",
|
||||
"questMayhemMistiflying1DropSkeletonPotion": "Skeleton Hatching Potion",
|
||||
"questMayhemMistiflying1DropWhitePotion": "White Hatching Potion",
|
||||
"questMayhemMistiflying1DropArmor": "Roguish Rainbow Messenger Robes (Armor)",
|
||||
@@ -623,7 +623,7 @@
|
||||
"questLostMasterclasser3Boss": "Void Skull Swarm",
|
||||
"questLostMasterclasser3RageTitle": "Swarm Respawn",
|
||||
"questLostMasterclasser3RageDescription": "Swarm Respawn: This bar fills when you don't complete your Dailies. When it is full, the Void Skull Swarm will heal 30% of its remaining health!",
|
||||
"questLostMasterclasser3RageEffect": "`Void Skull Swarm uses SWARM RESPAWN!`\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!",
|
||||
"questLostMasterclasser3RageEffect": "Void Skull Swarm uses SWARM RESPAWN!\n\nEmboldened by their victories, more skulls scream down from the heavens, bolstering the swarm!",
|
||||
"questLostMasterclasser3DropBodyAccessory": "Aether Amulet (Body Accessory)",
|
||||
"questLostMasterclasser3DropBasePotion": "Base Hatching Potion",
|
||||
"questLostMasterclasser3DropGoldenPotion": "Golden Hatching Potion",
|
||||
@@ -637,7 +637,7 @@
|
||||
"questLostMasterclasser4Boss": "Anti'zinnya",
|
||||
"questLostMasterclasser4RageTitle": "Siphoning Void",
|
||||
"questLostMasterclasser4RageDescription": "Siphoning Void: This bar fills when you don't complete your Dailies. When it is full, Anti'zinnya will remove the party's Mana!",
|
||||
"questLostMasterclasser4RageEffect": "`Anti'zinnya uses SIPHONING VOID!` In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!",
|
||||
"questLostMasterclasser4RageEffect": "Anti'zinnya uses SIPHONING VOID! In a twisted inversion of the Ethereal Surge spell, you feel your magic drain away into the darkness!",
|
||||
"questLostMasterclasser4DropBackAccessory": "Aether Cloak (Back Accessory)",
|
||||
"questLostMasterclasser4DropWeapon": "Aether Crystals (Two-Handed Weapon)",
|
||||
"questLostMasterclasser4DropMount": "Invisible Aether Mount",
|
||||
@@ -804,7 +804,7 @@
|
||||
"questWaffleBoss": "Awful Waffle",
|
||||
"questWaffleRageTitle": "Maple Mire",
|
||||
"questWaffleRageDescription": "Maple Mire: This bar fills when you don't complete your Dailies. When it is full, the Awful Waffle will subtract from the pending damage that party members have built up!",
|
||||
"questWaffleRageEffect": "`Awful Waffle uses MAPLE MIRE!` Sticky sappy syrup slows your swings and spells! Pending damage reduced.",
|
||||
"questWaffleRageEffect": "Awful Waffle uses MAPLE MIRE! Sticky sappy syrup slows your swings and spells! Pending damage reduced.",
|
||||
"questWaffleDropDessertPotion": "Confection Hatching Potion",
|
||||
"questWaffleUnlockText": "Unlocks Confection Hatching Potions for purchase in the Market",
|
||||
|
||||
@@ -875,7 +875,7 @@
|
||||
"questVirtualPetBoss": "Wotchimon",
|
||||
"questVirtualPetRageTitle": "The Beepening",
|
||||
"questVirtualPetRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Wotchimon will take away some of your party's pending damage!",
|
||||
"questVirtualPetRageEffect": "`Wotchimon uses Bothersome Beep!` Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.",
|
||||
"questVirtualPetRageEffect": "Wotchimon uses Bothersome Beep! Wotchimon sounds a bothersome beep, and its happiness bar suddenly disappears! Pending damage reduced.",
|
||||
"questVirtualPetDropVirtualPetPotion": "Virtual Pet Hatching Potion",
|
||||
"questVirtualPetUnlockText": "Unlocks Virtual Pet Hatching Potion for purchase in the Market",
|
||||
|
||||
@@ -885,7 +885,7 @@
|
||||
"questPinkMarbleBoss": "Cupido",
|
||||
"questPinkMarbleRageTitle": "Pink Punch",
|
||||
"questPinkMarbleRageDescription": "This bar fills when you don't complete your Dailies. When it is full, Cupido will take away some of your party's pending damage!",
|
||||
"questPinkMarbleRageEffect": "`Cupido uses Pink Punch!` That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.",
|
||||
"questPinkMarbleRageEffect": "Cupido uses Pink Punch! That wasn't affectionate at all! Your partymates are taken aback. Pending damage reduced.",
|
||||
"questPinkMarbleDropPinkMarblePotion": "Pink Marble Hatching Potion",
|
||||
"questPinkMarbleUnlockText": "Unlocks Pink Marble Hatching Potions for purchase in the Market.",
|
||||
|
||||
@@ -997,5 +997,15 @@
|
||||
"questFungiRageDescription": "This bar fills when you don't complete your Dailies. When it's full, the Moody Mushroom will take away some of your party's pending damage",
|
||||
"questFungiRageEffect": "A Mist emanates from the Moody Mushroom and surrounds your party, dampening the mood and subduing your magic. The party's MP is reduced!",
|
||||
"questFungiDropFungiPotion": "Fungi Hatching Potion",
|
||||
"questFungiUnlockText": "Unlocks Fungi Hatching Potions for purchase in the Market."
|
||||
"questFungiUnlockText": "Unlocks Fungi Hatching Potions for purchase in the Market.",
|
||||
|
||||
"questAlienText": "Invasion of the Motivation Snatchers",
|
||||
"questAlienNotes": "It’s been a strange few days in Habitica. The great flying saucer still hovers near the Flourishing Fields. It hums oddly. Why is it lingering? April Fool’s Day has passed, and the Master of Rogues' time in the spotlight has ended.<br><br>You wander toward the light of the space ship. You may as well check it out and get a few steps in while you’re at it.<br><br>As you get closer you see the April Fool, looking a bit grim. His face appears greenish in the light of the ship’s beam.<br><br>”'Twas my plan to get some potions for everyone, a little gift so all can enjoy their little extraterrestrial pals again! But I just can’t work up the gumption… I do believe I know why,” the Fool says, nodding toward the beam.<br><br>Little symbols are being sucked up into the ship. It’s all your checked off tasks! No wonder your motivation’s been lackluster.<br><br>”Our motivation is being abducted!” you exclaim. “We have to rescue it before it ends up in deep space somewhere!”<br><br>The Fool smiles. “Concentrate your thoughts on the tasks you know you need to finish! I’ll do the rest with a bit of magic.”",
|
||||
"questAlienCompletion": "You’ve managed to wrestle back the stolen motivation with your determination and the Fool’s magic power. As you feel your drive returning, the UFO descends, and a ramp slowly comes out along with a large, green, one-eyed creature. While strange-looking, it doesn’t seem threatening.<br><br>“Looks like we went a little far trying to harvest a little extra encouragement from your fine city,” it says. “Apologies for that, and fantastic work getting it back. The extra aura of your efforts actually charged up the ship’s engine enough to get us home! Please, take these with our thanks.”<br><br>“Ooh potions,” says the Fool, “how delightful, and how convenient for me that you have them all ready to go!”",
|
||||
"questAlienBoss": "Encouragement Thief, the Extraterrestrial",
|
||||
"questAlienRageTitle": "Intergalactic Impediment",
|
||||
"questAlienRageDescription": "This bar fills when you don't complete your Dailies. When it is full, the Extraterrestrial will discourage you by recovering some of its Health!",
|
||||
"questAlienRageEffect": "Encouragement Thief uses Intergalactic Impediment! You've backslid right through hyperspace. Your opponent recovers HP!",
|
||||
"questAlienDropAlienPotion": "Alien Hatching Potion",
|
||||
"questAlienUnlockText": "Unlocks Alien Hatching Potion for purchase in the Market"
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"resetAccPop": "Start over, removing all levels, gold, gear, history, and tasks.",
|
||||
"deleteAccount": "Delete Account",
|
||||
"deleteAccPop": "Cancel and remove your Habitica account.",
|
||||
"feedback": "If you'd like to give us feedback, please enter it below - we'd love to hear your feedback! It will be anonymous unless you choose to enter your contact details. Don't speak English well? No problem! Use the language you prefer.",
|
||||
"feedback": "We'd love to hear your feedback! If you'd like to share any, enter it below. It will be anonymous unless you choose to include your contact details.",
|
||||
"feedbackPlaceholder": "Add your feedback",
|
||||
"dataExport": "Data Export",
|
||||
"saveData": "Here are a few options for saving your data.",
|
||||
@@ -82,8 +82,8 @@
|
||||
"resetText2": "Another option is using an <b>Orb of Rebirth</b>, which will reset everything else while preserving your Tasks and Equipment.",
|
||||
"resetTextLocal": "If you're absolutely certain, type your password into the text box below.",
|
||||
"resetTextSocial": "If you're absolutely certain, type <b>\"<%= magicWord %>\"</b> into the text box below.",
|
||||
"deleteLocalAccountText": "<b>Are you sure?</b> This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type your password into the text box below.",
|
||||
"deleteSocialAccountText": "<b>Are you sure?</b> This will delete your account forever, and it can never be restored! You will need to register a new account to use Habitica again. Banked or spent Gems will not be refunded. If you're absolutely certain, type <b>\"<%= magicWord %>\"</b> into the text box below.",
|
||||
"deleteLocalAccountText": "<b>Are you sure?</b> This action is permanent. Deleting your account will remove all of your data, and it cannot be recovered. Gems will not be refunded.<br><br>Please allow up to 24 hours for account deletion to complete, and up to 30 days for analytics data to be removed if you opted in. Once complete, you'll be able to register for a new Habitica account using your previous login information.<br><br>To continue, type your password below.",
|
||||
"deleteSocialAccountText": "<b>Are you sure?</b> This action is permanent. Deleting your account will remove all of your data, and it cannot be recovered. Gems will not be refunded.<br><br>Please allow up to 24 hours for account deletion to complete, and up to 30 days for analytics data to be removed if you opted in. Once complete, you'll be able to register for a new Habitica account using your previous login information.<br><br>To continue, type <%= magicWord %> below.",
|
||||
"API": "API",
|
||||
"APICopied": "API token copied to clipboard.",
|
||||
"APITokenTitle": "API Token",
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"clearBrowserData": "브라우저 데이터 삭제하기",
|
||||
"footerMobile": "모바일",
|
||||
"footerCommunity": "커뮤니티",
|
||||
"marketing3Header": "앱 및 확장",
|
||||
"marketing2Header": "친구와 경쟁하고, 관심 그룹에 참여하세요",
|
||||
"marketing1Lead1Title": "당신의 삶, 롤플레잉 게임",
|
||||
"marketing3Header": "Habitica의 더 많은 활용",
|
||||
"marketing2Header": "친구와 한 팀이 되세요",
|
||||
"marketing1Lead1Title": "삶을 게임처럼",
|
||||
"logout": "로그아웃",
|
||||
"login": "로그인",
|
||||
"history": "역사학",
|
||||
@@ -39,22 +39,22 @@
|
||||
"learnMore": "더 배우기",
|
||||
"wrongPassword": "잘못된 암호.",
|
||||
"muchmuchMore": "그리고 훨씬 더!",
|
||||
"marketing1Lead2Title": "좋은 장비를 얻으세요",
|
||||
"marketing1Lead1": "Habitica는 실생활의 습관들을 향상시키도록 돕는 비디오 게임입니다. 당신의 모든 과제들(습관, 일과 및 해야할 일)을 당신이 정복해야할 작은 괴물들로 바꾸어서 당신의 삶을 게임화하는 것입니다. 당신이 잘 할수록 게임에서 더 큰 성과를 얻습니다. 만약 잘못한다면 당신의 캐릭터는 게임에서 퇴보하게 될 것입니다.",
|
||||
"marketing1Header": "게임을 해서 당신의 습관들을 향상시키세요",
|
||||
"marketing1Lead2Title": "멋있는 장비를 얻으세요",
|
||||
"marketing1Lead1": "Habitica는 해야할 일들과 씨름하는 여러분을 위한 완벽한 앱입니다. Habitica는 여러분이 과업을 완수할 때마다 골드, 경험치, 아이템 같은 익숙한 보상을 제공합니다. 여러분이 생산성과 성취감을 더 잘 느끼도록요. 과업을 잘 완수할수록, 여러분은 게임 속에서도 발전합니다.",
|
||||
"marketing1Header": "좋은 습관에 한 걸음씩 가까워지세요!",
|
||||
"invalidEmail": "암호 재설정을 수행하려면 유효한 전자 메일 주소가 필요합니다.",
|
||||
"guidanceForBlacksmiths": "대장장이를 위한 안내",
|
||||
"marketing1Lead3Title": "무작위로 나오는 상품을 찾아보세요",
|
||||
"marketing1Lead2": "당신의 아바타를 꾸미기 위해서 습관들을 개선시키세요. 당신이 획득한 좋은 장비들 자랑해보세요!",
|
||||
"marketing1Lead3Title": "노력의 보상을 받으세요",
|
||||
"marketing1Lead2": "과제를 완수하세요. 그리고 검과 갑옷, 수많은 골드를 모으세요. 수백 종류가 만들어내는 끝없는 조합을 즐기세요. 능력치나 스타일을 최적화해보세요. 어쩌면 둘 다를요! ",
|
||||
"mobileAndroid": "안드로이드",
|
||||
"marketing4Lead1": "조직적 사용",
|
||||
"marketing2Lead2Title": "몬스터와 싸우기",
|
||||
"marketing2Lead1": "Habitica를 혼자서 하는 동안에도 당신이 협력하거나 경쟁 그리고 서로 책임을 묻기 시작할 때 더 빛이 나게 됩니다. 자기계발 프로그램의 가장 효과적인 부분은 사회적 책임이며, 비디오 게임보다 더 책임과 경쟁을 위한 환경이 어디 있겠습니까?",
|
||||
"marketing2Lead2": "전투가 없으면 롤플레잉 게임이겠어요? 파티와 함께 몬스터와 싸우세요. 몬스터들은 \"초책임모드\"입니다. - 당신이 운동을 빼먹은 날은 몬스터가 *모두*를 다치게 만드는 날입니다.",
|
||||
"marketing2Lead3Title": "서로에게 도전심이 들게 해 보세요",
|
||||
"marketing2Lead3": "도전과제는 친구들과 낯선 사람들과 경쟁을 하게 해줍니다. 도전과제가 끝났을때 최고인 사람이 특별한 상을 받게 됩니다.",
|
||||
"marketing4Lead1": "교육만큼 작게 게임화하기 좋은 게 없죠! 매일 있는 수업의 지루함을 게임과 함께 깨버리세요. Habitica는 숙제를 따라가고, 학급 도전을 만들고, 학생들이 자신의 성취를 자랑하는 즐거운 방법이 될 겁니다.",
|
||||
"marketing2Lead2Title": "퀘스트에서 몬스터와 싸우기",
|
||||
"marketing2Lead1": "다른 사람이 있으면 동기가 샘솟습니다. 협력과 경쟁, 상호작용을 통해서요! Habitica는 사회적 책임을 활용합니다. 자기계발에 언제나 효율적인 길이죠.",
|
||||
"marketing2Lead2": "친구와 함께 수백 개의 퀘스트에 도전하고 전투에 참여하세요. 퀘스트 몬스터를 위해서는 최대한의 책임이 필요합니다. 치실을 까먹으면 모두에게 대미지가 가해집니다!",
|
||||
"marketing2Lead3Title": "서로 도전해 보세요",
|
||||
"marketing2Lead3": "저희가 만든 도전에 참여해보세요. 여러분의 관심과 목표에 맞는 과제 리스트를 받아보세요. 최선을 다해 경쟁하고 승자로서 보석을 가져가세요!",
|
||||
"marketing4Lead1Title": "교육의 게임화",
|
||||
"marketing4Header": "조직적 용도",
|
||||
"marketing4Header": "가사일 너머",
|
||||
"marketing4Lead2": "건강 관리 비용이 증가하고 있으며 무엇인가를 제공해야 합니다. 수백개의 프로그램들이 비용절감과 건강개선을 위해 만들어지고 있습니다. 우리는 Habitica가 건강한 생활 방식을 향한 실질적인 길을 열 수 있다고 믿습니다.",
|
||||
"username": "아이디",
|
||||
"emailOrUsername": "이메일 혹은 아이디 (대소문자 구별)",
|
||||
@@ -68,11 +68,11 @@
|
||||
"invalidLoginCredentialsLong": "이런! 당신의 이메일 주소 / 아이디 혹은 비밀번호가 바르지 않습니다.\n- 올바르게 입력됐는지 확인해주세요. 아이디와 비밀번호는 대소문자를 구별합니다.\n- Facebook이나 Google 연동으로 계정을 생성하셨다면, 다시 한 번 시도해보세요.\n- 만약 비밀번호를 잊으셨다면, \"비밀번호 찾기\"를 누르세요.",
|
||||
"usernamePlaceholder": "예: Honggildong",
|
||||
"emailUsernamePlaceholder": "예: Honggildong 또는 gildong@example.com",
|
||||
"marketing1Lead3": "\"확률적 보상\" 시스템을 통해 도박처럼 스릴 있는 동기 부여를 얻으세요. Habitica는 스스로를 다양한 방식으로 격려할 수 있습니다. 스스로에게 긍정적인 보상을 주거나, 부정적인 습관에 벌을 주거나, 정해진 보상도 얻고, 랜덤한 보상도 노릴 수 있죠.",
|
||||
"marketing3Lead1": "**아이폰과 안드로이드** 앱은 이동 중에도 업무를 처리할 수 있게 돕습니다. 웹사이트에 로그인해서 작업 완료 버튼을 누리는 것이 귀찮을 때가 있지요.",
|
||||
"marketing2Lead1Title": "친구와 함께 올라가는 생산성",
|
||||
"marketing1Lead3": "몇 주간의 힘든 과제도 기대가 있으면 할 만하죠. 삶은 보상을 주지 않더라도, Habitica는 아닙니다! 과제를 완수할 때마다 보상이 있습니다. 놀라실 거예요. 그러니 계속 진행하세요! ",
|
||||
"marketing3Lead1": "어디서든 과제를 확인해보세요. 안드로이드와 iOS 기기에서도 Habitica를 이용할 수 있습니다. 과제를 완료하는 새로운 방법을 확인해보세요. 수상 경력이 있는 저희 앱에서요.",
|
||||
"marketing2Lead1Title": "함께하는 생산성",
|
||||
"marketing3Lead2": "**서드 파티 툴**은 Habitica를 삶의 다양한 측면과 연결합니다. 우리의 API 서비스는 [Chrome 확장 프로그램](https://chrome.google.com/webstore/detail/habitica/pidkmpibnnnhneohdgjclfdjpijggmjj?hl=en-US) 등과 손쉬운 통합을 가능하게 합니다. 비생산적인 웹서핑할 때 포인트를 잃게 하거나, 생산적일 때는 포인트를 얻게할 수 있죠. [자세한 정보는 여기를 클릭하세요](https://habitica.fandom.com/wiki/Extensions,_Add-Ons,_and_Customizations).",
|
||||
"marketing3Lead2Title": "서드 파티 지원",
|
||||
"marketing3Lead2Title": "오픈 소스 커뮤니티",
|
||||
"marketing4Lead3-1": "삶을 게임화하고 싶으세요?",
|
||||
"joinMany": "목표를 달성하면서 <%= userCountInMillions %> million이 넘는 유저들과 함께 즐기세요!",
|
||||
"marketing4Lead2Title": "건강과 웰빙의 게임화",
|
||||
@@ -89,5 +89,6 @@
|
||||
"playButton": "플레이",
|
||||
"enterHabitica": "Habitica 들어가기",
|
||||
"pkQuestion2": "해비티카는 어떻게 작동하나요?",
|
||||
"passwordResetPage": "비밀번호 초기화"
|
||||
"passwordResetPage": "비밀번호 초기화",
|
||||
"marketing3Lead1Title": "안드로이드 & iOS 앱"
|
||||
}
|
||||
|
||||
@@ -930,5 +930,14 @@
|
||||
"backgrounds022026": "SET 141: Uitgebracht Februari 2026",
|
||||
"backgroundElegantPalaceText": "Elegant Paleis",
|
||||
"backgroundElegantPalaceNotes": "Bewonder de kleurrijke hallen van een Elegant Paleis.",
|
||||
"backgroundBirthdayBashNotes": "Habitica viert zijn verjaardagsfeest en iedereen is uitgenodigd!"
|
||||
"backgroundBirthdayBashNotes": "Habitica viert zijn verjaardagsfeest en iedereen is uitgenodigd!",
|
||||
"backgrounds032026": "SET 142: Uitgebracht Maart 2026",
|
||||
"backgroundWaterfallWithRainbowText": "Waterval met Regenboog",
|
||||
"backgroundWaterfallWithRainbowNotes": "Bewonder de adembenemende schoonheid van een Waterval met een Regenboog.",
|
||||
"backgrounds042026": "SET 143: Uitgebracht April 2026",
|
||||
"backgroundRidingACometText": "Rijden op een Komeet",
|
||||
"backgroundRidingACometNotes": "Reis door de ruimte terwijl je rijdt op een Komeet!",
|
||||
"backgrounds052026": "SET 144: Uitgebracht Mei 2026",
|
||||
"backgroundElvenCitadelText": "Elfenburcht",
|
||||
"backgroundElvenCitadelNotes": "Onderneem de schilderachtige reis naar een Elfenburcht."
|
||||
}
|
||||
|
||||
@@ -147,5 +147,17 @@
|
||||
"subscriptionBenefitsAdjustments": "Aanpassingen aan de voordelen voor abonnees",
|
||||
"contentAnswer501": "Huidskleuren",
|
||||
"contentAnswer400": "Huisdier Queesten",
|
||||
"contentAnswer402": "Magische Uitbroeddranken"
|
||||
"contentAnswer402": "Magische Uitbroeddranken",
|
||||
"webFaqAnswer60": "Hier zijn enkele snelle tips om te starten met je nieuwe Habitica Groepsplan:\n\n* Promoveer een lid tot manager zodat die taken kan aanmaken en bewerken\n* Laat taken niet toegewezen als iedereen ze kan voltooien en ze maar één keer gedaan moeten worden\n* Wijs een taak toe aan één persoon om te zorgen dat niemand anders die taak kan voltooien\n* Wijs een taak toe aan meerdere personen als iedereen die taak moet voltooien\n* Schakel de optie in om gedeelde taken op je persoonlijke bord te tonen, zodat je niets mist\n* Je krijgt beloningen voor de taken die je voltooit, zelfs als ze aan meerdere personen zijn toegewezen\n* Taakbeloningen worden niet verdeeld tussen leden\n* Gebruik de taakkleur op het teambord om het gemiddelde voltooiingspercentage van taken te beoordelen\n* Controleer regelmatig de taken op het gedeelde takenbord om te zien of ze nog relevant zijn\n* Als je een Daily mist, krijg jij of je team geen schade, maar de taak zal wel in kleur verslechteren",
|
||||
"webFaqAnswer67": "Klassen zijn verschillende rollen die je personage kan aannemen. Elke klasse heeft zijn eigen unieke voordelen en vaardigheden naarmate je in niveau stijgt. Deze vaardigheden kunnen de manier waarop je met je taken omgaat ondersteunen of je helpen bij het voltooien van Quests met je Party.\n\nJe klasse bepaalt ook welke uitrusting beschikbaar is om te kopen bij je Beloningen, op de Markt en in de Seizoenswinkel.\n\nHieronder vind je een overzicht van elke klasse om je te helpen kiezen welke het beste bij jouw speelstijl past:\n####**Warrior**\n* Warriors richten veel schade aan bij bazen en hebben een grote kans op kritieke treffers wanneer ze taken voltooien, waardoor je extra Ervaring en Goud krijgt.\n* Strength is hun primaire stat en verhoogt de schade die ze doen.\n* Constitution is hun secundaire stat en vermindert de schade die ze ontvangen.\n* De vaardigheden van Warriors versterken de Constitution en Strength van hun Partyleden.\n* Overweeg Warrior te spelen als je graag bazen bevecht, maar ook wat bescherming wilt wanneer je af en toe een taak mist.\n####**Healer**\n* Healers hebben hoge verdediging en kunnen zowel zichzelf als hun Partyleden genezen.\n* Constitution is hun primaire stat en verhoogt hun genezingen terwijl het schade vermindert.\n* Intelligence is hun secundaire stat en verhoogt hun Mana en Ervaring.\n* De vaardigheden van Healers maken hun taken minder rood en versterken de Constitution van hun Partyleden.\n* Overweeg Healer te spelen als je vaak taken mist en jezelf of je Partyleden wilt kunnen genezen. Healers stijgen ook snel in niveau.\n####**Mage**\n* Mages stijgen snel in niveau, krijgen veel Mana en richten schade aan bij bazen in Quests.\n* Intelligence is hun primaire stat en verhoogt hun Mana en Ervaring.\n* Perception is hun secundaire stat en verhoogt hun Goud en itemdrops.\n* De vaardigheden van Mages bevriezen taakreeksen, herstellen Mana van Partyleden en versterken hun Intelligence.\n* Overweeg Mage te spelen als je gemotiveerd wordt door snel levels te behalen en schade bij te dragen in boss Quests.\n#### **Rogue**\n* Rogues krijgen de meeste itemdrops en Goud door taken te voltooien en hebben een grote kans op kritieke treffers, waardoor ze nog meer Ervaring en Goud krijgen.\n* Perception is hun primaire stat en verhoogt Goud en itemdrops.\n* Strength is hun secundaire stat en verhoogt de schade die ze doen.\n* De vaardigheden van Rogues helpen hen gemiste Dailies te ontwijken, Goud te stelen en de Perception van hun Partyleden te versterken.\n* Overweeg Rogue te spelen als je sterk gemotiveerd wordt door beloningen.",
|
||||
"webFaqAnswer68": "Als je merkt dat je vaak HP verliest, probeer dan een van deze tips:\n\n- Pauzeer je Dailies. De knop “Schade pauzeren” in Instellingen voorkomt dat je HP verliest wanneer je Dailies mist.\n- Pas het schema van je Dailies aan. Door ze in te stellen zodat ze nooit vervallen, kun je ze nog steeds voltooien voor beloningen zonder het risico op HP-verlies.\n- Probeer klasseskills te gebruiken:\n\t- Rogues kunnen Stealth gebruiken om schade van gemiste Dailies te voorkomen\n\t- Warriors kunnen Brutal Smash gebruiken om de roodheid van een Daily te verminderen, waardoor je minder schade krijgt als je die mist\n\t- Healers kunnen Searing Brightness gebruiken om de roodheid van Dailies te verminderen, waardoor je minder schade krijgt als je die mist",
|
||||
"webFaqAnswer70": "Statpunten laten je de kernstatistieken van je personage verhogen. Je verdient één statpunt telkens wanneer je in niveau stijgt (tot en met niveau 100). Deze kun je handmatig toewijzen of automatisch laten verdelen met de functie Automatische toewijzing. Het toewijzen van statpunten wordt ontgrendeld samen met het Klassesysteem op niveau 10.",
|
||||
"webFaqAnswer57": "Zodra je lid wordt van een Party, ontvang je geen verdere uitnodigingen meer.\nAls je uitnodigingen en toekomstige communicatie van een specifieke speler wilt voorkomen, ga dan naar hun profiel en klik op de knop Blokkeren. Op mobiele profielen tik je op de drie puntjes in de bovenhoek en kies je “Blokkeren”.\n\nAls je een situatie tegenkomt waarin je denkt dat een andere speler onze Communityrichtlijnen heeft overtreden in hun naam, profiel of in een bericht dat ze hebben gestuurd, rapporteer het bericht dan of neem contact met ons op via admin@habitica.com.",
|
||||
"webFaqAnswer55": "Ja! Als je de gebruikersnaam of het e-mailadres van een Habitica-speler hebt, kun je die uitnodigen om lid te worden van je Party. Zo stuur je een uitnodiging op de verschillende platforms:\n\nIemand uitnodigen via de mobiele apps:\n1. Open het menu, selecteer “Party” en scroll naar de sectie Leden\n2. Tik op “Leden zoeken” en ga vervolgens naar het tabblad “Via uitnodiging”\n3. Voer de gebruikersnamen of e-mailadressen in van de spelers die je wilt uitnodigen en tik op “Uitnodiging verzenden”\n\nIemand uitnodigen via de website:\n1. Ga naar je Party en klik op “Uitnodigen voor Party”\n2. Voer de gebruikersnamen of e-mailadressen in van de spelers die je wilt uitnodigen en klik op “Uitnodigingen verzenden”",
|
||||
"webFaqAnswer59": "Habitica Groepsplannen bieden een gedeelde ervaring doordat leden eenvoudig taken kunnen toevoegen, toewijzen en voltooien via een gedeeld takenbord. Met functies zoals ledenrollen, statusoverzicht en het toewijzen van taken zijn Groepsplannen ideaal voor gezinnen of teams van collega’s met gezamenlijke doelen. Ze zijn ook een geweldige manier om elkaar gemotiveerd te houden tijdens jullie reis om monsters te verslaan en je leven te verbeteren.",
|
||||
"webFaqAnswer62": "Groepsplannen geven je de unieke mogelijkheid om gedeelde taken toe te wijzen aan andere leden van je Groepsplan. Wanneer een gedeelde taak aan een lid wordt toegewezen, kunnen andere leden deze taak niet meer voltooien.\n\nJe kunt een taak ook aan meerdere leden tegelijk toewijzen. Als bijvoorbeeld iedereen zijn tanden moet poetsen, maak je één taak aan en wijs je die toe aan elk lid. Elk lid kan de taak voltooien en zijn of haar individuele beloningen verdienen. De hoofdtaak wordt als voltooid weergegeven zodra iedereen de taak heeft afgerond.",
|
||||
"webFaqAnswer61": "Alleen de leider van het Groepsplan en managers kunnen gedeelde taken aanmaken. Als je wilt dat een lid taken kan aanmaken, moet je die persoon promoveren tot manager.\n\nZo promoveer je een lid van het Groepsplan tot manager op de website:\n1. Ga naar je Groepsplan en schakel naar het tabblad “Groepsinformatie”\n2. Bekijk de ledenlijst en klik op het puntjes-icoon naast het lid dat je wilt promoveren\n3. Selecteer “Manager toewijzen”",
|
||||
"webFaqAnswer64": "Gedeelde taken worden voor iedereen op hetzelfde moment gereset zodat het gedeelde takenbord synchroon blijft. Dit tijdstip is zichtbaar op het gedeelde takenbord en wordt bepaald door de dagstarttijd van de leider van het Groepsplan. Omdat gedeelde taken automatisch resetten, krijg je de volgende ochtend geen kans meer om gisteren niet voltooide gedeelde Dailies alsnog af te werken.\n\nGedeelde Dailies veroorzaken geen schade als ze worden gemist, maar ze zullen in kleur achteruitgaan om de voortgang visueel weer te geven.",
|
||||
"webFaqAnswer65": "Hoewel de mobiele apps nog niet alle functies van Groepsplannen volledig ondersteunen, kun je gedeelde taken wel voltooien via de iOS- en Android-apps!\n\nOp Android kun je bovenaan het scherm op je weergavenaam tikken wanneer je taken bekijkt om over te schakelen naar het gedeelde takenbord. Van daaruit kun je leden bekijken, de chat openen en taken aanmaken, voltooien of toewijzen.\n\nJe kunt ook een instelling inschakelen om gedeelde taken naar het persoonlijke takenbord te kopiëren, zodat alle taken op één plek voltooid kunnen worden.\n\nZo doe je dit in de mobiele apps:\n*Open Instellingen en schakel “Gedeelde taken kopiëren” in\n\nZo doe je dit op de Habitica-website:\n* Ga naar het Groepsplan en schakel de “Taken kopiëren”-schakelaar in op het gedeelde takenbord",
|
||||
"webFaqAnswer66": "De gedeelde takenborden van Groepsplannen zijn dynamischer dan Challenges, omdat ze voortdurend kunnen worden bijgewerkt en gebruikt door leden. Challenges zijn vooral handig wanneer je één vaste set taken naar veel mensen wilt sturen.\n\nGroepsplannen zijn bovendien een betaalde functie, terwijl Challenges gratis beschikbaar zijn voor iedereen.\n\nJe kunt in Challenges geen specifieke taken toewijzen, en Challenges hebben geen gedeelde dagreset. Over het algemeen bieden Challenges minder controle en directe interactie."
|
||||
}
|
||||
|
||||
@@ -2672,5 +2672,6 @@
|
||||
"weaponSpecialFall2024MageNotes": "Met een aanraking van dit schitterende wapen worden je de stappen van je taak direct versimpeld. Verhoogt Intelligentie met <%= int %> en Perceptie met <%= per %>. Beperkte Oplage Herfst 2024 Uitrusting.",
|
||||
"weaponSpecialSummer2024MageNotes": "Deze verschrikkelijke tentakels kunnen tegelijkertijd magie afleiden, afbuigen en sturen. Verhoogt Intelligentie met <%= int %> en Perceptie met <%= per %>. Beperkte Oplage Zomer 2024 Uitrusting.",
|
||||
"weaponSpecialSummer2024HealerNotes": "Het zal je verbazen wanneer je ontdekt hoe hard de schelp aan het eind van deze staf is. Verhoogt Intelligentie met <%= int %>. Beperkte Oplage Zomer 2024 Uitrusting.",
|
||||
"weaponSpecialWinter2025RogueNotes": "Verblind die moeilijke taken tot onderwerping! Je zult niet te stoppen zijn! Verhoogt Kracht met <%= str %>. Beperkte Oplage Winter 2024-2025 Uitrusting."
|
||||
"weaponSpecialWinter2025RogueNotes": "Verblind die moeilijke taken tot onderwerping! Je zult niet te stoppen zijn! Verhoogt Kracht met <%= str %>. Beperkte Oplage Winter 2024-2025 Uitrusting.",
|
||||
"weaponSpecialWinter2025HealerNotes": "Wat je nu nodig hebt zijn meer lichtjes met een lichtgevende ster er bovenop! Je zal niet te stoppen zijn! Verhoogt Intelligentie met <%= int %>. Beperkte Oplage Winter 2024-2025 Uitrusting."
|
||||
}
|
||||
|
||||
@@ -11,5 +11,8 @@
|
||||
"rebirthPop": "Herstart je personage direct als een Niveau 1 Krijger zonder je prestaties, verzamelobjecten en uitrusting te verliezen. Je Taken en hun geschiedenis zal hetzelfde blijven maar ze worden gereset naar een gele kleur. Je Reeks word verwijderd behalve van taken die horen bij een actieve Uitdaging of een Groepsplan. Je Goud, Ervaring, Mana en de effecten van al je Vaardigheden gaan verloren. Dit alles gaat direct in werking.",
|
||||
"rebirthName": "Bol der Hergeboorte",
|
||||
"rebirthComplete": "Je bent herboren!",
|
||||
"nextFreeRebirth": "<strong><%= days %> dagen</strong> tot <strong>GRATIS</strong> Bol der Hergeboorte"
|
||||
"nextFreeRebirth": "<strong><%= days %> dagen</strong> tot <strong>GRATIS</strong> Bol der Hergeboorte",
|
||||
"rebirthNewAchievement": "Nieuwe prestatie",
|
||||
"rebirthNewAdventure": "Een nieuw avontuur begint nu!",
|
||||
"rebirthUnlockedOrb": "Een nieuw avontuur is beschikbaar!"
|
||||
}
|
||||
|
||||
@@ -921,5 +921,13 @@
|
||||
"backgroundInsideForestWitchsCottageText": "Домик лесной ведьмы",
|
||||
"backgroundAutumnSwampNotes": "Окунитесь в атмосферу осеннего болота.",
|
||||
"backgrounds102025": "Набор 137: Выпущен в октябре 2025 года",
|
||||
"backgroundInsideForestWitchsCottageNotes": "Сплетите заклинания в домике лесной ведьмы."
|
||||
"backgroundInsideForestWitchsCottageNotes": "Сплетите заклинания в домике лесной ведьмы.",
|
||||
"backgroundNighttimeStreetWithShopsText": "Ночная улица с магазинами",
|
||||
"backgrounds122025": "Набор 139: Выпущен в декабре 2025",
|
||||
"backgroundNighttimeStreetWithShopsNotes": "Наслаждайтесь теплым сиянием ночных торговых улиц.",
|
||||
"backgroundWinterDesertWithSaguarosNotes": "Вдохните свежий воздух зимней пустыни полной кактусов сагуаро.",
|
||||
"backgroundElegantPalaceText": "Прекрасный Дворец",
|
||||
"backgrounds012026": "Набор 140: Выпущен в январе 2026",
|
||||
"backgrounds022026": "Набор 141: Выпущен в феврале 2026",
|
||||
"backgroundWinterDesertWithSaguarosText": "Зимняя пустыня с кактусами сагуаро"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"rebirthNew": "Переродження: Нова пригода чекає на вас!",
|
||||
"rebirthUnlock": "Ви розблокували Переродження! Цей особливий предмет дозволить вам розпочати нову гру з 1-го рівня, зберігши ваші завдання, досягнення, улюбленців тощо. Використайте його, щоб вдихнути нове життя у Habitica, якщо ви відчуваєте, що досягли всього, або хочете пройти через нові пригоди з новими враженнями, наче початківець!",
|
||||
"rebirthAchievement": "Ви розпочали нову пригоду! Це ваше <%= number %> переродження, і найвищий рівень, якого ви досягали - <%= level %>. Аби отримати це досягнення ще раз, почніть вашу нову пригоду після того, як досягнете ще вищого рівня!",
|
||||
"rebirthAchievement100": "Ви розпочали нову пригоду! Це ваше <%= number %> переродження, і найвищий рівень, який ви отримали - 100 чи більше. Аби отримати це досягнення ще раз, почніть вашу нову пригоду після того, як отримаєте щонайменше рівень 100!",
|
||||
"rebirthAchievement": "Ви використовували Кулю Переродження <strong><%= number %></strong> разів, а ваш найвищий досягнутий рівень — <strong><%= level %></strong>.",
|
||||
"rebirthAchievement100": "Ви використовували Кулю Переродження <strong><%= number %></strong> разів, а ваш найвищий досягнутий рівень — <strong>100</strong> або вище.",
|
||||
"rebirthBegan": "Нова пригода починається",
|
||||
"rebirthText": "Розпочато <%= rebirths %> нові(их) пригоди",
|
||||
"rebirthOrb": "Після досягнення <%= level %> рівня використано Кулю Переродження, щоб розпочати заново.",
|
||||
@@ -11,5 +11,12 @@
|
||||
"rebirthPop": "Миттєво переродить вашого персонажа як воїна 1-го рівня, зберігши ваші досягнення, предмети колекціонування та спорядження. Ваші завдання та їхня історія залишаться, проте вони будуть скинуті до жовтого кольору. Ваші серії будуть видалені, окрім завдань з активних Випробувань та Спільнот. Ваше золото, досвід, мана та ефекти всіх навичок буде видалено. Все вищеперераховане буде застосовано негайно.",
|
||||
"rebirthName": "Куля Переродження",
|
||||
"rebirthComplete": "Ви переродилися!",
|
||||
"nextFreeRebirth": "<strong><%= days %> дн.</strong> до <strong>БЕЗКОШТОВНОЇ</strong> Кулі Переродження"
|
||||
"nextFreeRebirth": "<strong><%= days %> дн.</strong> до <strong>БЕЗКОШТОВНОЇ</strong> Кулі Переродження",
|
||||
"rebirthUnlockedOrb": "Нова пригода доступна!",
|
||||
"rebirthNewAchievement": "Нове Досягнення",
|
||||
"rebirthNewAdventure": "Нова пригода починається зараз!",
|
||||
"rebirthAchievementPlural": "Ви використовували Кулю Переродження <strong><%= number %></strong> разів, а ваш найвищий досягнутий рівень — <strong><%= level %></strong>.",
|
||||
"rebirthStackInfo": "Це досягнення буде накопичуватися щоразу, коли ви використовуєте Кулю Переродження.",
|
||||
"rebirthUnlockedNewItem": "Куля Переродження Розблокована",
|
||||
"rebirthUnlockedDesc": "Використовуйте Кулю Переродження, щоб вдихнути нове життя у свої пригоди в Habitica, коли відчуєте, що досягли всього! Почніть заново з 1-го рівня, зберігши свої завдання, досягнення та улюбленців за допомогою цього особливого предмета, який можна знайти на Ринку."
|
||||
}
|
||||
|
||||
@@ -257,5 +257,21 @@
|
||||
"contentRelease": "Релізи контенту + Події",
|
||||
"resetTextLocal": "Якщо ви абсолютно впевнені, введіть свій пароль у полі нижче.",
|
||||
"resetTextSocial": "Якщо ви абсолютно впевнені, введіть <b>\"<%= magicWord %>\"</b> у полі нижче.",
|
||||
"transaction_subscription_bonus": "Бонус <b>Підписки</b>"
|
||||
"transaction_subscription_bonus": "Бонус <b>Підписки</b>",
|
||||
"acceptAllCookies": "Прийняти всі файли cookie",
|
||||
"denyNonEssentialCookies": "Заборонити несуттєві файли cookie",
|
||||
"managePrivacyPreferences": "Керуйте налаштуваннями конфіденційності",
|
||||
"yourPrivacyPreferences": "Ваші налаштування конфіденційності",
|
||||
"learnMorePrivacy": "Щоб дізнатися більше, перегляньте нашу <a href='/static/privacy' target='_blank'>Політику конфіденційності</a>.",
|
||||
"strictlyNecessary": "Строго необхідні",
|
||||
"alwaysActive": "Завжди активні",
|
||||
"requiredToRun": "Вони потрібні нашому веб-сайту та додаткам для найкращої роботи.",
|
||||
"performanceAnalytics": "Ефективність і аналітика",
|
||||
"savePreferences": "Зберегти налаштування",
|
||||
"habiticaPrivacyPolicy": "Політика конфіденційності Habitica",
|
||||
"privacyOverview": "У сучасному світі здається, ніби кожна компанія прагне отримати прибуток з ваших даних. Через це буває важко знайти правильний додаток для вдосконалення своїх звичок. Habitica використовує файли cookie, які зберігають дані виключно для аналізу продуктивності, обробки запитів у службу підтримки та забезпечення найкращого ігрового досвіду. Ви можете змінити ці налаштування в будь-який час у налаштуваннях вашого акаунта.",
|
||||
"privacySettingsOverview": "Habitica використовує файли cookie для аналізу продуктивності, обробки запитів у службу підтримки та забезпечення найкращого ігрового досвіду. Для цього нам потрібно отримати наступні дозволи. Ви можете змінити їх у будь-який час у налаштуваннях вашого акаунта.",
|
||||
"usedForSupport": "Вони використовуються для покращення взаємодії з користувачем, продуктивності та послуг нашого веб-сайту та програм. Ці дані використовуються нашою командою підтримки під час обробки запитів і звітів про помилки.",
|
||||
"gpcWarning": "<a href='<%= url %>' target='_blank'>GPC</a> увімкнено. Увімкнення відстеження нижче скасує цей параметр і надішле дані нашим аналітичним партнерам.",
|
||||
"gpcPlusAnalytics": "<a href='<%= url %>' target='_blank'>GPC</a> увімкнено. Ви надали згоду на відстеження та надсилання даних нашим аналітичним партнерам."
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"rebirthNew": "重生:开始新的冒险!",
|
||||
"rebirthUnlock": "你已经解锁了重生之球! 这个特殊的市场商品可使你从 1 级开始新的游戏,同时保留你的任务、成就、宠物等。 如果你觉得自己已经达成了所有目标,或者想要以新手的视角体验新功能,请使用它为Habitica注入新的活力!",
|
||||
"rebirthAchievement": "你开始了新的冒险! 这是你的第<%= number %>次重生,你达到的最高等级是<%= level %>。 要叠加此成就,请在达到更高等级时开始下一次新的冒险!",
|
||||
"rebirthAchievement100": "你开始了新的冒险! 这是你最高等级达到100级以上的第<%= number %>次重生。要叠加此成就,请在达到至少 100 级时开始下次新的冒险!",
|
||||
"rebirthAchievement": "这是你的第<strong><%= number %></strong>次使用重生之球,你达到的最高等级是<strong><%= level %></strong>。",
|
||||
"rebirthAchievement100": "这是你的第<strong><%= number %></strong>次使用重生之球,你达到的最高等级是<strong>100</strong> 或更高。",
|
||||
"rebirthBegan": "已开始新的冒险",
|
||||
"rebirthText": "已开始<%= rebirths %>次新的冒险",
|
||||
"rebirthOrb": "在达到<%= level %>级后,使用了重生之球重新开始。",
|
||||
@@ -11,5 +11,12 @@
|
||||
"rebirthPop": "立即将你的角色重置成为1级战士,同时保留成就、物品和装备。你的任务及其历史记录会保留,但会被重置为黄色;你的连击次数将被移除,挑战任务和团队计划任务除外;你的金币、经验值以及法力值和及所有技能的效果都将被移除。上述内容将立即生效。",
|
||||
"rebirthName": "重生之球",
|
||||
"rebirthComplete": "你已经重生!",
|
||||
"nextFreeRebirth": "下个<strong>免费</strong>的重生之球可在<strong><%= days %>天</strong>后获得"
|
||||
"nextFreeRebirth": "下个<strong>免费</strong>的重生之球可在<strong><%= days %>天</strong>后获得",
|
||||
"rebirthUnlockedNewItem": "已解锁重生之球",
|
||||
"rebirthUnlockedOrb": "全新冒险现已开启!",
|
||||
"rebirthUnlockedDesc": "当你感觉已征服Habitica的全部内容时,不妨使用重生之球为你的冒险注入新活力!通过在市场获取的这个特殊道具,你将重启旅程回到1级,同时保留所有任务、成就和宠物。",
|
||||
"rebirthNewAchievement": "新成就",
|
||||
"rebirthNewAdventure": "新的冒险现在开始!",
|
||||
"rebirthAchievementPlural": "这是你的第<strong><%= number %></strong>次使用重生之球,你达到的最高等级是<strong><%= level %></strong>。",
|
||||
"rebirthStackInfo": "每使用一次重生之球,本成就就会叠加一次。"
|
||||
}
|
||||
|
||||
@@ -696,6 +696,9 @@ const backgrounds = {
|
||||
birthday_bash: {
|
||||
price: 0,
|
||||
},
|
||||
on_a_strange_planet: {
|
||||
price: 0,
|
||||
},
|
||||
},
|
||||
timeTravelBackgrounds: {
|
||||
airship: {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import eggs from '../eggs';
|
||||
import stable from '../stable';
|
||||
|
||||
const SWAPS = [
|
||||
'Veggie',
|
||||
'Dessert',
|
||||
'VirtualPet',
|
||||
'TeaShop',
|
||||
'Fungi',
|
||||
'Cryptid',
|
||||
'Alien',
|
||||
];
|
||||
|
||||
export function getMatchingSwap (date = new Date()) {
|
||||
const year = date.getFullYear();
|
||||
const diff = year - 2020;
|
||||
return SWAPS[diff % SWAPS.length];
|
||||
}
|
||||
|
||||
export function makeSubstitutionMap (swappedPotion) {
|
||||
const substitutions = {
|
||||
pets: {
|
||||
default: `Pet-Dragon-${swappedPotion}`,
|
||||
noPet: `Pet-Wolf-${swappedPotion}`,
|
||||
noPetIOS: `Pet-TigerCub-${swappedPotion}`,
|
||||
noPetAndroid: `Pet-Cactus-${swappedPotion}`,
|
||||
},
|
||||
};
|
||||
for (const pet of Object.keys(stable.specialPets)) {
|
||||
substitutions.pets[`Pet-${pet}`] = `Pet-Dragon-${swappedPotion}`;
|
||||
}
|
||||
for (const egg of Object.keys(eggs.drops)) {
|
||||
substitutions.pets[`Pet-${egg}-`] = `Pet-${egg}-${swappedPotion}`;
|
||||
}
|
||||
for (const egg of Object.keys(eggs.quests)) {
|
||||
substitutions.pets[`Pet-${egg}-`] = `Pet-BearCub-${swappedPotion}`;
|
||||
}
|
||||
return substitutions;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable key-spacing */
|
||||
import moment from 'moment';
|
||||
import { getMatchingSwap, makeSubstitutionMap } from './aprilFools';
|
||||
|
||||
// gem block: number of gems
|
||||
const gemsPromo = {
|
||||
@@ -53,7 +54,7 @@ export const REPEATING_EVENTS = {
|
||||
aprilFools: {
|
||||
start: new Date('1970-04-01T04:00-04:00'),
|
||||
end: new Date('1970-04-02T03:59-04:00'),
|
||||
aprilFools: 'Cryptid',
|
||||
spriteSubstitutions: makeSubstitutionMap(getMatchingSwap()),
|
||||
},
|
||||
aprilFoolsResale: {
|
||||
start: new Date('1970-04-03T04:00-04:00'),
|
||||
@@ -65,6 +66,7 @@ export const REPEATING_EVENTS = {
|
||||
'virtualpet',
|
||||
'waffle',
|
||||
'fungi',
|
||||
'alien',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ export default [
|
||||
'Mount_Body_Gryphon-Gryphatrice',
|
||||
'Mount_Head_Dragon-Hydra',
|
||||
'Mount_Head_Gryphon-Gryphatrice',
|
||||
'Pet_HatchingPotion_Alien',
|
||||
'Pet_HatchingPotion_Cryptid',
|
||||
'Pet_HatchingPotion_Dessert',
|
||||
'Pet_HatchingPotion_Fungi',
|
||||
@@ -24,6 +25,8 @@ export default [
|
||||
'Pet-Gryphatrice-Jubilant',
|
||||
'Pet-Gryphon-Gryphatrice',
|
||||
'Pet-Wolf-Cerberus',
|
||||
'quest_alien',
|
||||
'quest_lostMasterclasser4',
|
||||
'quest_solarSystem',
|
||||
'quest_virtualpet',
|
||||
'quest_windup',
|
||||
|
||||
@@ -53,4 +53,5 @@ export const HATCHING_POTIONS_RELEASE_DATES = {
|
||||
Cryptid: { year: 2025, month: 4, day: 3 },
|
||||
Balloon: { year: 2025, month: 4, day: 21 },
|
||||
Opal: { year: 2025, month: 5, day: 14 },
|
||||
Alien: { year: 2026, month: 4, day: 3 },
|
||||
};
|
||||
|
||||
@@ -155,6 +155,10 @@ const wacky = {
|
||||
canBuy: hasQuestAchievementFunction('fungi'),
|
||||
},
|
||||
Cryptid: {},
|
||||
Alien: {
|
||||
questPotion: true,
|
||||
canBuy: hasQuestAchievementFunction('alien'),
|
||||
},
|
||||
};
|
||||
|
||||
each(drops, (pot, key) => {
|
||||
|
||||
@@ -233,6 +233,45 @@ const QUEST_SEASONAL = {
|
||||
unlock: t('questFungiUnlockText'),
|
||||
},
|
||||
},
|
||||
alien: {
|
||||
text: t('questAlienText'),
|
||||
notes: t('questAlienNotes'),
|
||||
completion: t('questAlienCompletion'),
|
||||
value: 4,
|
||||
category: 'hatchingPotion',
|
||||
boss: {
|
||||
name: t('questAlienBoss'),
|
||||
hp: 500,
|
||||
str: 2,
|
||||
rage: {
|
||||
title: t('questAlienRageTitle'),
|
||||
description: t('questAlienRageDescription'),
|
||||
value: 50,
|
||||
healing: 0.3,
|
||||
effect: t('questAlienRageEffect'),
|
||||
},
|
||||
},
|
||||
drop: {
|
||||
items: [
|
||||
{
|
||||
type: 'hatchingPotions',
|
||||
key: 'Alien',
|
||||
text: t('questAlienDropAlienPotion'),
|
||||
}, {
|
||||
type: 'hatchingPotions',
|
||||
key: 'Alien',
|
||||
text: t('questAlienDropAlienPotion'),
|
||||
}, {
|
||||
type: 'hatchingPotions',
|
||||
key: 'Alien',
|
||||
text: t('questAlienDropAlienPotion'),
|
||||
},
|
||||
],
|
||||
gp: 40,
|
||||
exp: 500,
|
||||
unlock: t('questAlienUnlockText'),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default QUEST_SEASONAL;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import each from 'lodash/each';
|
||||
import moment from 'moment';
|
||||
import { EVENTS } from './constants/events';
|
||||
import allEggs from './eggs';
|
||||
import allPotions from './hatching-potions';
|
||||
import t from './translation';
|
||||
@@ -193,7 +191,7 @@ function buildInfo () {
|
||||
|
||||
Object.assign(petInfo['Gryphatrice-Jubilant'], {
|
||||
canBuy () {
|
||||
return moment().isBetween(EVENTS.birthday10.start, EVENTS.birthday10.end);
|
||||
return false;
|
||||
},
|
||||
currency: 'gems',
|
||||
event: 'birthday10',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import forEach from 'lodash/forEach';
|
||||
import isFunction from 'lodash/isFunction';
|
||||
import pick from 'lodash/pick';
|
||||
import nconf from 'nconf';
|
||||
import get from 'lodash/get';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
@@ -10,10 +9,6 @@ import {
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import {
|
||||
basicFields as basicGroupFields,
|
||||
model as Group,
|
||||
} from '../../models/group';
|
||||
import * as Tasks from '../../models/task';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
import {
|
||||
@@ -23,6 +18,7 @@ import {
|
||||
getUserInfo,
|
||||
sendTxn,
|
||||
} from '../../libs/email';
|
||||
import worker from '../../libs/worker';
|
||||
import * as inboxLib from '../../libs/inbox';
|
||||
import * as userLib from '../../libs/user';
|
||||
import { model as UserHistory } from '../../models/userHistory';
|
||||
@@ -299,21 +295,6 @@ api.deleteUser = {
|
||||
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
||||
}
|
||||
|
||||
const types = ['party', 'guilds'];
|
||||
const groupFields = basicGroupFields.concat(' leader memberCount purchased');
|
||||
|
||||
const groupsUserIsMemberOf = await Group.getGroups({ user, types, groupFields });
|
||||
|
||||
const groupLeavePromises = groupsUserIsMemberOf.map(group => group.leave(user, 'remove-all'));
|
||||
|
||||
await Promise.all(groupLeavePromises);
|
||||
|
||||
await Tasks.Task.deleteMany({
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
|
||||
await user.deleteOne();
|
||||
|
||||
if (feedback) {
|
||||
sendTxn({ email: TECH_ASSISTANCE_EMAIL }, 'admin-feedback', [
|
||||
{ name: 'PROFILE_NAME', content: user.profile.name },
|
||||
@@ -325,11 +306,13 @@ api.deleteUser = {
|
||||
]);
|
||||
}
|
||||
|
||||
res.analytics.track('account delete', {
|
||||
user: pick(user, ['preferences', 'registeredThrough']),
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
category: 'behavior',
|
||||
worker.sendJob('deleteUser', {
|
||||
identifier: user._id,
|
||||
data: {
|
||||
userId: user._id,
|
||||
deleteAccount: true,
|
||||
deleteAmplitude: true,
|
||||
},
|
||||
});
|
||||
|
||||
res.respond(200, {});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { sendJob } from '../../libs/worker';
|
||||
import worker from '../../libs/worker';
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import { ensurePermission } from '../../middlewares/ensureAccessRight';
|
||||
import { TransactionModel as Transaction } from '../../models/transaction';
|
||||
@@ -48,7 +48,8 @@ api.deleteMember = {
|
||||
req.checkQuery('deleteAmplitude').optional().isIn(['true', 'false']);
|
||||
const validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
sendJob('delete-user', {
|
||||
await worker.sendJob('deleteUser', {
|
||||
identifier: req.params.memberId,
|
||||
data: {
|
||||
userId: req.params.memberId,
|
||||
deleteAccount: req.query.deleteAccount === 'true',
|
||||
|
||||
@@ -24,7 +24,10 @@ api.getReady = {
|
||||
middlewares: [disableCache],
|
||||
async handler (req, res) {
|
||||
// This allows kubernetes to determine if the server is ready to receive traffic
|
||||
if (!SERVER_STATUS.MONGODB || !SERVER_STATUS.REDIS || !SERVER_STATUS.EXPRESS) {
|
||||
if (!SERVER_STATUS.MONGODB
|
||||
|| !SERVER_STATUS.RATE_LIMITER
|
||||
|| !SERVER_STATUS.WORKER
|
||||
|| !SERVER_STATUS.EXPRESS) {
|
||||
res.respond(503, {
|
||||
status: 'not ready',
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import nconf from 'nconf';
|
||||
import { TAVERN_ID } from '../models/group'; // eslint-disable-line import/no-cycle
|
||||
import { encrypt } from './encryption';
|
||||
import common from '../../common';
|
||||
import { sendJob } from './worker';
|
||||
import worker from './worker';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
@@ -148,7 +148,8 @@ export async function sendTxn (mailingInfoArray, emailType, variables, personalV
|
||||
}
|
||||
|
||||
if (IS_PROD && mailingInfoArray.length > 0) {
|
||||
return sendJob('email', {
|
||||
return worker.sendJob('email', {
|
||||
identifier: emailType,
|
||||
data: {
|
||||
emailType,
|
||||
to: mailingInfoArray,
|
||||
|
||||
@@ -200,7 +200,7 @@ async function inviteByEmail (invite, group, inviter, req, res) {
|
||||
];
|
||||
|
||||
if (group.type === 'guild') {
|
||||
variables.push({ name: 'GUILD_NAME', content: group.name });
|
||||
variables.push({ name: 'GROUP_NAME', content: group.name });
|
||||
}
|
||||
|
||||
// Check for the email address not to be unsubscribed
|
||||
|
||||
@@ -316,6 +316,7 @@ api.subscribe = async function subscribe (options) {
|
||||
const group = await Group.getGroup({
|
||||
user, groupId, populateLeader: false, groupFields,
|
||||
});
|
||||
if (!group) throw new NotFound(i18n.t('groupNotFound'));
|
||||
const membersCount = await group.getMemberCount();
|
||||
amount = sub.price + (membersCount - leaderCount) * priceOfSingleMember;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import IORedis from 'ioredis';
|
||||
|
||||
export default function setupRedis (connectionOptions, config) {
|
||||
const redisConfig = { ...config };
|
||||
if (connectionOptions.username) {
|
||||
redisConfig.username = connectionOptions.username;
|
||||
}
|
||||
if (connectionOptions.password) {
|
||||
redisConfig.password = connectionOptions.password;
|
||||
}
|
||||
if (connectionOptions.db) {
|
||||
redisConfig.db = connectionOptions.db;
|
||||
}
|
||||
let connection;
|
||||
const redisUrl = connectionOptions.url;
|
||||
if (redisUrl) {
|
||||
connection = new IORedis(redisUrl, redisConfig);
|
||||
} else {
|
||||
connection = new IORedis(connectionOptions.port, connectionOptions.host, redisConfig);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
const SERVER_STATUS = {
|
||||
MONGODB: false,
|
||||
REDIS: false,
|
||||
RATE_LIMITER: false,
|
||||
WORKER: false,
|
||||
EXPRESS: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,33 +1,49 @@
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import logger from './logger';
|
||||
import { Queue } from 'bullmq';
|
||||
import setupRedis from './redis';
|
||||
import SERVER_STATUS from './serverStatus';
|
||||
|
||||
const EMAIL_SERVER = {
|
||||
url: nconf.get('EMAIL_SERVER_URL'),
|
||||
auth: {
|
||||
user: nconf.get('EMAIL_SERVER_AUTH_USER'),
|
||||
password: nconf.get('EMAIL_SERVER_AUTH_PASSWORD'),
|
||||
},
|
||||
};
|
||||
let redisClient;
|
||||
const queues = {};
|
||||
|
||||
export function sendJob (type, config) {
|
||||
const { data, options } = config;
|
||||
const usedOptions = {
|
||||
backoff: { delay: 10 * 60 * 1000, type: 'exponential' },
|
||||
...options,
|
||||
};
|
||||
if (nconf.get('WORKER_REDIS_URL')) {
|
||||
redisClient = setupRedis({
|
||||
url: nconf.get('WORKER_REDIS_URL'),
|
||||
username: nconf.get('WORKER_REDIS_USERNAME'),
|
||||
password: nconf.get('WORKER_REDIS_PASSWORD'),
|
||||
});
|
||||
|
||||
return got.post(`${EMAIL_SERVER.url}/job`, {
|
||||
retry: 5, // retry the http request to the email server 5 times
|
||||
timeout: 60000, // wait up to 60s before timing out
|
||||
username: EMAIL_SERVER.auth.user,
|
||||
password: EMAIL_SERVER.auth.password,
|
||||
json: {
|
||||
type,
|
||||
data,
|
||||
options: usedOptions,
|
||||
},
|
||||
}).json().catch(err => logger.error(err, {
|
||||
extraMessage: 'Error while sending an email.',
|
||||
}));
|
||||
redisClient.on('ready', () => {
|
||||
SERVER_STATUS.WORKER = true;
|
||||
});
|
||||
|
||||
redisClient.on('reconnecting', () => {
|
||||
SERVER_STATUS.WORKER = false;
|
||||
});
|
||||
|
||||
const queueConfig = {
|
||||
connection: redisClient,
|
||||
}
|
||||
if (nconf.get('WORKER_REDIS_KEY_PREFIX')) {
|
||||
queueConfig.prefix = nconf.get('WORKER_REDIS_KEY_PREFIX');
|
||||
}
|
||||
|
||||
queues.email = new Queue('emails', queueConfig);
|
||||
queues.deleteUser = new Queue('DeleteUsers', queueConfig);
|
||||
} else {
|
||||
SERVER_STATUS.WORKER = true;
|
||||
}
|
||||
|
||||
function sendJob (type, config) {
|
||||
if (!queues[type]) {
|
||||
return Promise.reject(new Error(`Queue ${type} does not exist`));
|
||||
}
|
||||
const { identifier, data } = config;
|
||||
return queues[type].add(identifier, data);
|
||||
}
|
||||
|
||||
export function getRedisClient () {
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
export default { sendJob };
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import nconf from 'nconf';
|
||||
import redis from 'redis';
|
||||
import {
|
||||
RateLimiterRedis,
|
||||
RateLimiterMemory,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
import logger from '../libs/logger';
|
||||
import { apiError } from '../libs/apiError';
|
||||
import SERVER_STATUS from '../libs/serverStatus';
|
||||
import setupRedis from '../libs/redis';
|
||||
|
||||
// Middleware to rate limit requests to the API
|
||||
|
||||
@@ -19,9 +19,6 @@ import SERVER_STATUS from '../libs/serverStatus';
|
||||
|
||||
const IS_TEST = nconf.get('IS_TEST');
|
||||
const RATE_LIMITER_ENABLED = nconf.get('RATE_LIMITER_ENABLED') === 'true';
|
||||
const REDIS_HOST = nconf.get('REDIS_HOST');
|
||||
const REDIS_PASSWORD = nconf.get('REDIS_PASSWORD');
|
||||
const REDIS_PORT = nconf.get('REDIS_PORT');
|
||||
const LIVELINESS_PROBE_KEY = nconf.get('LIVELINESS_PROBE_KEY');
|
||||
const REGISTRATION_COST = nconf.get('RATE_LIMITER_REGISTRATION_COST') || 5;
|
||||
const IP_RATE_LIMIT_COST = nconf.get('RATE_LIMITER_IP_COST') || 5;
|
||||
@@ -41,19 +38,21 @@ if (RATE_LIMITER_ENABLED) {
|
||||
...rateLimiterOpts,
|
||||
});
|
||||
} else {
|
||||
redisClient = redis.createClient({
|
||||
host: REDIS_HOST,
|
||||
password: REDIS_PASSWORD,
|
||||
port: REDIS_PORT,
|
||||
enable_offline_queue: false,
|
||||
redisClient = setupRedis({
|
||||
host: nconf.get('REDIS_HOST'),
|
||||
username: nconf.get('REDIS_USERNAME'),
|
||||
password: nconf.get('REDIS_PASSWORD'),
|
||||
port: nconf.get('REDIS_PORT'),
|
||||
}, {
|
||||
enableOfflineQueue: false,
|
||||
});
|
||||
|
||||
redisClient.on('ready', () => {
|
||||
SERVER_STATUS.REDIS = true;
|
||||
SERVER_STATUS.RATE_LIMITER = true;
|
||||
});
|
||||
|
||||
redisClient.on('reconnecting', () => {
|
||||
SERVER_STATUS.REDIS = false;
|
||||
SERVER_STATUS.RATE_LIMITER = false;
|
||||
});
|
||||
|
||||
redisClient.on('error', error => {
|
||||
@@ -66,7 +65,7 @@ if (RATE_LIMITER_ENABLED) {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
SERVER_STATUS.REDIS = true;
|
||||
SERVER_STATUS.RATE_LIMITER = true;
|
||||
}
|
||||
|
||||
function setResponseHeaders (res, rateLimiterRes) {
|
||||
@@ -83,6 +82,10 @@ function setResponseHeaders (res, rateLimiterRes) {
|
||||
res.set(headers);
|
||||
}
|
||||
|
||||
export function getRedisClient () {
|
||||
return redisClient;
|
||||
}
|
||||
|
||||
export default function rateLimiterMiddleware (req, res, next) {
|
||||
if (!RATE_LIMITER_ENABLED) return next();
|
||||
if (LIVELINESS_PROBE_KEY && req.query.liveliness === LIVELINESS_PROBE_KEY) return next();
|
||||
|
||||
@@ -261,11 +261,11 @@ schema.statics.getGroup = async function getGroup (options = {}) {
|
||||
} else if (isTavern) {
|
||||
query = { _id: TAVERN_ID };
|
||||
} else if (optionalMembership === true) {
|
||||
query = { _id: groupId };
|
||||
query = { privacy: 'private', _id: groupId };
|
||||
} else if (isUserGuild) {
|
||||
query = { type: 'guild', _id: groupId };
|
||||
query = { type: 'guild', privacy: 'private', _id: groupId };
|
||||
} else {
|
||||
query = { type: 'guild', privacy: 'public', _id: groupId };
|
||||
return null;
|
||||
}
|
||||
|
||||
const mQuery = this.findOne(query);
|
||||
@@ -1431,6 +1431,7 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all', keepC
|
||||
|
||||
if (members.length === 0) {
|
||||
promises.push(group.deleteOne());
|
||||
promises.push(Chat.deleteMany({ groupId: group._id }));
|
||||
return Promise.all(promises);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@ import nconf from 'nconf';
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import mongoose from 'mongoose';
|
||||
import redis from 'redis';
|
||||
import logger from './libs/logger';
|
||||
import { getRedisClient as getWorkerRedisClient } from './libs/worker';
|
||||
import { getRedisClient as getRateLimiterRedisClient } from './middlewares/rateLimiter';
|
||||
|
||||
// Setup translations
|
||||
// Must come before attach middlewares so Mongoose validations can use translations
|
||||
@@ -31,7 +32,10 @@ process.on('SIGTERM', async () => {
|
||||
console.log('SIGTERM signal received: closing HTTP server');
|
||||
server.close(async () => {
|
||||
await mongoose.disconnect();
|
||||
await redis.quit();
|
||||
const workerRedisClient = getWorkerRedisClient();
|
||||
if (workerRedisClient) await workerRedisClient.quit();
|
||||
const rateLimiterRedisClient = getRateLimiterRedisClient();
|
||||
if (rateLimiterRedisClient) await rateLimiterRedisClient.quit();
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user