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
;