mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
Merge branch 'develop' into release
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
Habitica [](https://travis-ci.org/HabitRPG/habitica) [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE)
|
||||
Habitica [](https://travis-ci.org/HabitRPG/habitica) [](https://codeclimate.com/github/HabitRPG/habitrpg) [](https://coveralls.io/github/HabitRPG/habitica?branch=develop) [](https://www.bountysource.com/trackers/68393-habitrpg?utm_source=68393&utm_medium=shield&utm_campaign=TRACKER_BADGE) [](https://www.codetriage.com/habitrpg/habitica)
|
||||
===============
|
||||
|
||||
[](https://greenkeeper.io/)
|
||||
|
||||
[Habitica](https://habitica.com) is an open source habit building program which treats your life like a Role Playing Game. Level up as you succeed, lose HP as you fail, earn money to buy weapons and armor.
|
||||
|
||||
We need more programmers! Your assistance will be greatly appreciated.
|
||||
|
||||
@@ -105,5 +105,12 @@
|
||||
"LOGGLY" : {
|
||||
"TOKEN" : "example-token",
|
||||
"SUBDOMAIN" : "exmaple-subdomain"
|
||||
},
|
||||
"KAFKA": {
|
||||
"GROUP_ID": "",
|
||||
"CLOUDKARAFKA_BROKERS": "",
|
||||
"CLOUDKARAFKA_USERNAME": "",
|
||||
"CLOUDKARAFKA_PASSWORD": "",
|
||||
"CLOUDKARAFKA_TOPIC_PREFIX": ""
|
||||
}
|
||||
}
|
||||
|
||||
790
package-lock.json
generated
790
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
@@ -3,20 +3,24 @@
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "4.28.2",
|
||||
"main": "./website/server/index.js",
|
||||
"greenkeeper": {
|
||||
"ignore": [
|
||||
"mongoose"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@slack/client": "^3.8.1",
|
||||
"accepts": "^1.3.2",
|
||||
"amazon-payments": "^0.2.6",
|
||||
"amplitude": "^3.5.0",
|
||||
"apidoc": "^0.17.5",
|
||||
"apn": "^1.7.6",
|
||||
"autoprefixer": "^8.0.0",
|
||||
"aws-sdk": "^2.0.25",
|
||||
"axios": "^0.17.1",
|
||||
"aws-sdk": "^2.200.0",
|
||||
"axios": "^0.18.0",
|
||||
"axios-progress-bar": "^1.1.8",
|
||||
"babel-core": "^6.0.0",
|
||||
"babel-eslint": "^8.2.2",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-eslint": "^8.2.1",
|
||||
"babel-plugin-syntax-async-functions": "^6.13.0",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"babel-plugin-transform-async-to-module-method": "^6.8.0",
|
||||
@@ -31,20 +35,20 @@
|
||||
"body-parser": "^1.15.0",
|
||||
"bootstrap": "^4.0.0",
|
||||
"bootstrap-vue": "^2.0.0-rc.1",
|
||||
"compression": "^1.6.1",
|
||||
"compression": "^1.7.2",
|
||||
"cookie-session": "^1.2.0",
|
||||
"coupon-code": "^0.4.5",
|
||||
"cross-env": "^5.1.3",
|
||||
"css-loader": "^0.28.0",
|
||||
"csv-stringify": "^2.0.1",
|
||||
"csv-stringify": "^2.0.4",
|
||||
"cwait": "^1.1.1",
|
||||
"domain-middleware": "~0.1.0",
|
||||
"express": "^4.16.2",
|
||||
"express-basic-auth": "^1.0.1",
|
||||
"express-validator": "^2.18.0",
|
||||
"express-basic-auth": "^1.1.4",
|
||||
"express-validator": "^5.0.1",
|
||||
"extract-text-webpack-plugin": "^3.0.2",
|
||||
"glob": "^7.1.2",
|
||||
"got": "^6.1.1",
|
||||
"got": "^8.2.0",
|
||||
"gulp": "^4.0.0",
|
||||
"gulp-babel": "^7.0.1",
|
||||
"gulp-imagemin": "^4.1.0",
|
||||
@@ -57,19 +61,20 @@
|
||||
"in-app-purchase": "^1.1.6",
|
||||
"intro.js": "^2.6.0",
|
||||
"jquery": ">=3.0.0",
|
||||
"js2xmlparser": "~1.0.0",
|
||||
"js2xmlparser": "^3.0.0",
|
||||
"lodash": "^4.17.4",
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
"moment": "^2.13.0",
|
||||
"moment-recur": "git://github.com/habitrpg/moment-recur.git#f147ef27bbc26ca67638385f3db4a44084c76626",
|
||||
"mongoose": "^4.13.10",
|
||||
"moment-recur": "^1.0.7",
|
||||
"mongoose": "^4.13.11",
|
||||
"morgan": "^1.7.0",
|
||||
"nconf": "^0.10.0",
|
||||
"node-gcm": "^0.14.4",
|
||||
"node-rdkafka": "^2.2.3",
|
||||
"node-sass": "^4.5.0",
|
||||
"nodemailer": "^2.3.2",
|
||||
"ora": "^1.1.0",
|
||||
"nodemailer": "^4.5.0",
|
||||
"ora": "^2.0.0",
|
||||
"pageres": "^4.1.1",
|
||||
"passport": "^0.4.0",
|
||||
"passport-facebook": "^2.0.0",
|
||||
@@ -82,11 +87,10 @@
|
||||
"pug": "^2.0.0-rc.4",
|
||||
"push-notify": "git://github.com/habitrpg/push-notify.git#6bc2b5fdb1bdc9649b9ec1964d79ca50187fc8a9",
|
||||
"pusher": "^1.3.0",
|
||||
"request": "^2.83.0",
|
||||
"rimraf": "^2.4.3",
|
||||
"sass-loader": "^6.0.2",
|
||||
"shelljs": "^0.8.1",
|
||||
"stripe": "^5.4.0",
|
||||
"stripe": "^5.5.0",
|
||||
"superagent": "^3.4.3",
|
||||
"svg-inline-loader": "^0.8.0",
|
||||
"svg-url-loader": "^2.0.2",
|
||||
@@ -96,13 +100,13 @@
|
||||
"url-loader": "^0.6.2",
|
||||
"useragent": "^2.1.9",
|
||||
"uuid": "^3.0.1",
|
||||
"validator": "^4.9.0",
|
||||
"validator": "^9.4.1",
|
||||
"vinyl-buffer": "^1.0.1",
|
||||
"vue": "^2.5.2",
|
||||
"vue-loader": "^14.1.1",
|
||||
"vue-mugen-scroll": "^0.2.1",
|
||||
"vue-router": "^3.0.0",
|
||||
"vue-style-loader": "^3.0.0",
|
||||
"vue-style-loader": "^4.0.2",
|
||||
"vue-template-compiler": "^2.5.2",
|
||||
"vuedraggable": "^2.15.0",
|
||||
"vuejs-datepicker": "git://github.com/habitrpg/vuejs-datepicker.git#5d237615463a84a23dd6f3f77c6ab577d68593ec",
|
||||
@@ -145,12 +149,12 @@
|
||||
"babel-plugin-istanbul": "^4.0.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"chalk": "^2.3.0",
|
||||
"chalk": "^2.3.1",
|
||||
"chromedriver": "^2.27.2",
|
||||
"connect-history-api-fallback": "^1.1.0",
|
||||
"coveralls": "^3.0.0",
|
||||
"cross-spawn": "^6.0.4",
|
||||
"eslint": "^4.17.0",
|
||||
"eslint": "^4.18.1",
|
||||
"eslint-config-habitrpg": "^4.0.0",
|
||||
"eslint-friendly-formatter": "^3.0.0",
|
||||
"eslint-loader": "^1.3.0",
|
||||
@@ -173,13 +177,13 @@
|
||||
"karma-spec-reporter": "0.0.32",
|
||||
"karma-webpack": "^2.0.2",
|
||||
"lcov-result-merger": "^2.0.0",
|
||||
"mocha": "^5.0.0",
|
||||
"mocha": "^5.0.1",
|
||||
"monk": "^6.0.5",
|
||||
"nightwatch": "^0.9.12",
|
||||
"puppeteer": "^1.0.0",
|
||||
"puppeteer": "^1.1.0",
|
||||
"require-again": "^2.0.0",
|
||||
"selenium-server": "^3.0.1",
|
||||
"sinon": "^4.2.2",
|
||||
"selenium-server": "^3.9.1",
|
||||
"sinon": "^4.3.0",
|
||||
"sinon-chai": "^2.8.0",
|
||||
"sinon-stub-promise": "^4.0.0",
|
||||
"webpack-bundle-analyzer": "^2.2.1",
|
||||
|
||||
@@ -478,8 +478,8 @@ describe('POST /chat', () => {
|
||||
context('Spam prevention', () => {
|
||||
it('Returns an error when the user has been posting too many messages', async () => {
|
||||
// Post as many messages are needed to reach the spam limit
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) { // eslint-disable-line no-await-in-loop
|
||||
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage });
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT; i++) {
|
||||
let result = await additionalMember.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
|
||||
expect(result.message.id).to.exist;
|
||||
}
|
||||
|
||||
@@ -494,8 +494,8 @@ describe('POST /chat', () => {
|
||||
let userSocialite = await member.update({'contributor.level': SPAM_MIN_EXEMPT_CONTRIB_LEVEL, 'flags.chatRevoked': false});
|
||||
|
||||
// Post 1 more message than the spam limit to ensure they do not reach the limit
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) { // eslint-disable-line no-await-in-loop
|
||||
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage });
|
||||
for (let i = 0; i < SPAM_MESSAGE_LIMIT + 1; i++) {
|
||||
let result = await userSocialite.post(`/groups/${TAVERN_ID}/chat`, { message: testMessage }); // eslint-disable-line no-await-in-loop
|
||||
expect(result.message.id).to.exist;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,10 +113,10 @@ describe('GET /tasks/user', () => {
|
||||
await user.sync();
|
||||
let initialTodoCount = user.tasksOrder.todos.length;
|
||||
|
||||
for (let i = 0; i < numberOfTodos; i++) { // eslint-disable-line no-await-in-loop
|
||||
for (let i = 0; i < numberOfTodos; i++) {
|
||||
let id = todos[i]._id;
|
||||
|
||||
await user.post(`/tasks/${id}/score/up`);
|
||||
await user.post(`/tasks/${id}/score/up`); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
await user.sync();
|
||||
|
||||
|
||||
@@ -33,9 +33,9 @@ describe('POST /tasks/clearCompletedTodos', () => {
|
||||
let tasks = await user.get('/tasks/user?type=todos');
|
||||
expect(tasks.length).to.equal(initialTodoCount + 7);
|
||||
|
||||
for (let task of tasks) { // eslint-disable-line no-await-in-loop
|
||||
for (let task of tasks) {
|
||||
if (['todo 2', 'todo 3', 'todo 6'].indexOf(task.text) !== -1) {
|
||||
await user.post(`/tasks/${task._id}/score/up`);
|
||||
await user.post(`/tasks/${task._id}/score/up`); // eslint-disable-line no-await-in-loop
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,22 @@ describe('POST /user/class/cast/:spellId', () => {
|
||||
expect(group.chat[0].uuid).to.equal('system');
|
||||
});
|
||||
|
||||
it('cast bulk', async () => {
|
||||
let { group, groupLeader } = await createAndPopulateGroup({
|
||||
groupDetails: { type: 'party', privacy: 'private' },
|
||||
members: 1,
|
||||
});
|
||||
|
||||
await groupLeader.update({'stats.mp': 200, 'stats.class': 'wizard', 'stats.lvl': 13});
|
||||
await groupLeader.post('/user/class/cast/earth', {quantity: 2});
|
||||
|
||||
await sleep(1);
|
||||
await group.sync();
|
||||
|
||||
expect(group.chat[0]).to.exist;
|
||||
expect(group.chat[0].uuid).to.equal('system');
|
||||
});
|
||||
|
||||
it('searing brightness does not affect challenge or group tasks', async () => {
|
||||
let guild = await generateGroup(user);
|
||||
let challenge = await generateChallenge(user, guild);
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('PUT /user/webhook/:id', () => {
|
||||
});
|
||||
|
||||
it('returns an error if validation fails', async () => {
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, { url: 'foo', enabled: true })).to.eventually.be.rejected.and.eql({
|
||||
await expect(user.put(`/user/webhook/${webhookToUpdate.id}`, { url: 'foo_invalid', enabled: true })).to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: 'User validation failed',
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
/* eslint-disable global-require */
|
||||
import request from 'request';
|
||||
import got from 'got';
|
||||
import nconf from 'nconf';
|
||||
import nodemailer from 'nodemailer';
|
||||
import Bluebird from 'bluebird';
|
||||
import requireAgain from 'require-again';
|
||||
import logger from '../../../../../website/server/libs/logger';
|
||||
import { TAVERN_ID } from '../../../../../website/server/models/group';
|
||||
|
||||
function defer () {
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
let promise = new Bluebird((resolveParam, rejectParam) => {
|
||||
resolve = resolveParam;
|
||||
reject = rejectParam;
|
||||
});
|
||||
|
||||
return {
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
};
|
||||
}
|
||||
import { defer } from '../../../../helpers/api-unit.helper';
|
||||
|
||||
function getUser () {
|
||||
return {
|
||||
@@ -158,7 +142,7 @@ describe('emails', () => {
|
||||
|
||||
describe('sendTxnEmail', () => {
|
||||
beforeEach(() => {
|
||||
sandbox.stub(request, 'post');
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -176,8 +160,9 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(request.post).to.be.calledWith(sinon.match({
|
||||
json: {
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match((value) => {
|
||||
@@ -199,7 +184,7 @@ describe('emails', () => {
|
||||
};
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(request.post).not.to.be.called;
|
||||
expect(got.post).not.to.be.called;
|
||||
});
|
||||
|
||||
it('uses getUserInfo in case of user data', () => {
|
||||
@@ -210,8 +195,9 @@ describe('emails', () => {
|
||||
let mailingInfo = getUser();
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType);
|
||||
expect(request.post).to.be.calledWith(sinon.match({
|
||||
json: {
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
data: {
|
||||
emailType: sinon.match.same(emailType),
|
||||
to: sinon.match(val => val[0]._id === mailingInfo._id),
|
||||
@@ -232,8 +218,9 @@ describe('emails', () => {
|
||||
let variables = [1, 2, 3];
|
||||
|
||||
sendTxnEmail(mailingInfo, emailType, variables);
|
||||
expect(request.post).to.be.calledWith(sinon.match({
|
||||
json: {
|
||||
expect(got.post).to.be.calledWith('undefined/job', sinon.match({
|
||||
json: true,
|
||||
body: {
|
||||
data: {
|
||||
variables: sinon.match((value) => {
|
||||
return value[0].name === 'BASE_URL';
|
||||
|
||||
@@ -216,6 +216,9 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
});
|
||||
|
||||
it('subscribes with amazon', async () => {
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
await amzLib.subscribe({
|
||||
billingAgreementId,
|
||||
sub,
|
||||
@@ -241,8 +244,13 @@ describe('Amazon Payments - Subscribe', () => {
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
|
||||
// Add existing users
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
// Set expected amount
|
||||
sub.key = 'group_monthly';
|
||||
sub.price = 9;
|
||||
amount = 12;
|
||||
|
||||
@@ -227,6 +227,11 @@ describe('checkout with subscription', () => {
|
||||
sub = data.sub;
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
headers = {};
|
||||
|
||||
await stripePayments.checkout({
|
||||
@@ -267,9 +272,15 @@ describe('checkout with subscription', () => {
|
||||
groupId = group._id;
|
||||
email = 'test@test.com';
|
||||
headers = {};
|
||||
|
||||
// Add user to group
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
user = new User();
|
||||
user.guilds.push(groupId);
|
||||
await user.save();
|
||||
|
||||
group.memberCount = 2;
|
||||
await group.save();
|
||||
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import request from 'request';
|
||||
import got from 'got';
|
||||
import {
|
||||
WebhookSender,
|
||||
taskScoredWebhook,
|
||||
groupChatReceivedWebhook,
|
||||
taskActivityWebhook,
|
||||
} from '../../../../../website/server/libs/webhook';
|
||||
import { defer } from '../../../../helpers/api-unit.helper';
|
||||
|
||||
describe('webhooks', () => {
|
||||
let webhooks;
|
||||
|
||||
beforeEach(() => {
|
||||
sandbox.stub(request, 'post');
|
||||
sandbox.stub(got, 'post').returns(defer().promise);
|
||||
|
||||
webhooks = [{
|
||||
id: 'taskActivity',
|
||||
@@ -59,8 +60,9 @@ describe('webhooks', () => {
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body,
|
||||
});
|
||||
});
|
||||
@@ -81,8 +83,9 @@ describe('webhooks', () => {
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultTransformData).to.not.be.called;
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
json: true,
|
||||
body: {
|
||||
foo: 'bar',
|
||||
baz: 'biz',
|
||||
@@ -117,7 +120,7 @@ describe('webhooks', () => {
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
|
||||
expect(request.post).to.not.be.called;
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('can pass in a webhook filter function that filters on data', () => {
|
||||
@@ -136,10 +139,8 @@ describe('webhooks', () => {
|
||||
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
});
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
||||
});
|
||||
|
||||
it('ignores disabled webhooks', () => {
|
||||
@@ -151,7 +152,7 @@ describe('webhooks', () => {
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks with invalid urls', () => {
|
||||
@@ -163,7 +164,7 @@ describe('webhooks', () => {
|
||||
|
||||
sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
|
||||
it('ignores webhooks of other types', () => {
|
||||
@@ -178,9 +179,8 @@ describe('webhooks', () => {
|
||||
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
@@ -198,14 +198,12 @@ describe('webhooks', () => {
|
||||
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
|
||||
], body);
|
||||
|
||||
expect(request.post).to.be.calledTwice;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://custom-url.com',
|
||||
expect(got.post).to.be.calledTwice;
|
||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
url: 'http://other-url.com',
|
||||
expect(got.post).to.be.calledWithMatch('http://other-url.com', {
|
||||
body,
|
||||
json: true,
|
||||
});
|
||||
@@ -252,8 +250,9 @@ describe('webhooks', () => {
|
||||
it('sends task and stats data', () => {
|
||||
taskScoredWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
type: 'scored',
|
||||
user: {
|
||||
@@ -283,7 +282,7 @@ describe('webhooks', () => {
|
||||
|
||||
taskScoredWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -304,8 +303,9 @@ describe('webhooks', () => {
|
||||
|
||||
taskActivityWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||
json: true,
|
||||
body: {
|
||||
type,
|
||||
task: data.task,
|
||||
@@ -319,7 +319,7 @@ describe('webhooks', () => {
|
||||
|
||||
taskActivityWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -340,8 +340,9 @@ describe('webhooks', () => {
|
||||
|
||||
groupChatReceivedWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.be.calledOnce;
|
||||
expect(request.post).to.be.calledWithMatch({
|
||||
expect(got.post).to.be.calledOnce;
|
||||
expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, {
|
||||
json: true,
|
||||
body: {
|
||||
group: {
|
||||
id: 'group-id',
|
||||
@@ -370,7 +371,7 @@ describe('webhooks', () => {
|
||||
|
||||
groupChatReceivedWebhook.send(webhooks, data);
|
||||
|
||||
expect(request.post).to.not.be.called;
|
||||
expect(got.post).to.not.be.called;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -352,7 +352,12 @@ describe('User Model', () => {
|
||||
context('manage unallocated stats points notifications', () => {
|
||||
it('doesn\'t add a notification if there are no points to allocate', async () => {
|
||||
let user = new User();
|
||||
|
||||
user.flags.classSelected = true;
|
||||
user.preferences.disableClasses = false;
|
||||
user.stats.class = 'warrior';
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
user.stats.points = 0;
|
||||
@@ -363,6 +368,10 @@ describe('User Model', () => {
|
||||
|
||||
it('removes a notification if there are no more points to allocate', async () => {
|
||||
let user = new User();
|
||||
|
||||
user.flags.classSelected = true;
|
||||
user.preferences.disableClasses = false;
|
||||
user.stats.class = 'warrior';
|
||||
user.stats.points = 9;
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
|
||||
@@ -377,6 +386,9 @@ describe('User Model', () => {
|
||||
|
||||
it('adds a notification if there are points to allocate', async () => {
|
||||
let user = new User();
|
||||
user.flags.classSelected = true;
|
||||
user.preferences.disableClasses = false;
|
||||
user.stats.class = 'warrior';
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
@@ -391,6 +403,9 @@ describe('User Model', () => {
|
||||
it('adds a notification if the points to allocate have changed', async () => {
|
||||
let user = new User();
|
||||
user.stats.points = 9;
|
||||
user.flags.classSelected = true;
|
||||
user.preferences.disableClasses = false;
|
||||
user.stats.class = 'warrior';
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
@@ -406,6 +421,37 @@ describe('User Model', () => {
|
||||
expect(user.notifications[0].data.points).to.equal(11);
|
||||
expect(user.notifications[0].id).to.not.equal(oldNotificationsUUID);
|
||||
});
|
||||
|
||||
it('does not add a notification if the user has disabled classes', async () => {
|
||||
let user = new User();
|
||||
user.stats.points = 9;
|
||||
user.flags.classSelected = true;
|
||||
user.preferences.disableClasses = true;
|
||||
user.stats.class = 'warrior';
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
user.stats.points = 9;
|
||||
user = await user.save();
|
||||
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount);
|
||||
});
|
||||
|
||||
it('does not add a notification if the user has not selected a class', async () => {
|
||||
let user = new User();
|
||||
user.stats.points = 9;
|
||||
user.flags.classSelected = false;
|
||||
user.stats.class = 'warrior';
|
||||
user = await user.save(); // necessary for user.isSelected to work correctly
|
||||
|
||||
const oldNotificationsCount = user.notifications.length;
|
||||
|
||||
user.stats.points = 9;
|
||||
user = await user.save();
|
||||
|
||||
expect(user.notifications.length).to.equal(oldNotificationsCount);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -108,3 +108,19 @@ export function generateDaily (user) {
|
||||
|
||||
return task;
|
||||
}
|
||||
|
||||
export function defer () {
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
let promise = new Promise((resolveParam, rejectParam) => {
|
||||
resolve = resolveParam;
|
||||
reject = rejectParam;
|
||||
});
|
||||
|
||||
return {
|
||||
resolve,
|
||||
reject,
|
||||
promise,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
:key="group.key",
|
||||
)
|
||||
.custom-control.custom-checkbox
|
||||
input.custom-control-input(type="checkbox", v-model="viewOptions[group.key].selected", :id="group.key")
|
||||
label.custom-control-label(v-once, :for="group.key") {{ group.label }}
|
||||
input.custom-control-input(type="checkbox", v-model="viewOptions[group.key].selected", :id="groupBy + group.key")
|
||||
label.custom-control-label(v-once, :for="groupBy + group.key") {{ group.label }}
|
||||
|
||||
.standard-page
|
||||
.clearfix
|
||||
|
||||
@@ -160,6 +160,7 @@ import Vue from 'vue';
|
||||
import moment from 'moment';
|
||||
import filter from 'lodash/filter';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import groupBy from 'lodash/groupBy';
|
||||
import { mapState } from 'client/libs/store';
|
||||
import styleHelper from 'client/mixins/styleHelper';
|
||||
|
||||
@@ -220,45 +221,53 @@ export default {
|
||||
computed: {
|
||||
...mapState({user: 'user.data'}),
|
||||
conversations () {
|
||||
let conversations = {};
|
||||
for (let messageId in this.user.inbox.messages) {
|
||||
let message = this.user.inbox.messages[messageId];
|
||||
let userId = message.uuid;
|
||||
const inboxGroup = groupBy(this.user.inbox.messages, 'uuid');
|
||||
|
||||
if (!conversations[userId]) {
|
||||
conversations[userId] = {
|
||||
name: message.user,
|
||||
key: userId,
|
||||
messages: [],
|
||||
};
|
||||
// Create conversation objects
|
||||
const convos = [];
|
||||
for (let key in inboxGroup) {
|
||||
const convoSorted = sortBy(inboxGroup[key], [(o) => {
|
||||
return o.timestamp;
|
||||
}]);
|
||||
|
||||
// Fix poor inbox chat models
|
||||
const newChatModels = convoSorted.map(chat => {
|
||||
let newChat = Object.assign({}, chat);
|
||||
if (newChat.sent) {
|
||||
newChat.toUUID = newChat.uuid;
|
||||
newChat.toUser = newChat.user;
|
||||
newChat.uuid = this.user._id;
|
||||
newChat.user = this.user.profile.name;
|
||||
newChat.contributor = this.user.contributor;
|
||||
newChat.backer = this.user.backer;
|
||||
}
|
||||
return newChat;
|
||||
});
|
||||
|
||||
const recentMessage = newChatModels[newChatModels.length - 1];
|
||||
|
||||
// Special case where we have placeholder message because conversations are just grouped messages for now
|
||||
if (!recentMessage.text) {
|
||||
newChatModels.splice(newChatModels.length - 1, 1);
|
||||
}
|
||||
|
||||
let newMessage = {
|
||||
text: message.text,
|
||||
timestamp: message.timestamp,
|
||||
user: message.user,
|
||||
uuid: message.uuid,
|
||||
id: message.id,
|
||||
contributor: message.contributor,
|
||||
const convoModel = {
|
||||
name: recentMessage.toUser ? recentMessage.toUser : recentMessage.user, // Handles case where from user sent the only message or the to user sent the only message
|
||||
key: recentMessage.toUUID ? recentMessage.toUUID : recentMessage.uuid,
|
||||
messages: newChatModels,
|
||||
lastMessageText: recentMessage.text,
|
||||
date: recentMessage.timestamp,
|
||||
};
|
||||
|
||||
if (message.sent) {
|
||||
newMessage.user = this.user.profile.name;
|
||||
newMessage.uuid = this.user._id;
|
||||
newMessage.contributor = this.user.contributor;
|
||||
}
|
||||
|
||||
if (newMessage.text) conversations[userId].messages.push(newMessage);
|
||||
conversations[userId].lastMessageText = message.text;
|
||||
conversations[userId].date = message.timestamp;
|
||||
convos.push(convoModel);
|
||||
}
|
||||
|
||||
conversations = sortBy(conversations, [(o) => {
|
||||
// Sort models by most recent
|
||||
const conversations = sortBy(convos, [(o) => {
|
||||
return moment(o.date).toDate();
|
||||
}]);
|
||||
conversations = conversations.reverse();
|
||||
|
||||
return conversations;
|
||||
return conversations.reverse();
|
||||
},
|
||||
filtersConversations () {
|
||||
if (!this.search) return this.conversations;
|
||||
|
||||
@@ -21,7 +21,7 @@ export function fetch (store, options = {}) { // eslint-disable-line no-shadow
|
||||
export async function set (store, changes) {
|
||||
const user = store.state.user.data;
|
||||
|
||||
for (let key in changes) { // eslint-disable-line no-await-in-loop
|
||||
for (let key in changes) {
|
||||
if (key === 'tags') {
|
||||
// Keep challenge and group tags
|
||||
const oldTags = user.tags.filter(t => {
|
||||
@@ -31,7 +31,7 @@ export async function set (store, changes) {
|
||||
user.tags = changes[key].concat(oldTags);
|
||||
|
||||
// Remove deleted tags from tasks
|
||||
const userTasksByType = (await store.dispatch('tasks:fetchUserTasks')).data;
|
||||
const userTasksByType = (await store.dispatch('tasks:fetchUserTasks')).data; // eslint-disable-line no-await-in-loop
|
||||
|
||||
Object.keys(userTasksByType).forEach(taskType => {
|
||||
userTasksByType[taskType].forEach(task => {
|
||||
@@ -119,7 +119,8 @@ export function openMysteryItem () {
|
||||
return axios.post('/api/v3/user/open-mystery-item');
|
||||
}
|
||||
|
||||
export function newStuffLater () {
|
||||
export function newStuffLater (store) {
|
||||
store.state.user.data.flags.newStuff = false;
|
||||
return axios.post('/api/v3/news/tell-me-later');
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"tip10": "You can win gems by competing in Challenges. New ones are added every day!",
|
||||
"tip11": "Having more than four Party members increases accountability!",
|
||||
"tip12": "Add checklists to your To-Dos to multiply your rewards!",
|
||||
"tip13": "Click “Filters” on your task page to make an unwieldy task list very manageable!",
|
||||
"tip13": "Click “Tags” on your task page to make an unwieldy task list very manageable!",
|
||||
"tip14": "You can add headers or inspirational quotes to your list as Habits with no (+/-).",
|
||||
"tip15": "Complete all the Masterclasser Quest-lines to learn about Habitica’s secret lore.",
|
||||
"tip16": "Click the link to the Data Display Tool in the footer for valuable insights on your progress.",
|
||||
|
||||
@@ -149,6 +149,8 @@
|
||||
"taskAliasAlreadyUsed": "Task alias already used on another task.",
|
||||
"taskNotFound": "Task not found.",
|
||||
"invalidTaskType": "Task type must be one of \"habit\", \"daily\", \"todo\", \"reward\".",
|
||||
"invalidTasksType": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\".",
|
||||
"invalidTasksTypeExtra": "Task type must be one of \"habits\", \"dailys\", \"todos\", \"rewards\", \"completedTodos\".",
|
||||
"cantDeleteChallengeTasks": "A task belonging to a challenge can't be deleted.",
|
||||
"checklistOnlyDailyTodo": "Checklists are supported only on Dailies and To-Dos",
|
||||
"checklistItemNotFound": "No checklist item was found with given id.",
|
||||
|
||||
@@ -247,7 +247,7 @@ api.loginLocal = {
|
||||
let username = req.body.username;
|
||||
let password = req.body.password;
|
||||
|
||||
if (validator.isEmail(username)) {
|
||||
if (validator.isEmail(String(username))) {
|
||||
login = {'auth.local.email': username.toLowerCase()}; // Emails are stored lowercase
|
||||
} else {
|
||||
login = {'auth.local.username': username};
|
||||
@@ -410,7 +410,7 @@ api.pusherAuth = {
|
||||
}
|
||||
|
||||
resourceId = resourceId.join('-'); // the split at the beginning had split resourceId too
|
||||
if (!validator.isUUID(resourceId)) {
|
||||
if (!validator.isUUID(String(resourceId))) {
|
||||
throw new BadRequest('Invalid Pusher resource id, must be a UUID.');
|
||||
}
|
||||
|
||||
|
||||
@@ -287,7 +287,7 @@ api.getUserTasks = {
|
||||
async handler (req, res) {
|
||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
types.push('completedTodos', '_allCompletedTodos'); // _allCompletedTodos is currently in BETA and is likely to be removed in future
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
|
||||
req.checkQuery('type', res.t('invalidTasksTypeExtra')).optional().isIn(types);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
@@ -325,7 +325,7 @@ api.getChallengeTasks = {
|
||||
async handler (req, res) {
|
||||
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||
let types = Tasks.tasksTypes.map(type => `${type}s`);
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
|
||||
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
@@ -86,7 +86,7 @@ api.getGroupTasks = {
|
||||
middlewares: [authWithHeaders()],
|
||||
async handler (req, res) {
|
||||
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty().isUUID();
|
||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(types);
|
||||
req.checkQuery('type', res.t('invalidTasksType')).optional().isIn(types);
|
||||
|
||||
let validationErrors = req.validationErrors();
|
||||
if (validationErrors) throw validationErrors;
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { authWithHeaders } from '../../middlewares/auth';
|
||||
import common from '../../../common';
|
||||
import {
|
||||
NotFound,
|
||||
BadRequest,
|
||||
NotAuthorized,
|
||||
} from '../../libs/errors';
|
||||
import * as Tasks from '../../models/task';
|
||||
import {
|
||||
basicFields as basicGroupFields,
|
||||
model as Group,
|
||||
} from '../../models/group';
|
||||
import { model as User } from '../../models/user';
|
||||
import * as Tasks from '../../models/task';
|
||||
import Bluebird from 'bluebird';
|
||||
import _ from 'lodash';
|
||||
import * as passwordUtils from '../../libs/password';
|
||||
@@ -18,6 +16,7 @@ import {
|
||||
getUserInfo,
|
||||
sendTxn as txnEmail,
|
||||
} from '../../libs/email';
|
||||
import Queue from '../../libs/queue';
|
||||
import nconf from 'nconf';
|
||||
import get from 'lodash/get';
|
||||
|
||||
@@ -434,6 +433,8 @@ api.deleteUser = {
|
||||
]);
|
||||
}
|
||||
|
||||
if (feedback) Queue.sendMessage({feedback, username: user.profile.name}, user._id);
|
||||
|
||||
res.analytics.track('account delete', {
|
||||
uuid: user._id,
|
||||
hitType: 'event',
|
||||
@@ -524,224 +525,6 @@ api.getUserAnonymized = {
|
||||
},
|
||||
};
|
||||
|
||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||
|
||||
async function castTaskSpell (res, req, targetId, user, spell) {
|
||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||
|
||||
const task = await Tasks.Task.findOne({
|
||||
_id: targetId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast'));
|
||||
if (task.group.id) throw new BadRequest(res.t('groupTasksNoCast'));
|
||||
|
||||
spell.cast(user, task, req);
|
||||
|
||||
const results = await Bluebird.all([
|
||||
user.save(),
|
||||
task.save(),
|
||||
]);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function castMultiTaskSpell (req, user, spell) {
|
||||
const tasks = await Tasks.Task.find({
|
||||
userId: user._id,
|
||||
...Tasks.taskIsGroupOrChallengeQuery,
|
||||
}).exec();
|
||||
|
||||
spell.cast(user, tasks, req);
|
||||
|
||||
const toSave = tasks
|
||||
.filter(t => t.isModified())
|
||||
.map(t => t.save());
|
||||
toSave.unshift(user.save());
|
||||
const saved = await Bluebird.all(toSave);
|
||||
|
||||
const response = {
|
||||
tasks: saved,
|
||||
user,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function castSelfSpell (req, user, spell) {
|
||||
spell.cast(user, null, req);
|
||||
await user.save();
|
||||
}
|
||||
|
||||
async function castPartySpell (req, party, partyMembers, user, spell) {
|
||||
if (!party) {
|
||||
partyMembers = [user]; // Act as solo party
|
||||
} else {
|
||||
partyMembers = await User
|
||||
.find({
|
||||
'party._id': party._id,
|
||||
_id: { $ne: user._id }, // add separately
|
||||
})
|
||||
// .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save
|
||||
// default values for non-selected fields and pre('save') will mess up thinking some values are missing
|
||||
.exec();
|
||||
|
||||
partyMembers.unshift(user);
|
||||
}
|
||||
|
||||
spell.cast(user, partyMembers, req);
|
||||
await Bluebird.all(partyMembers.map(m => m.save()));
|
||||
|
||||
return partyMembers;
|
||||
}
|
||||
|
||||
async function castUserSpell (res, req, party, partyMembers, targetId, user, spell) {
|
||||
if (!party && (!targetId || user._id === targetId)) {
|
||||
partyMembers = user;
|
||||
} else {
|
||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||
if (!party) throw new NotFound(res.t('partyNotFound'));
|
||||
partyMembers = await User
|
||||
.findOne({_id: targetId, 'party._id': party._id})
|
||||
// .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save
|
||||
// default values for non-selected fields and pre('save') will mess up thinking some values are missing
|
||||
.exec();
|
||||
}
|
||||
|
||||
if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId}));
|
||||
|
||||
spell.cast(user, partyMembers, req);
|
||||
|
||||
if (partyMembers !== user) {
|
||||
await Bluebird.all([
|
||||
user.save(),
|
||||
partyMembers.save(),
|
||||
]);
|
||||
} else {
|
||||
await partyMembers.save(); // partyMembers is user
|
||||
}
|
||||
|
||||
return partyMembers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target
|
||||
* @apiName UserCast
|
||||
* @apiGroup User
|
||||
*
|
||||
|
||||
* @apiParam (Path) {String=fireball, mpheal, earth, frost, smash, defensiveStance, valorousPresence, intimidate, pickPocket, backStab, toolsOfTrade, stealth, heal, protectAura, brightness, healAll} spellId The skill to cast.
|
||||
* @apiParam (Query) {UUID} targetId Query parameter, necessary if the spell is cast on a party member or task. Not used if the spell is case on the user or the user's current party.
|
||||
* @apiParamExample {json} Query example:
|
||||
* Cast "Pickpocket" on a task:
|
||||
* https://habitica.com/api/v3/user/class/cast/pickPocket?targetId=fd427623...
|
||||
*
|
||||
* Cast "Tools of the Trade" on the party:
|
||||
* https://habitica.com/api/v3/user/class/cast/toolsOfTrade
|
||||
*
|
||||
* @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned.
|
||||
*
|
||||
* @apiDescription Skill Key to Name Mapping
|
||||
* Mage
|
||||
* fireball: "Burst of Flames"
|
||||
* mpheal: "Ethereal Surge"
|
||||
* earth: "Earthquake"
|
||||
* frost: "Chilling Frost"
|
||||
*
|
||||
* Warrior
|
||||
* smash: "Brutal Smash"
|
||||
* defensiveStance: "Defensive Stance"
|
||||
* valorousPresence: "Valorous Presence"
|
||||
* intimidate: "Intimidating Gaze"
|
||||
*
|
||||
* Rogue
|
||||
* pickPocket: "Pickpocket"
|
||||
* backStab: "Backstab"
|
||||
* toolsOfTrade: "Tools of the Trade"
|
||||
* stealth: "Stealth"
|
||||
*
|
||||
* Healer
|
||||
* heal: "Healing Light"
|
||||
* protectAura: "Protective Aura"
|
||||
* brightness: "Searing Brightness"
|
||||
* healAll: "Blessing"
|
||||
*
|
||||
* @apiError (400) {NotAuthorized} Not enough mana.
|
||||
* @apiUse TaskNotFound
|
||||
* @apiUse PartyNotFound
|
||||
* @apiUse UserNotFound
|
||||
*/
|
||||
api.castSpell = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/class/cast/:spellId',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let spellId = req.params.spellId;
|
||||
let targetId = req.query.targetId;
|
||||
|
||||
// optional because not required by all targetTypes, presence is checked later if necessary
|
||||
req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
|
||||
let spell = common.content.spells[klass][spellId];
|
||||
|
||||
if (!spell) throw new NotFound(res.t('spellNotFound', {spellId}));
|
||||
if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana'));
|
||||
if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold'));
|
||||
if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl}));
|
||||
|
||||
let targetType = spell.target;
|
||||
|
||||
if (targetType === 'task') {
|
||||
const results = await castTaskSpell(res, req, targetId, user, spell);
|
||||
res.respond(200, {
|
||||
user: results[0],
|
||||
task: results[1],
|
||||
});
|
||||
} else if (targetType === 'self') {
|
||||
await castSelfSpell(req, user, spell);
|
||||
res.respond(200, { user });
|
||||
} else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary
|
||||
const response = await castMultiTaskSpell(req, user, spell);
|
||||
res.respond(200, response);
|
||||
} else if (targetType === 'party' || targetType === 'user') {
|
||||
const party = await Group.getGroup({groupId: 'party', user});
|
||||
// arrays of users when targetType is 'party' otherwise single users
|
||||
let partyMembers;
|
||||
|
||||
if (targetType === 'party') {
|
||||
partyMembers = await castPartySpell(req, party, partyMembers, user, spell);
|
||||
} else {
|
||||
partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell);
|
||||
}
|
||||
|
||||
let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers];
|
||||
|
||||
// Only return some fields.
|
||||
// See comment above on why we can't just select the necessary fields when querying
|
||||
partyMembersRes = partyMembersRes.map(partyMember => {
|
||||
return common.pickDeep(partyMember.toJSON(), common.$w(partyMembersFields));
|
||||
});
|
||||
|
||||
res.respond(200, {
|
||||
partyMembers: partyMembersRes,
|
||||
user,
|
||||
});
|
||||
|
||||
if (party && !spell.silent) {
|
||||
let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``;
|
||||
party.sendChat(message);
|
||||
await party.save();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/sleep Make the user start / stop sleeping (resting in the Inn)
|
||||
* @apiName UserSleep
|
||||
|
||||
140
website/server/controllers/api-v3/user/spells.js
Normal file
140
website/server/controllers/api-v3/user/spells.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import { authWithHeaders } from '../../../middlewares/auth';
|
||||
import common from '../../../../common';
|
||||
import {
|
||||
model as Group,
|
||||
} from '../../../models/group';
|
||||
import {
|
||||
NotAuthorized,
|
||||
NotFound,
|
||||
} from '../../../libs/errors';
|
||||
import {
|
||||
castTaskSpell,
|
||||
castMultiTaskSpell,
|
||||
castSelfSpell,
|
||||
castPartySpell,
|
||||
castUserSpell,
|
||||
} from '../../../libs/spells';
|
||||
|
||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||
|
||||
let api = {};
|
||||
|
||||
/**
|
||||
* @api {post} /api/v3/user/class/cast/:spellId Cast a skill (spell) on a target
|
||||
* @apiName UserCast
|
||||
* @apiGroup User
|
||||
*
|
||||
|
||||
* @apiParam (Path) {String=fireball, mpheal, earth, frost, smash, defensiveStance, valorousPresence, intimidate, pickPocket, backStab, toolsOfTrade, stealth, heal, protectAura, brightness, healAll} spellId The skill to cast.
|
||||
* @apiParam (Query) {UUID} targetId Query parameter, necessary if the spell is cast on a party member or task. Not used if the spell is case on the user or the user's current party.
|
||||
* @apiParamExample {json} Query example:
|
||||
* Cast "Pickpocket" on a task:
|
||||
* https://habitica.com/api/v3/user/class/cast/pickPocket?targetId=fd427623...
|
||||
*
|
||||
* Cast "Tools of the Trade" on the party:
|
||||
* https://habitica.com/api/v3/user/class/cast/toolsOfTrade
|
||||
*
|
||||
* @apiSuccess data Will return the modified targets. For party members only the necessary fields will be populated. The user is always returned.
|
||||
*
|
||||
* @apiDescription Skill Key to Name Mapping
|
||||
* Mage
|
||||
* fireball: "Burst of Flames"
|
||||
* mpheal: "Ethereal Surge"
|
||||
* earth: "Earthquake"
|
||||
* frost: "Chilling Frost"
|
||||
*
|
||||
* Warrior
|
||||
* smash: "Brutal Smash"
|
||||
* defensiveStance: "Defensive Stance"
|
||||
* valorousPresence: "Valorous Presence"
|
||||
* intimidate: "Intimidating Gaze"
|
||||
*
|
||||
* Rogue
|
||||
* pickPocket: "Pickpocket"
|
||||
* backStab: "Backstab"
|
||||
* toolsOfTrade: "Tools of the Trade"
|
||||
* stealth: "Stealth"
|
||||
*
|
||||
* Healer
|
||||
* heal: "Healing Light"
|
||||
* protectAura: "Protective Aura"
|
||||
* brightness: "Searing Brightness"
|
||||
* healAll: "Blessing"
|
||||
*
|
||||
* @apiError (400) {NotAuthorized} Not enough mana.
|
||||
* @apiUse TaskNotFound
|
||||
* @apiUse PartyNotFound
|
||||
* @apiUse UserNotFound
|
||||
*/
|
||||
api.castSpell = {
|
||||
method: 'POST',
|
||||
middlewares: [authWithHeaders()],
|
||||
url: '/user/class/cast/:spellId',
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
let spellId = req.params.spellId;
|
||||
let targetId = req.query.targetId;
|
||||
const quantity = req.body.quantity || 1;
|
||||
|
||||
// optional because not required by all targetTypes, presence is checked later if necessary
|
||||
req.checkQuery('targetId', res.t('targetIdUUID')).optional().isUUID();
|
||||
|
||||
let reqValidationErrors = req.validationErrors();
|
||||
if (reqValidationErrors) throw reqValidationErrors;
|
||||
|
||||
let klass = common.content.spells.special[spellId] ? 'special' : user.stats.class;
|
||||
let spell = common.content.spells[klass][spellId];
|
||||
|
||||
if (!spell) throw new NotFound(res.t('spellNotFound', {spellId}));
|
||||
if (spell.mana > user.stats.mp) throw new NotAuthorized(res.t('notEnoughMana'));
|
||||
if (spell.value > user.stats.gp && !spell.previousPurchase) throw new NotAuthorized(res.t('messageNotEnoughGold'));
|
||||
if (spell.lvl > user.stats.lvl) throw new NotAuthorized(res.t('spellLevelTooHigh', {level: spell.lvl}));
|
||||
|
||||
let targetType = spell.target;
|
||||
|
||||
if (targetType === 'task') {
|
||||
const results = await castTaskSpell(res, req, targetId, user, spell, quantity);
|
||||
res.respond(200, {
|
||||
user: results[0],
|
||||
task: results[1],
|
||||
});
|
||||
} else if (targetType === 'self') {
|
||||
await castSelfSpell(req, user, spell, quantity);
|
||||
res.respond(200, { user });
|
||||
} else if (targetType === 'tasks') { // new target type in v3: when all the user's tasks are necessary
|
||||
const response = await castMultiTaskSpell(req, user, spell, quantity);
|
||||
res.respond(200, response);
|
||||
} else if (targetType === 'party' || targetType === 'user') {
|
||||
const party = await Group.getGroup({groupId: 'party', user});
|
||||
// arrays of users when targetType is 'party' otherwise single users
|
||||
let partyMembers;
|
||||
|
||||
if (targetType === 'party') {
|
||||
partyMembers = await castPartySpell(req, party, partyMembers, user, spell, quantity);
|
||||
} else {
|
||||
partyMembers = await castUserSpell(res, req, party, partyMembers, targetId, user, spell, quantity);
|
||||
}
|
||||
|
||||
let partyMembersRes = Array.isArray(partyMembers) ? partyMembers : [partyMembers];
|
||||
|
||||
// Only return some fields.
|
||||
// See comment above on why we can't just select the necessary fields when querying
|
||||
partyMembersRes = partyMembersRes.map(partyMember => {
|
||||
return common.pickDeep(partyMember.toJSON(), common.$w(partyMembersFields));
|
||||
});
|
||||
|
||||
res.respond(200, {
|
||||
partyMembers: partyMembersRes,
|
||||
user,
|
||||
});
|
||||
|
||||
if (party && !spell.silent) {
|
||||
let message = `\`${user.profile.name} casts ${spell.text()}${targetType === 'user' ? ` on ${partyMembers.profile.name}` : ' for the party'}.\``;
|
||||
party.sendChat(message);
|
||||
await party.save();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
@@ -144,7 +144,12 @@ api.exportUserDataXml = {
|
||||
'Content-Type': 'text/xml',
|
||||
'Content-disposition': 'attachment; filename=habitica-user-data.xml',
|
||||
});
|
||||
res.status(200).send(js2xml('user', userData));
|
||||
res.status(200).send(js2xml.parse('user', userData, {
|
||||
cdataInvalidChars: true,
|
||||
declaration: {
|
||||
include: false,
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -270,10 +270,10 @@ api.subscribe = async function subscribe (options) {
|
||||
let priceOfSingleMember = 3;
|
||||
|
||||
if (groupId) {
|
||||
let groupFields = basicGroupFields.concat(' purchased');
|
||||
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||
|
||||
amount = sub.price + (group.memberCount - leaderCount) * priceOfSingleMember;
|
||||
const groupFields = basicGroupFields.concat(' purchased');
|
||||
const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||
const membersCount = await group.getMemberCount();
|
||||
amount = sub.price + (membersCount - leaderCount) * priceOfSingleMember;
|
||||
}
|
||||
|
||||
await this.setBillingAgreementDetails({
|
||||
|
||||
@@ -33,16 +33,16 @@ api.verifyGemPurchase = async function verifyGemPurchase (user, receipt, headers
|
||||
let correctReceipt = false;
|
||||
|
||||
// Purchasing one item at a time (processing of await(s) below is sequential not parallel)
|
||||
for (let index in purchaseDataList) { // eslint-disable-line no-await-in-loop
|
||||
for (let index in purchaseDataList) {
|
||||
let purchaseData = purchaseDataList[index];
|
||||
let token = purchaseData.transactionId;
|
||||
|
||||
let existingReceipt = await IapPurchaseReceipt.findOne({
|
||||
let existingReceipt = await IapPurchaseReceipt.findOne({ // eslint-disable-line no-await-in-loop
|
||||
_id: token,
|
||||
}).exec();
|
||||
|
||||
if (!existingReceipt) {
|
||||
await IapPurchaseReceipt.create({
|
||||
await IapPurchaseReceipt.create({ // eslint-disable-line no-await-in-loop
|
||||
_id: token,
|
||||
consumed: true,
|
||||
userId: user._id,
|
||||
|
||||
@@ -80,6 +80,10 @@ let bannedWords = [
|
||||
'oh, god',
|
||||
'g\\*d',
|
||||
|
||||
'bugger',
|
||||
'buggery',
|
||||
'buggering',
|
||||
'buggered',
|
||||
'shit',
|
||||
'shitty',
|
||||
'shitting',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createTransport } from 'nodemailer';
|
||||
import nodemailer from 'nodemailer';
|
||||
import nconf from 'nconf';
|
||||
import { TAVERN_ID } from '../models/group';
|
||||
import { encrypt } from './encryption';
|
||||
import request from 'request';
|
||||
import got from 'got';
|
||||
import logger from './logger';
|
||||
import common from '../../common';
|
||||
|
||||
@@ -16,7 +16,7 @@ const EMAIL_SERVER = {
|
||||
};
|
||||
const BASE_URL = nconf.get('BASE_URL');
|
||||
|
||||
let smtpTransporter = createTransport({
|
||||
let smtpTransporter = nodemailer.createTransport({
|
||||
service: nconf.get('SMTP_SERVICE'),
|
||||
auth: {
|
||||
user: nconf.get('SMTP_USER'),
|
||||
@@ -150,13 +150,10 @@ export function sendTxn (mailingInfoArray, emailType, variables, personalVariabl
|
||||
}
|
||||
|
||||
if (IS_PROD && mailingInfoArray.length > 0) {
|
||||
request.post({
|
||||
url: `${EMAIL_SERVER.url}/job`,
|
||||
auth: {
|
||||
user: EMAIL_SERVER.auth.user,
|
||||
pass: EMAIL_SERVER.auth.password,
|
||||
},
|
||||
json: {
|
||||
got.post(`${EMAIL_SERVER.url}/job`, {
|
||||
auth: `${EMAIL_SERVER.auth.user}:${EMAIL_SERVER.auth.password}`,
|
||||
json: true,
|
||||
body: {
|
||||
type: 'email',
|
||||
data: {
|
||||
emailType,
|
||||
@@ -170,6 +167,6 @@ export function sendTxn (mailingInfoArray, emailType, variables, personalVariabl
|
||||
backoff: {delay: 10 * 60 * 1000, type: 'fixed'},
|
||||
},
|
||||
},
|
||||
}, (err) => logger.error(err));
|
||||
}).catch((err) => logger.error(err));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import _ from 'lodash';
|
||||
import nconf from 'nconf';
|
||||
// TODO remove this lib and use directly the apn module
|
||||
// @TODO remove this lib and use directly the apn module
|
||||
import pushNotify from 'push-notify';
|
||||
import apnLib from 'apn';
|
||||
import logger from './logger';
|
||||
import Bluebird from 'bluebird';
|
||||
import {
|
||||
@@ -44,21 +43,9 @@ if (APN_ENABLED) {
|
||||
apn.on('transmissionError', (errorCode, notification, device) => {
|
||||
logger.error('APN transmissionError', errorCode, notification, device);
|
||||
});
|
||||
|
||||
let feedback = new apnLib.Feedback({
|
||||
key,
|
||||
cert,
|
||||
batchFeedback: true,
|
||||
interval: 3600, // Check for feedback once an hour
|
||||
});
|
||||
|
||||
feedback.on('feedback', (devices) => {
|
||||
if (devices && devices.length > 0) {
|
||||
logger.info('Delivery of push notifications failed for some Apple devices.', devices);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function sendNotification (user, details = {}) {
|
||||
if (!user) throw new Error('User is required.');
|
||||
if (user.preferences.pushNotifications.unsubscribeFromAll === true) return;
|
||||
|
||||
43
website/server/libs/queue/index.js
Normal file
43
website/server/libs/queue/index.js
Normal file
@@ -0,0 +1,43 @@
|
||||
import Kafka from 'node-rdkafka';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const GROUP_ID = nconf.get('KAFKA:GROUP_ID');
|
||||
const CLOUDKARAFKA_BROKERS = nconf.get('KAFKA:CLOUDKARAFKA_BROKERS');
|
||||
const CLOUDKARAFKA_USERNAME = nconf.get('KAFKA:CLOUDKARAFKA_USERNAME');
|
||||
const CLOUDKARAFKA_PASSWORD = nconf.get('KAFKA:CLOUDKARAFKA_PASSWORD');
|
||||
const CLOUDKARAFKA_TOPIC_PREFIX = nconf.get('KAFKA:CLOUDKARAFKA_TOPIC_PREFIX');
|
||||
|
||||
const kafkaConf = {
|
||||
'group.id': GROUP_ID,
|
||||
'metadata.broker.list': CLOUDKARAFKA_BROKERS ? CLOUDKARAFKA_BROKERS.split(',') : '',
|
||||
'socket.keepalive.enable': true,
|
||||
'security.protocol': 'SASL_SSL',
|
||||
'sasl.mechanisms': 'SCRAM-SHA-256',
|
||||
'sasl.username': CLOUDKARAFKA_USERNAME,
|
||||
'sasl.password': CLOUDKARAFKA_PASSWORD,
|
||||
debug: 'generic,broker,security',
|
||||
};
|
||||
|
||||
const prefix = CLOUDKARAFKA_TOPIC_PREFIX;
|
||||
const topic = `${prefix}-default`;
|
||||
const producer = new Kafka.Producer(kafkaConf);
|
||||
|
||||
producer.connect();
|
||||
|
||||
process.on('exit', () => {
|
||||
if (producer.isConnected()) producer.disconnect();
|
||||
});
|
||||
|
||||
const api = {};
|
||||
|
||||
api.sendMessage = function sendMessage (message, key) {
|
||||
if (!producer.isConnected()) return;
|
||||
|
||||
try {
|
||||
producer.produce(topic, -1, new Buffer(JSON.stringify(message)), key);
|
||||
} catch (e) {
|
||||
// @TODO: Send the to loggly?
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = api;
|
||||
121
website/server/libs/spells.js
Normal file
121
website/server/libs/spells.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import Bluebird from 'bluebird';
|
||||
|
||||
import { model as User } from '../models/user';
|
||||
import * as Tasks from '../models/task';
|
||||
import {
|
||||
NotFound,
|
||||
BadRequest,
|
||||
} from './errors';
|
||||
|
||||
// @TODO: After refactoring individual spells, move quantity to the calculations
|
||||
|
||||
async function castTaskSpell (res, req, targetId, user, spell, quantity = 1) {
|
||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||
|
||||
const task = await Tasks.Task.findOne({
|
||||
_id: targetId,
|
||||
userId: user._id,
|
||||
}).exec();
|
||||
if (!task) throw new NotFound(res.t('taskNotFound'));
|
||||
if (task.challenge.id) throw new BadRequest(res.t('challengeTasksNoCast'));
|
||||
if (task.group.id) throw new BadRequest(res.t('groupTasksNoCast'));
|
||||
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
spell.cast(user, task, req);
|
||||
}
|
||||
|
||||
const results = await Bluebird.all([
|
||||
user.save(),
|
||||
task.save(),
|
||||
]);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function castMultiTaskSpell (req, user, spell, quantity = 1) {
|
||||
const tasks = await Tasks.Task.find({
|
||||
userId: user._id,
|
||||
...Tasks.taskIsGroupOrChallengeQuery,
|
||||
}).exec();
|
||||
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
spell.cast(user, tasks, req);
|
||||
}
|
||||
|
||||
const toSave = tasks
|
||||
.filter(t => t.isModified())
|
||||
.map(t => t.save());
|
||||
toSave.unshift(user.save());
|
||||
const saved = await Bluebird.all(toSave);
|
||||
|
||||
const response = {
|
||||
tasks: saved,
|
||||
user,
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async function castSelfSpell (req, user, spell, quantity = 1) {
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
spell.cast(user, null, req);
|
||||
}
|
||||
await user.save();
|
||||
}
|
||||
|
||||
async function castPartySpell (req, party, partyMembers, user, spell, quantity = 1) {
|
||||
if (!party) {
|
||||
partyMembers = [user]; // Act as solo party
|
||||
} else {
|
||||
partyMembers = await User
|
||||
.find({
|
||||
'party._id': party._id,
|
||||
_id: { $ne: user._id }, // add separately
|
||||
})
|
||||
// .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save
|
||||
// default values for non-selected fields and pre('save') will mess up thinking some values are missing
|
||||
.exec();
|
||||
|
||||
partyMembers.unshift(user);
|
||||
}
|
||||
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
spell.cast(user, partyMembers, req);
|
||||
}
|
||||
await Bluebird.all(partyMembers.map(m => m.save()));
|
||||
|
||||
return partyMembers;
|
||||
}
|
||||
|
||||
async function castUserSpell (res, req, party, partyMembers, targetId, user, spell, quantity = 1) {
|
||||
if (!party && (!targetId || user._id === targetId)) {
|
||||
partyMembers = user;
|
||||
} else {
|
||||
if (!targetId) throw new BadRequest(res.t('targetIdUUID'));
|
||||
if (!party) throw new NotFound(res.t('partyNotFound'));
|
||||
partyMembers = await User
|
||||
.findOne({_id: targetId, 'party._id': party._id})
|
||||
// .select(partyMembersFields) Selecting the entire user because otherwise when saving it'll save
|
||||
// default values for non-selected fields and pre('save') will mess up thinking some values are missing
|
||||
.exec();
|
||||
}
|
||||
|
||||
if (!partyMembers) throw new NotFound(res.t('userWithIDNotFound', {userId: targetId}));
|
||||
|
||||
for (let i = 0; i < quantity; i += 1) {
|
||||
spell.cast(user, partyMembers, req);
|
||||
}
|
||||
|
||||
if (partyMembers !== user) {
|
||||
await Bluebird.all([
|
||||
user.save(),
|
||||
partyMembers.save(),
|
||||
]);
|
||||
} else {
|
||||
await partyMembers.save(); // partyMembers is user
|
||||
}
|
||||
|
||||
return partyMembers;
|
||||
}
|
||||
|
||||
export {castTaskSpell, castMultiTaskSpell, castSelfSpell, castPartySpell, castUserSpell};
|
||||
@@ -97,9 +97,10 @@ api.checkout = async function checkout (options, stripeInc) {
|
||||
|
||||
if (groupId) {
|
||||
customerObject.quantity = sub.quantity;
|
||||
let groupFields = basicGroupFields.concat(' purchased');
|
||||
let group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||
customerObject.quantity = group.memberCount + sub.quantity - 1;
|
||||
const groupFields = basicGroupFields.concat(' purchased');
|
||||
const group = await Group.getGroup({user, groupId, populateLeader: false, groupFields});
|
||||
const membersCount = await group.getMemberCount();
|
||||
customerObject.quantity = membersCount + sub.quantity - 1;
|
||||
}
|
||||
|
||||
response = await stripeApi.customers.create(customerObject);
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { post } from 'request';
|
||||
import got from 'got';
|
||||
import { isURL } from 'validator';
|
||||
import logger from './logger';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const IS_PRODUCTION = nconf.get('IS_PROD');
|
||||
|
||||
function sendWebhook (url, body) {
|
||||
post({
|
||||
url,
|
||||
got.post(url, {
|
||||
body,
|
||||
json: true,
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
logger.error(err);
|
||||
}
|
||||
});
|
||||
}).catch(err => logger.error(err));
|
||||
}
|
||||
|
||||
function isValidWebhook (hook) {
|
||||
return hook.enabled && isURL(hook.url);
|
||||
return hook.enabled && isURL(hook.url, {
|
||||
require_tld: IS_PRODUCTION ? true : false, // eslint-disable-line camelcase
|
||||
});
|
||||
}
|
||||
|
||||
export class WebhookSender {
|
||||
|
||||
@@ -442,6 +442,16 @@ schema.methods.isMember = function isGroupMember (user) {
|
||||
}
|
||||
};
|
||||
|
||||
schema.methods.getMemberCount = async function getMemberCount () {
|
||||
let query = { guilds: this._id };
|
||||
|
||||
if (this.type === 'party') {
|
||||
query = { 'party._id': this._id };
|
||||
}
|
||||
|
||||
return await User.count(query).exec();
|
||||
};
|
||||
|
||||
export function chatDefaults (msg, user) {
|
||||
let message = {
|
||||
id: shared.uuid(),
|
||||
|
||||
@@ -144,7 +144,7 @@ TaskSchema.statics.findByIdOrAlias = async function findByIdOrAlias (identifier,
|
||||
|
||||
let query = _.cloneDeep(additionalQueries);
|
||||
|
||||
if (validator.isUUID(identifier)) {
|
||||
if (validator.isUUID(String(identifier))) {
|
||||
query._id = identifier;
|
||||
} else {
|
||||
query.userId = userId;
|
||||
|
||||
@@ -253,8 +253,9 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
||||
}
|
||||
|
||||
// Manage unallocated stats points notifications
|
||||
if (this.isSelected('stats') && this.isSelected('notifications')) {
|
||||
if (this.isSelected('stats') && this.isSelected('notifications') && this.isSelected('flags') && this.isSelected('preferences')) {
|
||||
const pointsToAllocate = this.stats.points;
|
||||
const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses;
|
||||
|
||||
// Sometimes there can be more than 1 notification
|
||||
const existingNotifications = this.notifications.filter(notification => {
|
||||
@@ -271,7 +272,7 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
||||
let notificationsToRemove = outdatedNotification ? existingNotificationsLength : existingNotificationsLength - 1;
|
||||
|
||||
// If there are points to allocate and the notification is outdated, add a new notifications
|
||||
if (pointsToAllocate > 0 && outdatedNotification) {
|
||||
if (pointsToAllocate > 0 && !classNotEnabled && outdatedNotification) {
|
||||
this.addNotification('UNALLOCATED_STATS_POINTS', { points: pointsToAllocate });
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,9 @@ import shared from '../../common';
|
||||
import {v4 as uuid} from 'uuid';
|
||||
import _ from 'lodash';
|
||||
import { BadRequest } from '../libs/errors';
|
||||
import nconf from 'nconf';
|
||||
|
||||
const IS_PRODUCTION = nconf.get('IS_PROD');
|
||||
const Schema = mongoose.Schema;
|
||||
|
||||
const TASK_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({
|
||||
@@ -36,7 +38,11 @@ export let schema = new Schema({
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
validate: [validator.isURL, shared.i18n.t('invalidUrl')],
|
||||
validate: [(v) => {
|
||||
return validator.isURL(v, {
|
||||
require_tld: IS_PRODUCTION ? true : false, // eslint-disable-line camelcase
|
||||
});
|
||||
}, shared.i18n.t('invalidUrl')],
|
||||
},
|
||||
enabled: { type: Boolean, required: true, default: true },
|
||||
options: {
|
||||
@@ -72,7 +78,7 @@ schema.methods.formatOptions = function formatOptions (res) {
|
||||
} else if (this.type === 'groupChatReceived') {
|
||||
this.options = _.pick(this.options, 'groupId');
|
||||
|
||||
if (!validator.isUUID(this.options.groupId)) {
|
||||
if (!validator.isUUID(String(this.options.groupId))) {
|
||||
throw new BadRequest(res.t('groupIdRequired'));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user