Merge branch 'hotfix/21.56.9' into master
[gitter.git] / server / serializers / rest / troupe-strategy.js
blobf6bc045f231246ad17b1a39efce34982bfa5375f
1 /* eslint complexity: ["error", 24] */
2 'use strict';
4 var Promise = require('bluebird');
5 var debug = require('debug')('gitter:infra:serializer:troupe');
6 var getVersion = require('gitter-web-serialization/lib/get-model-version');
7 var UserIdStrategy = require('./user-id-strategy');
8 var mongoUtils = require('gitter-web-persistence-utils/lib/mongo-utils');
9 var avatars = require('gitter-web-avatars');
10 var getRoomNameFromTroupeName = require('gitter-web-shared/get-room-name-from-troupe-name');
11 var AllUnreadItemCountStrategy = require('./troupes/all-unread-item-count-strategy');
12 var FavouriteTroupesForUserStrategy = require('./troupes/favourite-troupes-for-user-strategy');
13 var LastTroupeAccessTimesForUserStrategy = require('./troupes/last-access-times-for-user-strategy');
14 var LurkAndActivityForUserStrategy = require('./troupes/lurk-and-activity-for-user-strategy');
15 var ProOrgStrategy = require('./troupes/pro-org-strategy');
16 var RoomMembershipStrategy = require('./troupes/room-membership-strategy');
17 var TagsStrategy = require('./troupes/tags-strategy');
18 var TroupePermissionsStrategy = require('./troupes/troupe-permissions-strategy');
19 var GroupIdStrategy = require('./group-id-strategy');
20 var SecurityDescriptorStrategy = require('./security-descriptor-strategy');
21 var AssociatedRepoStrategy = require('./troupes/associated-repo-strategy');
22 const MatrixBridgedRoomStrategy = require('./troupes/matrix-bridged-room-strategy');
23 const {
24   getCanonicalAliasForGitterRoomUri
25 } = require('gitter-web-matrix-bridge/lib/matrix-alias-utils');
28   room-based-feature-toggle
29   We used to use this strategy for the threadedConversations feature toggle but don't need it now that is generally available.
30   If you're introducing another room based feature toggle, uncomment all of these pieces of code (search for room-based-feature-toggle)
32 // const TroupeMetaIdStrategy = require('./troupes/troupe-meta-id-strategy');
34 function getAvatarUrlForTroupe(serializedTroupe, options) {
35   if (serializedTroupe.oneToOne && options && options.user) {
36     return avatars.getForUser(options.user);
37   } else if (serializedTroupe.oneToOne && (!options || !options.user)) {
38     return avatars.getForRoomUri(options.name);
39   } else if (options && options.group) {
40     return options.group.avatarUrl || avatars.getForGroup(options.group);
41   } else {
42     return avatars.getForRoomUri(serializedTroupe.uri);
43   }
46 /**
47  * Given the currentUser and a sequence of troupes
48  * returns the 'other' userId for all one to one rooms
49  */
50 function oneToOneOtherUserSequence(currentUserId, troupes) {
51   return troupes
52     .filter(function(troupe) {
53       return troupe.oneToOne;
54     })
55     .map(function(troupe) {
56       var a = troupe.oneToOneUsers[0] && troupe.oneToOneUsers[0].userId;
57       var b = troupe.oneToOneUsers[1] && troupe.oneToOneUsers[1].userId;
59       if (mongoUtils.objectIDsEqual(currentUserId, a)) {
60         return b;
61       } else {
62         return a;
63       }
64     });
67 /** Best guess efforts */
68 function guessLegacyGitHubType(item) {
69   if (item.githubType) {
70     return item.githubType;
71   }
73   if (item.oneToOne) {
74     return 'ONETOONE';
75   }
77   if (!item.sd) return 'REPO_CHANNEL'; // Could we do better?
79   var linkPath = item.sd.linkPath;
81   switch (item.sd.type) {
82     case 'GH_REPO':
83       if (item.uri === linkPath) {
84         return 'REPO';
85       } else {
86         return 'REPO_CHANNEL';
87       }
88     /* break */
90     case 'GH_ORG':
91       if (item.uri === linkPath) {
92         return 'REPO';
93       } else {
94         return 'REPO_CHANNEL';
95       }
96     /* break */
98     case 'GH_USER':
99       return 'USER_CHANNEL';
100   }
102   return 'REPO_CHANNEL';
105 /** Best guess efforts */
106 function guessLegacySecurity(item) {
107   if (item.security) {
108     return item.security;
109   }
111   // One-to-one rooms in legacy had security=null
112   if (item.oneToOne) {
113     return undefined;
114   }
116   if (item.sd.public) {
117     return 'PUBLIC';
118   }
120   var type = item.sd.type;
121   if (type === 'GH_REPO' || type === 'GH_ORG') {
122     if (item.sd.linkPath && item.sd.linkPath !== item.uri) {
123       return 'INHERITED';
124     }
125   }
127   return 'PRIVATE';
130 function TroupeStrategy(options) {
131   if (!options) options = {};
133   var currentUserId = mongoUtils.asObjectID(options.currentUserId);
135   var unreadItemStrategy;
136   var lastAccessTimeStrategy;
137   var favouriteStrategy;
138   var lurkActivityStrategy;
139   var tagsStrategy;
140   var userIdStrategy;
141   var proOrgStrategy;
142   var permissionsStrategy;
143   var roomMembershipStrategy;
144   var groupIdStrategy;
145   var securityDescriptorStrategy;
146   var associatedRepoStrategy;
147   let matrixBridgedRoomStrategy;
149   // eslint-disable-next-line max-statements
150   this.preload = function(items) {
151     // eslint-disable-line max-statements
152     if (items.isEmpty()) return;
154     var troupeIds = items.map(function(troupe) {
155       return troupe._id;
156     });
158     var strategies = [];
160     // Pro-org
161     if (options.includePremium !== false) {
162       proOrgStrategy = new ProOrgStrategy(options);
163       strategies.push(proOrgStrategy.preload(items));
164     }
166     // Room Membership
167     if (currentUserId || options.isRoomMember !== undefined) {
168       roomMembershipStrategy = new RoomMembershipStrategy(options);
169       strategies.push(roomMembershipStrategy.preload(troupeIds));
170     }
172     // Unread items
173     if (currentUserId && !options.skipUnreadCounts) {
174       unreadItemStrategy = new AllUnreadItemCountStrategy(options);
175       strategies.push(unreadItemStrategy.preload(troupeIds));
176     }
178     if (currentUserId) {
179       // The other user in one-to-one rooms
180       var otherUserIds = oneToOneOtherUserSequence(currentUserId, items);
181       if (!otherUserIds.isEmpty()) {
182         userIdStrategy = new UserIdStrategy(options);
183         strategies.push(userIdStrategy.preload(otherUserIds));
184       }
186       // Favourites for user
187       favouriteStrategy = new FavouriteTroupesForUserStrategy(options);
188       strategies.push(favouriteStrategy.preload());
190       // Last Access Time
191       lastAccessTimeStrategy = new LastTroupeAccessTimesForUserStrategy(options);
192       strategies.push(lastAccessTimeStrategy.preload());
194       // Lurk Activity
195       lurkActivityStrategy = new LurkAndActivityForUserStrategy(options);
196       strategies.push(lurkActivityStrategy.preload());
197     }
199     // Permissions
200     if ((currentUserId || options.currentUser) && options.includePermissions) {
201       permissionsStrategy = new TroupePermissionsStrategy(options);
202       strategies.push(permissionsStrategy.preload(items));
203     }
205     // Include the tags
206     if (options.includeTags) {
207       tagsStrategy = new TagsStrategy(options);
208       strategies.push(tagsStrategy.preload(items));
209     }
211     groupIdStrategy = new GroupIdStrategy(options);
212     var groupIds = items
213       .map(function(troupe) {
214         return troupe.groupId;
215       })
216       .filter(function(f) {
217         return !!f;
218       });
220     strategies.push(groupIdStrategy.preload(groupIds));
222     if (options.includeBackend) {
223       securityDescriptorStrategy = SecurityDescriptorStrategy.slim();
224       // Backend strategy needs no mapping stage
225     }
227     if (options.includeAssociatedRepo) {
228       associatedRepoStrategy = new AssociatedRepoStrategy();
229       strategies.push(associatedRepoStrategy.preload(items));
230     }
232     /* room-based-feature-toggle */
233     // troupeMetaIdStrategy = new TroupeMetaIdStrategy();
234     // strategies.push(troupeMetaIdStrategy.preload(troupeIds));
236     matrixBridgedRoomStrategy = new MatrixBridgedRoomStrategy();
237     strategies.push(matrixBridgedRoomStrategy.preload(troupeIds));
239     return Promise.all(strategies);
240   };
242   function mapOtherUser(users) {
243     var otherUser = users.filter(function(troupeUser) {
244       return '' + troupeUser.userId !== '' + currentUserId;
245     })[0];
247     if (otherUser) {
248       var user = userIdStrategy.map(otherUser.userId);
249       if (user) {
250         return user;
251       }
252     }
253   }
255   function resolveOneToOneOtherUser(item) {
256     if (!currentUserId) {
257       debug(
258         'TroupeStrategy initiated without currentUserId, but generating oneToOne troupes. This can be a problem!'
259       );
260       return null;
261     }
263     var otherUser = mapOtherUser(item.oneToOneUsers);
265     if (!otherUser) {
266       debug('Troupe %s appears to contain bad users', item._id);
267       return null;
268     }
270     return otherUser;
271   }
273   // eslint-disable-next-line complexity, max-statements
274   this.map = function(item) {
275     var id = item.id || item._id;
276     var uri = item.uri;
278     var isPro = proOrgStrategy ? proOrgStrategy.map(item) : undefined;
279     var group = groupIdStrategy && item.groupId ? groupIdStrategy.map(item.groupId) : undefined;
281     var troupeName, troupeUrl;
282     if (item.oneToOne) {
283       var otherUser = resolveOneToOneOtherUser(item);
284       if (otherUser) {
285         troupeName = otherUser.displayName;
286         troupeUrl = '/' + otherUser.username;
287       } else {
288         return null;
289       }
290     } else {
291       var roomName = getRoomNameFromTroupeName(uri);
292       troupeName = group ? group.name + '/' + getRoomNameFromTroupeName(uri) : uri;
293       if (roomName === uri) {
294         troupeName = group ? group.name : uri;
295       }
297       troupeUrl = '/' + uri;
298     }
300     var unreadCounts = unreadItemStrategy && unreadItemStrategy.map(id);
302     // mongoose is upgrading old undefineds to [] on load and we don't want to
303     // send through that no providers are allowed in that case
304     const providers = item.providers && item.providers.length ? item.providers : undefined;
306     var isLurking;
307     var hasActivity;
308     if (lurkActivityStrategy) {
309       isLurking = lurkActivityStrategy.mapLurkStatus(id);
310       if (isLurking) {
311         // Can only have activity if you're lurking
312         hasActivity = lurkActivityStrategy.mapActivity(id);
313       }
314     }
316     var isPublic;
317     if (item.oneToOne) {
318       // Double-check here
319       isPublic = false;
320     } else {
321       isPublic = item.sd.public;
322     }
324     var avatarUrl = getAvatarUrlForTroupe(item, {
325       name: troupeName,
326       group: group,
327       user: otherUser
328     });
330     // Let's only have a Matrix room link for public rooms otherwise people will
331     // be confused when they can't see their own private or one to one rooms on
332     // Matrix even they are still bridged behind the scenes
333     let matrixRoomLink;
334     if (matrixBridgedRoomStrategy && isPublic) {
335       const matrixRoomId = matrixBridgedRoomStrategy.map(id);
336       if (matrixRoomId) {
337         // ONE_TO_ONE rooms don't have a `uri` so lets default to the matrixRoomId instead
338         let roomAliasOrId = matrixRoomId;
339         if (item.uri) {
340           roomAliasOrId = getCanonicalAliasForGitterRoomUri(item.uri);
341         }
343         matrixRoomLink = `https://matrix.to/#/${roomAliasOrId}?utm_source=gitter`;
344       }
345     }
347     return {
348       id: id,
349       name: troupeName,
350       topic: item.topic,
351       // This is a fallback for the change to the suggestions API
352       // It can be removed once the mobile clients are using topic instead
353       // of description. See https://github.com/troupe/gitter-webapp/issues/2115
354       description: options.includeDescription ? item.topic : undefined,
355       avatarUrl: avatarUrl,
356       uri: uri,
357       oneToOne: item.oneToOne,
358       userCount: item.userCount,
359       user: otherUser,
360       unreadItems: unreadCounts ? unreadCounts.unreadItems : undefined,
361       mentions: unreadCounts ? unreadCounts.mentions : undefined,
362       lastAccessTime: lastAccessTimeStrategy ? lastAccessTimeStrategy.map(id).time : undefined,
363       favourite: favouriteStrategy ? favouriteStrategy.map(id) : undefined,
364       lurk: isLurking,
365       activity: hasActivity,
366       url: troupeUrl,
367       githubType: guessLegacyGitHubType(item),
368       associatedRepo: associatedRepoStrategy ? associatedRepoStrategy.map(item) : undefined,
369       security: guessLegacySecurity(item),
370       premium: isPro,
371       noindex: item.noindex, // TODO: this should not always be here
372       tags: tagsStrategy ? tagsStrategy.map(item) : undefined,
373       providers: providers,
374       permissions: permissionsStrategy ? permissionsStrategy.map(item) : undefined,
375       roomMember: roomMembershipStrategy ? roomMembershipStrategy.map(id) : undefined,
376       groupId: item.groupId,
377       group: options.includeGroups ? group : undefined,
378       backend: securityDescriptorStrategy ? securityDescriptorStrategy.map(item.sd) : undefined,
379       public: isPublic,
380       exists: options.includeExists ? !!id : undefined,
381       matrixRoomLink,
382       /* room-based-feature-toggle */
383       // meta: troupeMetaIdStrategy.map(id) || {},
384       v: getVersion(item)
385     };
386   };
389 TroupeStrategy.prototype = {
390   name: 'TroupeStrategy'
393 TroupeStrategy.createSuggestionStrategy = function() {
394   return new TroupeStrategy({
395     includePremium: false,
396     includeTags: true,
397     includeExists: true,
398     // TODO: remove this option in future
399     includeDescription: true,
400     currentUser: null,
401     currentUserId: null
402   });
405 module.exports = TroupeStrategy;
406 module.exports.testOnly = {
407   oneToOneOtherUserSequence: oneToOneOtherUserSequence