Merge branch 'hotfix/21.56.9' into master
[gitter.git] / server / serializers / rest / chat-strategy.js
blob93aec17a70dc7f7957f451b048f12dde1d1c39b4
1 'use strict';
3 var env = require('gitter-web-env');
4 var logger = env.logger;
5 var _ = require('lodash');
6 var unreadItemService = require('gitter-web-unread-items');
7 var getVersion = require('gitter-web-serialization/lib/get-model-version');
8 var UserIdStrategy = require('./user-id-strategy');
9 const TroupeIdStrategy = require('./troupe-id-strategy');
10 var mongoUtils = require('gitter-web-persistence-utils/lib/mongo-utils');
11 var Promise = require('bluebird');
12 const {
13   getMockIdFromVirtualUser,
14   transformVirtualUserIntoMockedFromUser
15 } = require('gitter-web-users/lib/virtual-user-service');
16 const generateProxyUrl = require('gitter-web-text-processor/lib/generate-proxy-url');
18 function formatDate(d) {
19   return d ? d.toISOString() : null;
22 function UnreadItemStrategy(options) {
23   var unreadItemsHash;
25   this.preload = function() {
26     return unreadItemService.getUnreadItems(options.userId, options.roomId).then(function(ids) {
27       var hash = {};
29       _.each(ids, function(id) {
30         hash[id] = true;
31       });
33       unreadItemsHash = hash;
34     });
35   };
37   this.map = function(id) {
38     return !!unreadItemsHash[id];
39   };
42 UnreadItemStrategy.prototype = {
43   name: 'UnreadItemStrategy'
46 /**
47  * Serializes chat into JSON
48  * - if there is no `currentUserId`, all the messages are going to have default {unread: false}
49  */
50 function ChatStrategy({
51   lookups,
52   lean,
53   user,
54   currentUserId,
55   serializeFromUserId = true,
56   serializeToTroupeId = false,
57   troupeId,
58   initialId
59 } = {}) {
60   // useLookups will be set to true if there are any lookups that this strategy
61   // understands. Currently it only knows about user lookups.
62   var useLookups = false;
63   var userLookups;
64   if (lookups && lookups.indexOf('user') !== -1) {
65     useLookups = true;
66     userLookups = {};
67   }
69   if (useLookups) {
70     if (lean) {
71       // we're breaking users out, but then not returning their displayNames
72       // which kinda defeats the purpose
73       logger.warn('ChatStrategy was called with lookups, but also with lean', { lookups, lean });
74     }
75   }
77   var userStrategy, troupeStrategy, unreadItemStrategy;
79   this.preload = function(items) {
80     if (items.isEmpty()) return;
82     var strategies = [];
84     // If the user is fixed in options, we don't need to look them up using a strategy...
85     if (!user && serializeFromUserId) {
86       userStrategy = new UserIdStrategy({ lean });
88       var users = items.map(function(i) {
89         return i.fromUserId;
90       });
91       strategies.push(userStrategy.preload(users));
92     }
94     if (serializeToTroupeId) {
95       troupeStrategy = new TroupeIdStrategy({ lean });
97       const troupeIds = items.map(function(i) {
98         return i.toTroupeId;
99       });
100       strategies.push(troupeStrategy.preload(troupeIds));
101     }
103     if (currentUserId) {
104       unreadItemStrategy = new UnreadItemStrategy({
105         userId: currentUserId,
106         roomId: troupeId
107       });
108       strategies.push(unreadItemStrategy.preload());
109     }
111     return Promise.all(strategies);
112   };
114   function safeArray(array) {
115     if (!array) return [];
116     return array;
117   }
119   function undefinedForEmptyArray(array) {
120     if (!array) return undefined;
121     if (!array.length) return undefined;
122     return array;
123   }
125   function mapUser(userId) {
126     if (!serializeFromUserId) {
127       return userId;
128     } else if (userLookups) {
129       if (!userLookups[userId]) {
130         userLookups[userId] = userStrategy.map(userId);
131       }
133       return userId;
134     } else {
135       return userStrategy.map(userId);
136     }
137   }
139   function mapVirtualUser(virtualUser) {
140     if (!serializeFromUserId) {
141       return undefined;
142     } else if (userLookups) {
143       const mockUserId = getMockIdFromVirtualUser(virtualUser);
144       if (!userLookups[mockUserId]) {
145         userLookups[mockUserId] = transformVirtualUserIntoMockedFromUser(virtualUser);
146       }
148       return mockUserId;
149     } else {
150       return transformVirtualUserIntoMockedFromUser(virtualUser);
151     }
152   }
154   this.map = function(item) {
155     // If there is no unread strategy(meaning currentUserId was undefined), don't define how it's unread/read.
156     //
157     // We don't want to default to true/false because even when someone is signed in,
158     // some places of code don't define `currentUserId`. We don't want to accidentally
159     // override the actual value. See `live-collection-chats.js` as an example
160     var unread = unreadItemStrategy ? unreadItemStrategy.map(item._id) : undefined;
162     var castArray = lean ? undefinedForEmptyArray : safeArray;
164     var initial;
165     if (initialId) {
166       initial = mongoUtils.objectIDsEqual(item._id, initialId);
167     }
169     const serializedData = {
170       id: item._id,
171       text: item.text,
172       status: item.status,
173       html: item.html,
174       sent: formatDate(item.sent),
175       editedAt: item.editedAt ? formatDate(item.editedAt) : undefined,
176       parentId: item.parentId,
177       threadMessageCount: item.threadMessageCount,
178       unread: unread,
179       readBy: item.readBy ? item.readBy.length : undefined,
180       urls: castArray(item.urls),
181       initial: initial || undefined,
182       mentions: castArray(
183         item.mentions &&
184           _.map(item.mentions, function(m) {
185             return {
186               screenName: m.screenName,
187               userId: m.userId,
188               userIds: m.userIds, // For groups
189               group: m.group || undefined,
190               announcement: m.announcement || undefined
191             };
192           })
193       ),
194       issues: castArray(item.issues),
195       meta: castArray(item.meta),
196       highlights: item.highlights,
197       v: getVersion(item)
198     };
200     if (troupeStrategy) {
201       serializedData.toTroupe = troupeStrategy.map(item.toTroupeId);
202     }
204     if (item.virtualUser) {
205       serializedData.virtualUser = {
206         type: item.virtualUser.type,
207         externalId: item.virtualUser.externalId,
208         displayName: item.virtualUser.displayName,
209         avatarUrl: item.virtualUser.avatarUrl
210           ? generateProxyUrl(item.virtualUser.avatarUrl)
211           : undefined
212       };
214       // Mock the fromUser at a basic level for legacy apps (Android and iOS)
215       serializedData.fromUser = mapVirtualUser(item.virtualUser);
216     } else {
217       serializedData.fromUser = user ? user : mapUser(item.fromUserId);
218     }
220     return serializedData;
221   };
223   this.postProcess = function(serialized) {
224     if (useLookups) {
225       return {
226         items: serialized.toArray(),
227         lookups: {
228           users: userLookups
229         }
230       };
231     } else {
232       return serialized.toArray();
233     }
234   };
237 ChatStrategy.prototype = {
238   name: 'ChatStrategy'
241 module.exports = ChatStrategy;