3 var Promise
= require('bluebird');
4 var _
= require('lodash');
5 var env
= require('gitter-web-env');
6 var config
= env
.config
;
7 var promiseUtils
= require('../utils/promise-utils');
8 var mongooseUtils
= require('gitter-web-persistence-utils/lib/mongoose-utils');
9 var troupeService
= require('gitter-web-rooms/lib/troupe-service');
10 var roomMembershipService
= require('gitter-web-rooms/lib/room-membership-service');
11 var userService
= require('gitter-web-users');
12 var userSettingsService
= require('gitter-web-user-settings');
13 var userScopes
= require('gitter-web-identity/lib/user-scopes');
14 var graphSuggestions
= require('gitter-web-suggestions');
15 var cacheWrapper
= require('gitter-web-cache-wrapper');
16 var debug
= require('debug')('gitter:app:suggestions');
17 var logger
= require('gitter-web-env').logger
;
18 var groupRoomSuggestions
= require('gitter-web-groups/lib/group-room-suggestions');
20 // the old github recommenders that find repos, to be filtered to rooms
21 // var ownedRepos = require('./recommendations/owned-repos');
22 var starredRepos
= require('./recommendations/starred-repos');
23 // var watchedRepos = require('./recommendations/watched-repos');
25 var EXPIRES_SECONDS
= config
.get('suggestions:cache-timeout');
27 var NUM_SUGGESTIONS
= 12;
28 var MAX_SUGGESTIONS_PER_ORG
= 2;
29 var HIGHLIGHTED_ROOMS
= [
35 uri
: 'gitter/developers',
39 uri
: 'LaravelRUS/chat',
43 uri
: 'google/material-design-lite',
51 uri
: 'PerfectlySoft/Perfect',
55 uri
: 'twbs/bootstrap',
59 uri
: 'scala-js/scala-js',
67 uri
: 'FreeCodeCamp/FreeCodeCamp',
71 uri
: 'webpack/webpack',
75 uri
: 'angular-ui/ng-grid',
79 uri
: 'dev-ua/frontend-ua',
83 uri
: 'rus-speaking/android',
87 uri
: 'FreeCodeCamp/Espanol',
92 function reposToRooms(repos
) {
93 // Limit to a sane number that's a bit higher than the number we'll use
94 // because we're still going to be filtering out the ones the user is already
96 repos
= repos
.slice(0, 100);
98 var uris
= _
.map(repos
, function(repo
) {
102 return troupeService
.findPublicRoomsByTypeAndLinkPaths('GH_REPO', uris
);
105 // var ownedRepoRooms = Promise.method(function(options) {
106 // var user = options.user;
107 // if (!user || !userScopes.isGitHubUser(user)) {
111 // debug('checking ownedRepoRooms');
113 // return ownedRepos(user)
114 // .then(reposToRooms)
115 // .then(function(rooms) {
116 // if (debug.enabled) {
117 // debug("ownedRepoRooms", _.pluck(rooms, "uri"));
123 var starredRepoRooms
= Promise
.method(function(options
) {
124 var user
= options
.user
;
125 if (!user
|| !userScopes
.isGitHubUser(user
)) {
129 debug('checking starredRepoRooms');
131 return starredRepos(user
)
133 .then(function(rooms
) {
135 debug('starredRepoRooms', _
.pluck(rooms
, 'uri'));
141 // var watchedRepoRooms = Promise.method(function(options) {
142 // var user = options.user;
143 // if (!user || !userScopes.isGitHubUser(user)) {
147 // debug('checking watchedRepoRooms');
149 // return watchedRepos(user)
150 // .then(reposToRooms)
151 // .then(function(rooms) {
152 // if (debug.enabled) {
153 // debug("watchedRepoRooms", _.pluck(rooms, "uri"));
159 var graphRooms
= Promise
.method(function(options
) {
160 var existingRooms
= options
.rooms
;
162 if (!existingRooms
|| !existingRooms
.length
) {
166 debug('checking graphRooms');
168 var language
= options
.language
;
170 // limit how many we send to neo4j
171 var firstTen
= existingRooms
.slice(0, 10);
172 return graphSuggestions
173 .getSuggestionsForRooms(firstTen
, language
)
175 .then(function(roomIds
) {
176 return troupeService
.findByIdsLean(roomIds
);
178 .then(function(suggestedRooms
) {
179 // Make sure there are no more than MAX_SUGGESTIONS_PER_ORG per
180 // organisation coming out of the graph.
181 // (The siblingRooms step might just add them back in anyway which is why
182 // this is not a standard step in filterRooms())
185 _
.remove(suggestedRooms
, function(room
) {
186 if (orgTotals
[room
.groupId
]) {
187 orgTotals
[room
.groupId
] += 1;
189 orgTotals
[room
.groupId
] = 1;
192 return orgTotals
[room
.groupId
] > MAX_SUGGESTIONS_PER_ORG
;
196 debug('graphRooms', _
.pluck(suggestedRooms
, 'uri'));
199 return suggestedRooms
;
201 .catch(function(err
) {
202 logger
.error('Neo4J error: ' + err
, {
209 var siblingRooms
= Promise
.method(function(options
) {
210 var existingRooms
= options
.rooms
;
211 var user
= options
.user
;
213 if (!user
|| !existingRooms
|| !existingRooms
.length
) {
217 var userId
= user
._id
;
219 debug('checking siblingRooms');
221 var groupIds
= _
.pluck(existingRooms
, 'groupId').filter(function(groupId
) {
225 return groupRoomSuggestions
226 .findUnjoinedRoomsInGroups(userId
, _
.uniq(groupIds
))
227 .then(function(results
) {
229 debug('siblingRooms', _
.pluck(results
, 'uri'));
236 function hilightedRooms(options
) {
237 var language
= options
.language
;
239 // shuffle so we don't always present the same ones first
240 var shuffled
= _
.shuffle(HIGHLIGHTED_ROOMS
);
242 var filtered
= _
.filter(shuffled
, function(roomInfo
) {
243 var roomLang
= roomInfo
.localeLanguage
;
244 return roomLang
=== 'en' || roomLang
=== language
;
247 var uris
= _
.map(filtered
, function(highlighted
) {
248 return highlighted
.uri
;
251 return troupeService
.findByUris(uris
).then(function(suggestedRooms
) {
253 debug('hilightedRooms', _
.pluck(suggestedRooms
, 'uri'));
256 return suggestedRooms
;
260 function filterRooms(suggested
, existing
) {
261 // not necessarily sure where suggested comes from, so make sure id is filled
262 // in and a string so we can safely use that as a key in a map.
263 mongooseUtils
.addIdToLeanArray(suggested
);
265 // remove all the nulls/undefineds from things that didn't exist
266 var filtered
= _
.filter(suggested
);
268 // filter out the existing rooms
269 var existingMap
= _
.indexBy(existing
, 'id');
270 filtered
= _
.filter(filtered
, function(room
) {
271 // make very sure we only find public rooms
272 if (room
.security
!== 'PUBLIC') {
275 return existingMap
[room
.id
] === undefined;
278 // filter out duplicates
280 filtered
= _
.filter(filtered
, function(room
) {
281 if (roomMap
[room
.id
]) {
284 roomMap
[room
.id
] = true;
293 // Disabling these for now because they both just tend to find "my-org/*" and
294 // we have other places to suggest those already and you certainly have other
295 // ways of discovering or being told about your orgs' own rooms so I feel
296 // doubtful about the potential network effect here.
306 * options can have the following and they are all optional
309 * - language (defaults to 'en')
310 * The plugins can just skip themselves if options doesn't contain what they need.
312 function findSuggestionsForRooms(options
) {
313 var existingRooms
= options
.rooms
|| [];
314 var language
= options
.language
|| 'en';
316 // copy the defaults back in so all the plugins get the defaults
317 options
.rooms
= existingRooms
;
318 options
.language
= language
;
320 // 1to1 rooms aren't included in the graph anyway, so filter them out first
321 existingRooms
= _
.filter(existingRooms
, function(room
) {
322 return room
.oneToOne
!== true;
325 function filterSuggestions(results
) {
326 var filtered
= filterRooms(results
, existingRooms
);
327 // Add all the rooms we've found so far to the rooms used by subsequent
328 // lookups. This means that a new github user that hasn't joined any rooms
329 // yet but has starred some GitHub rooms can get graph results whereas
330 // otherwise graphRooms would be skipped because it wouldn't have any rooms
331 // to use as input. This should also benefit siblingRooms because it will
332 // find siblings of suggested rooms (if it gets there) which is probably
333 // better than just serving hilighted rooms.
334 options
.rooms
= existingRooms
.concat(filtered
);
338 return promiseUtils
.waterfall(recommenders
, [options
], filterSuggestions
, NUM_SUGGESTIONS
);
342 * Returns rooms for a user
345 function findRoomsByUserId(userId
) {
346 return roomMembershipService
.findRoomIdsForUser(userId
).then(function(roomIds
) {
347 return troupeService
.findByIdsLean(roomIds
, {
357 * Find some suggestions for the current user
359 function findSuggestionsForUserId(userId
) {
361 userService
.findById(userId
),
362 findRoomsByUserId(userId
),
363 userSettingsService
.getUserSettings(userId
, 'lang')
364 ]).spread(function(user
, existingRooms
, language
) {
365 return findSuggestionsForRooms({
367 rooms
: existingRooms
,
374 findSuggestionsForUserId
: cacheWrapper('findSuggestionsForUserId', findSuggestionsForUserId
, {
377 findSuggestionsForRooms
: findSuggestionsForRooms
,
379 HIGHLIGHTED_ROOMS
: HIGHLIGHTED_ROOMS