3 var env = require('gitter-web-env');
4 var logger = env.logger;
5 var config = env.config;
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);
33 * Send email notifications to users. Returns true if there were any outstanding
36 function sendEmailNotifications(since) {
37 var start = Date.now();
40 .subtract('m', emailNotificationsAfterMins)
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];
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];
71 return userTroupeUnreadHash;
73 .then(function(userTroupeUnreadHash) {
75 * Filter out all users who've opted out of notification emails
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
88 if (settings[userId]) {
90 'User %s has opted out of unread_notifications, removing from results',
93 delete userTroupeUnreadHash[userId];
97 return userTroupeUnreadHash;
100 // .then(function(userTroupeUnreadHash) {
102 // * Now we need to filter out users who've turned off notifications for a specific troupe
104 // var userTroupes = [];
105 // var userIds = Object.keys(userTroupeUnreadHash);
107 // if(!userIds.length) return {};
109 // debug('After removing opt-out users: %s', userIds.length);
111 // userIds.forEach(function(userId) {
112 // var troupeIds = Object.keys(userTroupeUnreadHash[userId]);
113 // troupeIds.forEach(function(troupeId) {
114 // userTroupes.push({ userId: userId, troupeId: troupeId });
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];
125 // if(setting && setting !== 'all') {
126 // debug('User %s has disabled notifications for this troupe', userId);
127 // delete userTroupeUnreadHash[userId][troupeId];
129 // if(Object.keys(userTroupeUnreadHash[userId]).length === 0) {
130 // delete userTroupeUnreadHash[userId];
136 // return userTroupeUnreadHash;
139 .then(function(userTroupeUnreadHash) {
141 *load the data we're going to need for the emails
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]);
156 userService.findByIds(userIds),
157 troupeService.findByIds(troupeIds),
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');
172 // NOTE: some twitter accounts might not actually have an email address
177 userIds = users.map(function(user) {
181 debug('After removing users without the correct token: %s', userIds.length);
183 return [userIds, users, allTroupes, userTroupeUnreadHash];
185 .spread(function(userIds, users, allTroupes, userTroupeUnreadHash) {
186 if (!userIds.length) return;
189 * Step 2: loop through the users
191 var troupeHash = collections.indexById(allTroupes);
192 var userHash = collections.indexById(users);
196 // Limit the loop to 10 simultaneous sends
200 var user = userHash[userId];
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];
211 .filter(collections.predicates.notNull);
213 return serializer.serialize(troupes, strategy).then(function(serializedTroupes) {
214 var troupeData = serializedTroupes
216 var a = userTroupeUnreadHash[userId];
217 var b = a && a[t.id];
218 var unreadCount = b && b.length;
224 return { troupe: t, unreadCount: unreadCount, unreadItems: b };
226 .filter(function(d) {
227 return !!d.unreadCount; // This needs to be one or more
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));
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];
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);
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);
273 /* Return whether the queue was empty or not */
274 return hadEmailsInQueue;
279 module.exports = sendEmailNotifications;