diff options
-rw-r--r-- | .eslintrc.json | 4 | ||||
-rw-r--r-- | .meteor/packages | 1 | ||||
-rw-r--r-- | client/components/boards/boardHeader.jade | 11 | ||||
-rw-r--r-- | client/components/boards/boardHeader.js | 43 | ||||
-rw-r--r-- | i18n/en.i18n.json | 4 | ||||
-rw-r--r-- | models/activities.js | 5 | ||||
-rw-r--r-- | models/integrations.js | 54 | ||||
-rw-r--r-- | server/notifications/outgoing.js | 47 | ||||
-rw-r--r-- | server/publications/boards.js | 1 |
9 files changed, 168 insertions, 2 deletions
diff --git a/.eslintrc.json b/.eslintrc.json index 64e2b702..c1ee03b8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -127,6 +127,8 @@ "InvitationCodes": true, "Winston":true, "JsonRoutes": true, - "Authentication": true + "Authentication": true, + "Integrations": true, + "HTTP": true } } diff --git a/.meteor/packages b/.meteor/packages index 1705935f..8bb66922 100644 --- a/.meteor/packages +++ b/.meteor/packages @@ -57,6 +57,7 @@ mquandalle:moment ongoworks:speakingurl raix:handlebar-helpers tap:i18n +http # UI components blaze diff --git a/client/components/boards/boardHeader.jade b/client/components/boards/boardHeader.jade index a5b7face..d33ee11b 100644 --- a/client/components/boards/boardHeader.jade +++ b/client/components/boards/boardHeader.jade @@ -112,6 +112,7 @@ template(name="boardMenuPopup") ul.pop-over-list li: a(href="{{exportUrl}}", download="{{exportFilename}}") {{_ 'export-board'}} li: a.js-archive-board {{_ 'archive-board'}} + li: a.js-outgoing-webhooks {{_ 'outgoing-webhooks'}} template(name="boardVisibilityList") ul.pop-over-list @@ -213,3 +214,13 @@ template(name="boardChangeTitlePopup") template(name="archiveBoardPopup") p {{_ 'close-board-pop'}} button.js-confirm.negate.full(type="submit") {{_ 'archive'}} + +template(name="outgoingWebhooksPopup") + form + label + | URL + if integration.enabled + input.js-outgoing-webhooks-url(type="text" value=integration.url autofocus) + else + input.js-outgoing-webhooks-url(type="text" autofocus) + input.primary.wide(type="submit" value="{{_ 'save'}}") diff --git a/client/components/boards/boardHeader.js b/client/components/boards/boardHeader.js index c8b44824..dafbfd30 100644 --- a/client/components/boards/boardHeader.js +++ b/client/components/boards/boardHeader.js @@ -13,6 +13,7 @@ Template.boardMenuPopup.events({ // confirm that the board was successfully archived. FlowRouter.go('home'); }), + 'click .js-outgoing-webhooks': Popup.open('outgoingWebhooks'), }); Template.boardMenuPopup.helpers({ @@ -234,3 +235,45 @@ BlazeComponent.extendComponent({ }]; }, }).register('boardChangeWatchPopup'); + +BlazeComponent.extendComponent({ + integration() { + const boardId = Session.get('currentBoard'); + return Integrations.findOne({ boardId: `${boardId}` }); + }, + + events() { + return [{ + 'submit'(evt) { + evt.preventDefault(); + const url = this.find('.js-outgoing-webhooks-url').value.trim(); + const boardId = Session.get('currentBoard'); + const integration = this.integration(); + if (integration) { + if (url) { + Integrations.update(integration._id, { + $set: { + enabled: true, + url: `${url}`, + }, + }); + } else { + Integrations.update(integration._id, { + $set: { + enabled: false, + }, + }); + } + } else if (url) { + Integrations.insert({ + enabled: true, + type: 'outgoing-webhooks', + url: `${url}`, + boardId: `${boardId}`, + }); + } + Popup.close(); + }, + }]; + }, +}).register('outgoingWebhooksPopup'); diff --git a/i18n/en.i18n.json b/i18n/en.i18n.json index ed80fad2..c8377d73 100644 --- a/i18n/en.i18n.json +++ b/i18n/en.i18n.json @@ -360,5 +360,7 @@ "email-invite-register-subject": "__inviter__ sent you an invitation", "email-invite-register-text": "Dear __user__,\n\n__inviter__ invites you to Wekan for collaborations.\n\nPlease follow the link below:\n__url__\n\nAnd your invitation code is: __icode__\n\nThanks.", "error-invitation-code-not-exist": "Invitation code doesn't exist", - "error-notAuthorized": "You are not authorized to view this page." + "error-notAuthorized": "You are not authorized to view this page.", + "outgoing-webhooks": "Outgoing Webhooks", + "outgoingWebhooksPopup-title": "Outgoing Webhooks" } diff --git a/models/activities.js b/models/activities.js index 9a41d4aa..f1e52493 100644 --- a/models/activities.js +++ b/models/activities.js @@ -131,5 +131,10 @@ if (Meteor.isServer) { Notifications.getUsers(participants, watchers).forEach((user) => { Notifications.notify(user, title, description, params); }); + + const integration = Integrations.findOne({ boardId: board._id, type: 'outgoing-webhooks', enabled: true }); + if (integration) { + Meteor.call('outgoingWebhooks', integration, description, params); + } }); } diff --git a/models/integrations.js b/models/integrations.js new file mode 100644 index 00000000..b9bf248f --- /dev/null +++ b/models/integrations.js @@ -0,0 +1,54 @@ +Integrations = new Mongo.Collection('integrations'); + +Integrations.attachSchema(new SimpleSchema({ + enabled: { + type: Boolean, + defaultValue: true, + }, + title: { + type: String, + optional: true, + }, + type: { + type: String, + }, + url: { // URL validation regex (https://mathiasbynens.be/demo/url-regex) + type: String, + }, + token: { + type: String, + optional: true, + }, + boardId: { + type: String, + }, + createdAt: { + type: Date, + denyUpdate: false, + autoValue() { // eslint-disable-line consistent-return + if (this.isInsert) { + return new Date(); + } else { + this.unset(); + } + }, + }, + userId: { + type: String, + autoValue() { // eslint-disable-line consistent-return + if (this.isInsert || this.isUpdate) { + return this.userId; + } + }, + }, +})); + +Integrations.allow({ + insert(userId, doc) { + return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); + }, + update(userId, doc) { + return allowIsBoardAdmin(userId, Boards.findOne(doc.boardId)); + }, + fetch: ['boardId'], +}); diff --git a/server/notifications/outgoing.js b/server/notifications/outgoing.js new file mode 100644 index 00000000..a5bbc737 --- /dev/null +++ b/server/notifications/outgoing.js @@ -0,0 +1,47 @@ +const postCatchError = Meteor.wrapAsync((url, options, resolve) => { + HTTP.post(url, options, (err, res) => { + if (err) { + resolve(null, err.response); + } else { + resolve(null, res); + } + }); +}); + +Meteor.methods({ + outgoingWebhooks(integration, description, params) { + check(integration, Object); + check(description, String); + check(params, Object); + + const quoteParams = _.clone(params); + ['card', 'list', 'oldList', 'board', 'comment'].forEach((key) => { + if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`; + }); + + const user = Users.findOne(integration.userId); + const text = `${params.user} ${TAPi18n.__(description, quoteParams, user.getLanguage())}\n${params.url}`; + + if (text.length === 0) return; + + const value = { + text: `${text}`, + }; + + const options = { + headers: { + // 'Content-Type': 'application/json', + // 'X-Wekan-Activities-Token': 'Random.Id()', + }, + data: value, + }; + + 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'); + } + }, +}); diff --git a/server/publications/boards.js b/server/publications/boards.js index 133082dd..f482f619 100644 --- a/server/publications/boards.js +++ b/server/publications/boards.js @@ -73,6 +73,7 @@ Meteor.publishRelations('board', function(boardId) { ], }, { limit: 1 }), function(boardId, board) { this.cursor(Lists.find({ boardId })); + this.cursor(Integrations.find({ boardId })); // Cards and cards comments // XXX Originally we were publishing the card documents as a child of the |