outgoing.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. if (Meteor.isServer) {
  2. const postCatchError = Meteor.wrapAsync((url, options, resolve) => {
  3. HTTP.post(url, options, (err, res) => {
  4. if (err) {
  5. resolve(null, err.response);
  6. } else {
  7. resolve(null, res);
  8. }
  9. });
  10. });
  11. const Lock = {
  12. _lock: {},
  13. _timer: {},
  14. echoDelay: 500, // echo should be happening much faster
  15. normalDelay: 1e3, // normally user typed comment will be much slower
  16. ECHO: 2,
  17. NORMAL: 1,
  18. NULL: 0,
  19. has(id, value) {
  20. const existing = this._lock[id];
  21. let ret = this.NULL;
  22. if (existing) {
  23. ret = existing === value ? this.ECHO : this.NORMAL;
  24. }
  25. return ret;
  26. },
  27. clear(id, delay) {
  28. const previous = this._timer[id];
  29. if (previous) {
  30. Meteor.clearTimeout(previous);
  31. }
  32. this._timer[id] = Meteor.setTimeout(() => this.unset(id), delay);
  33. },
  34. set(id, value) {
  35. const state = this.has(id, value);
  36. let delay = this.normalDelay;
  37. if (state === this.ECHO) {
  38. delay = this.echoDelay;
  39. }
  40. if (!value) {
  41. // user commented, we set a lock
  42. value = 1;
  43. }
  44. this._lock[id] = value;
  45. this.clear(id, delay); // always auto reset the locker after delay
  46. },
  47. unset(id) {
  48. delete this._lock[id];
  49. },
  50. };
  51. const webhooksAtbts = (process.env.WEBHOOKS_ATTRIBUTES &&
  52. process.env.WEBHOOKS_ATTRIBUTES.split(',')) || [
  53. 'cardId',
  54. 'listId',
  55. 'oldListId',
  56. 'boardId',
  57. 'comment',
  58. 'user',
  59. 'card',
  60. 'commentId',
  61. 'swimlaneId',
  62. 'customField',
  63. 'customFieldValue',
  64. ];
  65. const responseFunc = data => {
  66. const paramCommentId = data.commentId;
  67. const paramCardId = data.cardId;
  68. const paramBoardId = data.boardId;
  69. const newComment = data.comment;
  70. if (paramCardId && paramBoardId && newComment) {
  71. // only process data with the cardid, boardid and comment text, TODO can expand other functions here to react on returned data
  72. const comment = CardComments.findOne({
  73. _id: paramCommentId,
  74. cardId: paramCardId,
  75. boardId: paramBoardId,
  76. });
  77. const board = Boards.findOne(paramBoardId);
  78. const card = Cards.findOne(paramCardId);
  79. if (board && card) {
  80. if (comment) {
  81. Lock.set(comment._id, newComment);
  82. CardComments.direct.update(comment._id, {
  83. $set: {
  84. text: newComment,
  85. },
  86. });
  87. }
  88. } else {
  89. const userId = data.userId;
  90. if (userId) {
  91. const inserted = CardComments.direct.insert({
  92. text: newComment,
  93. userId,
  94. cardId,
  95. boardId,
  96. });
  97. Lock.set(inserted._id, newComment);
  98. }
  99. }
  100. }
  101. };
  102. Meteor.methods({
  103. outgoingWebhooks(integration, description, params) {
  104. if (Meteor.user()) {
  105. check(integration, Object);
  106. check(description, String);
  107. check(params, Object);
  108. this.unblock();
  109. // label activity did not work yet, see wekan/models/activities.js
  110. const quoteParams = _.clone(params);
  111. const clonedParams = _.clone(params);
  112. [
  113. 'card',
  114. 'list',
  115. 'oldList',
  116. 'board',
  117. 'oldBoard',
  118. 'comment',
  119. 'checklist',
  120. 'swimlane',
  121. 'oldSwimlane',
  122. 'label',
  123. 'attachment',
  124. ].forEach(key => {
  125. if (quoteParams[key]) quoteParams[key] = `"${params[key]}"`;
  126. });
  127. const userId = params.userId ? params.userId : integrations[0].userId;
  128. const user = Users.findOne(userId);
  129. const text = `${params.user} ${TAPi18n.__(
  130. description,
  131. quoteParams,
  132. user.getLanguage(),
  133. )}\n${params.url}`;
  134. if (text.length === 0) return;
  135. const value = {
  136. text: `${text}`,
  137. };
  138. webhooksAtbts.forEach(key => {
  139. if (params[key]) value[key] = params[key];
  140. });
  141. value.description = description;
  142. //integrations.forEach(integration => {
  143. const is2way = integration.type === Integrations.Const.TWOWAY;
  144. const token = integration.token || '';
  145. const headers = {
  146. 'Content-Type': 'application/json',
  147. };
  148. if (token) headers['X-Wekan-Token'] = token;
  149. const options = {
  150. headers,
  151. data: is2way ? { description, ...clonedParams } : value,
  152. };
  153. if (!Integrations.findOne({ url: integration.url })) return;
  154. const url = integration.url;
  155. if (is2way) {
  156. const cid = params.commentId;
  157. const comment = params.comment;
  158. const lockState = cid && Lock.has(cid, comment);
  159. if (cid && lockState !== Lock.NULL) {
  160. // it's a comment and there is a previous lock
  161. return;
  162. } else if (cid) {
  163. Lock.set(cid, comment); // set a lock here
  164. }
  165. }
  166. const response = postCatchError(url, options);
  167. if (
  168. response &&
  169. response.statusCode &&
  170. response.statusCode >= 200 &&
  171. response.statusCode < 300
  172. ) {
  173. if (is2way) {
  174. const data = response.data; // only an JSON encoded response will be actioned
  175. if (data) {
  176. try {
  177. responseFunc(data);
  178. } catch (e) {
  179. throw new Meteor.Error('error-process-data');
  180. }
  181. }
  182. }
  183. return response; // eslint-disable-line consistent-return
  184. } else {
  185. throw new Meteor.Error('error-invalid-webhook-response');
  186. }
  187. }
  188. },
  189. });
  190. }