From aca5d24a7d83916fcf4ebf22560a7e05c34a30c2 Mon Sep 17 00:00:00 2001
From: Julian Lam <julian@designcreateplay.com>
Date: Tue, 26 May 2015 14:45:17 -0400
Subject: [PATCH] split groups.js into more subsidiary files

---
 src/groups.js            | 482 +--------------------------------------
 src/groups/membership.js | 317 +++++++++++++++++++++++++
 src/groups/ownership.js  |  31 +++
 src/groups/search.js     |  86 +++++++
 src/groups/update.js     |  73 +++++-
 5 files changed, 510 insertions(+), 479 deletions(-)
 create mode 100644 src/groups/membership.js
 create mode 100644 src/groups/ownership.js
 create mode 100644 src/groups/search.js

diff --git a/src/groups.js b/src/groups.js
index 0c667c9871..17d39c413b 100644
--- a/src/groups.js
+++ b/src/groups.js
@@ -2,8 +2,6 @@
 
 var async = require('async'),
 	winston = require('winston'),
-	_ = require('underscore'),
-	crypto = require('crypto'),
 	path = require('path'),
 	nconf = require('nconf'),
 	fs = require('fs'),
@@ -16,15 +14,16 @@ var async = require('async'),
 	posts = require('./posts'),
 	privileges = require('./privileges'),
 	utils = require('../public/src/utils'),
-	util = require('util'),
-
-	uploadsController = require('./controllers/uploads');
+	util = require('util');
 
 (function(Groups) {
 
 	require('./groups/create')(Groups);
 	require('./groups/delete')(Groups);
 	require('./groups/update')(Groups);
+	require('./groups/membership')(Groups);
+	require('./groups/ownership')(Groups);
+	require('./groups/search')(Groups);
 
 	var ephemeralGroups = ['guests'],
 
@@ -70,6 +69,8 @@ var async = require('async'),
 			}
 		};
 
+	Groups.internals = internals;
+
 	var isPrivilegeGroupRegex = /^cid:\d+:privileges:[\w:]+$/;
 	Groups.isPrivilegeGroup = function(groupName) {
 		return isPrivilegeGroupRegex.test(groupName);
@@ -330,144 +331,6 @@ var async = require('async'),
 		});
 	};
 
-	Groups.getMembers = function(groupName, start, stop, callback) {
-		db.getSortedSetRevRange('group:' + groupName + ':members', start, stop, 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) {
-			return callback(null, false);
-		}
-		db.isSortedSetMember('group:' + groupName + ':members', uid, callback);
-	};
-
-	Groups.isMembers = function(uids, groupName, callback) {
-		db.isSortedSetMembers('group:' + groupName + ':members', uids, callback);
-	};
-
-	Groups.isMemberOfGroups = function(uid, groups, callback) {
-		if (!uid || parseInt(uid, 10) <= 0) {
-			return callback(null, groups.map(function() {return false;}));
-		}
-		groups = groups.map(function(groupName) {
-			return 'group:' + groupName + ':members';
-		});
-
-		db.isMemberOfSortedSets(groups, uid, callback);
-	};
-
-	Groups.getMemberCount = function(groupName, callback) {
-		db.getObjectField('group:' + groupName, 'memberCount', function(err, count) {
-			if (err) {
-				return callback(err);
-			}
-			callback(null, parseInt(count, 10));
-		});
-	};
-
-	Groups.isMemberOfGroupList = function(uid, groupListKey, callback) {
-		db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) {
-			if (err) {
-				return callback(err);
-			}
-			groupNames = internals.removeEphemeralGroups(groupNames);
-			if (groupNames.length === 0) {
-				return callback(null, false);
-			}
-
-			Groups.isMemberOfGroups(uid, groupNames, function(err, isMembers) {
-				if (err) {
-					return callback(err);
-				}
-
-				callback(null, isMembers.indexOf(true) !== -1);
-			});
-		});
-	};
-
-	Groups.isMemberOfGroupsList = function(uid, groupListKeys, callback) {
-		var sets = groupListKeys.map(function(groupName) {
-			return 'group:' + groupName + ':members';
-		});
-
-		db.getSortedSetsMembers(sets, function(err, members) {
-			if (err) {
-				return callback(err);
-			}
-
-			var uniqueGroups = _.unique(_.flatten(members));
-			uniqueGroups = internals.removeEphemeralGroups(uniqueGroups);
-
-			Groups.isMemberOfGroups(uid, uniqueGroups, function(err, isMembers) {
-				if (err) {
-					return callback(err);
-				}
-
-				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) {
-						if (map[groupNames[i]]) {
-							return true;
-						}
-					}
-					return false;
-				});
-
-				callback(null, result);
-			});
-		});
-	};
-
-	Groups.isMembersOfGroupList = function(uids, groupListKey, callback) {
-		db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) {
-			if (err) {
-				return callback(err);
-			}
-
-			var results = [];
-			uids.forEach(function() {
-				results.push(false);
-			});
-
-			groupNames = internals.removeEphemeralGroups(groupNames);
-			if (groupNames.length === 0) {
-				return callback(null, results);
-			}
-
-			async.each(groupNames, function(groupName, next) {
-				Groups.isMembers(uids, groupName, function(err, isMembers) {
-					if (err) {
-						return next(err);
-					}
-					results.forEach(function(isMember, index) {
-						if (!isMember && isMembers[index]) {
-							results[index] = true;
-						}
-					});
-					next();
-				});
-			}, function(err) {
-				callback(err, results);
-			});
-		});
-	};
-
-	Groups.isInvited = function(uid, groupName, callback) {
-		if (!uid) { return callback(null, false); }
-		db.isSetMember('group:' + groupName + ':invited', uid, callback);
-	};
-
 	Groups.exists = function(name, callback) {
 		if (Array.isArray(name)) {
 			var slugs = name.map(function(groupName) {
@@ -510,179 +373,6 @@ var async = require('async'),
 		}
 	};
 
-	Groups.hide = function(groupName, callback) {
-		callback = callback || function() {};
-		db.setObjectField('group:' + groupName, 'hidden', 1, callback);
-	};
-
-	Groups.join = function(groupName, uid, callback) {
-		function join() {
-			var tasks = [
-				async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid),
-				async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
-			];
-
-			async.waterfall([
-				function(next) {
-					user.isAdministrator(uid, next);
-				},
-				function(isAdmin, next) {
-					if (isAdmin) {
-						tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid));
-					}
-					async.parallel(tasks, next);
-				},
-				function(results, next) {
-					user.setGroupTitle(groupName, uid, next);
-				},
-				function(next) {
-					plugins.fireHook('action:group.join', {
-						groupName: groupName,
-						uid: uid
-					});
-					next();
-				}
-			], callback);
-		}
-
-		callback = callback || function() {};
-
-		Groups.exists(groupName, function(err, exists) {
-			if (err) {
-				return callback(err);
-			}
-
-			if (exists) {
-				return join();
-			}
-
-			Groups.create({
-				name: groupName,
-				description: '',
-				hidden: 1
-			}, function(err) {
-				if (err && err.message !== '[[error:group-already-exists]]') {
-					winston.error('[groups.join] Could not create new hidden group: ' + err.message);
-					return callback(err);
-				}
-				join();
-			});
-		});
-	};
-
-	Groups.requestMembership = function(groupName, uid, callback) {
-		async.parallel({
-			exists: async.apply(Groups.exists, groupName),
-			isMember: async.apply(Groups.isMember, uid, groupName)
-		}, function(err, checks) {
-			if (!checks.exists) {
-				return callback(new Error('[[error:no-group]]'));
-			} else if (checks.isMember) {
-				return callback(new Error('[[error:group-already-member]]'));
-			}
-
-			if (parseInt(uid, 10) > 0) {
-				db.setAdd('group:' + groupName + ':pending', uid, callback);
-				plugins.fireHook('action:group.requestMembership', {
-					groupName: groupName,
-					uid: uid
-				});
-			} else {
-				callback(new Error('[[error:not-logged-in]]'));
-			}
-		});
-	};
-
-	Groups.acceptMembership = function(groupName, uid, callback) {
-		// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
-		async.waterfall([
-			async.apply(db.setRemove, 'group:' + groupName + ':pending', uid),
-			async.apply(db.setRemove, 'group:' + groupName + ':invited', uid),
-			async.apply(Groups.join, groupName, uid)
-		], callback);
-	};
-
-	Groups.rejectMembership = function(groupName, uid, callback) {
-		// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
-		async.parallel([
-			async.apply(db.setRemove, 'group:' + groupName + ':pending', uid),
-			async.apply(db.setRemove, 'group:' + groupName + ':invited', uid)
-		], callback);
-	};
-
-	Groups.invite = function(groupName, uid, callback) {
-		async.parallel({
-			exists: async.apply(Groups.exists, groupName),
-			isMember: async.apply(Groups.isMember, uid, groupName)
-		}, function(err, checks) {
-			if (!checks.exists) {
-				return callback(new Error('[[error:no-group]]'));
-			} else if (checks.isMember) {
-				return callback(new Error('[[error:group-already-member]]'));
-			}
-
-			if (parseInt(uid, 10) > 0) {
-				db.setAdd('group:' + groupName + ':invited', uid, callback);
-				plugins.fireHook('action:group.inviteMember', {
-					groupName: groupName,
-					uid: uid
-				});
-			} else {
-				callback(new Error('[[error:not-logged-in]]'));
-			}
-		});
-	};
-
-	Groups.leave = function(groupName, uid, callback) {
-		callback = callback || function() {};
-
-		var tasks = [
-			async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid),
-			async.apply(db.setRemove, 'group:' + groupName + ':owners', uid),
-			async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount')
-		];
-
-		async.parallel(tasks, function(err) {
-			if (err) {
-				return callback(err);
-			}
-
-			plugins.fireHook('action:group.leave', {
-				groupName: groupName,
-				uid: uid
-			});
-
-			Groups.getGroupFields(groupName, ['hidden', 'memberCount'], function(err, groupData) {
-				if (err || !groupData) {
-					return callback(err);
-				}
-
-				if (parseInt(groupData.hidden, 10) === 1 && parseInt(groupData.memberCount, 10) === 0) {
-					Groups.destroy(groupName, callback);
-				} else {
-					callback();
-				}
-			});
-		});
-	};
-
-	Groups.leaveAllGroups = function(uid, callback) {
-		db.getSortedSetRange('groups:createtime', 0, -1, function(err, groups) {
-			if (err) {
-				return callback(err);
-			}
-			async.each(groups, function(groupName, next) {
-				Groups.isMember(uid, groupName, function(err, isMember) {
-					if (!err && isMember) {
-						Groups.leave(groupName, uid, next);
-					} else {
-						next();
-					}
-				});
-			}, callback);
-		});
-	};
-
 	Groups.getLatestMemberPosts = function(groupName, max, uid, callback) {
 		async.waterfall([
 			async.apply(Groups.getMembers, groupName, 0, -1),
@@ -780,164 +470,4 @@ var async = require('async'),
 			});
 		});
 	};
-
-	Groups.updateCoverPosition = function(groupName, position, callback) {
-		Groups.setGroupField(groupName, 'cover:position', position, callback);
-	};
-
-	Groups.updateCover = function(data, callback) {
-		var tempPath, md5sum, url;
-
-		// Position only? That's fine
-		if (!data.imageData && data.position) {
-			return Groups.updateCoverPosition(data.groupName, data.position, callback);
-		}
-
-		async.series([
-			function(next) {
-				// Calculate md5sum of image
-				// This is required because user data can be private
-				md5sum = crypto.createHash('md5');
-				md5sum.update(data.imageData);
-				md5sum = md5sum.digest('hex');
-				next();
-			},
-			function(next) {
-				// Save image
-				tempPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), md5sum);
-				var buffer = new Buffer(data.imageData.slice(data.imageData.indexOf('base64') + 7), 'base64');
-
-				fs.writeFile(tempPath, buffer, {
-					encoding: 'base64'
-				}, next);
-			},
-			function(next) {
-				uploadsController.uploadGroupCover({
-					path: tempPath
-				}, function(err, uploadData) {
-					if (err) {
-						return next(err);
-					}
-
-					url = uploadData.url;
-					next();
-				});
-			},
-			function(next) {
-				Groups.setGroupField(data.groupName, 'cover:url', url, next);
-			},
-			function(next) {
-				fs.unlink(tempPath, next);	// Delete temporary file
-			}
-		], function(err) {
-			if (err) {
-				return callback(err);
-			}
-
-			Groups.updateCoverPosition(data.groupName, data.position, callback);
-		});
-	};
-
-	Groups.ownership = {};
-
-	Groups.ownership.isOwner = function(uid, groupName, callback) {
-		// Note: All admins automatically become owners upon joining
-		db.isSetMember('group:' + groupName + ':owners', uid, callback);
-	};
-
-	Groups.ownership.grant = function(toUid, groupName, callback) {
-		// Note: No ownership checking is done here on purpose!
-		db.setAdd('group:' + groupName + ':owners', toUid, callback);
-	};
-
-	Groups.ownership.rescind = function(toUid, groupName, callback) {
-		// Note: No ownership checking is done here on purpose!
-
-		// If the owners set only contains one member, error out!
-		db.setCount('group:' + groupName + ':owners', function(err, numOwners) {
-			if (numOwners <= 1) {
-				return callback(new Error('[[error:group-needs-owner]]'));
-			}
-
-			db.setRemove('group:' + groupName + ':owners', toUid, callback);
-		});
-	};
-
-	Groups.search = function(query, options, callback) {
-		if (!query) {
-			return callback(null, []);
-		}
-		query = query.toLowerCase();
-		async.waterfall([
-			async.apply(db.getObjectValues, 'groupslug:groupname'),
-			function(groupNames, next) {
-				groupNames = groupNames.filter(function(name) {
-					return name.toLowerCase().indexOf(query) !== -1 && name !== 'administrators';
-				});
-				groupNames = groupNames.slice(0, 100);
-				Groups.getGroupsData(groupNames, next);
-			},
-			async.apply(Groups.sort, options.sort)
-		], callback);
-	};
-
-	Groups.sort = function(strategy, groups, next) {
-		switch(strategy) {
-			case 'count':
-				groups = groups.sort(function(a, b) {
-					return a.slug > b.slug;
-				}).sort(function(a, b) {
-					return a.memberCount < b.memberCount;
-				});
-				break;
-
-			case 'date':
-				groups = groups.sort(function(a, b) {
-					return a.createtime < b.createtime;
-				});
-				break;
-
-			case 'alpha':	// intentional fall-through
-			default:
-				groups = groups.sort(function(a, b) {
-					return a.slug > b.slug ? 1 : -1;
-				});
-		}
-
-		next(null, groups);
-	};
-
-	Groups.searchMembers = function(data, callback) {
-
-		function findUids(query, searchBy, callback) {
-			if (!query) {
-				return Groups.getMembers(data.groupName, 0, -1, callback);
-			}
-
-			query = query.toLowerCase();
-
-			async.waterfall([
-				function(next) {
-					Groups.getMembers(data.groupName, 0, -1, next);
-				},
-				function(members, next) {
-					user.getMultipleUserFields(members, ['uid'].concat([searchBy]), next);
-				},
-				function(users, next) {
-					var uids = [];
-					for(var i=0; i<users.length; ++i) {
-						var field = users[i][searchBy];
-						if (field.toLowerCase().startsWith(query)) {
-							uids.push(users[i].uid);
-						}
-					}
-					next(null, uids);
-				}
-			], callback);
-		}
-
-		data.findUids = findUids;
-		user.search(data, callback);
-	};
-
 }(module.exports));
diff --git a/src/groups/membership.js b/src/groups/membership.js
new file mode 100644
index 0000000000..152029d6b1
--- /dev/null
+++ b/src/groups/membership.js
@@ -0,0 +1,317 @@
+'use strict';
+
+var	async = require('async'),
+	winston = require('winston'),
+	_ = require('underscore'),
+
+	user = require('../user'),
+	plugins = require('../plugins'),
+	db = require('./../database');
+
+module.exports = function(Groups) {
+	Groups.join = function(groupName, uid, callback) {
+		function join() {
+			var tasks = [
+				async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid),
+				async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
+			];
+
+			async.waterfall([
+				function(next) {
+					user.isAdministrator(uid, next);
+				},
+				function(isAdmin, next) {
+					if (isAdmin) {
+						tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid));
+					}
+					async.parallel(tasks, next);
+				},
+				function(results, next) {
+					user.setGroupTitle(groupName, uid, next);
+				},
+				function(next) {
+					plugins.fireHook('action:group.join', {
+						groupName: groupName,
+						uid: uid
+					});
+					next();
+				}
+			], callback);
+		}
+
+		callback = callback || function() {};
+
+		Groups.exists(groupName, function(err, exists) {
+			if (err) {
+				return callback(err);
+			}
+
+			if (exists) {
+				return join();
+			}
+
+			Groups.create({
+				name: groupName,
+				description: '',
+				hidden: 1
+			}, function(err) {
+				if (err && err.message !== '[[error:group-already-exists]]') {
+					winston.error('[groups.join] Could not create new hidden group: ' + err.message);
+					return callback(err);
+				}
+				join();
+			});
+		});
+	};
+
+	Groups.requestMembership = function(groupName, uid, callback) {
+		async.parallel({
+			exists: async.apply(Groups.exists, groupName),
+			isMember: async.apply(Groups.isMember, uid, groupName)
+		}, function(err, checks) {
+			if (!checks.exists) {
+				return callback(new Error('[[error:no-group]]'));
+			} else if (checks.isMember) {
+				return callback(new Error('[[error:group-already-member]]'));
+			}
+
+			if (parseInt(uid, 10) > 0) {
+				db.setAdd('group:' + groupName + ':pending', uid, callback);
+				plugins.fireHook('action:group.requestMembership', {
+					groupName: groupName,
+					uid: uid
+				});
+			} else {
+				callback(new Error('[[error:not-logged-in]]'));
+			}
+		});
+	};
+
+	Groups.acceptMembership = function(groupName, uid, callback) {
+		// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
+		async.waterfall([
+			async.apply(db.setRemove, 'group:' + groupName + ':pending', uid),
+			async.apply(db.setRemove, 'group:' + groupName + ':invited', uid),
+			async.apply(Groups.join, groupName, uid)
+		], callback);
+	};
+
+	Groups.rejectMembership = function(groupName, uid, callback) {
+		// Note: For simplicity, this method intentially doesn't check the caller uid for ownership!
+		async.parallel([
+			async.apply(db.setRemove, 'group:' + groupName + ':pending', uid),
+			async.apply(db.setRemove, 'group:' + groupName + ':invited', uid)
+		], callback);
+	};
+
+	Groups.invite = function(groupName, uid, callback) {
+		async.parallel({
+			exists: async.apply(Groups.exists, groupName),
+			isMember: async.apply(Groups.isMember, uid, groupName)
+		}, function(err, checks) {
+			if (!checks.exists) {
+				return callback(new Error('[[error:no-group]]'));
+			} else if (checks.isMember) {
+				return callback(new Error('[[error:group-already-member]]'));
+			}
+
+			if (parseInt(uid, 10) > 0) {
+				db.setAdd('group:' + groupName + ':invited', uid, callback);
+				plugins.fireHook('action:group.inviteMember', {
+					groupName: groupName,
+					uid: uid
+				});
+			} else {
+				callback(new Error('[[error:not-logged-in]]'));
+			}
+		});
+	};
+
+	Groups.leave = function(groupName, uid, callback) {
+		callback = callback || function() {};
+
+		var tasks = [
+			async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid),
+			async.apply(db.setRemove, 'group:' + groupName + ':owners', uid),
+			async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount')
+		];
+
+		async.parallel(tasks, function(err) {
+			if (err) {
+				return callback(err);
+			}
+
+			plugins.fireHook('action:group.leave', {
+				groupName: groupName,
+				uid: uid
+			});
+
+			Groups.getGroupFields(groupName, ['hidden', 'memberCount'], function(err, groupData) {
+				if (err || !groupData) {
+					return callback(err);
+				}
+
+				if (parseInt(groupData.hidden, 10) === 1 && parseInt(groupData.memberCount, 10) === 0) {
+					Groups.destroy(groupName, callback);
+				} else {
+					callback();
+				}
+			});
+		});
+	};
+
+	Groups.leaveAllGroups = function(uid, callback) {
+		db.getSortedSetRange('groups:createtime', 0, -1, function(err, groups) {
+			if (err) {
+				return callback(err);
+			}
+			async.each(groups, function(groupName, next) {
+				Groups.isMember(uid, groupName, function(err, isMember) {
+					if (!err && isMember) {
+						Groups.leave(groupName, uid, next);
+					} else {
+						next();
+					}
+				});
+			}, callback);
+		});
+	};
+
+	Groups.getMembers = function(groupName, start, stop, callback) {
+		db.getSortedSetRevRange('group:' + groupName + ':members', start, stop, 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) {
+			return callback(null, false);
+		}
+		db.isSortedSetMember('group:' + groupName + ':members', uid, callback);
+	};
+
+	Groups.isMembers = function(uids, groupName, callback) {
+		db.isSortedSetMembers('group:' + groupName + ':members', uids, callback);
+	};
+
+	Groups.isMemberOfGroups = function(uid, groups, callback) {
+		if (!uid || parseInt(uid, 10) <= 0) {
+			return callback(null, groups.map(function() {return false;}));
+		}
+		groups = groups.map(function(groupName) {
+			return 'group:' + groupName + ':members';
+		});
+
+		db.isMemberOfSortedSets(groups, uid, callback);
+	};
+
+	Groups.getMemberCount = function(groupName, callback) {
+		db.getObjectField('group:' + groupName, 'memberCount', function(err, count) {
+			if (err) {
+				return callback(err);
+			}
+			callback(null, parseInt(count, 10));
+		});
+	};
+
+	Groups.isMemberOfGroupList = function(uid, groupListKey, callback) {
+		db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) {
+			if (err) {
+				return callback(err);
+			}
+			groupNames = Groups.internals.removeEphemeralGroups(groupNames);
+			if (groupNames.length === 0) {
+				return callback(null, false);
+			}
+
+			Groups.isMemberOfGroups(uid, groupNames, function(err, isMembers) {
+				if (err) {
+					return callback(err);
+				}
+
+				callback(null, isMembers.indexOf(true) !== -1);
+			});
+		});
+	};
+
+	Groups.isMemberOfGroupsList = function(uid, groupListKeys, callback) {
+		var sets = groupListKeys.map(function(groupName) {
+			return 'group:' + groupName + ':members';
+		});
+
+		db.getSortedSetsMembers(sets, function(err, members) {
+			if (err) {
+				return callback(err);
+			}
+
+			var uniqueGroups = _.unique(_.flatten(members));
+			uniqueGroups = Groups.internals.removeEphemeralGroups(uniqueGroups);
+
+			Groups.isMemberOfGroups(uid, uniqueGroups, function(err, isMembers) {
+				if (err) {
+					return callback(err);
+				}
+
+				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) {
+						if (map[groupNames[i]]) {
+							return true;
+						}
+					}
+					return false;
+				});
+
+				callback(null, result);
+			});
+		});
+	};
+
+	Groups.isMembersOfGroupList = function(uids, groupListKey, callback) {
+		db.getSortedSetRange('group:' + groupListKey + ':members', 0, -1, function(err, groupNames) {
+			if (err) {
+				return callback(err);
+			}
+
+			var results = [];
+			uids.forEach(function() {
+				results.push(false);
+			});
+
+			groupNames = Groups.internals.removeEphemeralGroups(groupNames);
+			if (groupNames.length === 0) {
+				return callback(null, results);
+			}
+
+			async.each(groupNames, function(groupName, next) {
+				Groups.isMembers(uids, groupName, function(err, isMembers) {
+					if (err) {
+						return next(err);
+					}
+					results.forEach(function(isMember, index) {
+						if (!isMember && isMembers[index]) {
+							results[index] = true;
+						}
+					});
+					next();
+				});
+			}, function(err) {
+				callback(err, results);
+			});
+		});
+	};
+
+	Groups.isInvited = function(uid, groupName, callback) {
+		if (!uid) { return callback(null, false); }
+		db.isSetMember('group:' + groupName + ':invited', uid, callback);
+	};
+};
diff --git a/src/groups/ownership.js b/src/groups/ownership.js
new file mode 100644
index 0000000000..38d5a699d8
--- /dev/null
+++ b/src/groups/ownership.js
@@ -0,0 +1,31 @@
+'use strict';
+
+var	db = require('./../database');
+
+module.exports = function(Groups) {
+
+	Groups.ownership = {};
+
+	Groups.ownership.isOwner = function(uid, groupName, callback) {
+		// Note: All admins automatically become owners upon joining
+		db.isSetMember('group:' + groupName + ':owners', uid, callback);
+	};
+
+	Groups.ownership.grant = function(toUid, groupName, callback) {
+		// Note: No ownership checking is done here on purpose!
+		db.setAdd('group:' + groupName + ':owners', toUid, callback);
+	};
+
+	Groups.ownership.rescind = function(toUid, groupName, callback) {
+		// Note: No ownership checking is done here on purpose!
+
+		// If the owners set only contains one member, error out!
+		db.setCount('group:' + groupName + ':owners', function(err, numOwners) {
+			if (numOwners <= 1) {
+				return callback(new Error('[[error:group-needs-owner]]'));
+			}
+
+			db.setRemove('group:' + groupName + ':owners', toUid, callback);
+		});
+	};
+};
diff --git a/src/groups/search.js b/src/groups/search.js
new file mode 100644
index 0000000000..4629d17af2
--- /dev/null
+++ b/src/groups/search.js
@@ -0,0 +1,86 @@
+'use strict';
+
+var	async = require('async'),
+
+	user = require('../user'),
+	db = require('./../database');
+
+module.exports = function(Groups) {
+
+	Groups.search = function(query, options, callback) {
+		if (!query) {
+			return callback(null, []);
+		}
+		query = query.toLowerCase();
+		async.waterfall([
+			async.apply(db.getObjectValues, 'groupslug:groupname'),
+			function(groupNames, next) {
+				groupNames = groupNames.filter(function(name) {
+					return name.toLowerCase().indexOf(query) !== -1 && name !== 'administrators';
+				});
+				groupNames = groupNames.slice(0, 100);
+				Groups.getGroupsData(groupNames, next);
+			},
+			async.apply(Groups.sort, options.sort)
+		], callback);
+	};
+
+	Groups.sort = function(strategy, groups, next) {
+		switch(strategy) {
+			case 'count':
+				groups = groups.sort(function(a, b) {
+					return a.slug > b.slug;
+				}).sort(function(a, b) {
+					return a.memberCount < b.memberCount;
+				});
+				break;
+
+			case 'date':
+				groups = groups.sort(function(a, b) {
+					return a.createtime < b.createtime;
+				});
+				break;
+
+			case 'alpha':	// intentional fall-through
+			default:
+				groups = groups.sort(function(a, b) {
+					return a.slug > b.slug ? 1 : -1;
+				});
+		}
+
+		next(null, groups);
+	};
+
+	Groups.searchMembers = function(data, callback) {
+
+		function findUids(query, searchBy, callback) {
+			if (!query) {
+				return Groups.getMembers(data.groupName, 0, -1, callback);
+			}
+
+			query = query.toLowerCase();
+
+			async.waterfall([
+				function(next) {
+					Groups.getMembers(data.groupName, 0, -1, next);
+				},
+				function(members, next) {
+					user.getMultipleUserFields(members, ['uid'].concat([searchBy]), next);
+				},
+				function(users, next) {
+					var uids = [];
+					for(var i=0; i<users.length; ++i) {
+						var field = users[i][searchBy];
+						if (field.toLowerCase().startsWith(query)) {
+							uids.push(users[i].uid);
+						}
+					}
+					next(null, uids);
+				}
+			], callback);
+		}
+
+		data.findUids = findUids;
+		user.search(data, callback);
+	};
+};
diff --git a/src/groups/update.js b/src/groups/update.js
index 4b2974b013..b533f63c4e 100644
--- a/src/groups/update.js
+++ b/src/groups/update.js
@@ -2,9 +2,16 @@
 
 var async = require('async'),
 	winston = require('winston'),
+	crypto = require('crypto'),
+	path = require('path'),
+	nconf = require('nconf'),
+	fs = require('fs'),
+
 	plugins = require('../plugins'),
 	utils = require('../../public/src/utils'),
-	db = require('./../database');
+	db = require('./../database'),
+
+	uploadsController = require('../controllers/uploads');
 
 module.exports = function(Groups) {
 
@@ -55,6 +62,68 @@ module.exports = function(Groups) {
 		});
 	};
 
+	Groups.hide = function(groupName, callback) {
+		callback = callback || function() {};
+		db.setObjectField('group:' + groupName, 'hidden', 1, callback);
+	};
+
+	Groups.updateCoverPosition = function(groupName, position, callback) {
+		Groups.setGroupField(groupName, 'cover:position', position, callback);
+	};
+
+	Groups.updateCover = function(data, callback) {
+		var tempPath, md5sum, url;
+
+		// Position only? That's fine
+		if (!data.imageData && data.position) {
+			return Groups.updateCoverPosition(data.groupName, data.position, callback);
+		}
+
+		async.series([
+			function(next) {
+				// Calculate md5sum of image
+				// This is required because user data can be private
+				md5sum = crypto.createHash('md5');
+				md5sum.update(data.imageData);
+				md5sum = md5sum.digest('hex');
+				next();
+			},
+			function(next) {
+				// Save image
+				tempPath = path.join(nconf.get('base_dir'), nconf.get('upload_path'), md5sum);
+				var buffer = new Buffer(data.imageData.slice(data.imageData.indexOf('base64') + 7), 'base64');
+
+				fs.writeFile(tempPath, buffer, {
+					encoding: 'base64'
+				}, next);
+			},
+			function(next) {
+				uploadsController.uploadGroupCover({
+					path: tempPath
+				}, function(err, uploadData) {
+					if (err) {
+						return next(err);
+					}
+
+					url = uploadData.url;
+					next();
+				});
+			},
+			function(next) {
+				Groups.setGroupField(data.groupName, 'cover:url', url, next);
+			},
+			function(next) {
+				fs.unlink(tempPath, next);	// Delete temporary file
+			}
+		], function(err) {
+			if (err) {
+				return callback(err);
+			}
+
+			Groups.updateCoverPosition(data.groupName, data.position, callback);
+		});
+	};
+
 	function updatePrivacy(groupName, newValue, callback) {
 		if (!newValue) {
 			return callback();
@@ -160,6 +229,4 @@ module.exports = function(Groups) {
 			], callback);
 		});
 	}
-
-
 };