'use strict'; var async = require('async'); var _ = require('lodash'); var user = require('../user'); var utils = require('../utils'); var plugins = require('../plugins'); var notifications = require('../notifications'); var db = require('../database'); module.exports = function (Groups) { Groups.requestMembership = function (groupName, uid, callback) { async.waterfall([ async.apply(inviteOrRequestMembership, groupName, uid, 'request'), function (next) { user.getUserField(uid, 'username', next); }, function (username, next) { async.parallel({ notification: function (next) { notifications.create({ bodyShort: '[[groups:request.notification_title, ' + username + ']]', bodyLong: '[[groups:request.notification_text, ' + username + ', ' + groupName + ']]', nid: 'group:' + groupName + ':uid:' + uid + ':request', path: '/groups/' + utils.slugify(groupName), from: uid, }, next); }, owners: function (next) { Groups.getOwners(groupName, next); }, }, next); }, function (results, next) { if (!results.notification || !results.owners.length) { return next(); } notifications.push(results.notification, results.owners, next); }, ], callback); }; Groups.acceptMembership = function (groupName, uid, callback) { async.waterfall([ async.apply(db.setsRemove, ['group:' + groupName + ':pending', 'group:' + groupName + ':invited'], uid), async.apply(Groups.join, groupName, uid), ], callback); }; Groups.rejectMembership = function (groupNames, uid, callback) { if (!Array.isArray(groupNames)) { groupNames = [groupNames]; } var sets = []; groupNames.forEach(function (groupName) { sets.push('group:' + groupName + ':pending', 'group:' + groupName + ':invited'); }); db.setsRemove(sets, uid, callback); }; Groups.invite = function (groupName, uid, callback) { async.waterfall([ async.apply(inviteOrRequestMembership, groupName, uid, 'invite'), async.apply(notifications.create, { type: 'group-invite', bodyShort: '[[groups:invited.notification_title, ' + groupName + ']]', bodyLong: '', nid: 'group:' + groupName + ':uid:' + uid + ':invite', path: '/groups/' + utils.slugify(groupName), }), function (notification, next) { notifications.push(notification, [uid], next); }, ], callback); }; function inviteOrRequestMembership(groupName, uid, type, callback) { if (!parseInt(uid, 10)) { return callback(new Error('[[error:not-logged-in]]')); } var hookName = type === 'invite' ? 'action:group.inviteMember' : 'action:group.requestMembership'; var set = type === 'invite' ? 'group:' + groupName + ':invited' : 'group:' + groupName + ':pending'; async.waterfall([ function (next) { async.parallel({ exists: async.apply(Groups.exists, groupName), isMember: async.apply(Groups.isMember, uid, groupName), isPending: async.apply(Groups.isPending, uid, groupName), isInvited: async.apply(Groups.isInvited, uid, groupName), }, next); }, function (checks, next) { if (!checks.exists) { return next(new Error('[[error:no-group]]')); } else if (checks.isMember) { return callback(); } else if (type === 'invite' && checks.isInvited) { return callback(); } else if (type === 'request' && checks.isPending) { return next(new Error('[[error:group-already-requested]]')); } db.setAdd(set, uid, next); }, function (next) { plugins.fireHook(hookName, { groupName: groupName, uid: uid, }); next(); }, ], callback); } Groups.getMembers = function (groupName, start, stop, callback) { db.getSortedSetRevRange('group:' + groupName + ':members', start, stop, callback); }; Groups.getMemberUsers = function (groupNames, start, stop, callback) { async.map(groupNames, function (groupName, next) { async.waterfall([ function (next) { Groups.getMembers(groupName, start, stop, next); }, function (uids, next) { user.getUsersFields(uids, ['uid', 'username', 'picture', 'userslug'], next); }, ], next); }, callback); }; Groups.getMembersOfGroups = function (groupNames, callback) { db.getSortedSetsMembers(groupNames.map(function (name) { return 'group:' + name + ':members'; }), callback); }; Groups.isMember = function (uid, groupName, callback) { if (!uid || parseInt(uid, 10) <= 0 || !groupName) { return setImmediate(callback, null, false); } var cacheKey = uid + ':' + groupName; var isMember = Groups.cache.get(cacheKey); if (isMember !== undefined) { Groups.cache.hits += 1; return setImmediate(callback, null, isMember); } Groups.cache.misses += 1; async.waterfall([ function (next) { db.isSortedSetMember('group:' + groupName + ':members', uid, next); }, function (isMember, next) { Groups.cache.set(cacheKey, isMember); next(null, isMember); }, ], callback); }; Groups.isMembers = function (uids, groupName, callback) { var cachedData = {}; function getFromCache() { setImmediate(callback, null, uids.map(function (uid) { return cachedData[uid + ':' + groupName]; })); } if (!groupName || !uids.length) { return callback(null, uids.map(function () { return false; })); } var nonCachedUids = uids.filter(function (uid) { return filterNonCached(cachedData, uid, groupName); }); if (!nonCachedUids.length) { return getFromCache(callback); } async.waterfall([ function (next) { db.isSortedSetMembers('group:' + groupName + ':members', nonCachedUids, next); }, function (isMembers, next) { nonCachedUids.forEach(function (uid, index) { cachedData[uid + ':' + groupName] = isMembers[index]; Groups.cache.set(uid + ':' + groupName, isMembers[index]); }); getFromCache(next); }, ], callback); }; function filterNonCached(cachedData, uid, groupName) { var isMember = Groups.cache.get(uid + ':' + groupName); var isInCache = isMember !== undefined; if (isInCache) { Groups.cache.hits += 1; cachedData[uid + ':' + groupName] = isMember; } else { Groups.cache.misses += 1; } return !isInCache; } Groups.isMemberOfGroups = function (uid, groups, callback) { var cachedData = {}; function getFromCache(next) { setImmediate(next, null, groups.map(function (groupName) { return cachedData[uid + ':' + groupName]; })); } if (!uid || parseInt(uid, 10) <= 0 || !groups.length) { return callback(null, groups.map(function () { return false; })); } var nonCachedGroups = groups.filter(function (groupName) { return filterNonCached(cachedData, uid, groupName); }); if (!nonCachedGroups.length) { return getFromCache(callback); } var nonCachedGroupsMemberSets = nonCachedGroups.map(function (groupName) { return 'group:' + groupName + ':members'; }); async.waterfall([ function (next) { db.isMemberOfSortedSets(nonCachedGroupsMemberSets, uid, next); }, function (isMembers, next) { nonCachedGroups.forEach(function (groupName, index) { cachedData[uid + ':' + groupName] = isMembers[index]; Groups.cache.set(uid + ':' + groupName, isMembers[index]); }); getFromCache(next); }, ], callback); }; Groups.getMemberCount = function (groupName, callback) { async.waterfall([ function (next) { db.getObjectField('group:' + groupName, 'memberCount', next); }, function (count, next) { next(null, parseInt(count, 10)); }, ], callback); }; Groups.isMemberOfGroupList = function (uid, groupListKey, callback) { async.waterfall([ function (next) { db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, next); }, function (groupNames, next) { groupNames = Groups.removeEphemeralGroups(groupNames); if (groupNames.length === 0) { return callback(null, false); } Groups.isMemberOfGroups(uid, groupNames, next); }, function (isMembers, next) { next(null, isMembers.indexOf(true) !== -1); }, ], callback); }; Groups.isMemberOfGroupsList = function (uid, groupListKeys, callback) { var sets = groupListKeys.map(function (groupName) { return 'group:' + groupName + ':members'; }); var uniqueGroups; var members; async.waterfall([ function (next) { db.getSortedSetsMembers(sets, next); }, function (_members, next) { members = _members; uniqueGroups = _.uniq(_.flatten(members)); uniqueGroups = Groups.removeEphemeralGroups(uniqueGroups); Groups.isMemberOfGroups(uid, uniqueGroups, next); }, function (isMembers, next) { var map = {}; uniqueGroups.forEach(function (groupName, index) { map[groupName] = isMembers[index]; }); var result = members.map(function (groupNames) { for (var i = 0; i < groupNames.length; i += 1) { if (map[groupNames[i]]) { return true; } } return false; }); next(null, result); }, ], callback); }; Groups.isMembersOfGroupList = function (uids, groupListKey, callback) { var groupNames; var results = []; uids.forEach(function () { results.push(false); }); async.waterfall([ function (next) { db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, next); }, function (_groupNames, next) { groupNames = Groups.removeEphemeralGroups(_groupNames); if (groupNames.length === 0) { return callback(null, results); } async.map(groupNames, function (groupName, next) { Groups.isMembers(uids, groupName, next); }, next); }, function (isGroupMembers, next) { isGroupMembers.forEach(function (isMembers) { results.forEach(function (isMember, index) { if (!isMember && isMembers[index]) { results[index] = true; } }); }); next(null, results); }, ], callback); }; Groups.isInvited = function (uid, groupName, callback) { if (!uid) { return setImmediate(callback, null, false); } db.isSetMember('group:' + groupName + ':invited', uid, callback); }; Groups.isPending = function (uid, groupName, callback) { if (!uid) { return setImmediate(callback, null, false); } db.isSetMember('group:' + groupName + ':pending', uid, callback); }; Groups.getPending = function (groupName, callback) { if (!groupName) { return setImmediate(callback, null, []); } db.getSetMembers('group:' + groupName + ':pending', callback); }; Groups.kick = function (uid, groupName, isOwner, callback) { if (isOwner) { // If the owners set only contains one member, error out! async.waterfall([ function (next) { db.setCount('group:' + groupName + ':owners', next); }, function (numOwners, next) { if (numOwners <= 1) { return next(new Error('[[error:group-needs-owner]]')); } Groups.leave(groupName, uid, next); }, ], callback); } else { Groups.leave(groupName, uid, callback); } }; };