Merge branch 'release/21.49.0' into master
[gitter.git] / modules / invites / lib / invites-service.js
blob586fd9f27acf26f27ae4c0004887b8fe072b9d51
1 'use strict';
3 var Promise = require('bluebird');
4 var TroupeInvite = require('gitter-web-persistence').TroupeInvite;
5 var uuid = require('uuid/v4');
6 var assert = require('assert');
7 var StatusError = require('statuserror');
8 var mongoUtils = require('gitter-web-persistence-utils/lib/mongo-utils');
9 const userService = require('gitter-web-users');
10 var GitHubUserEmailAddressService = require('gitter-web-github').GitHubUserEmailAddressService;
11 var persistence = require('gitter-web-persistence');
12 var identityService = require('gitter-web-identity');
13 const mongoReadPrefs = require('gitter-web-persistence-utils/lib/mongo-read-prefs');
15 var MS_PER_DAY = 24 * 60 * 60 * 1000;
17 var GITTER_IDENTITY_TYPE = 'gitter';
18 var GITHUB_IDENTITY_PROVIDER = identityService.GITHUB_IDENTITY_PROVIDER;
20 function findExistingGitterUser(username) {
21   return persistence.User.findOne({ username: username }).exec();
24 function findExistingIdentityUsername(provider, username) {
25   return identityService.findUserIdForProviderUsername(provider, username).then(function(userId) {
26     if (!userId) return;
27     return persistence.User.findById(userId).exec();
28   });
31 function findExistingUser(type, externalId) {
32   switch (type) {
33     case GITTER_IDENTITY_TYPE:
34       return findExistingGitterUser(externalId);
36     case GITHUB_IDENTITY_PROVIDER:
37       // TODO: Note that we will need to do a lookup once
38       // splitville is complete and gitter usernames <> github usernames
39       return findExistingGitterUser(externalId);
41     // TODO: twitter?
42     // TODO: gitlab?
43   }
45   return findExistingIdentityUsername(type, externalId);
48 function resolveGitHubUserEmail(invitingUser, githubUsername) {
49   var githubUserEmailAddressService = new GitHubUserEmailAddressService(invitingUser);
50   return githubUserEmailAddressService.findEmailAddressForGitHubUser(githubUsername);
53 function resolveEmailAddress(invitingUser, type, externalId) {
54   // For now, we only try resolve email addresses for GitHub users
55   if (type === GITHUB_IDENTITY_PROVIDER) {
56     return resolveGitHubUserEmail(invitingUser, externalId);
57   }
59   return null;
62 /**
63  *
64  */
65 function createInvite(roomId, options) {
66   var type = options.type;
67   var externalId = options.externalId;
68   var invitedByUserId = options.invitedByUserId;
69   var emailAddress = options.emailAddress;
71   if (type === 'email') {
72     // Email address is mandatory
73     if (!emailAddress) throw new StatusError(400);
74     externalId = emailAddress;
75   } else {
76     if (!externalId) throw new StatusError(400);
77     // Email address is optional
78   }
80   externalId = externalId.toLowerCase();
81   var secret = uuid();
82   const newInvite = new TroupeInvite({
83     troupeId: roomId,
84     type: type,
85     externalId: externalId,
86     emailAddress: emailAddress,
87     userId: null,
88     secret: secret,
89     invitedByUserId: invitedByUserId,
90     state: 'PENDING'
91   });
93   return userService
94     .findById(invitedByUserId)
95     .then(invitedByUser => {
96       if (!invitedByUser) {
97         throw new StatusError(
98           404,
99           `Invited by user does not exist invitedByUserId=${invitedByUserId}`
100         );
101       }
103       // Hellbanned users are not allowed to send out invites
104       // Just fake it for them and don't save it to the database
105       if (invitedByUser.hellbanned) {
106         return newInvite;
107       }
109       return newInvite.save();
110     })
111     .catch(mongoUtils.mongoErrorWithCode(11000), function() {
112       throw new StatusError(409); // Conflict
113     });
118  */
119 function accept(userId, secret) {
120   assert(secret);
121   return TroupeInvite.findOne({ secret: String(secret) })
122     .lean()
123     .exec()
124     .then(function(invite) {
125       if (!invite) throw new StatusError(404);
126       if (invite.userId) {
127         // Is this user re-using the invite?
129         if (!mongoUtils.objectIDsEqual(invite.userId, userId)) {
130           throw new StatusError(404);
131         }
132       }
134       return invite;
135     });
140  */
141 function markInviteAccepted(inviteId, userId) {
142   return TroupeInvite.update(
143     {
144       _id: inviteId,
145       state: { $ne: 'ACCEPTED' }
146     },
147     {
148       $set: {
149         state: 'ACCEPTED',
150         userId: userId
151       }
152     }
153   ).exec();
158  */
159 function markInviteRejected(inviteId, userId) {
160   return TroupeInvite.update(
161     {
162       _id: inviteId,
163       state: { $ne: 'REJECTED' }
164     },
165     {
166       $set: {
167         state: 'REJECTED',
168         userId: userId
169       }
170     }
171   ).exec();
176  */
177 function markInviteReminded(inviteId) {
178   return TroupeInvite.update(
179     {
180       _id: inviteId
181     },
182     {
183       $set: {
184         reminderSent: new Date()
185       }
186     }
187   ).exec();
190 function findInvitesForReminder(timeHorizonDays) {
191   var cutoffId = mongoUtils.createIdForTimestamp(Date.now() - timeHorizonDays * MS_PER_DAY);
193   return TroupeInvite.aggregate([
194     {
195       $match: {
196         state: 'PENDING',
197         _id: { $lt: cutoffId },
198         reminderSent: null
199       }
200     },
201     {
202       $project: {
203         _id: 0,
204         invite: '$$ROOT'
205       }
206     },
207     {
208       $lookup: {
209         from: 'troupes',
210         localField: 'invite.troupeId',
211         foreignField: '_id',
212         as: 'troupe'
213       }
214     },
215     {
216       $unwind: {
217         path: '$troupe',
218         preserveNullAndEmptyArrays: true
219       }
220     },
221     {
222       $lookup: {
223         from: 'users',
224         localField: 'invite.invitedByUserId',
225         foreignField: '_id',
226         as: 'invitedByUser'
227       }
228     },
229     {
230       $unwind: {
231         path: '$invitedByUser',
232         preserveNullAndEmptyArrays: true
233       }
234     }
235   ]).exec();
239  * For exporting things
240  */
241 function getInvitesCursorByUserId(userId) {
242   const cursor = TroupeInvite.find({
243     userId
244   })
245     .lean()
246     .read(mongoReadPrefs.secondaryPreferred)
247     .batchSize(100)
248     .cursor();
250   return cursor;
254  * For exporting things
255  */
256 function getSentInvitesCursorByUserId(userId) {
257   const cursor = TroupeInvite.find({
258     invitedByUserId: userId
259   })
260     .lean()
261     .read(mongoReadPrefs.secondaryPreferred)
262     .batchSize(100)
263     .cursor();
265   return cursor;
268 module.exports = {
269   findExistingUser: findExistingUser,
270   resolveEmailAddress: resolveEmailAddress,
271   createInvite: Promise.method(createInvite),
272   accept: Promise.method(accept),
273   markInviteAccepted: Promise.method(markInviteAccepted),
274   markInviteRejected: Promise.method(markInviteRejected),
275   findInvitesForReminder: findInvitesForReminder,
276   markInviteReminded: markInviteReminded,
277   getInvitesCursorByUserId,
278   getSentInvitesCursorByUserId