activities.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. import { ReactiveCache } from '/imports/reactiveCache';
  2. // Activities don't need a schema because they are always set from the a trusted
  3. // environment - the server - and there is no risk that a user change the logic
  4. // we use with this collection. Moreover using a schema for this collection
  5. // would be difficult (different activities have different fields) and wouldn't
  6. // bring any direct advantage.
  7. //
  8. // XXX The activities API is not so nice and need some functionalities. For
  9. // instance if a user archive a card, and un-archive it a few seconds later we
  10. // should remove both activities assuming it was an error the user decided to
  11. // revert.
  12. Activities = new Mongo.Collection('activities');
  13. Activities.helpers({
  14. board() {
  15. return ReactiveCache.getBoard(this.boardId);
  16. },
  17. oldBoard() {
  18. return ReactiveCache.getBoard(this.oldBoardId);
  19. },
  20. user() {
  21. return ReactiveCache.getUser(this.userId);
  22. },
  23. member() {
  24. return ReactiveCache.getUser(this.memberId);
  25. },
  26. list() {
  27. return ReactiveCache.getList(this.listId);
  28. },
  29. swimlane() {
  30. return ReactiveCache.getSwimlane(this.swimlaneId);
  31. },
  32. oldSwimlane() {
  33. return ReactiveCache.getSwimlane(this.oldSwimlaneId);
  34. },
  35. oldList() {
  36. return ReactiveCache.getList(this.oldListId);
  37. },
  38. card() {
  39. return ReactiveCache.getCard(this.cardId);
  40. },
  41. comment() {
  42. return ReactiveCache.getCardComment(this.commentId);
  43. },
  44. attachment() {
  45. return ReactiveCache.getAttachment(this.attachmentId);
  46. },
  47. checklist() {
  48. return ReactiveCache.getChecklist(this.checklistId);
  49. },
  50. checklistItem() {
  51. return ReactiveCache.getChecklistItem(this.checklistItemId);
  52. },
  53. subtasks() {
  54. return ReactiveCache.getCard(this.subtaskId);
  55. },
  56. customField() {
  57. return ReactiveCache.getCustomField(this.customFieldId);
  58. },
  59. label() {
  60. // Label activity did not work yet, unable to edit labels when tried this.
  61. return ReactiveCache.getCard(this.labelId);
  62. },
  63. });
  64. Activities.before.update((userId, doc, fieldNames, modifier) => {
  65. modifier.$set = modifier.$set || {};
  66. modifier.$set.modifiedAt = new Date();
  67. });
  68. Activities.before.insert((userId, doc) => {
  69. doc.createdAt = new Date();
  70. doc.modifiedAt = doc.createdAt;
  71. });
  72. Activities.after.insert((userId, doc) => {
  73. const activity = Activities._transform(doc);
  74. RulesHelper.executeRules(activity);
  75. });
  76. if (Meteor.isServer) {
  77. // For efficiency create indexes on the date of creation, and on the date of
  78. // creation in conjunction with the card or board id, as corresponding views
  79. // are largely used in the App. See #524.
  80. Meteor.startup(() => {
  81. Activities._collection.createIndex({ createdAt: -1 });
  82. Activities._collection.createIndex({ modifiedAt: -1 });
  83. Activities._collection.createIndex({ cardId: 1, createdAt: -1 });
  84. Activities._collection.createIndex({ boardId: 1, createdAt: -1 });
  85. Activities._collection.createIndex(
  86. { commentId: 1 },
  87. { partialFilterExpression: { commentId: { $exists: true } } },
  88. );
  89. Activities._collection.createIndex(
  90. { attachmentId: 1 },
  91. { partialFilterExpression: { attachmentId: { $exists: true } } },
  92. );
  93. Activities._collection.createIndex(
  94. { customFieldId: 1 },
  95. { partialFilterExpression: { customFieldId: { $exists: true } } },
  96. );
  97. // Label activity did not work yet, unable to edit labels when tried this.
  98. //Activities._collection.dropIndex({ labelId: 1 }, { "indexKey": -1 });
  99. //Activities._collection.dropIndex({ labelId: 1 }, { partialFilterExpression: { labelId: { $exists: true } } });
  100. });
  101. Activities.after.insert((userId, doc) => {
  102. const activity = Activities._transform(doc);
  103. let participants = [];
  104. let watchers = [];
  105. let title = 'act-activity-notify';
  106. const board = ReactiveCache.getBoard(activity.boardId);
  107. const description = `act-${activity.activityType}`;
  108. const params = {
  109. activityId: activity._id,
  110. };
  111. if (activity.userId) {
  112. // No need send notification to user of activity
  113. // participants = _.union(participants, [activity.userId]);
  114. const user = activity.user();
  115. if (user) {
  116. if (user.getName()) {
  117. params.user = user.getName();
  118. }
  119. if (user.emails) {
  120. params.userEmails = user.emails;
  121. }
  122. if (activity.userId) {
  123. params.userId = activity.userId;
  124. }
  125. }
  126. }
  127. if (activity.boardId) {
  128. if (board.title) {
  129. if (board.title.length > 0) {
  130. params.board = board.title;
  131. } else {
  132. params.board = '';
  133. }
  134. } else {
  135. params.board = '';
  136. }
  137. title = 'act-withBoardTitle';
  138. params.url = board.absoluteUrl();
  139. params.boardId = activity.boardId;
  140. }
  141. if (activity.oldBoardId) {
  142. const oldBoard = activity.oldBoard();
  143. if (oldBoard) {
  144. watchers = _.union(watchers, oldBoard.watchers || []);
  145. params.oldBoard = oldBoard.title;
  146. params.oldBoardId = activity.oldBoardId;
  147. }
  148. }
  149. if (activity.memberId) {
  150. participants = _.union(participants, [activity.memberId]);
  151. params.member = activity.member().getName();
  152. }
  153. if (activity.listId) {
  154. const list = activity.list();
  155. if (list) {
  156. if (list.watchers !== undefined) {
  157. watchers = _.union(watchers, list.watchers || []);
  158. }
  159. params.list = list.title;
  160. params.listId = activity.listId;
  161. }
  162. }
  163. if (activity.oldListId) {
  164. const oldList = activity.oldList();
  165. if (oldList) {
  166. watchers = _.union(watchers, oldList.watchers || []);
  167. params.oldList = oldList.title;
  168. params.oldListId = activity.oldListId;
  169. }
  170. }
  171. if (activity.oldSwimlaneId) {
  172. const oldSwimlane = activity.oldSwimlane();
  173. if (oldSwimlane) {
  174. watchers = _.union(watchers, oldSwimlane.watchers || []);
  175. params.oldSwimlane = oldSwimlane.title;
  176. params.oldSwimlaneId = activity.oldSwimlaneId;
  177. }
  178. }
  179. if (activity.cardId) {
  180. const card = activity.card();
  181. participants = _.union(participants, [card.userId], card.members || []);
  182. watchers = _.union(watchers, card.watchers || []);
  183. params.card = card.title;
  184. title = 'act-withCardTitle';
  185. params.url = card.absoluteUrl();
  186. params.cardId = activity.cardId;
  187. }
  188. if (activity.swimlaneId) {
  189. const swimlane = activity.swimlane();
  190. params.swimlane = swimlane.title;
  191. params.swimlaneId = activity.swimlaneId;
  192. }
  193. if (activity.commentId) {
  194. const comment = activity.comment();
  195. params.comment = comment.text;
  196. if (board) {
  197. const comment = params.comment;
  198. const knownUsers = board.members.map(member => {
  199. const u = ReactiveCache.getUser(member.userId);
  200. if (u) {
  201. member.username = u.username;
  202. member.emails = u.emails;
  203. }
  204. return member;
  205. });
  206. const mentionRegex = /\B@(?:(?:"([\w.\s-]*)")|([\w.-]+))/gi; // including space in username
  207. let currentMention;
  208. while ((currentMention = mentionRegex.exec(comment)) !== null) {
  209. /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[iI]gnored" }]*/
  210. const [ignored, quoteduser, simple] = currentMention;
  211. const username = quoteduser || simple;
  212. if (username === params.user) {
  213. // ignore commenter mention himself?
  214. continue;
  215. }
  216. if (activity.boardId && username === 'board_members') {
  217. // mentions all board members
  218. const knownUids = knownUsers.map(u => u.userId);
  219. watchers = _.union(watchers, [...knownUids]);
  220. title = 'act-atUserComment';
  221. } else if (activity.cardId && username === 'card_members') {
  222. // mentions all card members if assigned
  223. const card = activity.card();
  224. watchers = _.union(watchers, [...card.members]);
  225. title = 'act-atUserComment';
  226. } else {
  227. const atUser = _.findWhere(knownUsers, { username });
  228. if (!atUser) {
  229. continue;
  230. }
  231. const uid = atUser.userId;
  232. params.atUsername = username;
  233. params.atEmails = atUser.emails;
  234. title = 'act-atUserComment';
  235. watchers = _.union(watchers, [uid]);
  236. }
  237. }
  238. }
  239. params.commentId = comment._id;
  240. }
  241. if (activity.attachmentId) {
  242. params.attachment = activity.attachmentName;
  243. params.attachmentId = activity.attachmentId;
  244. }
  245. if (activity.checklistId) {
  246. const checklist = activity.checklist();
  247. if (checklist) {
  248. if (checklist.title) {
  249. params.checklist = checklist.title;
  250. }
  251. }
  252. }
  253. if (activity.checklistItemId) {
  254. const checklistItem = activity.checklistItem();
  255. if (checklistItem) {
  256. if (checklistItem.title) {
  257. params.checklistItem = checklistItem.title;
  258. }
  259. }
  260. }
  261. if (activity.customFieldId) {
  262. const customField = activity.customField();
  263. if (customField) {
  264. if (customField.name) {
  265. params.customField = customField.name;
  266. }
  267. if (activity.value) {
  268. params.customFieldValue = activity.value;
  269. }
  270. }
  271. }
  272. // Label activity did not work yet, unable to edit labels when tried this.
  273. if (activity.labelId) {
  274. const label = activity.label();
  275. if (label) {
  276. if (label.name) {
  277. params.label = label.name;
  278. } else if (label.color) {
  279. params.label = label.color;
  280. }
  281. if (label._id) {
  282. params.labelId = label._id;
  283. }
  284. }
  285. }
  286. if (
  287. (!activity.timeKey || activity.timeKey === 'dueAt') &&
  288. activity.timeValue
  289. ) {
  290. // due time reminder, if it doesn't have old value, it's a brand new set, need some differentiation
  291. title = activity.timeOldValue ? 'act-withDue' : 'act-newDue';
  292. }
  293. ['timeValue', 'timeOldValue'].forEach(key => {
  294. // copy time related keys & values to params
  295. const value = activity[key];
  296. if (value) params[key] = value;
  297. });
  298. if (board) {
  299. const BIGEVENTS = process.env.BIGEVENTS_PATTERN; // if environment BIGEVENTS_PATTERN is set, any activityType matching it is important
  300. if (BIGEVENTS) {
  301. try {
  302. const atype = activity.activityType;
  303. if (new RegExp(BIGEVENTS).exec(atype)) {
  304. watchers = _.union(
  305. watchers,
  306. board.activeMembers().map(member => member.userId),
  307. ); // notify all active members for important events
  308. }
  309. } catch (e) {
  310. // passed env var BIGEVENTS_PATTERN is not a valid regex
  311. }
  312. }
  313. const watchingUsers = _.pluck(
  314. _.where(board.watchers, { level: 'watching' }),
  315. 'userId',
  316. );
  317. const trackingUsers = _.pluck(
  318. _.where(board.watchers, { level: 'tracking' }),
  319. 'userId',
  320. );
  321. watchers = _.union(
  322. watchers,
  323. watchingUsers,
  324. _.intersection(participants, trackingUsers),
  325. );
  326. }
  327. Notifications.getUsers(watchers).forEach(user => {
  328. // don't notify a user of their own behavior
  329. if (user._id !== userId) {
  330. Notifications.notify(user, title, description, params);
  331. }
  332. });
  333. const integrations = ReactiveCache.getIntegrations({
  334. boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] },
  335. // type: 'outgoing-webhooks', // all types
  336. enabled: true,
  337. activities: { $in: [description, 'all'] },
  338. });
  339. if (integrations.length > 0) {
  340. params.watchers = watchers;
  341. integrations.forEach(integration => {
  342. Meteor.call(
  343. 'outgoingWebhooks',
  344. integration,
  345. description,
  346. params,
  347. () => {
  348. return;
  349. },
  350. );
  351. });
  352. }
  353. });
  354. }
  355. export default Activities;