2
0

activities.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import Attachments from './attachments';
  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 Boards.findOne(this.boardId);
  16. },
  17. oldBoard() {
  18. return Boards.findOne(this.oldBoardId);
  19. },
  20. user() {
  21. return Users.findOne(this.userId);
  22. },
  23. member() {
  24. return Users.findOne(this.memberId);
  25. },
  26. list() {
  27. return Lists.findOne(this.listId);
  28. },
  29. swimlane() {
  30. return Swimlanes.findOne(this.swimlaneId);
  31. },
  32. oldSwimlane() {
  33. return Swimlanes.findOne(this.oldSwimlaneId);
  34. },
  35. oldList() {
  36. return Lists.findOne(this.oldListId);
  37. },
  38. card() {
  39. return Cards.findOne(this.cardId);
  40. },
  41. comment() {
  42. return CardComments.findOne(this.commentId);
  43. },
  44. attachment() {
  45. return Attachments.findOne(this.attachmentId);
  46. },
  47. checklist() {
  48. return Checklists.findOne(this.checklistId);
  49. },
  50. checklistItem() {
  51. return ChecklistItems.findOne(this.checklistItemId);
  52. },
  53. subtasks() {
  54. return Cards.findOne(this.subtaskId);
  55. },
  56. customField() {
  57. return CustomFields.findOne(this.customFieldId);
  58. },
  59. // Label activity did not work yet, unable to edit labels when tried this.
  60. //label() {
  61. // return Cards.findOne(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 = Boards.findOne(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.length > 0) {
  129. params.board = board.title;
  130. } else {
  131. params.board = '';
  132. }
  133. title = 'act-withBoardTitle';
  134. params.url = board.absoluteUrl();
  135. params.boardId = activity.boardId;
  136. }
  137. if (activity.oldBoardId) {
  138. const oldBoard = activity.oldBoard();
  139. if (oldBoard) {
  140. watchers = _.union(watchers, oldBoard.watchers || []);
  141. params.oldBoard = oldBoard.title;
  142. params.oldBoardId = activity.oldBoardId;
  143. }
  144. }
  145. if (activity.memberId) {
  146. participants = _.union(participants, [activity.memberId]);
  147. params.member = activity.member().getName();
  148. }
  149. if (activity.listId) {
  150. const list = activity.list();
  151. if (list.watchers !== undefined) {
  152. watchers = _.union(watchers, list.watchers || []);
  153. }
  154. params.list = list.title;
  155. params.listId = activity.listId;
  156. }
  157. if (activity.oldListId) {
  158. const oldList = activity.oldList();
  159. if (oldList) {
  160. watchers = _.union(watchers, oldList.watchers || []);
  161. params.oldList = oldList.title;
  162. params.oldListId = activity.oldListId;
  163. }
  164. }
  165. if (activity.oldSwimlaneId) {
  166. const oldSwimlane = activity.oldSwimlane();
  167. if (oldSwimlane) {
  168. watchers = _.union(watchers, oldSwimlane.watchers || []);
  169. params.oldSwimlane = oldSwimlane.title;
  170. params.oldSwimlaneId = activity.oldSwimlaneId;
  171. }
  172. }
  173. if (activity.cardId) {
  174. const card = activity.card();
  175. participants = _.union(participants, [card.userId], card.members || []);
  176. watchers = _.union(watchers, card.watchers || []);
  177. params.card = card.title;
  178. title = 'act-withCardTitle';
  179. params.url = card.absoluteUrl();
  180. params.cardId = activity.cardId;
  181. }
  182. if (activity.swimlaneId) {
  183. const swimlane = activity.swimlane();
  184. params.swimlane = swimlane.title;
  185. params.swimlaneId = activity.swimlaneId;
  186. }
  187. if (activity.commentId) {
  188. const comment = activity.comment();
  189. params.comment = comment.text;
  190. if (board) {
  191. const comment = params.comment;
  192. const knownUsers = board.members.map(member => {
  193. const u = Users.findOne(member.userId);
  194. if (u) {
  195. member.username = u.username;
  196. member.emails = u.emails;
  197. }
  198. return member;
  199. });
  200. const mentionRegex = /\B@(?:(?:"([\w.\s-]*)")|([\w.-]+))/gi; // including space in username
  201. let currentMention;
  202. while ((currentMention = mentionRegex.exec(comment)) !== null) {
  203. /*eslint no-unused-vars: ["error", { "varsIgnorePattern": "[iI]gnored" }]*/
  204. const [ignored, quoteduser, simple] = currentMention;
  205. const username = quoteduser || simple;
  206. if (username === params.user) {
  207. // ignore commenter mention himself?
  208. continue;
  209. }
  210. if (activity.boardId && username === 'board_members') {
  211. // mentions all board members
  212. const knownUids = knownUsers.map(u => u.userId);
  213. watchers = _.union(watchers, [...knownUids]);
  214. title = 'act-atUserComment';
  215. } else if (activity.cardId && username === 'card_members') {
  216. // mentions all card members if assigned
  217. const card = activity.card();
  218. watchers = _.union(watchers, [...card.members]);
  219. title = 'act-atUserComment';
  220. } else {
  221. const atUser = _.findWhere(knownUsers, { username });
  222. if (!atUser) {
  223. continue;
  224. }
  225. const uid = atUser.userId;
  226. params.atUsername = username;
  227. params.atEmails = atUser.emails;
  228. title = 'act-atUserComment';
  229. watchers = _.union(watchers, [uid]);
  230. }
  231. }
  232. }
  233. params.commentId = comment._id;
  234. }
  235. if (activity.attachmentId) {
  236. const attachment = activity.attachment();
  237. params.attachment = attachment.name;
  238. params.attachmentId = attachment._id;
  239. }
  240. if (activity.checklistId) {
  241. const checklist = activity.checklist();
  242. if (checklist) {
  243. if (checklist.title) {
  244. params.checklist = checklist.title;
  245. }
  246. }
  247. }
  248. if (activity.checklistItemId) {
  249. const checklistItem = activity.checklistItem();
  250. if (checklistItem) {
  251. if (checklistItem.title) {
  252. params.checklistItem = checklistItem.title;
  253. }
  254. }
  255. }
  256. if (activity.customFieldId) {
  257. const customField = activity.customField();
  258. if (customField) {
  259. if (customField.name) {
  260. params.customField = customField.name;
  261. }
  262. if (activity.value) {
  263. params.customFieldValue = activity.value;
  264. }
  265. }
  266. }
  267. // Label activity did not work yet, unable to edit labels when tried this.
  268. //if (activity.labelId) {
  269. // const label = activity.label();
  270. // params.label = label.name;
  271. // params.labelId = activity.labelId;
  272. //}
  273. if (
  274. (!activity.timeKey || activity.timeKey === 'dueAt') &&
  275. activity.timeValue
  276. ) {
  277. // due time reminder, if it doesn't have old value, it's a brand new set, need some differentiation
  278. title = activity.timeOldValue ? 'act-withDue' : 'act-newDue';
  279. }
  280. ['timeValue', 'timeOldValue'].forEach(key => {
  281. // copy time related keys & values to params
  282. const value = activity[key];
  283. if (value) params[key] = value;
  284. });
  285. if (board) {
  286. const BIGEVENTS = process.env.BIGEVENTS_PATTERN; // if environment BIGEVENTS_PATTERN is set, any activityType matching it is important
  287. if (BIGEVENTS) {
  288. try {
  289. const atype = activity.activityType;
  290. if (new RegExp(BIGEVENTS).exec(atype)) {
  291. watchers = _.union(
  292. watchers,
  293. board.activeMembers().map(member => member.userId),
  294. ); // notify all active members for important events
  295. }
  296. } catch (e) {
  297. // passed env var BIGEVENTS_PATTERN is not a valid regex
  298. }
  299. }
  300. const watchingUsers = _.pluck(
  301. _.where(board.watchers, { level: 'watching' }),
  302. 'userId',
  303. );
  304. const trackingUsers = _.pluck(
  305. _.where(board.watchers, { level: 'tracking' }),
  306. 'userId',
  307. );
  308. watchers = _.union(
  309. watchers,
  310. watchingUsers,
  311. _.intersection(participants, trackingUsers),
  312. );
  313. }
  314. Notifications.getUsers(watchers).forEach(user => {
  315. // don't notify a user of their own behavior
  316. if (user._id !== userId) {
  317. Notifications.notify(user, title, description, params);
  318. }
  319. });
  320. const integrations = Integrations.find({
  321. boardId: { $in: [board._id, Integrations.Const.GLOBAL_WEBHOOK_ID] },
  322. // type: 'outgoing-webhooks', // all types
  323. enabled: true,
  324. activities: { $in: [description, 'all'] },
  325. }).fetch();
  326. if (integrations.length > 0) {
  327. params.watchers = watchers;
  328. integrations.forEach(integration => {
  329. Meteor.call(
  330. 'outgoingWebhooks',
  331. integration,
  332. description,
  333. params,
  334. () => {
  335. return;
  336. },
  337. );
  338. });
  339. }
  340. });
  341. }
  342. export default Activities;