Gitter migration: Setup redirects (rollout pt. 3)
[gitter.git] / server / services / notifications / email-notification-generator-service.js
blobac9a2d9c820fe11bdb0a981b15524f95367f19f8
1 'use strict';
3 var env = require('gitter-web-env');
4 var logger = env.logger;
5 var config = env.config;
6 var stats = env.stats;
8 var _ = require('lodash');
9 var troupeService = require('gitter-web-rooms/lib/troupe-service');
10 var userService = require('gitter-web-users');
11 var unreadItemService = require('gitter-web-unread-items');
12 var serializer = require('../../serializers/notification-serializer');
13 var moment = require('moment');
14 var Promise = require('bluebird');
15 var collections = require('gitter-web-utils/lib/collections');
16 var mongoUtils = require('gitter-web-persistence-utils/lib/mongo-utils');
17 var emailNotificationService = require('gitter-web-email-notifications');
18 var userSettingsService = require('gitter-web-user-settings');
19 var debug = require('debug')('gitter:app:email-notification-generator-service');
20 var userScopes = require('gitter-web-identity/lib/user-scopes');
22 var filterTestValues = config.get('notifications:filterTestValues');
24 var timeBeforeNextEmailNotificationS =
25   config.get('notifications:timeBeforeNextEmailNotificationMins') * 60;
26 var emailNotificationsAfterMins = config.get('notifications:emailNotificationsAfterMins');
28 function isTestId(id) {
29   return id.indexOf('USER') === 0 || id.indexOf('TROUPE') === 0 || !mongoUtils.isLikeObjectId(id);
32 /**
33  * Send email notifications to users. Returns true if there were any outstanding
34  * emails in the queue
35  */
36 function sendEmailNotifications(since) {
37   var start = Date.now();
38   if (!since) {
39     since = moment()
40       .subtract('m', emailNotificationsAfterMins)
41       .valueOf();
42   }
44   var hadEmailsInQueue;
46   return (
47     unreadItemService
48       .listTroupeUsersForEmailNotifications(since, timeBeforeNextEmailNotificationS)
49       .then(function(userTroupeUnreadHash) {
50         hadEmailsInQueue = !!Object.keys(userTroupeUnreadHash).length;
52         if (!filterTestValues) return userTroupeUnreadHash;
54         /* Remove testing rubbish */
55         Object.keys(userTroupeUnreadHash).forEach(function(userId) {
56           if (isTestId(userId)) {
57             delete userTroupeUnreadHash[userId];
58             return;
59           }
61           Object.keys(userTroupeUnreadHash[userId]).forEach(function(troupeId) {
62             if (isTestId(troupeId)) {
63               delete userTroupeUnreadHash[userId][troupeId];
64               if (Object.keys(userTroupeUnreadHash[userId]).length === 1) {
65                 delete userTroupeUnreadHash[userId];
66               }
67             }
68           });
69         });
71         return userTroupeUnreadHash;
72       })
73       .then(function(userTroupeUnreadHash) {
74         /**
75          * Filter out all users who've opted out of notification emails
76          */
77         var userIds = Object.keys(userTroupeUnreadHash);
78         debug('Initial user count %s', userIds.length);
79         if (!userIds.length) return {};
81         return userSettingsService
82           .getMultiUserSettings(userIds, 'unread_notifications_optout')
83           .then(function(settings) {
84             // Check which users have opted out
85             userIds.forEach(function(userId) {
86               // If unread_notifications_optout is truish, the
87               // user has opted out
88               if (settings[userId]) {
89                 debug(
90                   'User %s has opted out of unread_notifications, removing from results',
91                   userId
92                 );
93                 delete userTroupeUnreadHash[userId];
94               }
95             });
97             return userTroupeUnreadHash;
98           });
99       })
100       // .then(function(userTroupeUnreadHash) {
101       //   /**
102       //    * Now we need to filter out users who've turned off notifications for a specific troupe
103       //    */
104       //   var userTroupes = [];
105       //   var userIds = Object.keys(userTroupeUnreadHash);
106       //
107       //   if(!userIds.length) return {};
108       //
109       //   debug('After removing opt-out users: %s', userIds.length);
110       //
111       //   userIds.forEach(function(userId) {
112       //       var troupeIds = Object.keys(userTroupeUnreadHash[userId]);
113       //       troupeIds.forEach(function(troupeId) {
114       //         userTroupes.push({ userId: userId, troupeId: troupeId });
115       //       });
116       //   });
117       //
118       //   return userRoomNotificationService.findSettingsForMultiUserRooms(userTroupes)
119       //     .then(function(notificationSettings) {
120       //       Object.keys(userTroupeUnreadHash).forEach(function(userId) {
121       //           var troupeIds = Object.keys(userTroupeUnreadHash[userId]);
122       //           troupeIds.forEach(function(troupeId) {
123       //             var setting = notificationSettings[userId + ':' + troupeId];
124       //
125       //             if(setting && setting !== 'all') {
126       //               debug('User %s has disabled notifications for this troupe', userId);
127       //               delete userTroupeUnreadHash[userId][troupeId];
128       //
129       //               if(Object.keys(userTroupeUnreadHash[userId]).length === 0) {
130       //                 delete userTroupeUnreadHash[userId];
131       //               }
132       //             }
133       //           });
134       //       });
135       //
136       //       return userTroupeUnreadHash;
137       //     });
138       // })
139       .then(function(userTroupeUnreadHash) {
140         /**
141          *load the data we're going to need for the emails
142          */
143         var userIds = Object.keys(userTroupeUnreadHash);
144         if (!userIds.length) return [userIds, [], [], {}];
146         debug('After removing room non-notify users: %s', userIds.length);
148         var troupeIds = _.flatten(
149           Object.keys(userTroupeUnreadHash).map(function(userId) {
150             return Object.keys(userTroupeUnreadHash[userId]);
151           })
152         );
154         return Promise.all([
155           userIds,
156           userService.findByIds(userIds),
157           troupeService.findByIds(troupeIds),
158           userTroupeUnreadHash
159         ]);
160       })
161       .spread(function(userIds, users, allTroupes, userTroupeUnreadHash) {
162         if (!userIds.length) return [userIds, [], [], {}];
164         /* Remove anyone that we don't have a token for */
165         users = users.filter(function(user) {
166           // Using isGitHubUser is bad, but loading the user's identities just to be
167           // able to call into the exact right backend is really slow for this
168           // use case. The right way is to use the backend muxer.
169           if (userScopes.isGitHubUser(user)) {
170             return userScopes.hasGitHubScope(user, 'user:email');
171           } else {
172             // NOTE: some twitter accounts might not actually have an email address
173             return true;
174           }
175         });
177         userIds = users.map(function(user) {
178           return user.id;
179         });
181         debug('After removing users without the correct token: %s', userIds.length);
183         return [userIds, users, allTroupes, userTroupeUnreadHash];
184       })
185       .spread(function(userIds, users, allTroupes, userTroupeUnreadHash) {
186         if (!userIds.length) return;
188         /**
189          * Step 2: loop through the users
190          */
191         var troupeHash = collections.indexById(allTroupes);
192         var userHash = collections.indexById(users);
194         var count = 0;
196         // Limit the loop to 10 simultaneous sends
197         return Promise.map(
198           userIds,
199           function(userId) {
200             var user = userHash[userId];
201             if (!user) return;
203             var strategy = new serializer.TroupeStrategy({ recipientUserId: user.id });
205             var unreadItemsForTroupe = userTroupeUnreadHash[user.id];
206             var troupeIds = Object.keys(unreadItemsForTroupe);
207             var troupes = troupeIds
208               .map(function(troupeId) {
209                 return troupeHash[troupeId];
210               })
211               .filter(collections.predicates.notNull);
213             return serializer.serialize(troupes, strategy).then(function(serializedTroupes) {
214               var troupeData = serializedTroupes
215                 .map(function(t) {
216                   var a = userTroupeUnreadHash[userId];
217                   var b = a && a[t.id];
218                   var unreadCount = b && b.length;
220                   if (b) {
221                     b.sort();
222                   }
224                   return { troupe: t, unreadCount: unreadCount, unreadItems: b };
225                 })
226                 .filter(function(d) {
227                   return !!d.unreadCount; // This needs to be one or more
228                 });
230               // Somehow we've ended up with no chat messages?
231               if (!troupeData.length) return;
233               var chatIdsForUser = troupeData.reduce(function(memo, d) {
234                 return memo.concat(d.unreadItems.slice(-3));
235               }, []);
237               var chatStrategy = new serializer.ChatIdStrategy({ recipientUserId: user.id });
238               return serializer.serialize(chatIdsForUser, chatStrategy).then(function(chats) {
239                 var chatsIndexed = collections.indexById(chats);
241                 troupeData.forEach(function(d) {
242                   // Reassemble the chats for the troupe
243                   d.chats = d.unreadItems.slice(-3).reduce(function(memo, chatId) {
244                     var chat = chatsIndexed[chatId];
245                     if (chat) {
246                       memo.push(chat);
247                     }
248                     return memo;
249                   }, []);
250                 });
252                 count++;
253                 return emailNotificationService
254                   .sendUnreadItemsNotification(user, troupeData)
255                   .catch(function(err) {
256                     if (err.gitterAction === 'logout_destroy_user_tokens') {
257                       stats.event('logout_destroy_user_tokens', { userId: user.id });
259                       userService.destroyTokensForUserId(user.id);
260                     }
261                   });
262               });
263             });
264           },
265           { concurrency: 10 }
266         ).then(function() {
267           var time = Date.now() - start;
268           logger.info('Sent unread notification emails to ' + count + ' users in ' + time + 'ms');
269           stats.gaugeHF('unread_email_notifications.sent_emails', count, 1);
270         });
271       })
272       .then(function() {
273         /* Return whether the queue was empty or not */
274         return hadEmailsInQueue;
275       })
276   );
279 module.exports = sendEmailNotifications;