summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.json4
-rw-r--r--.meteor/packages1
-rw-r--r--client/components/boards/boardHeader.jade11
-rw-r--r--client/components/boards/boardHeader.js43
-rw-r--r--i18n/en.i18n.json4
-rw-r--r--models/activities.js5
-rw-r--r--models/integrations.js54
-rw-r--r--server/notifications/outgoing.js47
-rw-r--r--server/publications/boards.js1
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