From dd0682328bc26bbe852fb19a85131e4017c547b0 Mon Sep 17 00:00:00 2001 From: "Sam X. Chen" Date: Thu, 29 Aug 2019 22:07:40 -0400 Subject: Add Feature: enable two-way webhooks - stage two --- models/activities.js | 23 +++++++--- models/integrations.js | 6 ++- models/settings.js | 1 - server/notifications/outgoing.js | 97 +++++++++++++++++++++++++++++++++------- 4 files changed, 102 insertions(+), 25 deletions(-) diff --git a/models/activities.js b/models/activities.js index 3ecd5c8c..f64fce10 100644 --- a/models/activities.js +++ b/models/activities.js @@ -184,10 +184,11 @@ if (Meteor.isServer) { // it's person at himself, ignore it? continue; } - const user = Users.findOne(username) || Users.findOne({ username }); - const uid = user && user._id; + const atUser = + Users.findOne(username) || Users.findOne({ username }); + const uid = atUser && atUser._id; params.atUsername = username; - params.atEmails = user.emails; + params.atEmails = atUser.emails; if (board.hasMember(uid)) { title = 'act-atUserComment'; watchers = _.union(watchers, [uid]); @@ -268,13 +269,23 @@ if (Meteor.isServer) { }); const integrations = Integrations.find({ - boardId: board._id, - type: 'outgoing-webhooks', + boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] }, + // type: 'outgoing-webhooks', // all types enabled: true, activities: { $in: [description, 'all'] }, }).fetch(); if (integrations.length > 0) { - Meteor.call('outgoingWebhooks', integrations, description, params); + integrations.forEach(integration => { + Meteor.call( + 'outgoingWebhooks', + integration, + description, + params, + () => { + return; + }, + ); + }); } }); } diff --git a/models/integrations.js b/models/integrations.js index 0313c959..ce843680 100644 --- a/models/integrations.js +++ b/models/integrations.js @@ -90,7 +90,11 @@ Integrations.attachSchema( ); Integrations.Const = { GLOBAL_WEBHOOK_ID: '_global', - WEBHOOK_TYPES: ['outgoing-webhooks', 'bidirectional-webhooks'], + ONEWAY: 'outgoing-webhooks', + TWOWAY: 'bidirectional-webhooks', + get WEBHOOK_TYPES() { + return [this.ONEWAY, this.TWOWAY]; + }, }; const permissionHelper = { allow(userId, doc) { diff --git a/models/settings.js b/models/settings.js index 80c2d8e0..4a0359d5 100644 --- a/models/settings.js +++ b/models/settings.js @@ -147,7 +147,6 @@ if (Meteor.isServer) { }:${doc.mailServer.port}/`; } Accounts.emailTemplates.from = doc.mailServer.from; - console.log('Settings saved:', Accounts.emailTemplates); } }); diff --git a/server/notifications/outgoing.js b/server/notifications/outgoing.js index 85d54968..850b3acd 100644 --- a/server/notifications/outgoing.js +++ b/server/notifications/outgoing.js @@ -8,6 +8,19 @@ const postCatchError = Meteor.wrapAsync((url, options, resolve) => { }); }); +const Lock = { + _lock: {}, + has(id) { + return !!this._lock[id]; + }, + set(id) { + this._lock[id] = 1; + }, + unset(id) { + delete this._lock[id]; + }, +}; + const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES && process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [ 'cardId', @@ -20,15 +33,44 @@ const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES && 'commentId', 'swimlaneId', ]; - +const responseFunc = 'reactOnHookResponse'; Meteor.methods({ - outgoingWebhooks(integrations, description, params) { - check(integrations, Array); + [responseFunc](data) { + check(data, Object); + const paramCommentId = data.commentId; + const paramCardId = data.cardId; + const paramBoardId = data.boardId; + const newComment = data.comment; + if (paramCardId && paramBoardId && newComment) { // only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data + const comment = CardComments.findOne({ + _id: paramCommentId, + cardId: paramCardId, + boardId: paramBoardId, + }); + if (comment) { + CardComments.update(comment._id, { + $set: { + text: newComment, + }, + }); + } else { + CardComments.insert({ + text: newComment, + cardId, + boardId, + }); + } + } + }, + outgoingWebhooks(integration, description, params) { + check(integration, Object); check(description, String); check(params, Object); + this.unblock(); // label activity did not work yet, see wekan/models/activities.js const quoteParams = _.clone(params); + const clonedParams = _.clone(params); [ 'card', 'list', @@ -63,23 +105,44 @@ Meteor.methods({ if (params[key]) value[key] = params[key]; }); value.description = description; - + //integrations.forEach(integration => { + const is2way = integration.type === Integrations.Const.TWOWAY; + const token = integration.token || ''; + const headers = { + 'Content-Type': 'application/json', + }; + if (token) headers['X-Wekan-Token'] = token; const options = { - headers: { - // 'Content-Type': 'application/json', - // 'X-Wekan-Activities-Token': 'Random.Id()', - }, - data: value, + headers, + data: is2way ? clonedParams : value, }; + const url = integration.url; + const response = postCatchError(url, options); - integrations.forEach(integration => { - const response = postCatchError(integration.url, options); - - if (response && response.statusCode && response.statusCode === 200) { - return true; // eslint-disable-line consistent-return - } else { - throw new Meteor.Error('error-invalid-webhook-response'); + if (response && response.statusCode && response.statusCode === 200) { + if (is2way) { + const cid = params.commentId; + const tooSoon = Lock.has(cid); // if an activity happens to fast, notification shouldn't fire with the same id + if (!tooSoon) { + let clearNotification = () => {}; + if (cid) { + Lock.set(cid); + const clearNotificationFlagTimeout = 1000; + clearNotification = () => Lock.unset(cid); + Meteor.setTimeout(clearNotification, clearNotificationFlagTimeout); + } + const data = response.data; // only an JSON encoded response will be actioned + if (data) { + Meteor.call(responseFunc, data, () => { + clearNotification(); + }); + } + } } - }); + return response; // eslint-disable-line consistent-return + } else { + throw new Meteor.Error('error-invalid-webhook-response'); + } + //}); }, }); -- cgit v1.2.3-1-g7c22