Gitter migration: Setup redirects (rollout pt. 3)
[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);
46 /**
47 * Given the currentUser and a sequence of troupes
48 * returns the 'other' userId for all one to one rooms
50 function oneToOneOtherUserSequence(currentUserId, troupes) {
51 return troupes
52 .filter(function(troupe) {
53 return troupe.oneToOne;
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;
64 });
67 /** Best guess efforts */
68 function guessLegacyGitHubType(item) {
69 if (item.githubType) {
70 return item.githubType;
73 if (item.oneToOne) {
74 return 'ONETOONE';
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';
88 /* break */
90 case 'GH_ORG':
91 if (item.uri === linkPath) {
92 return 'REPO';
93 } else {
94 return 'REPO_CHANNEL';
96 /* break */
98 case 'GH_USER':
99 return 'USER_CHANNEL';
102 return 'REPO_CHANNEL';
105 /** Best guess efforts */
106 function guessLegacySecurity(item) {
107 if (item.security) {
108 return item.security;
111 // One-to-one rooms in legacy had security=null
112 if (item.oneToOne) {
113 return undefined;
116 if (item.sd.public) {
117 return 'PUBLIC';
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';
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;
158 var strategies = [];
160 // Pro-org
161 if (options.includePremium !== false) {
162 proOrgStrategy = new ProOrgStrategy(options);
163 strategies.push(proOrgStrategy.preload(items));
166 // Room Membership
167 if (currentUserId || options.isRoomMember !== undefined) {
168 roomMembershipStrategy = new RoomMembershipStrategy(options);
169 strategies.push(roomMembershipStrategy.preload(troupeIds));
172 // Unread items
173 if (currentUserId && !options.skipUnreadCounts) {
174 unreadItemStrategy = new AllUnreadItemCountStrategy(options);
175 strategies.push(unreadItemStrategy.preload(troupeIds));
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));
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());
199 // Permissions
200 if ((currentUserId || options.currentUser) && options.includePermissions) {
201 permissionsStrategy = new TroupePermissionsStrategy(options);
202 strategies.push(permissionsStrategy.preload(items));
205 // Include the tags
206 if (options.includeTags) {
207 tagsStrategy = new TagsStrategy(options);
208 strategies.push(tagsStrategy.preload(items));
211 groupIdStrategy = new GroupIdStrategy(options);
212 var groupIds = items
213 .map(function(troupe) {
214 return troupe.groupId;
216 .filter(function(f) {
217 return !!f;
220 strategies.push(groupIdStrategy.preload(groupIds));
222 if (options.includeBackend) {
223 securityDescriptorStrategy = SecurityDescriptorStrategy.slim();
224 // Backend strategy needs no mapping stage
227 if (options.includeAssociatedRepo) {
228 associatedRepoStrategy = new AssociatedRepoStrategy();
229 strategies.push(associatedRepoStrategy.preload(items));
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);
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;
255 function resolveOneToOneOtherUser(item) {
256 if (!currentUserId) {
257 debug(
258 'TroupeStrategy initiated without currentUserId, but generating oneToOne troupes. This can be a problem!'
260 return null;
263 var otherUser = mapOtherUser(item.oneToOneUsers);
265 if (!otherUser) {
266 debug('Troupe %s appears to contain bad users', item._id);
267 return null;
270 return otherUser;
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;
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;
297 troupeUrl = '/' + uri;
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);
316 var isPublic;
317 if (item.oneToOne) {
318 // Double-check here
319 isPublic = false;
320 } else {
321 isPublic = item.sd.public;
324 var avatarUrl = getAvatarUrlForTroupe(item, {
325 name: troupeName,
326 group: group,
327 user: otherUser
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);
343 matrixRoomLink = `https://matrix.to/#/${roomAliasOrId}?utm_source=gitter`;
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)
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
405 module.exports = TroupeStrategy;
406 module.exports.testOnly = {
407 oneToOneOtherUserSequence: oneToOneOtherUserSequence