mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Webhooks v2 (and other fixes) (#10265)
* begin implementing global webhooks * add checklist item scored webhook * add pet hatched and mount raised webhooks (no tests) * fix typo * add lvl up webhooks, remove corrupt notifications and reorganize pre-save hook * fix typo * add some tests, globalActivity webhook * fix bug in global activiy webhook and add more tests * add tests and fix typo for petHatched and mountRaised webhooks * fix errors and add tests for level up webhook * wip: add default data to all webhooks, change signature for WebhookSender.send (missing tests) * remove unused code * fix unit tests * fix chat webhooks * remove console * fix lint * add and fix webhook tests * add questStarted webhook and questActivity type * add unit tests * add finial tests and features
This commit is contained in:
@@ -81,6 +81,49 @@ describe('POST /tasks/:id/score/:direction', () => {
|
|||||||
expect(body.direction).to.eql('up');
|
expect(body.direction).to.eql('up');
|
||||||
expect(body.delta).to.be.greaterThan(0);
|
expect(body.delta).to.be.greaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('sending user activity webhooks', () => {
|
||||||
|
before(async () => {
|
||||||
|
await server.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends user activity webhook when the user levels up', async () => {
|
||||||
|
let uuid = generateUUID();
|
||||||
|
|
||||||
|
await user.post('/user/webhook', {
|
||||||
|
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||||
|
type: 'userActivity',
|
||||||
|
enabled: true,
|
||||||
|
options: {
|
||||||
|
leveledUp: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialLvl = user.stats.lvl;
|
||||||
|
|
||||||
|
await user.update({
|
||||||
|
'stats.exp': 3000,
|
||||||
|
});
|
||||||
|
let task = await user.post('/tasks/user', {
|
||||||
|
text: 'test habit',
|
||||||
|
type: 'habit',
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.post(`/tasks/${task.id}/score/up`);
|
||||||
|
await user.sync();
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
let body = server.getWebhookData(uuid);
|
||||||
|
|
||||||
|
expect(body.type).to.eql('leveledUp');
|
||||||
|
expect(body.initialLvl).to.eql(initialLvl);
|
||||||
|
expect(body.finalLvl).to.eql(user.stats.lvl);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('todos', () => {
|
context('todos', () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
translate as t,
|
translate as t,
|
||||||
|
server,
|
||||||
|
sleep,
|
||||||
} from '../../../../../helpers/api-integration/v3';
|
} from '../../../../../helpers/api-integration/v3';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
@@ -94,4 +96,49 @@ describe('POST /tasks/:taskId/checklist/:itemId/score', () => {
|
|||||||
message: t('checklistItemNotFound'),
|
message: t('checklistItemNotFound'),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('sending task activity webhooks', () => {
|
||||||
|
before(async () => {
|
||||||
|
await server.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends task activity webhooks', async () => {
|
||||||
|
let uuid = generateUUID();
|
||||||
|
|
||||||
|
await user.post('/user/webhook', {
|
||||||
|
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||||
|
type: 'taskActivity',
|
||||||
|
enabled: true,
|
||||||
|
options: {
|
||||||
|
checklistScored: true,
|
||||||
|
updated: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = await user.post('/tasks/user', {
|
||||||
|
text: 'test daily',
|
||||||
|
type: 'daily',
|
||||||
|
});
|
||||||
|
|
||||||
|
let updatedTask = await user.post(`/tasks/${task.id}/checklist`, {
|
||||||
|
text: 'checklist item text',
|
||||||
|
});
|
||||||
|
|
||||||
|
let checklistItem = updatedTask.checklist[0];
|
||||||
|
|
||||||
|
let scoredItemTask = await user.post(`/tasks/${task.id}/checklist/${checklistItem.id}/score`);
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
let body = server.getWebhookData(uuid);
|
||||||
|
|
||||||
|
expect(body.type).to.eql('checklistScored');
|
||||||
|
expect(body.task).to.eql(scoredItemTask);
|
||||||
|
expect(body.item).to.eql(scoredItemTask.checklist[0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,8 +3,11 @@
|
|||||||
import {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
translate as t,
|
translate as t,
|
||||||
|
server,
|
||||||
|
sleep,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
import content from '../../../../../website/common/script/content';
|
import content from '../../../../../website/common/script/content';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
describe('POST /user/feed/:pet/:food', () => {
|
describe('POST /user/feed/:pet/:food', () => {
|
||||||
let user;
|
let user;
|
||||||
@@ -37,4 +40,41 @@ describe('POST /user/feed/:pet/:food', () => {
|
|||||||
expect(user.items.food.Milk).to.equal(1);
|
expect(user.items.food.Milk).to.equal(1);
|
||||||
expect(user.items.pets['Wolf-Base']).to.equal(7);
|
expect(user.items.pets['Wolf-Base']).to.equal(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('sending user activity webhooks', () => {
|
||||||
|
before(async () => {
|
||||||
|
await server.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends user activity webhook when a new mount is raised', async () => {
|
||||||
|
let uuid = generateUUID();
|
||||||
|
|
||||||
|
await user.post('/user/webhook', {
|
||||||
|
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||||
|
type: 'userActivity',
|
||||||
|
enabled: true,
|
||||||
|
options: {
|
||||||
|
mountRaised: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.update({
|
||||||
|
'items.pets.Wolf-Base': 49,
|
||||||
|
'items.food.Milk': 2,
|
||||||
|
});
|
||||||
|
let res = await user.post('/user/feed/Wolf-Base/Milk');
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
let body = server.getWebhookData(uuid);
|
||||||
|
|
||||||
|
expect(body.type).to.eql('mountRaised');
|
||||||
|
expect(body.pet).to.eql('Wolf-Base');
|
||||||
|
expect(body.message).to.eql(res.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
generateUser,
|
generateUser,
|
||||||
translate as t,
|
translate as t,
|
||||||
|
server,
|
||||||
|
sleep,
|
||||||
} from '../../../../helpers/api-integration/v3';
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
describe('POST /user/hatch/:egg/:hatchingPotion', () => {
|
describe('POST /user/hatch/:egg/:hatchingPotion', () => {
|
||||||
let user;
|
let user;
|
||||||
@@ -28,4 +31,41 @@ describe('POST /user/hatch/:egg/:hatchingPotion', () => {
|
|||||||
data: JSON.parse(JSON.stringify(user.items)),
|
data: JSON.parse(JSON.stringify(user.items)),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
context('sending user activity webhooks', () => {
|
||||||
|
before(async () => {
|
||||||
|
await server.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends user activity webhook when a new pet is hatched', async () => {
|
||||||
|
let uuid = generateUUID();
|
||||||
|
|
||||||
|
await user.post('/user/webhook', {
|
||||||
|
url: `http://localhost:${server.port}/webhooks/${uuid}`,
|
||||||
|
type: 'userActivity',
|
||||||
|
enabled: true,
|
||||||
|
options: {
|
||||||
|
petHatched: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await user.update({
|
||||||
|
'items.eggs.Wolf': 1,
|
||||||
|
'items.hatchingPotions.Base': 1,
|
||||||
|
});
|
||||||
|
let res = await user.post('/user/hatch/Wolf/Base');
|
||||||
|
|
||||||
|
await sleep();
|
||||||
|
|
||||||
|
let body = server.getWebhookData(uuid);
|
||||||
|
|
||||||
|
expect(body.type).to.eql('petHatched');
|
||||||
|
expect(body.pet).to.eql('Wolf-Base');
|
||||||
|
expect(body.message).to.eql(res.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ describe('POST /user/webhook', () => {
|
|||||||
let webhook = await user.post('/user/webhook', body);
|
let webhook = await user.post('/user/webhook', body);
|
||||||
|
|
||||||
expect(webhook.options).to.eql({
|
expect(webhook.options).to.eql({
|
||||||
|
checklistScored: false,
|
||||||
created: false,
|
created: false,
|
||||||
updated: false,
|
updated: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
@@ -126,6 +127,7 @@ describe('POST /user/webhook', () => {
|
|||||||
it('can set taskActivity options', async () => {
|
it('can set taskActivity options', async () => {
|
||||||
body.type = 'taskActivity';
|
body.type = 'taskActivity';
|
||||||
body.options = {
|
body.options = {
|
||||||
|
checklistScored: true,
|
||||||
created: true,
|
created: true,
|
||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
@@ -135,6 +137,7 @@ describe('POST /user/webhook', () => {
|
|||||||
let webhook = await user.post('/user/webhook', body);
|
let webhook = await user.post('/user/webhook', body);
|
||||||
|
|
||||||
expect(webhook.options).to.eql({
|
expect(webhook.options).to.eql({
|
||||||
|
checklistScored: true,
|
||||||
created: true,
|
created: true,
|
||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
@@ -145,6 +148,7 @@ describe('POST /user/webhook', () => {
|
|||||||
it('discards extra properties in taskActivity options', async () => {
|
it('discards extra properties in taskActivity options', async () => {
|
||||||
body.type = 'taskActivity';
|
body.type = 'taskActivity';
|
||||||
body.options = {
|
body.options = {
|
||||||
|
checklistScored: false,
|
||||||
created: true,
|
created: true,
|
||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
@@ -156,6 +160,7 @@ describe('POST /user/webhook', () => {
|
|||||||
|
|
||||||
expect(webhook.options.foo).to.not.exist;
|
expect(webhook.options.foo).to.not.exist;
|
||||||
expect(webhook.options).to.eql({
|
expect(webhook.options).to.eql({
|
||||||
|
checklistScored: false,
|
||||||
created: true,
|
created: true,
|
||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
@@ -218,4 +223,16 @@ describe('POST /user/webhook', () => {
|
|||||||
groupId: body.options.groupId,
|
groupId: body.options.groupId,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('discards extra properties in globalActivity options', async () => {
|
||||||
|
body.type = 'globalActivity';
|
||||||
|
body.options = {
|
||||||
|
foo: 'bar',
|
||||||
|
};
|
||||||
|
|
||||||
|
let webhook = await user.post('/user/webhook', body);
|
||||||
|
|
||||||
|
expect(webhook.options.foo).to.not.exist;
|
||||||
|
expect(webhook.options).to.eql({});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ describe('PUT /user/webhook/:id', () => {
|
|||||||
let webhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options});
|
let webhook = await user.put(`/user/webhook/${webhookToUpdate.id}`, {type, options});
|
||||||
|
|
||||||
expect(webhook.options).to.eql({
|
expect(webhook.options).to.eql({
|
||||||
|
checklistScored: false, // starting value
|
||||||
created: true, // starting value
|
created: true, // starting value
|
||||||
updated: false,
|
updated: false,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import {
|
|||||||
taskScoredWebhook,
|
taskScoredWebhook,
|
||||||
groupChatReceivedWebhook,
|
groupChatReceivedWebhook,
|
||||||
taskActivityWebhook,
|
taskActivityWebhook,
|
||||||
|
questActivityWebhook,
|
||||||
|
userActivityWebhook,
|
||||||
} from '../../../../../website/server/libs/webhook';
|
} from '../../../../../website/server/libs/webhook';
|
||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
} from '../../../../helpers/api-unit.helper.js';
|
||||||
import { defer } from '../../../../helpers/api-unit.helper';
|
import { defer } from '../../../../helpers/api-unit.helper';
|
||||||
|
|
||||||
describe('webhooks', () => {
|
describe('webhooks', () => {
|
||||||
let webhooks;
|
let webhooks, user;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
sandbox.stub(got, 'post').returns(defer().promise);
|
sandbox.stub(got, 'post').returns(defer().promise);
|
||||||
@@ -23,6 +28,26 @@ describe('webhooks', () => {
|
|||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
scored: true,
|
scored: true,
|
||||||
|
checklistScored: true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
id: 'questActivity',
|
||||||
|
url: 'http://quest-activity.com',
|
||||||
|
enabled: true,
|
||||||
|
type: 'questActivity',
|
||||||
|
options: {
|
||||||
|
questStarted: true,
|
||||||
|
questFinised: true,
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
id: 'userActivity',
|
||||||
|
url: 'http://user-activity.com',
|
||||||
|
enabled: true,
|
||||||
|
type: 'userActivity',
|
||||||
|
options: {
|
||||||
|
petHatched: true,
|
||||||
|
mountRaised: true,
|
||||||
|
leveledUp: true,
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
id: 'groupChatReceived',
|
id: 'groupChatReceived',
|
||||||
@@ -33,6 +58,9 @@ describe('webhooks', () => {
|
|||||||
groupId: 'group-id',
|
groupId: 'group-id',
|
||||||
},
|
},
|
||||||
}];
|
}];
|
||||||
|
|
||||||
|
user = generateUser();
|
||||||
|
user.webhooks = webhooks;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -57,7 +85,8 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
|
expect(WebhookSender.defaultTransformData).to.be.calledOnce;
|
||||||
expect(got.post).to.be.calledOnce;
|
expect(got.post).to.be.calledOnce;
|
||||||
@@ -67,6 +96,30 @@ describe('webhooks', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds default data (user and webhookType) to the body', () => {
|
||||||
|
let sendWebhook = new WebhookSender({
|
||||||
|
type: 'custom',
|
||||||
|
});
|
||||||
|
sandbox.spy(sendWebhook, 'attachDefaultData');
|
||||||
|
|
||||||
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
|
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
|
expect(sendWebhook.attachDefaultData).to.be.calledOnce;
|
||||||
|
expect(got.post).to.be.calledOnce;
|
||||||
|
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(body).to.eql({
|
||||||
|
foo: 'bar',
|
||||||
|
user: {_id: user._id},
|
||||||
|
webhookType: 'custom',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('can pass in a data transformation function', () => {
|
it('can pass in a data transformation function', () => {
|
||||||
sandbox.spy(WebhookSender, 'defaultTransformData');
|
sandbox.spy(WebhookSender, 'defaultTransformData');
|
||||||
let sendWebhook = new WebhookSender({
|
let sendWebhook = new WebhookSender({
|
||||||
@@ -80,7 +133,8 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(WebhookSender.defaultTransformData).to.not.be.called;
|
expect(WebhookSender.defaultTransformData).to.not.be.called;
|
||||||
expect(got.post).to.be.calledOnce;
|
expect(got.post).to.be.calledOnce;
|
||||||
@@ -93,7 +147,7 @@ describe('webhooks', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('provieds a default filter function', () => {
|
it('provides a default filter function', () => {
|
||||||
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
sandbox.spy(WebhookSender, 'defaultWebhookFilter');
|
||||||
let sendWebhook = new WebhookSender({
|
let sendWebhook = new WebhookSender({
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
@@ -101,7 +155,8 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
|
expect(WebhookSender.defaultWebhookFilter).to.be.calledOnce;
|
||||||
});
|
});
|
||||||
@@ -117,7 +172,8 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}], body);
|
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'}];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
|
expect(WebhookSender.defaultWebhookFilter).to.not.be.called;
|
||||||
expect(got.post).to.not.be.called;
|
expect(got.post).to.not.be.called;
|
||||||
@@ -134,10 +190,11 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([
|
user.webhooks = [
|
||||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }},
|
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom', options: { foo: 'bar' }},
|
||||||
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
|
{ id: 'other-custom-webhook', url: 'http://other-custom-url.com', enabled: true, type: 'custom', options: { foo: 'foo' }},
|
||||||
], body);
|
];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(got.post).to.be.calledOnce;
|
expect(got.post).to.be.calledOnce;
|
||||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
expect(got.post).to.be.calledWithMatch('http://custom-url.com');
|
||||||
@@ -150,7 +207,8 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}], body);
|
user.webhooks = [{id: 'custom-webhook', url: 'http://custom-url.com', enabled: false, type: 'custom'}];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(got.post).to.not.be.called;
|
expect(got.post).to.not.be.called;
|
||||||
});
|
});
|
||||||
@@ -162,7 +220,8 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([{id: 'custom-webhook', url: 'httxp://custom-url!!', enabled: true, type: 'custom'}], body);
|
user.webhooks = [{id: 'custom-webhook', url: 'httxp://custom-url!!!', enabled: true, type: 'custom'}];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(got.post).to.not.be.called;
|
expect(got.post).to.not.be.called;
|
||||||
});
|
});
|
||||||
@@ -174,10 +233,30 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([
|
user.webhooks = [
|
||||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||||
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
|
{ id: 'other-webhook', url: 'http://other-url.com', enabled: true, type: 'other'},
|
||||||
], body);
|
];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
|
expect(got.post).to.be.calledOnce;
|
||||||
|
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||||
|
body,
|
||||||
|
json: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends every type of activity to global webhooks', () => {
|
||||||
|
let sendWebhook = new WebhookSender({
|
||||||
|
type: 'custom',
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
|
user.webhooks = [
|
||||||
|
{ id: 'global-webhook', url: 'http://custom-url.com', enabled: true, type: 'globalActivity'},
|
||||||
|
];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(got.post).to.be.calledOnce;
|
expect(got.post).to.be.calledOnce;
|
||||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||||
@@ -193,10 +272,11 @@ describe('webhooks', () => {
|
|||||||
|
|
||||||
let body = { foo: 'bar' };
|
let body = { foo: 'bar' };
|
||||||
|
|
||||||
sendWebhook.send([
|
user.webhooks = [
|
||||||
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
{ id: 'custom-webhook', url: 'http://custom-url.com', enabled: true, type: 'custom'},
|
||||||
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
|
{ id: 'other-custom-webhook', url: 'http://other-url.com', enabled: true, type: 'custom'},
|
||||||
], body);
|
];
|
||||||
|
sendWebhook.send(user, body);
|
||||||
|
|
||||||
expect(got.post).to.be.calledTwice;
|
expect(got.post).to.be.calledTwice;
|
||||||
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
expect(got.post).to.be.calledWithMatch('http://custom-url.com', {
|
||||||
@@ -216,7 +296,6 @@ describe('webhooks', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
data = {
|
data = {
|
||||||
user: {
|
user: {
|
||||||
_id: 'user-id',
|
|
||||||
_tmp: {foo: 'bar'},
|
_tmp: {foo: 'bar'},
|
||||||
stats: {
|
stats: {
|
||||||
lvl: 5,
|
lvl: 5,
|
||||||
@@ -248,15 +327,54 @@ describe('webhooks', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sends task and stats data', () => {
|
it('sends task and stats data', () => {
|
||||||
taskScoredWebhook.send(webhooks, data);
|
taskScoredWebhook.send(user, data);
|
||||||
|
|
||||||
expect(got.post).to.be.calledOnce;
|
expect(got.post).to.be.calledOnce;
|
||||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||||
json: true,
|
json: true,
|
||||||
body: {
|
body: {
|
||||||
type: 'scored',
|
type: 'scored',
|
||||||
|
webhookType: 'taskActivity',
|
||||||
user: {
|
user: {
|
||||||
_id: 'user-id',
|
_id: user._id,
|
||||||
|
_tmp: {foo: 'bar'},
|
||||||
|
stats: {
|
||||||
|
lvl: 5,
|
||||||
|
int: 10,
|
||||||
|
str: 5,
|
||||||
|
exp: 423,
|
||||||
|
toNextLevel: 40,
|
||||||
|
maxHealth: 50,
|
||||||
|
maxMP: 103,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
task: {
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
direction: 'up',
|
||||||
|
delta: 176,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends task and stats data to globalActivity webhookd', () => {
|
||||||
|
user.webhooks = [{
|
||||||
|
id: 'globalActivity',
|
||||||
|
url: 'http://global-activity.com',
|
||||||
|
enabled: true,
|
||||||
|
type: 'globalActivity',
|
||||||
|
}];
|
||||||
|
|
||||||
|
taskScoredWebhook.send(user, data);
|
||||||
|
|
||||||
|
expect(got.post).to.be.calledOnce;
|
||||||
|
expect(got.post).to.be.calledWithMatch('http://global-activity.com', {
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
type: 'scored',
|
||||||
|
webhookType: 'taskActivity',
|
||||||
|
user: {
|
||||||
|
_id: user._id,
|
||||||
_tmp: {foo: 'bar'},
|
_tmp: {foo: 'bar'},
|
||||||
stats: {
|
stats: {
|
||||||
lvl: 5,
|
lvl: 5,
|
||||||
@@ -280,7 +398,7 @@ describe('webhooks', () => {
|
|||||||
it('does not send task scored data if scored option is not true', () => {
|
it('does not send task scored data if scored option is not true', () => {
|
||||||
webhooks[0].options.scored = false;
|
webhooks[0].options.scored = false;
|
||||||
|
|
||||||
taskScoredWebhook.send(webhooks, data);
|
taskScoredWebhook.send(user, data);
|
||||||
|
|
||||||
expect(got.post).to.not.be.called;
|
expect(got.post).to.not.be.called;
|
||||||
});
|
});
|
||||||
@@ -301,13 +419,17 @@ describe('webhooks', () => {
|
|||||||
it(`sends ${type} tasks`, () => {
|
it(`sends ${type} tasks`, () => {
|
||||||
data.type = type;
|
data.type = type;
|
||||||
|
|
||||||
taskActivityWebhook.send(webhooks, data);
|
taskActivityWebhook.send(user, data);
|
||||||
|
|
||||||
expect(got.post).to.be.calledOnce;
|
expect(got.post).to.be.calledOnce;
|
||||||
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||||
json: true,
|
json: true,
|
||||||
body: {
|
body: {
|
||||||
type,
|
type,
|
||||||
|
webhookType: 'taskActivity',
|
||||||
|
user: {
|
||||||
|
_id: user._id,
|
||||||
|
},
|
||||||
task: data.task,
|
task: data.task,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -317,7 +439,142 @@ describe('webhooks', () => {
|
|||||||
data.type = type;
|
data.type = type;
|
||||||
webhooks[0].options[type] = false;
|
webhooks[0].options[type] = false;
|
||||||
|
|
||||||
taskActivityWebhook.send(webhooks, data);
|
taskActivityWebhook.send(user, data);
|
||||||
|
|
||||||
|
expect(got.post).to.not.be.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checklistScored', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
data = {
|
||||||
|
task: {
|
||||||
|
text: 'text',
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
text: 'item-text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends \'checklistScored\' tasks', () => {
|
||||||
|
data.type = 'checklistScored';
|
||||||
|
|
||||||
|
taskActivityWebhook.send(user, data);
|
||||||
|
|
||||||
|
expect(got.post).to.be.calledOnce;
|
||||||
|
expect(got.post).to.be.calledWithMatch(webhooks[0].url, {
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
webhookType: 'taskActivity',
|
||||||
|
user: {
|
||||||
|
_id: user._id,
|
||||||
|
},
|
||||||
|
type: data.type,
|
||||||
|
task: data.task,
|
||||||
|
item: data.item,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not send task \'checklistScored\' data if \'checklistScored\' option is not true', () => {
|
||||||
|
data.type = 'checklistScored';
|
||||||
|
webhooks[0].options.checklistScored = false;
|
||||||
|
|
||||||
|
taskActivityWebhook.send(user, data);
|
||||||
|
|
||||||
|
expect(got.post).to.not.be.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('userActivityWebhook', () => {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
data = {
|
||||||
|
something: true,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
['petHatched', 'mountRaised', 'leveledUp'].forEach((type) => {
|
||||||
|
it(`sends ${type} webhooks`, () => {
|
||||||
|
data.type = type;
|
||||||
|
|
||||||
|
userActivityWebhook.send(user, data);
|
||||||
|
|
||||||
|
expect(got.post).to.be.calledOnce;
|
||||||
|
expect(got.post).to.be.calledWithMatch(webhooks[2].url, {
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
type,
|
||||||
|
webhookType: 'userActivity',
|
||||||
|
user: {
|
||||||
|
_id: user._id,
|
||||||
|
},
|
||||||
|
something: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
|
||||||
|
data.type = type;
|
||||||
|
webhooks[2].options[type] = false;
|
||||||
|
|
||||||
|
userActivityWebhook.send(user, data);
|
||||||
|
|
||||||
|
expect(got.post).to.not.be.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('questActivityWebhook', () => {
|
||||||
|
let data;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
data = {
|
||||||
|
group: {
|
||||||
|
id: 'group-id',
|
||||||
|
name: 'some group',
|
||||||
|
otherData: 'foo',
|
||||||
|
},
|
||||||
|
quest: {
|
||||||
|
key: 'some-key',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
['questStarted', 'questFinised'].forEach((type) => {
|
||||||
|
it(`sends ${type} webhooks`, () => {
|
||||||
|
data.type = type;
|
||||||
|
|
||||||
|
questActivityWebhook.send(user, data);
|
||||||
|
|
||||||
|
expect(got.post).to.be.calledOnce;
|
||||||
|
expect(got.post).to.be.calledWithMatch(webhooks[1].url, {
|
||||||
|
json: true,
|
||||||
|
body: {
|
||||||
|
type,
|
||||||
|
webhookType: 'questActivity',
|
||||||
|
user: {
|
||||||
|
_id: user._id,
|
||||||
|
},
|
||||||
|
group: {
|
||||||
|
id: 'group-id',
|
||||||
|
name: 'some group',
|
||||||
|
},
|
||||||
|
quest: {
|
||||||
|
key: 'some-key',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`does not send webhook ${type} data if ${type} option is not true`, () => {
|
||||||
|
data.type = type;
|
||||||
|
webhooks[1].options[type] = false;
|
||||||
|
|
||||||
|
userActivityWebhook.send(user, data);
|
||||||
|
|
||||||
expect(got.post).to.not.be.called;
|
expect(got.post).to.not.be.called;
|
||||||
});
|
});
|
||||||
@@ -338,12 +595,16 @@ describe('webhooks', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
groupChatReceivedWebhook.send(webhooks, data);
|
groupChatReceivedWebhook.send(user, data);
|
||||||
|
|
||||||
expect(got.post).to.be.calledOnce;
|
expect(got.post).to.be.calledOnce;
|
||||||
expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, {
|
expect(got.post).to.be.calledWithMatch(webhooks[webhooks.length - 1].url, {
|
||||||
json: true,
|
json: true,
|
||||||
body: {
|
body: {
|
||||||
|
webhookType: 'groupChatReceived',
|
||||||
|
user: {
|
||||||
|
_id: user._id,
|
||||||
|
},
|
||||||
group: {
|
group: {
|
||||||
id: 'group-id',
|
id: 'group-id',
|
||||||
name: 'some group',
|
name: 'some group',
|
||||||
@@ -369,7 +630,7 @@ describe('webhooks', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
groupChatReceivedWebhook.send(webhooks, data);
|
groupChatReceivedWebhook.send(user, data);
|
||||||
|
|
||||||
expect(got.post).to.not.be.called;
|
expect(got.post).to.not.be.called;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import {
|
|||||||
} from '../../../../../website/server/models/group';
|
} from '../../../../../website/server/models/group';
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
import { quests as questScrolls } from '../../../../../website/common/script/content';
|
||||||
import { groupChatReceivedWebhook } from '../../../../../website/server/libs/webhook';
|
import {
|
||||||
|
groupChatReceivedWebhook,
|
||||||
|
questActivityWebhook,
|
||||||
|
} from '../../../../../website/server/libs/webhook';
|
||||||
import * as email from '../../../../../website/server/libs/email';
|
import * as email from '../../../../../website/server/libs/email';
|
||||||
import { TAVERN_ID } from '../../../../../website/common/script/';
|
import { TAVERN_ID } from '../../../../../website/common/script/';
|
||||||
import shared from '../../../../../website/common';
|
import shared from '../../../../../website/common';
|
||||||
@@ -21,6 +24,7 @@ describe('Group Model', () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
sandbox.stub(email, 'sendTxn');
|
sandbox.stub(email, 'sendTxn');
|
||||||
|
sandbox.stub(questActivityWebhook, 'send');
|
||||||
|
|
||||||
party = new Group({
|
party = new Group({
|
||||||
name: 'test party',
|
name: 'test party',
|
||||||
@@ -1189,6 +1193,47 @@ describe('Group Model', () => {
|
|||||||
expect(typeOfEmail).to.eql('quest-started');
|
expect(typeOfEmail).to.eql('quest-started');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sends webhook to participating members that quest has started', async () => {
|
||||||
|
// should receive webhook
|
||||||
|
participatingMember.webhooks = [{
|
||||||
|
type: 'questActivity',
|
||||||
|
url: 'http://someurl.com',
|
||||||
|
options: {
|
||||||
|
questStarted: true,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
questLeader.webhooks = [{
|
||||||
|
type: 'questActivity',
|
||||||
|
url: 'http://someurl.com',
|
||||||
|
options: {
|
||||||
|
questStarted: true,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
await Promise.all([participatingMember.save(), questLeader.save()]);
|
||||||
|
|
||||||
|
await party.startQuest(nonParticipatingMember);
|
||||||
|
|
||||||
|
await sleep(0.5);
|
||||||
|
|
||||||
|
expect(questActivityWebhook.send).to.be.calledTwice; // for 2 participating members
|
||||||
|
|
||||||
|
let args = questActivityWebhook.send.args[0];
|
||||||
|
let webhooks = args[0].webhooks;
|
||||||
|
let webhookOwner = args[0]._id;
|
||||||
|
let options = args[1];
|
||||||
|
|
||||||
|
expect(webhooks).to.have.a.lengthOf(1);
|
||||||
|
if (webhookOwner === questLeader._id) {
|
||||||
|
expect(webhooks[0].id).to.eql(questLeader.webhooks[0].id);
|
||||||
|
} else {
|
||||||
|
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
|
||||||
|
}
|
||||||
|
expect(webhooks[0].type).to.eql('questActivity');
|
||||||
|
expect(options.group).to.eql(party);
|
||||||
|
expect(options.quest.key).to.eql('whale');
|
||||||
|
});
|
||||||
|
|
||||||
it('sends email only to members who have not opted out', async () => {
|
it('sends email only to members who have not opted out', async () => {
|
||||||
participatingMember.preferences.emailNotifications.questStarted = false;
|
participatingMember.preferences.emailNotifications.questStarted = false;
|
||||||
questLeader.preferences.emailNotifications.questStarted = true;
|
questLeader.preferences.emailNotifications.questStarted = true;
|
||||||
@@ -1570,6 +1615,42 @@ describe('Group Model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('sends webhook to participating members that quest has finished', async () => {
|
||||||
|
// should receive webhook
|
||||||
|
participatingMember.webhooks = [{
|
||||||
|
type: 'questActivity',
|
||||||
|
url: 'http://someurl.com',
|
||||||
|
options: {
|
||||||
|
questFinished: true,
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
questLeader.webhooks = [{
|
||||||
|
type: 'questActivity',
|
||||||
|
url: 'http://someurl.com',
|
||||||
|
options: {
|
||||||
|
questStarted: true, // will not receive the webhook
|
||||||
|
},
|
||||||
|
}];
|
||||||
|
|
||||||
|
await Promise.all([participatingMember.save(), questLeader.save()]);
|
||||||
|
|
||||||
|
await party.finishQuest(quest);
|
||||||
|
|
||||||
|
await sleep(0.5);
|
||||||
|
|
||||||
|
expect(questActivityWebhook.send).to.be.calledOnce;
|
||||||
|
|
||||||
|
let args = questActivityWebhook.send.args[0];
|
||||||
|
let webhooks = args[0].webhooks;
|
||||||
|
let options = args[1];
|
||||||
|
|
||||||
|
expect(webhooks).to.have.a.lengthOf(1);
|
||||||
|
expect(webhooks[0].id).to.eql(participatingMember.webhooks[0].id);
|
||||||
|
expect(webhooks[0].type).to.eql('questActivity');
|
||||||
|
expect(options.group).to.eql(party);
|
||||||
|
expect(options.quest.key).to.eql(quest.key);
|
||||||
|
});
|
||||||
|
|
||||||
context('World quests in Tavern', () => {
|
context('World quests in Tavern', () => {
|
||||||
let tavernQuest;
|
let tavernQuest;
|
||||||
|
|
||||||
@@ -1685,7 +1766,7 @@ describe('Group Model', () => {
|
|||||||
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
|
expect(groupChatReceivedWebhook.send).to.be.calledOnce;
|
||||||
|
|
||||||
let args = groupChatReceivedWebhook.send.args[0];
|
let args = groupChatReceivedWebhook.send.args[0];
|
||||||
let webhooks = args[0];
|
let webhooks = args[0].webhooks;
|
||||||
let options = args[1];
|
let options = args[1];
|
||||||
|
|
||||||
expect(webhooks).to.have.a.lengthOf(1);
|
expect(webhooks).to.have.a.lengthOf(1);
|
||||||
@@ -1749,9 +1830,9 @@ describe('Group Model', () => {
|
|||||||
expect(groupChatReceivedWebhook.send).to.be.calledThrice;
|
expect(groupChatReceivedWebhook.send).to.be.calledThrice;
|
||||||
|
|
||||||
let args = groupChatReceivedWebhook.send.args;
|
let args = groupChatReceivedWebhook.send.args;
|
||||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
|
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook.webhooks[0].id)).to.be.exist;
|
||||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
|
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook2.webhooks[0].id)).to.be.exist;
|
||||||
expect(args.find(arg => arg[0][0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
|
expect(args.find(arg => arg[0].webhooks[0].id === memberWithWebhook3.webhooks[0].id)).to.be.exist;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ describe('Webhook Model', () => {
|
|||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
scored: true,
|
scored: true,
|
||||||
|
checklistScored: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -36,6 +37,7 @@ describe('Webhook Model', () => {
|
|||||||
wh.formatOptions(res);
|
wh.formatOptions(res);
|
||||||
|
|
||||||
expect(wh.options).to.eql({
|
expect(wh.options).to.eql({
|
||||||
|
checklistScored: false,
|
||||||
created: false,
|
created: false,
|
||||||
updated: false,
|
updated: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
@@ -51,6 +53,7 @@ describe('Webhook Model', () => {
|
|||||||
wh.formatOptions(res);
|
wh.formatOptions(res);
|
||||||
|
|
||||||
expect(wh.options).to.eql({
|
expect(wh.options).to.eql({
|
||||||
|
checklistScored: true,
|
||||||
created: false,
|
created: false,
|
||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
@@ -67,6 +70,7 @@ describe('Webhook Model', () => {
|
|||||||
|
|
||||||
expect(wh.options.foo).to.not.exist;
|
expect(wh.options.foo).to.not.exist;
|
||||||
expect(wh.options).to.eql({
|
expect(wh.options).to.eql({
|
||||||
|
checklistScored: true,
|
||||||
created: true,
|
created: true,
|
||||||
updated: true,
|
updated: true,
|
||||||
deleted: true,
|
deleted: true,
|
||||||
@@ -74,7 +78,155 @@ describe('Webhook Model', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
['created', 'updated', 'deleted', 'scored'].forEach((option) => {
|
['created', 'updated', 'deleted', 'scored', 'checklistScored'].forEach((option) => {
|
||||||
|
it(`validates that ${option} is a boolean`, (done) => {
|
||||||
|
config.options[option] = 'not a boolean';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceOf(BadRequest);
|
||||||
|
expect(res.t).to.be.calledOnce;
|
||||||
|
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('type is userActivity', () => {
|
||||||
|
let config;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = {
|
||||||
|
type: 'userActivity',
|
||||||
|
url: 'https//exmaple.com/endpoint',
|
||||||
|
options: {
|
||||||
|
petHatched: true,
|
||||||
|
mountRaised: true,
|
||||||
|
leveledUp: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it provides default values for options', () => {
|
||||||
|
delete config.options;
|
||||||
|
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
|
||||||
|
expect(wh.options).to.eql({
|
||||||
|
petHatched: false,
|
||||||
|
mountRaised: false,
|
||||||
|
leveledUp: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides missing user options', () => {
|
||||||
|
delete config.options.petHatched;
|
||||||
|
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
|
||||||
|
expect(wh.options).to.eql({
|
||||||
|
petHatched: false,
|
||||||
|
mountRaised: true,
|
||||||
|
leveledUp: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discards additional options', () => {
|
||||||
|
config.options.foo = 'another option';
|
||||||
|
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
|
||||||
|
expect(wh.options.foo).to.not.exist;
|
||||||
|
expect(wh.options).to.eql({
|
||||||
|
petHatched: true,
|
||||||
|
mountRaised: true,
|
||||||
|
leveledUp: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
['petHatched', 'petHatched', 'leveledUp'].forEach((option) => {
|
||||||
|
it(`validates that ${option} is a boolean`, (done) => {
|
||||||
|
config.options[option] = 'not a boolean';
|
||||||
|
|
||||||
|
try {
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceOf(BadRequest);
|
||||||
|
expect(res.t).to.be.calledOnce;
|
||||||
|
expect(res.t).to.be.calledWith('webhookBooleanOption', { option });
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('type is questActivity', () => {
|
||||||
|
let config;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = {
|
||||||
|
type: 'questActivity',
|
||||||
|
url: 'https//exmaple.com/endpoint',
|
||||||
|
options: {
|
||||||
|
questStarted: true,
|
||||||
|
questFinished: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it provides default values for options', () => {
|
||||||
|
delete config.options;
|
||||||
|
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
|
||||||
|
expect(wh.options).to.eql({
|
||||||
|
questStarted: false,
|
||||||
|
questFinished: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('provides missing user options', () => {
|
||||||
|
delete config.options.questStarted;
|
||||||
|
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
|
||||||
|
expect(wh.options).to.eql({
|
||||||
|
questStarted: false,
|
||||||
|
questFinished: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discards additional options', () => {
|
||||||
|
config.options.foo = 'another option';
|
||||||
|
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
|
||||||
|
expect(wh.options.foo).to.not.exist;
|
||||||
|
expect(wh.options).to.eql({
|
||||||
|
questStarted: true,
|
||||||
|
questFinished: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
['questStarted', 'questFinished'].forEach((option) => {
|
||||||
it(`validates that ${option} is a boolean`, (done) => {
|
it(`validates that ${option} is a boolean`, (done) => {
|
||||||
config.options[option] = 'not a boolean';
|
config.options[option] = 'not a boolean';
|
||||||
|
|
||||||
@@ -141,6 +293,30 @@ describe('Webhook Model', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
context('type is globalActivity', () => {
|
||||||
|
let config;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
config = {
|
||||||
|
type: 'globalActivity',
|
||||||
|
url: 'https//exmaple.com/endpoint',
|
||||||
|
options: { },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('discards additional objects', () => {
|
||||||
|
config.options.foo = 'another thing';
|
||||||
|
|
||||||
|
let wh = new Webhook(config);
|
||||||
|
|
||||||
|
wh.formatOptions(res);
|
||||||
|
|
||||||
|
expect(wh.options.foo).to.not.exist;
|
||||||
|
expect(wh.options).to.eql({});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,6 +117,18 @@ describe('common.fns.updateStats', () => {
|
|||||||
expect(user.addNotification).to.be.calledWith('DROPS_ENABLED');
|
expect(user.addNotification).to.be.calledWith('DROPS_ENABLED');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('add user notification when the user levels up', () => {
|
||||||
|
const initialLvl = user.stats.lvl;
|
||||||
|
updateStats(user, {
|
||||||
|
exp: 3000,
|
||||||
|
});
|
||||||
|
expect(user.addNotification).to.be.calledTwice; // once is for drops enabled
|
||||||
|
expect(user.addNotification).to.be.calledWith('LEVELED_UP', {
|
||||||
|
initialLvl,
|
||||||
|
newLvl: user.stats.lvl,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('add user notification when rebirth is enabled', () => {
|
it('add user notification when rebirth is enabled', () => {
|
||||||
user.stats.lvl = 51;
|
user.stats.lvl = 51;
|
||||||
updateStats(user, { });
|
updateStats(user, { });
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ module.exports = function updateStats (user, stats, req = {}, analytics) {
|
|||||||
if (stats.exp >= experienceToNextLevel) {
|
if (stats.exp >= experienceToNextLevel) {
|
||||||
user.stats.exp = stats.exp;
|
user.stats.exp = stats.exp;
|
||||||
|
|
||||||
|
const initialLvl = user.stats.lvl;
|
||||||
|
|
||||||
while (stats.exp >= experienceToNextLevel) {
|
while (stats.exp >= experienceToNextLevel) {
|
||||||
stats.exp -= experienceToNextLevel;
|
stats.exp -= experienceToNextLevel;
|
||||||
user.stats.lvl++;
|
user.stats.lvl++;
|
||||||
@@ -47,6 +49,13 @@ module.exports = function updateStats (user, stats, req = {}, analytics) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newLvl = user.stats.lvl;
|
||||||
|
|
||||||
|
if (user.addNotification) user.addNotification('LEVELED_UP', {
|
||||||
|
initialLvl,
|
||||||
|
newLvl,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
user.stats.exp = stats.exp;
|
user.stats.exp = stats.exp;
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ api.createUserTasks = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
taskActivityWebhook.send(user.webhooks, {
|
taskActivityWebhook.send(user, {
|
||||||
type: 'created',
|
type: 'created',
|
||||||
task,
|
task,
|
||||||
});
|
});
|
||||||
@@ -502,7 +502,7 @@ api.updateTask = {
|
|||||||
} else if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
} else if (group && task.group.id && task.group.assignedUsers.length > 0) {
|
||||||
await group.updateTask(savedTask);
|
await group.updateTask(savedTask);
|
||||||
} else {
|
} else {
|
||||||
taskActivityWebhook.send(user.webhooks, {
|
taskActivityWebhook.send(user, {
|
||||||
type: 'updated',
|
type: 'updated',
|
||||||
task: savedTask,
|
task: savedTask,
|
||||||
});
|
});
|
||||||
@@ -654,7 +654,7 @@ api.scoreTask = {
|
|||||||
let resJsonData = _.assign({delta, _tmp: user._tmp}, userStats);
|
let resJsonData = _.assign({delta, _tmp: user._tmp}, userStats);
|
||||||
res.respond(200, resJsonData);
|
res.respond(200, resJsonData);
|
||||||
|
|
||||||
taskScoredWebhook.send(user.webhooks, {
|
taskScoredWebhook.send(user, {
|
||||||
task,
|
task,
|
||||||
direction,
|
direction,
|
||||||
delta,
|
delta,
|
||||||
@@ -860,6 +860,12 @@ api.scoreCheckListItem = {
|
|||||||
let savedTask = await task.save();
|
let savedTask = await task.save();
|
||||||
|
|
||||||
res.respond(200, savedTask);
|
res.respond(200, savedTask);
|
||||||
|
|
||||||
|
taskActivityWebhook.send(user, {
|
||||||
|
type: 'checklistScored',
|
||||||
|
task: savedTask,
|
||||||
|
item,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1326,7 +1332,7 @@ api.deleteTask = {
|
|||||||
if (challenge) {
|
if (challenge) {
|
||||||
challenge.removeTask(task);
|
challenge.removeTask(task);
|
||||||
} else {
|
} else {
|
||||||
taskActivityWebhook.send(user.webhooks, {
|
taskActivityWebhook.send(user, {
|
||||||
type: 'deleted',
|
type: 'deleted',
|
||||||
task,
|
task,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import {
|
|||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import * as passwordUtils from '../../libs/password';
|
import * as passwordUtils from '../../libs/password';
|
||||||
|
import {
|
||||||
|
userActivityWebhook,
|
||||||
|
} from '../../libs/webhook';
|
||||||
import {
|
import {
|
||||||
getUserInfo,
|
getUserInfo,
|
||||||
sendTxn as txnEmail,
|
sendTxn as txnEmail,
|
||||||
@@ -906,8 +909,19 @@ api.hatch = {
|
|||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let hatchRes = common.ops.hatch(user, req);
|
let hatchRes = common.ops.hatch(user, req);
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
res.respond(200, ...hatchRes);
|
res.respond(200, ...hatchRes);
|
||||||
|
|
||||||
|
// Send webhook
|
||||||
|
const petKey = `${req.params.egg}-${req.params.hatchingPotion}`;
|
||||||
|
|
||||||
|
userActivityWebhook.send(user, {
|
||||||
|
type: 'petHatched',
|
||||||
|
pet: petKey,
|
||||||
|
message: hatchRes[1],
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -982,8 +996,21 @@ api.feed = {
|
|||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
let feedRes = common.ops.feed(user, req);
|
let feedRes = common.ops.feed(user, req);
|
||||||
|
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
res.respond(200, ...feedRes);
|
res.respond(200, ...feedRes);
|
||||||
|
|
||||||
|
// Send webhook
|
||||||
|
const petValue = feedRes[0];
|
||||||
|
|
||||||
|
if (petValue === -1) { // evolved to mount
|
||||||
|
userActivityWebhook.send(user, {
|
||||||
|
type: 'mountRaised',
|
||||||
|
pet: req.params.pet,
|
||||||
|
message: feedRes[1],
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ let api = {};
|
|||||||
* @apiParam (Body) {String} url The webhook's URL
|
* @apiParam (Body) {String} url The webhook's URL
|
||||||
* @apiParam (Body) {String} [label] A label to remind you what this webhook does
|
* @apiParam (Body) {String} [label] A label to remind you what this webhook does
|
||||||
* @apiParam (Body) {Boolean} [enabled=true] If the webhook should be enabled
|
* @apiParam (Body) {Boolean} [enabled=true] If the webhook should be enabled
|
||||||
* @apiParam (Body) {Sring="taskActivity","groupChatReceived"} [type="taskActivity"] The webhook's type.
|
* @apiParam (Body) {Sring="taskActivity","groupChatReceived","userActivity"} [type="taskActivity"] The webhook's type.
|
||||||
* @apiParam (Body) {Object} [options] The webhook's options. Wil differ depending on type. Required for `groupChatReceived` type. If a webhook supports options, the default values are displayed in the examples below
|
* @apiParam (Body) {Object} [options] The webhook's options. Wil differ depending on type. Required for `groupChatReceived` type. If a webhook supports options, the default values are displayed in the examples below
|
||||||
* @apiParamExample {json} Task Activity Example
|
* @apiParamExample {json} Task Activity Example
|
||||||
* {
|
* {
|
||||||
|
|||||||
@@ -33,11 +33,20 @@ export class WebhookSender {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
send (webhooks, data) {
|
attachDefaultData (user, body) {
|
||||||
|
body.webhookType = this.type;
|
||||||
|
body.user = body.user || {};
|
||||||
|
body.user._id = user._id;
|
||||||
|
}
|
||||||
|
|
||||||
|
send (user, data) {
|
||||||
|
const webhooks = user.webhooks;
|
||||||
|
|
||||||
let hooks = webhooks.filter((hook) => {
|
let hooks = webhooks.filter((hook) => {
|
||||||
return isValidWebhook(hook) &&
|
if (!isValidWebhook(hook)) return false;
|
||||||
this.type === hook.type &&
|
if (hook.type === 'globalActivity') return true;
|
||||||
this.webhookFilter(hook, data);
|
|
||||||
|
return this.type === hook.type && this.webhookFilter(hook, data);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (hooks.length < 1) {
|
if (hooks.length < 1) {
|
||||||
@@ -45,6 +54,7 @@ export class WebhookSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let body = this.transformData(data);
|
let body = this.transformData(data);
|
||||||
|
this.attachDefaultData(user, body);
|
||||||
|
|
||||||
hooks.forEach((hook) => {
|
hooks.forEach((hook) => {
|
||||||
sendWebhook(hook.url, body);
|
sendWebhook(hook.url, body);
|
||||||
@@ -65,7 +75,7 @@ export let taskScoredWebhook = new WebhookSender({
|
|||||||
let extendedStats = user.addComputedStatsToJSONObj(user.stats.toJSON());
|
let extendedStats = user.addComputedStatsToJSONObj(user.stats.toJSON());
|
||||||
|
|
||||||
let userData = {
|
let userData = {
|
||||||
_id: user._id,
|
// _id: user._id, added automatically when the webhook is sent
|
||||||
_tmp: user._tmp,
|
_tmp: user._tmp,
|
||||||
stats: extendedStats,
|
stats: extendedStats,
|
||||||
};
|
};
|
||||||
@@ -90,6 +100,38 @@ export let taskActivityWebhook = new WebhookSender({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export let userActivityWebhook = new WebhookSender({
|
||||||
|
type: 'userActivity',
|
||||||
|
webhookFilter (hook, data) {
|
||||||
|
let { type } = data;
|
||||||
|
return hook.options[type];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export let questActivityWebhook = new WebhookSender({
|
||||||
|
type: 'questActivity',
|
||||||
|
webhookFilter (hook, data) {
|
||||||
|
let { type } = data;
|
||||||
|
return hook.options[type];
|
||||||
|
},
|
||||||
|
transformData (data) {
|
||||||
|
let { group, quest, type } = data;
|
||||||
|
|
||||||
|
let dataToSend = {
|
||||||
|
type,
|
||||||
|
group: {
|
||||||
|
id: group.id,
|
||||||
|
name: group.name,
|
||||||
|
},
|
||||||
|
quest: {
|
||||||
|
key: quest.key,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return dataToSend;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export let groupChatReceivedWebhook = new WebhookSender({
|
export let groupChatReceivedWebhook = new WebhookSender({
|
||||||
type: 'groupChatReceived',
|
type: 'groupChatReceived',
|
||||||
webhookFilter (hook, data) {
|
webhookFilter (hook, data) {
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ import * as Tasks from './task';
|
|||||||
import validator from 'validator';
|
import validator from 'validator';
|
||||||
import { removeFromArray } from '../libs/collectionManipulators';
|
import { removeFromArray } from '../libs/collectionManipulators';
|
||||||
import payments from '../libs/payments/payments';
|
import payments from '../libs/payments/payments';
|
||||||
import { groupChatReceivedWebhook } from '../libs/webhook';
|
import {
|
||||||
|
groupChatReceivedWebhook,
|
||||||
|
questActivityWebhook,
|
||||||
|
} from '../libs/webhook';
|
||||||
import {
|
import {
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
BadRequest,
|
BadRequest,
|
||||||
@@ -648,20 +651,24 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
removeFromArray(nonUserQuestMembers, user._id);
|
removeFromArray(nonUserQuestMembers, user._id);
|
||||||
|
|
||||||
// remove any users from quest.members who aren't in the party
|
// remove any users from quest.members who aren't in the party
|
||||||
let partyId = this._id;
|
// and get the data necessary to send webhooks
|
||||||
let questMembers = this.quest.members;
|
const members = [];
|
||||||
await Promise.all(Object.keys(this.quest.members).map(memberId => {
|
|
||||||
return User.findOne({_id: memberId, 'party._id': partyId})
|
await User.find({
|
||||||
.select('_id')
|
_id: {$in: Object.keys(this.quest.members)},
|
||||||
.lean()
|
})
|
||||||
.exec()
|
.select('party.quest party._id items.quests auth preferences.emailNotifications preferences.pushNotifications pushDevices profile.name webhooks')
|
||||||
.then((member) => {
|
.lean()
|
||||||
if (!member) {
|
.exec()
|
||||||
delete questMembers[memberId];
|
.then(partyMembers => {
|
||||||
|
partyMembers.forEach(member => {
|
||||||
|
if (!member.party || member.party._id !== this._id) {
|
||||||
|
delete this.quest.members[member._id];
|
||||||
|
} else {
|
||||||
|
members.push(member);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
}));
|
});
|
||||||
|
|
||||||
if (userIsParticipating) {
|
if (userIsParticipating) {
|
||||||
user.party.quest.key = this.quest.key;
|
user.party.quest.key = this.quest.key;
|
||||||
@@ -670,20 +677,23 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
user.markModified('party.quest');
|
user.markModified('party.quest');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const promises = [];
|
||||||
|
|
||||||
// Remove the quest from the quest leader items (if they are the current user)
|
// Remove the quest from the quest leader items (if they are the current user)
|
||||||
if (this.quest.leader === user._id) {
|
if (this.quest.leader === user._id) {
|
||||||
user.items.quests[this.quest.key] -= 1;
|
user.items.quests[this.quest.key] -= 1;
|
||||||
user.markModified('items.quests');
|
user.markModified('items.quests');
|
||||||
|
promises.push(user.save());
|
||||||
} else { // another user is starting the quest, update the leader separately
|
} else { // another user is starting the quest, update the leader separately
|
||||||
await User.update({_id: this.quest.leader}, {
|
promises.push(User.update({_id: this.quest.leader}, {
|
||||||
$inc: {
|
$inc: {
|
||||||
[`items.quests.${this.quest.key}`]: -1,
|
[`items.quests.${this.quest.key}`]: -1,
|
||||||
},
|
},
|
||||||
}).exec();
|
}).exec());
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the remaining users
|
// update the remaining users
|
||||||
await User.update({
|
promises.push(User.update({
|
||||||
_id: { $in: nonUserQuestMembers },
|
_id: { $in: nonUserQuestMembers },
|
||||||
}, {
|
}, {
|
||||||
$set: {
|
$set: {
|
||||||
@@ -691,7 +701,9 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
'party.quest.progress.down': 0,
|
'party.quest.progress.down': 0,
|
||||||
'party.quest.completed': null,
|
'party.quest.completed': null,
|
||||||
},
|
},
|
||||||
}, { multi: true }).exec();
|
}, { multi: true }).exec());
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
|
||||||
// update the users who are not participating
|
// update the users who are not participating
|
||||||
// Do not block updates
|
// Do not block updates
|
||||||
@@ -703,38 +715,45 @@ schema.methods.startQuest = async function startQuest (user) {
|
|||||||
},
|
},
|
||||||
}, { multi: true }).exec();
|
}, { multi: true }).exec();
|
||||||
|
|
||||||
// send notifications in the background without blocking
|
|
||||||
User.find(
|
|
||||||
{ _id: { $in: nonUserQuestMembers } },
|
|
||||||
'party.quest items.quests auth.facebook auth.local preferences.emailNotifications preferences.pushNotifications pushDevices profile.name'
|
|
||||||
).exec().then((membersToNotify) => {
|
|
||||||
let membersToEmail = _.filter(membersToNotify, (member) => {
|
|
||||||
// send push notifications and filter users that disabled emails
|
|
||||||
return member.preferences.emailNotifications.questStarted !== false &&
|
|
||||||
member._id !== user._id;
|
|
||||||
});
|
|
||||||
sendTxnEmail(membersToEmail, 'quest-started', [
|
|
||||||
{ name: 'PARTY_URL', content: '/party' },
|
|
||||||
]);
|
|
||||||
let membersToPush = _.filter(membersToNotify, (member) => {
|
|
||||||
// send push notifications and filter users that disabled emails
|
|
||||||
return member.preferences.pushNotifications.questStarted !== false &&
|
|
||||||
member._id !== user._id;
|
|
||||||
});
|
|
||||||
_.each(membersToPush, (member) => {
|
|
||||||
sendPushNotification(member,
|
|
||||||
{
|
|
||||||
title: quest.text(),
|
|
||||||
message: `${shared.i18n.t('questStarted')}: ${quest.text()}`,
|
|
||||||
identifier: 'questStarted',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
const newMessage = this.sendChat(`\`Your quest, ${quest.text('en')}, has started.\``, null, {
|
const newMessage = this.sendChat(`\`Your quest, ${quest.text('en')}, has started.\``, null, {
|
||||||
participatingMembers: this.getParticipatingQuestMembers().join(', '),
|
participatingMembers: this.getParticipatingQuestMembers().join(', '),
|
||||||
});
|
});
|
||||||
|
|
||||||
await newMessage.save();
|
await newMessage.save();
|
||||||
|
|
||||||
|
const membersToEmail = [];
|
||||||
|
const pushTitle = quest.text();
|
||||||
|
const pushMessage = `${shared.i18n.t('questStarted')}: ${quest.text()}`;
|
||||||
|
|
||||||
|
// send notifications and webhooks in the background without blocking
|
||||||
|
members.forEach(member => {
|
||||||
|
if (member._id !== user._id) {
|
||||||
|
// send push notifications and filter users that disabled emails
|
||||||
|
if (member.preferences.emailNotifications.questStarted !== false) {
|
||||||
|
membersToEmail.push(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send push notifications and filter users that disabled emails
|
||||||
|
if (member.preferences.pushNotifications.questStarted !== false) {
|
||||||
|
sendPushNotification(member, {
|
||||||
|
title: pushTitle,
|
||||||
|
message: pushMessage,
|
||||||
|
identifier: 'questStarted',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send webhooks
|
||||||
|
questActivityWebhook.send(member, {
|
||||||
|
type: 'questStarted',
|
||||||
|
group: this,
|
||||||
|
quest,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send emails in bulk
|
||||||
|
sendTxnEmail(membersToEmail, 'quest-started', [
|
||||||
|
{ name: 'PARTY_URL', content: '/party' },
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
schema.methods.sendGroupChatReceivedWebhooks = function sendGroupChatReceivedWebhooks (chat) {
|
schema.methods.sendGroupChatReceivedWebhooks = function sendGroupChatReceivedWebhooks (chat) {
|
||||||
@@ -755,8 +774,7 @@ schema.methods.sendGroupChatReceivedWebhooks = function sendGroupChatReceivedWeb
|
|||||||
|
|
||||||
User.find(query).select({webhooks: 1}).lean().exec().then((users) => {
|
User.find(query).select({webhooks: 1}).lean().exec().then((users) => {
|
||||||
users.forEach((user) => {
|
users.forEach((user) => {
|
||||||
let { webhooks } = user;
|
groupChatReceivedWebhook.send(user, {
|
||||||
groupChatReceivedWebhook.send(webhooks, {
|
|
||||||
group: this,
|
group: this,
|
||||||
chat,
|
chat,
|
||||||
});
|
});
|
||||||
@@ -907,6 +925,31 @@ schema.methods.finishQuest = async function finishQuest (quest) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send webhooks in background
|
||||||
|
// @TODO move the find users part to a worker as well, not just the http request
|
||||||
|
User.find({
|
||||||
|
_id: {$in: participants},
|
||||||
|
webhooks: {
|
||||||
|
$elemMatch: {
|
||||||
|
type: 'questActivity',
|
||||||
|
'options.questFinished': true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.select('_id webhooks')
|
||||||
|
.lean()
|
||||||
|
.exec()
|
||||||
|
.then(participantsWithWebhook => {
|
||||||
|
participantsWithWebhook.forEach(participantWithWebhook => {
|
||||||
|
// Send webhooks
|
||||||
|
questActivityWebhook.send(participantWithWebhook, {
|
||||||
|
type: 'questFinished',
|
||||||
|
group: this,
|
||||||
|
quest,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return await Promise.all(promises);
|
return await Promise.all(promises);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import * as Tasks from '../task';
|
|||||||
import {
|
import {
|
||||||
model as UserNotification,
|
model as UserNotification,
|
||||||
} from '../userNotification';
|
} from '../userNotification';
|
||||||
|
import {
|
||||||
|
userActivityWebhook,
|
||||||
|
} from '../../libs/webhook';
|
||||||
|
|
||||||
import schema from './schema';
|
import schema from './schema';
|
||||||
|
|
||||||
@@ -241,41 +244,73 @@ schema.pre('save', true, function preSaveUser (next, done) {
|
|||||||
// this.items.pets['JackOLantern-Base'] = 5;
|
// this.items.pets['JackOLantern-Base'] = 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manage unallocated stats points notifications
|
// Filter notifications, remove unvalid and not necessary, handle the ones that have special requirements
|
||||||
if (this.isDirectSelected('stats') && this.isDirectSelected('notifications') && this.isDirectSelected('flags') && this.isDirectSelected('preferences')) {
|
if ( // Make sure all the data is loaded
|
||||||
|
this.isDirectSelected('notifications') &&
|
||||||
|
this.isDirectSelected('webhooks') &&
|
||||||
|
this.isDirectSelected('stats') &&
|
||||||
|
this.isDirectSelected('flags') &&
|
||||||
|
this.isDirectSelected('preferences')
|
||||||
|
) {
|
||||||
|
const lvlUpNotifications = [];
|
||||||
|
const unallocatedPointsNotifications = [];
|
||||||
|
|
||||||
|
this.notifications = this.notifications.filter(notification => {
|
||||||
|
// Remove corrupt notifications
|
||||||
|
if (!notification || !notification.type) return false;
|
||||||
|
|
||||||
|
// Remove level up notifications, as they're only used to send webhooks
|
||||||
|
// Sometimes there can be more than 1 notification
|
||||||
|
if (notification && notification.type === 'LEVELED_UP') {
|
||||||
|
lvlUpNotifications.push(notification);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all unsallocated stats points
|
||||||
|
if (notification && notification.type === 'UNALLOCATED_STATS_POINTS') {
|
||||||
|
unallocatedPointsNotifications.push(notification);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Keep all the others
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Send lvl up notifications
|
||||||
|
if (lvlUpNotifications.length > 0) {
|
||||||
|
const firstLvlNotification = lvlUpNotifications[0];
|
||||||
|
const lastLvlNotification = lvlUpNotifications[lvlUpNotifications.length - 1];
|
||||||
|
|
||||||
|
const initialLvl = firstLvlNotification.data.initialLvl;
|
||||||
|
const finalLvl = lastLvlNotification.data.newLvl;
|
||||||
|
|
||||||
|
// Delayed so we don't block the user saving
|
||||||
|
setTimeout(() => {
|
||||||
|
userActivityWebhook.send(this, {
|
||||||
|
type: 'leveledUp',
|
||||||
|
initialLvl,
|
||||||
|
finalLvl,
|
||||||
|
});
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle unallocated stats points notifications (keep only one and up to date)
|
||||||
const pointsToAllocate = this.stats.points;
|
const pointsToAllocate = this.stats.points;
|
||||||
const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses;
|
const classNotEnabled = !this.flags.classSelected || this.preferences.disableClasses;
|
||||||
|
|
||||||
// Sometimes there can be more than 1 notification
|
|
||||||
const existingNotifications = this.notifications.filter(notification => {
|
|
||||||
return notification && notification.type === 'UNALLOCATED_STATS_POINTS';
|
|
||||||
});
|
|
||||||
|
|
||||||
const existingNotificationsLength = existingNotifications.length;
|
|
||||||
// Take the most recent notification
|
// Take the most recent notification
|
||||||
const lastExistingNotification = existingNotificationsLength > 0 ? existingNotifications[existingNotificationsLength - 1] : null;
|
const lastExistingNotification = unallocatedPointsNotifications[unallocatedPointsNotifications.length - 1];
|
||||||
|
|
||||||
// Decide if it's outdated or not
|
// Decide if it's outdated or not
|
||||||
const outdatedNotification = !lastExistingNotification || lastExistingNotification.data.points !== pointsToAllocate;
|
const outdatedNotification = !lastExistingNotification || lastExistingNotification.data.points !== pointsToAllocate;
|
||||||
|
|
||||||
// If the notification is outdated, remove all the existing notifications, otherwise all of them except the last
|
|
||||||
let notificationsToRemove = outdatedNotification ? existingNotificationsLength : existingNotificationsLength - 1;
|
|
||||||
|
|
||||||
// If there are points to allocate and the notification is outdated, add a new notifications
|
// If there are points to allocate and the notification is outdated, add a new notifications
|
||||||
if (pointsToAllocate > 0 && !classNotEnabled && outdatedNotification) {
|
if (pointsToAllocate > 0 && !classNotEnabled) {
|
||||||
this.addNotification('UNALLOCATED_STATS_POINTS', { points: pointsToAllocate });
|
if (outdatedNotification) {
|
||||||
}
|
this.addNotification('UNALLOCATED_STATS_POINTS', { points: pointsToAllocate });
|
||||||
|
} else { // otherwise add back the last one
|
||||||
// Remove the outdated notifications
|
this.notifications.push(lastExistingNotification);
|
||||||
if (notificationsToRemove > 0) {
|
}
|
||||||
let notificationsRemoved = 0;
|
|
||||||
|
|
||||||
this.notifications = this.notifications.filter(notification => {
|
|
||||||
if (notification && notification.type !== 'UNALLOCATED_STATS_POINTS') return true;
|
|
||||||
if (notificationsRemoved === notificationsToRemove) return true;
|
|
||||||
|
|
||||||
notificationsRemoved++;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const NOTIFICATION_TYPES = [
|
|||||||
'NEW_INBOX_MESSAGE',
|
'NEW_INBOX_MESSAGE',
|
||||||
'NEW_STUFF',
|
'NEW_STUFF',
|
||||||
'NEW_CHAT_MESSAGE',
|
'NEW_CHAT_MESSAGE',
|
||||||
|
'LEVELED_UP',
|
||||||
];
|
];
|
||||||
|
|
||||||
const Schema = mongoose.Schema;
|
const Schema = mongoose.Schema;
|
||||||
|
|||||||
@@ -14,9 +14,21 @@ const TASK_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({
|
|||||||
created: false,
|
created: false,
|
||||||
updated: false,
|
updated: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
checklistScored: false,
|
||||||
scored: true,
|
scored: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const USER_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({
|
||||||
|
petHatched: false,
|
||||||
|
mountRaised: false,
|
||||||
|
leveledUp: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const QUEST_ACTIVITY_DEFAULT_OPTIONS = Object.freeze({
|
||||||
|
questStarted: false,
|
||||||
|
questFinished: false,
|
||||||
|
});
|
||||||
|
|
||||||
export let schema = new Schema({
|
export let schema = new Schema({
|
||||||
id: {
|
id: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -27,7 +39,11 @@ export let schema = new Schema({
|
|||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
enum: ['taskActivity', 'groupChatReceived'],
|
enum: [
|
||||||
|
'globalActivity', // global webhooks send a request for every type of event
|
||||||
|
'taskActivity', 'groupChatReceived',
|
||||||
|
'userActivity', 'questActivity',
|
||||||
|
],
|
||||||
default: 'taskActivity',
|
default: 'taskActivity',
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
@@ -67,7 +83,7 @@ schema.plugin(baseModel, {
|
|||||||
schema.methods.formatOptions = function formatOptions (res) {
|
schema.methods.formatOptions = function formatOptions (res) {
|
||||||
if (this.type === 'taskActivity') {
|
if (this.type === 'taskActivity') {
|
||||||
_.defaults(this.options, TASK_ACTIVITY_DEFAULT_OPTIONS);
|
_.defaults(this.options, TASK_ACTIVITY_DEFAULT_OPTIONS);
|
||||||
this.options = _.pick(this.options, 'created', 'updated', 'deleted', 'scored');
|
this.options = _.pick(this.options, Object.keys(TASK_ACTIVITY_DEFAULT_OPTIONS));
|
||||||
|
|
||||||
let invalidOption = Object.keys(this.options)
|
let invalidOption = Object.keys(this.options)
|
||||||
.find(option => typeof this.options[option] !== 'boolean');
|
.find(option => typeof this.options[option] !== 'boolean');
|
||||||
@@ -81,6 +97,29 @@ schema.methods.formatOptions = function formatOptions (res) {
|
|||||||
if (!validator.isUUID(String(this.options.groupId))) {
|
if (!validator.isUUID(String(this.options.groupId))) {
|
||||||
throw new BadRequest(res.t('groupIdRequired'));
|
throw new BadRequest(res.t('groupIdRequired'));
|
||||||
}
|
}
|
||||||
|
} else if (this.type === 'userActivity') {
|
||||||
|
_.defaults(this.options, USER_ACTIVITY_DEFAULT_OPTIONS);
|
||||||
|
this.options = _.pick(this.options, Object.keys(USER_ACTIVITY_DEFAULT_OPTIONS));
|
||||||
|
|
||||||
|
let invalidOption = Object.keys(this.options)
|
||||||
|
.find(option => typeof this.options[option] !== 'boolean');
|
||||||
|
|
||||||
|
if (invalidOption) {
|
||||||
|
throw new BadRequest(res.t('webhookBooleanOption', { option: invalidOption }));
|
||||||
|
}
|
||||||
|
} else if (this.type === 'questActivity') {
|
||||||
|
_.defaults(this.options, QUEST_ACTIVITY_DEFAULT_OPTIONS);
|
||||||
|
this.options = _.pick(this.options, Object.keys(QUEST_ACTIVITY_DEFAULT_OPTIONS));
|
||||||
|
|
||||||
|
let invalidOption = Object.keys(this.options)
|
||||||
|
.find(option => typeof this.options[option] !== 'boolean');
|
||||||
|
|
||||||
|
if (invalidOption) {
|
||||||
|
throw new BadRequest(res.t('webhookBooleanOption', { option: invalidOption }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Discard all options
|
||||||
|
this.options = {};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user