mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-05-07 18:56:57 +02:00
Merge branch 'master' into notif-abort
This commit is contained in:
@@ -16,17 +16,21 @@ var uniquevisitors = 0;
|
||||
|
||||
var isCategory = /^(?:\/api)?\/category\/(\d+)/;
|
||||
|
||||
new cronJob('*/10 * * * *', function () {
|
||||
new cronJob('*/10 * * * * *', function () {
|
||||
Analytics.writeData();
|
||||
}, null, true);
|
||||
|
||||
Analytics.increment = function (keys) {
|
||||
Analytics.increment = function (keys, callback) {
|
||||
keys = Array.isArray(keys) ? keys : [keys];
|
||||
|
||||
keys.forEach(function (key) {
|
||||
counters[key] = counters[key] || 0;
|
||||
counters[key] += 1;
|
||||
});
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
Analytics.pageView = function (payload) {
|
||||
|
||||
10
src/batch.js
10
src/batch.js
@@ -21,6 +21,15 @@ exports.processSortedSet = function (setKey, process, options, callback) {
|
||||
return callback(new Error('[[error:process-not-a-function]]'));
|
||||
}
|
||||
|
||||
// Progress bar handling (upgrade scripts)
|
||||
if (options.progress) {
|
||||
db.sortedSetCard(setKey, function (err, total) {
|
||||
if (!err) {
|
||||
options.progress.total = total;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// use the fast path if possible
|
||||
if (db.processSortedSet && typeof options.doneIf !== 'function' && !utils.isNumber(options.alwaysStartAt)) {
|
||||
return db.processSortedSet(setKey, process, options.batch || DEFAULT_BATCH_SIZE, callback);
|
||||
@@ -53,6 +62,7 @@ exports.processSortedSet = function (setKey, process, options, callback) {
|
||||
}
|
||||
start += utils.isNumber(options.alwaysStartAt) ? options.alwaysStartAt : batch + 1;
|
||||
stop = start + batch;
|
||||
|
||||
next();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ process.on('message', function (msg) {
|
||||
if (msg.type === 'hash') {
|
||||
hashPassword(msg.password, msg.rounds);
|
||||
} else if (msg.type === 'compare') {
|
||||
bcrypt.compare(msg.password, msg.hash, done);
|
||||
bcrypt.compare(String(msg.password || ''), String(msg.hash || ''), done);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ module.exports = function (Categories) {
|
||||
next(null, category);
|
||||
},
|
||||
function (category, next) {
|
||||
plugins.fireHook('action:category.create', category);
|
||||
plugins.fireHook('action:category.create', { category: category });
|
||||
next(null, category);
|
||||
},
|
||||
], callback);
|
||||
@@ -138,9 +138,20 @@ module.exports = function (Categories) {
|
||||
};
|
||||
|
||||
Categories.copyPrivilegesFrom = function (fromCid, toCid, callback) {
|
||||
async.each(privileges.privilegeList, function (privilege, next) {
|
||||
copyPrivilege(privilege, fromCid, toCid, next);
|
||||
}, callback);
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
plugins.fireHook('filter:categories.copyPrivilegesFrom', {
|
||||
privileges: privileges.privilegeList,
|
||||
fromCid: fromCid,
|
||||
toCid: toCid,
|
||||
}, next);
|
||||
},
|
||||
function (data, next) {
|
||||
async.each(data.privileges, function (privilege, next) {
|
||||
copyPrivilege(privilege, data.fromCid, data.toCid, next);
|
||||
}, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
function copyPrivilege(privilege, fromCid, toCid, callback) {
|
||||
|
||||
@@ -30,7 +30,7 @@ module.exports = function (Categories) {
|
||||
purgeCategory(cid, next);
|
||||
},
|
||||
function (next) {
|
||||
plugins.fireHook('action:category.delete', cid);
|
||||
plugins.fireHook('action:category.delete', { cid: cid, uid: uid });
|
||||
next();
|
||||
},
|
||||
], callback);
|
||||
@@ -58,7 +58,7 @@ module.exports = function (Categories) {
|
||||
], next);
|
||||
},
|
||||
function (next) {
|
||||
async.each(privileges.privilegeList, function (privilege, next) {
|
||||
async.eachSeries(privileges.privilegeList, function (privilege, next) {
|
||||
groups.destroy('cid:' + cid + ':privileges:' + privilege, next);
|
||||
}, next);
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ editController.get = function (req, res, callback) {
|
||||
userData.maximumProfileImageSize = parseInt(meta.config.maximumProfileImageSize, 10);
|
||||
userData.allowProfileImageUploads = parseInt(meta.config.allowProfileImageUploads, 10) === 1;
|
||||
userData.allowAccountDelete = parseInt(meta.config.allowAccountDelete, 10) === 1;
|
||||
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 128;
|
||||
userData.profileImageDimension = parseInt(meta.config.profileImageDimension, 10) || 200;
|
||||
|
||||
userData.groups = userData.groups.filter(function (group) {
|
||||
return group && group.userTitleEnabled && !groups.isPrivilegeGroup(group.name) && group.name !== 'registered-users';
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
var async = require('async');
|
||||
var validator = require('validator');
|
||||
var winston = require('winston');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var user = require('../../user');
|
||||
var groups = require('../../groups');
|
||||
var plugins = require('../../plugins');
|
||||
var meta = require('../../meta');
|
||||
var utils = require('../../utils');
|
||||
var privileges = require('../../privileges');
|
||||
|
||||
var helpers = {};
|
||||
var helpers = module.exports;
|
||||
|
||||
helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
|
||||
async.waterfall([
|
||||
@@ -60,6 +62,9 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
|
||||
sso: function (next) {
|
||||
plugins.fireHook('filter:auth.list', { uid: uid, associations: [] }, next);
|
||||
},
|
||||
canBanUser: function (next) {
|
||||
privileges.users.canBanUser(callerUID, uid, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
@@ -109,7 +114,7 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
|
||||
userData.isAdminOrGlobalModeratorOrModerator = isAdmin || isGlobalModerator || isModerator;
|
||||
userData.isSelfOrAdminOrGlobalModerator = isSelf || isAdmin || isGlobalModerator;
|
||||
userData.canEdit = isAdmin || (isGlobalModerator && !results.isTargetAdmin);
|
||||
userData.canBan = isAdmin || (isGlobalModerator && !results.isTargetAdmin);
|
||||
userData.canBan = results.canBanUser;
|
||||
userData.canChangePassword = isAdmin || (isSelf && parseInt(meta.config['password:disableEdit'], 10) !== 1);
|
||||
userData.isSelf = isSelf;
|
||||
userData.isFollowing = results.isFollowing;
|
||||
@@ -119,7 +124,13 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
|
||||
userData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1;
|
||||
userData['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
|
||||
userData['email:confirmed'] = !!parseInt(userData['email:confirmed'], 10);
|
||||
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), isSelf);
|
||||
userData.profile_links = filterLinks(results.profile_links.concat(results.profile_menu.links), {
|
||||
self: isSelf,
|
||||
other: !isSelf,
|
||||
moderator: isModerator,
|
||||
globalMod: isGlobalModerator,
|
||||
admin: isAdmin,
|
||||
});
|
||||
|
||||
userData.sso = results.sso.associations;
|
||||
userData.status = user.getStatus(userData);
|
||||
@@ -138,7 +149,7 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) {
|
||||
userData.birthday = validator.escape(String(userData.birthday || ''));
|
||||
userData.moderationNote = validator.escape(String(userData.moderationNote || ''));
|
||||
|
||||
userData['cover:url'] = userData['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(userData.uid);
|
||||
userData['cover:url'] = userData['cover:url'] ? (nconf.get('relative_path') + userData['cover:url']) : require('../../coverPhoto').getDefaultProfileCover(userData.uid);
|
||||
userData['cover:position'] = validator.escape(String(userData['cover:position'] || '50% 50%'));
|
||||
userData['username:disableEdit'] = !userData.isAdmin && parseInt(meta.config['username:disableEdit'], 10) === 1;
|
||||
userData['email:disableEdit'] = !userData.isAdmin && parseInt(meta.config['email:disableEdit'], 10) === 1;
|
||||
@@ -154,10 +165,29 @@ helpers.getBaseUser = function (userslug, callerUID, callback) {
|
||||
helpers.getUserDataByUserSlug(userslug, callerUID, callback);
|
||||
};
|
||||
|
||||
function filterLinks(links, self) {
|
||||
return links.filter(function (link) {
|
||||
return link && (link.public || self);
|
||||
function filterLinks(links, states) {
|
||||
return links.filter(function (link, index) {
|
||||
// "public" is the old property, if visibility is defined, discard `public`
|
||||
if (link.hasOwnProperty('public') && !link.hasOwnProperty('visibility')) {
|
||||
winston.warn('[account/profileMenu (' + link.id + ')] Use of the `.public` property is deprecated, use `visibility` now');
|
||||
return link && (link.public || states.self);
|
||||
}
|
||||
|
||||
// Default visibility
|
||||
link.visibility = Object.assign({
|
||||
self: true,
|
||||
other: true,
|
||||
moderator: true,
|
||||
globalMod: true,
|
||||
admin: true,
|
||||
}, link.visibility);
|
||||
|
||||
// Iterate through states and permit if every test passes (or is not defined)
|
||||
var permit = Object.keys(states).some(function (state) {
|
||||
return states[state] === link.visibility[state];
|
||||
});
|
||||
|
||||
links[index].public = permit;
|
||||
return permit;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = helpers;
|
||||
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var db = require('../../database');
|
||||
var user = require('../../user');
|
||||
var helpers = require('../helpers');
|
||||
var accountHelpers = require('./helpers');
|
||||
var pagination = require('../../pagination');
|
||||
|
||||
var infoController = {};
|
||||
var infoController = module.exports;
|
||||
|
||||
infoController.get = function (req, res, callback) {
|
||||
var userData;
|
||||
var page = Math.max(1, req.query.page || 1);
|
||||
var itemsPerPage = 10;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
accountHelpers.getUserDataByUserSlug(req.params.userslug, req.uid, next);
|
||||
@@ -19,11 +24,27 @@ infoController.get = function (req, res, callback) {
|
||||
if (!userData) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var start = (page - 1) * itemsPerPage;
|
||||
var stop = start + itemsPerPage - 1;
|
||||
async.parallel({
|
||||
history: async.apply(user.getModerationHistory, userData.uid),
|
||||
sessions: async.apply(user.auth.getSessions, userData.uid, req.sessionID),
|
||||
usernames: async.apply(user.getHistory, 'user:' + userData.uid + ':usernames'),
|
||||
emails: async.apply(user.getHistory, 'user:' + userData.uid + ':emails'),
|
||||
notes: function (next) {
|
||||
if (!userData.isAdminOrGlobalModeratorOrModerator) {
|
||||
return setImmediate(next);
|
||||
}
|
||||
async.parallel({
|
||||
notes: function (next) {
|
||||
user.getModerationNotes(userData.uid, start, stop, next);
|
||||
},
|
||||
count: function (next) {
|
||||
db.sortedSetCard('uid:' + userData.uid + ':moderation:notes', next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
], function (err, data) {
|
||||
@@ -35,11 +56,15 @@ infoController.get = function (req, res, callback) {
|
||||
userData.sessions = data.sessions;
|
||||
userData.usernames = data.usernames;
|
||||
userData.emails = data.emails;
|
||||
|
||||
if (userData.isAdminOrGlobalModeratorOrModerator) {
|
||||
userData.moderationNotes = data.notes.notes;
|
||||
var pageCount = Math.ceil(data.notes.count / itemsPerPage);
|
||||
userData.pagination = pagination.create(page, pageCount, req.query);
|
||||
}
|
||||
userData.title = '[[pages:account/info]]';
|
||||
userData.breadcrumbs = helpers.buildBreadcrumbs([{ text: userData.username, url: '/user/' + userData.userslug }, { text: '[[user:account_info]]' }]);
|
||||
|
||||
res.render('account/info', userData);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = infoController;
|
||||
|
||||
@@ -1,23 +1,91 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var user = require('../../user');
|
||||
var helpers = require('../helpers');
|
||||
var plugins = require('../../plugins');
|
||||
var pagination = require('../../pagination');
|
||||
|
||||
var notificationsController = {};
|
||||
var notificationsController = module.exports;
|
||||
|
||||
notificationsController.get = function (req, res, next) {
|
||||
user.notifications.getAll(req.uid, 0, 39, function (err, notifications) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.render('notifications', {
|
||||
notifications: notifications,
|
||||
nextStart: 40,
|
||||
title: '[[pages:notifications]]',
|
||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:notifications]]' }]),
|
||||
});
|
||||
});
|
||||
var regularFilters = [
|
||||
{ name: '[[notifications:all]]', filter: '' },
|
||||
{ name: '[[notifications:topics]]', filter: 'new-topic' },
|
||||
{ name: '[[notifications:replies]]', filter: 'new-reply' },
|
||||
{ name: '[[notifications:chat]]', filter: 'new-chat' },
|
||||
{ name: '[[notifications:follows]]', filter: 'follow' },
|
||||
{ name: '[[notifications:upvote]]', filter: 'upvote' },
|
||||
];
|
||||
|
||||
var moderatorFilters = [
|
||||
{ name: '[[notifications:new-flags]]', filter: 'new-post-flag' },
|
||||
{ name: '[[notifications:my-flags]]', filter: 'my-flags' },
|
||||
{ name: '[[notifications:bans]]', filter: 'ban' },
|
||||
];
|
||||
|
||||
var filter = req.query.filter || '';
|
||||
var page = Math.max(1, req.query.page || 1);
|
||||
var itemsPerPage = 20;
|
||||
var start = (page - 1) * itemsPerPage;
|
||||
var stop = start + itemsPerPage - 1;
|
||||
var selectedFilter;
|
||||
var pageCount = 1;
|
||||
var allFilters = [];
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
filters: function (next) {
|
||||
plugins.fireHook('filter:notifications.addFilters', {
|
||||
regularFilters: regularFilters,
|
||||
moderatorFilters: moderatorFilters,
|
||||
uid: req.uid,
|
||||
}, next);
|
||||
},
|
||||
isPrivileged: function (next) {
|
||||
user.isPrivileged(req.uid, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (data, _next) {
|
||||
allFilters = data.filters.regularFilters;
|
||||
|
||||
if (data.isPrivileged) {
|
||||
allFilters = allFilters.concat([
|
||||
{ separator: true },
|
||||
]).concat(data.filters.moderatorFilters);
|
||||
}
|
||||
|
||||
selectedFilter = allFilters.find(function (filterData) {
|
||||
filterData.selected = filterData.filter === filter;
|
||||
return filterData.selected;
|
||||
});
|
||||
|
||||
if (!selectedFilter) {
|
||||
return next();
|
||||
}
|
||||
|
||||
user.notifications.getAll(req.uid, selectedFilter.filter, _next);
|
||||
},
|
||||
function (nids, next) {
|
||||
pageCount = nids.length / itemsPerPage;
|
||||
nids = nids.slice(start, stop + 1);
|
||||
|
||||
user.notifications.getNotifications(nids, req.uid, next);
|
||||
},
|
||||
function (notifications) {
|
||||
res.render('notifications', {
|
||||
notifications: notifications,
|
||||
pagination: pagination.create(page, pageCount, req.query),
|
||||
filters: allFilters,
|
||||
regularFilters: regularFilters,
|
||||
moderatorFilters: moderatorFilters,
|
||||
selectedFilter: selectedFilter,
|
||||
title: '[[pages:notifications]]',
|
||||
breadcrumbs: helpers.buildBreadcrumbs([{ text: '[[pages:notifications]]' }]),
|
||||
});
|
||||
},
|
||||
], next);
|
||||
};
|
||||
|
||||
|
||||
module.exports = notificationsController;
|
||||
|
||||
@@ -82,9 +82,6 @@ profileController.get = function (req, res, callback) {
|
||||
var pageCount = Math.ceil(userData.postcount / itemsPerPage);
|
||||
userData.pagination = pagination.create(page, pageCount, req.query);
|
||||
|
||||
userData['cover:url'] = userData['cover:url'] || require('../../coverPhoto').getDefaultProfileCover(userData.uid);
|
||||
userData['cover:position'] = userData['cover:position'] || '50% 50%';
|
||||
|
||||
if (!parseInt(userData.profileviews, 10)) {
|
||||
userData.profileviews = 1;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ var adminController = {
|
||||
dashboard: require('./admin/dashboard'),
|
||||
categories: require('./admin/categories'),
|
||||
tags: require('./admin/tags'),
|
||||
flags: require('./admin/flags'),
|
||||
blacklist: require('./admin/blacklist'),
|
||||
groups: require('./admin/groups'),
|
||||
appearance: require('./admin/appearance'),
|
||||
|
||||
@@ -5,6 +5,7 @@ var cacheController = {};
|
||||
cacheController.get = function (req, res) {
|
||||
var postCache = require('../../posts/cache');
|
||||
var groupCache = require('../../groups').cache;
|
||||
var userSettingsCache = require('../../user').settingsCache;
|
||||
|
||||
var avgPostSize = 0;
|
||||
var percentFull = 0;
|
||||
@@ -21,6 +22,12 @@ cacheController.get = function (req, res) {
|
||||
percentFull: percentFull,
|
||||
avgPostSize: avgPostSize,
|
||||
},
|
||||
userSettingsCache: {
|
||||
length: userSettingsCache.length,
|
||||
max: userSettingsCache.max,
|
||||
itemCount: userSettingsCache.itemCount,
|
||||
percentFull: ((userSettingsCache.length / userSettingsCache.max) * 100).toFixed(2),
|
||||
},
|
||||
groupCache: {
|
||||
length: groupCache.length,
|
||||
max: groupCache.max,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var validator = require('validator');
|
||||
|
||||
var posts = require('../../posts');
|
||||
var user = require('../../user');
|
||||
var categories = require('../../categories');
|
||||
var analytics = require('../../analytics');
|
||||
var pagination = require('../../pagination');
|
||||
|
||||
var flagsController = {};
|
||||
|
||||
var itemsPerPage = 20;
|
||||
|
||||
flagsController.get = function (req, res, next) {
|
||||
var byUsername = req.query.byUsername || '';
|
||||
var cid = req.query.cid || 0;
|
||||
var sortBy = req.query.sortBy || 'count';
|
||||
var page = parseInt(req.query.page, 10) || 1;
|
||||
|
||||
async.parallel({
|
||||
categories: function (next) {
|
||||
categories.buildForSelect(req.uid, next);
|
||||
},
|
||||
flagData: function (next) {
|
||||
getFlagData(req, res, next);
|
||||
},
|
||||
analytics: function (next) {
|
||||
analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
|
||||
},
|
||||
assignees: async.apply(user.getAdminsandGlobalModsandModerators),
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// Minimise data set for assignees so tjs does less work
|
||||
results.assignees = results.assignees.map(function (userObj) {
|
||||
return {
|
||||
uid: userObj.uid,
|
||||
username: userObj.username,
|
||||
};
|
||||
});
|
||||
|
||||
// If res.locals.cids is populated, then slim down the categories list
|
||||
if (res.locals.cids) {
|
||||
results.categories = results.categories.filter(function (category) {
|
||||
return res.locals.cids.indexOf(String(category.cid)) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
var pageCount = Math.max(1, Math.ceil(results.flagData.count / itemsPerPage));
|
||||
|
||||
results.categories.forEach(function (category) {
|
||||
category.selected = parseInt(category.cid, 10) === parseInt(cid, 10);
|
||||
});
|
||||
|
||||
var data = {
|
||||
posts: results.flagData.posts,
|
||||
assignees: results.assignees,
|
||||
analytics: results.analytics,
|
||||
categories: results.categories,
|
||||
byUsername: validator.escape(String(byUsername)),
|
||||
sortByCount: sortBy === 'count',
|
||||
sortByTime: sortBy === 'time',
|
||||
pagination: pagination.create(page, pageCount, req.query),
|
||||
};
|
||||
res.render('admin/manage/flags', data);
|
||||
});
|
||||
};
|
||||
|
||||
function getFlagData(req, res, callback) {
|
||||
var sortBy = req.query.sortBy || 'count';
|
||||
var byUsername = req.query.byUsername || '';
|
||||
var cid = req.query.cid || res.locals.cids || 0;
|
||||
var page = parseInt(req.query.page, 10) || 1;
|
||||
var start = (page - 1) * itemsPerPage;
|
||||
var stop = start + itemsPerPage - 1;
|
||||
|
||||
var sets = [sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged'];
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (byUsername) {
|
||||
user.getUidByUsername(byUsername, next);
|
||||
} else {
|
||||
process.nextTick(next, null, 0);
|
||||
}
|
||||
},
|
||||
function (uid, next) {
|
||||
if (uid) {
|
||||
sets.push('uid:' + uid + ':flag:pids');
|
||||
}
|
||||
|
||||
posts.getFlags(sets, cid, req.uid, start, stop, next);
|
||||
},
|
||||
], callback);
|
||||
}
|
||||
|
||||
|
||||
module.exports = flagsController;
|
||||
@@ -16,22 +16,30 @@ var info = {};
|
||||
infoController.get = function (req, res) {
|
||||
info = {};
|
||||
pubsub.publish('sync:node:info:start');
|
||||
var timeoutMS = 1000;
|
||||
setTimeout(function () {
|
||||
var data = [];
|
||||
Object.keys(info).forEach(function (key) {
|
||||
data.push(info[key]);
|
||||
});
|
||||
data.sort(function (a, b) {
|
||||
if (a.os.hostname < b.os.hostname) {
|
||||
if (a.id < b.id) {
|
||||
return -1;
|
||||
}
|
||||
if (a.os.hostname > b.os.hostname) {
|
||||
if (a.id > b.id) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
res.render('admin/development/info', { info: data, infoJSON: JSON.stringify(data, null, 4), host: os.hostname(), port: nconf.get('port') });
|
||||
}, 500);
|
||||
res.render('admin/development/info', {
|
||||
info: data,
|
||||
infoJSON: JSON.stringify(data, null, 4),
|
||||
host: os.hostname(),
|
||||
port: nconf.get('port'),
|
||||
nodeCount: data.length,
|
||||
timeout: timeoutMS,
|
||||
});
|
||||
}, timeoutMS);
|
||||
};
|
||||
|
||||
pubsub.on('sync:node:info:start', function () {
|
||||
@@ -39,7 +47,8 @@ pubsub.on('sync:node:info:start', function () {
|
||||
if (err) {
|
||||
return winston.error(err);
|
||||
}
|
||||
pubsub.publish('sync:node:info:end', { data: data, id: os.hostname() + ':' + nconf.get('port') });
|
||||
data.id = os.hostname() + ':' + nconf.get('port');
|
||||
pubsub.publish('sync:node:info:end', { data: data, id: data.id });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ languagesController.get = function (req, res, next) {
|
||||
|
||||
res.render('admin/general/languages', {
|
||||
languages: languages,
|
||||
autoDetectLang: parseInt(meta.config.autoDetectLang, 10) === 1,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,9 +7,9 @@ var themesController = {};
|
||||
|
||||
themesController.get = function (req, res, next) {
|
||||
var themeDir = path.join(__dirname, '../../../node_modules/' + req.params.theme);
|
||||
file.exists(themeDir, function (exists) {
|
||||
if (!exists) {
|
||||
return next();
|
||||
file.exists(themeDir, function (err, exists) {
|
||||
if (err || !exists) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var themeConfig = require(path.join(themeDir, 'theme.json'));
|
||||
|
||||
@@ -5,6 +5,7 @@ var path = require('path');
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
var winston = require('winston');
|
||||
var mime = require('mime');
|
||||
|
||||
var meta = require('../../meta');
|
||||
var file = require('../../file');
|
||||
@@ -102,6 +103,11 @@ uploadsController.uploadLogo = function (req, res, next) {
|
||||
uploadsController.uploadSound = function (req, res, next) {
|
||||
var uploadedFile = req.files.files[0];
|
||||
|
||||
var mimeType = mime.lookup(uploadedFile.name);
|
||||
if (!/^audio\//.test(mimeType)) {
|
||||
return next(Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
file.saveFileToLocal(uploadedFile.name, 'sounds', uploadedFile.path, function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
|
||||
@@ -127,8 +127,10 @@ function getUsers(set, section, min, max, req, res, next) {
|
||||
count: function (next) {
|
||||
if (byScore) {
|
||||
db.sortedSetCount(set, min, max, next);
|
||||
} else {
|
||||
} else if (set === 'users:banned' || set === 'users:notvalidated') {
|
||||
db.sortedSetCard(set, next);
|
||||
} else {
|
||||
db.getObjectField('global', 'userCount', next);
|
||||
}
|
||||
},
|
||||
users: function (next) {
|
||||
|
||||
@@ -13,10 +13,11 @@ var user = require('../user');
|
||||
var plugins = require('../plugins');
|
||||
var utils = require('../utils');
|
||||
var Password = require('../password');
|
||||
var translator = require('../translator');
|
||||
|
||||
var sockets = require('../socket.io');
|
||||
|
||||
var authenticationController = {};
|
||||
var authenticationController = module.exports;
|
||||
|
||||
authenticationController.register = function (req, res) {
|
||||
var registrationType = meta.config.registrationType || 'normal';
|
||||
@@ -330,7 +331,7 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) {
|
||||
// Force session check for all connected socket.io clients with the same session id
|
||||
sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
|
||||
|
||||
plugins.fireHook('action:user.loggedIn', uid);
|
||||
plugins.fireHook('action:user.loggedIn', { uid: uid, req: req });
|
||||
callback();
|
||||
});
|
||||
};
|
||||
@@ -344,21 +345,21 @@ authenticationController.localLogin = function (req, username, password, next) {
|
||||
var uid;
|
||||
var userData = {};
|
||||
|
||||
if (!password || !utils.isPasswordValid(password)) {
|
||||
return next(new Error('[[error:invalid-password]]'));
|
||||
}
|
||||
|
||||
if (password.length > 4096) {
|
||||
return next(new Error('[[error:password-too-long]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isPasswordValid(password, next);
|
||||
},
|
||||
function (next) {
|
||||
user.getUidByUserslug(userslug, next);
|
||||
},
|
||||
function (_uid, next) {
|
||||
if (!_uid) {
|
||||
return next(new Error('[[error:no-user]]'));
|
||||
}
|
||||
uid = _uid;
|
||||
user.auth.logAttempt(uid, req.ip, next);
|
||||
},
|
||||
function (next) {
|
||||
|
||||
async.parallel({
|
||||
userData: function (next) {
|
||||
db.getObjectFields('user:' + uid, ['password', 'passwordExpiry'], next);
|
||||
@@ -379,31 +380,19 @@ authenticationController.localLogin = function (req, username, password, next) {
|
||||
if (!result.isAdmin && parseInt(meta.config.allowLocalLogin, 10) === 0) {
|
||||
return next(new Error('[[error:local-login-disabled]]'));
|
||||
}
|
||||
if (!userData || !userData.password) {
|
||||
return next(new Error('[[error:invalid-user-data]]'));
|
||||
}
|
||||
|
||||
if (result.banned) {
|
||||
// Retrieve ban reason and show error
|
||||
return user.getLatestBanInfo(uid, function (err, banInfo) {
|
||||
if (err) {
|
||||
if (err.message === 'no-ban-info') {
|
||||
next(new Error('[[error:user-banned]]'));
|
||||
} else {
|
||||
next(err);
|
||||
}
|
||||
} else if (banInfo.reason) {
|
||||
next(new Error('[[error:user-banned-reason, ' + banInfo.reason + ']]'));
|
||||
} else {
|
||||
next(new Error('[[error:user-banned]]'));
|
||||
}
|
||||
});
|
||||
return banUser(uid, next);
|
||||
}
|
||||
|
||||
user.auth.logAttempt(uid, req.ip, next);
|
||||
},
|
||||
function (next) {
|
||||
Password.compare(password, userData.password, next);
|
||||
},
|
||||
function (passwordMatch, next) {
|
||||
if (!passwordMatch) {
|
||||
return next(new Error('[[error:invalid-password]]'));
|
||||
return next(new Error('[[error:invalid-login-credentials]]'));
|
||||
}
|
||||
user.auth.clearLoginAttempts(uid);
|
||||
next(null, userData, '[[success:authentication-successful]]');
|
||||
@@ -437,5 +426,23 @@ authenticationController.logout = function (req, res, next) {
|
||||
}
|
||||
};
|
||||
|
||||
function banUser(uid, next) {
|
||||
user.getLatestBanInfo(uid, function (err, banInfo) {
|
||||
if (err) {
|
||||
if (err.message === 'no-ban-info') {
|
||||
return next(new Error('[[error:user-banned]]'));
|
||||
}
|
||||
|
||||
module.exports = authenticationController;
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!banInfo.reason) {
|
||||
translator.translate('[[user:info.banned-no-reason]]', function (translated) {
|
||||
banInfo.reason = translated;
|
||||
next(new Error(banInfo.expiry ? '[[error:user-banned-reason-until, ' + banInfo.expiry_readable + ', ' + banInfo.reason + ']]' : '[[error:user-banned-reason, ' + banInfo.reason + ']]'));
|
||||
});
|
||||
} else {
|
||||
next(new Error(banInfo.expiry ? '[[error:user-banned-reason-until, ' + banInfo.expiry_readable + ', ' + banInfo.reason + ']]' : '[[error:user-banned-reason, ' + banInfo.reason + ']]'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,17 +21,6 @@ categoriesController.list = function (req, res, next) {
|
||||
content: 'website',
|
||||
}];
|
||||
|
||||
var ogImage = meta.config['og:image'] || meta.config['brand:logo'] || '';
|
||||
if (ogImage) {
|
||||
if (!ogImage.startsWith('http')) {
|
||||
ogImage = nconf.get('url') + ogImage;
|
||||
}
|
||||
res.locals.metaTags.push({
|
||||
property: 'og:image',
|
||||
content: ogImage,
|
||||
});
|
||||
}
|
||||
|
||||
var categoryData;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
|
||||
@@ -370,7 +370,3 @@ Controllers.termsOfUse = function (req, res, next) {
|
||||
termsOfUse: meta.config.termsOfUse,
|
||||
});
|
||||
};
|
||||
|
||||
Controllers.ping = function (req, res) {
|
||||
res.status(200).send(req.path === '/sping' ? 'healthy' : '200');
|
||||
};
|
||||
|
||||
@@ -3,24 +3,125 @@
|
||||
var async = require('async');
|
||||
|
||||
var user = require('../user');
|
||||
var adminFlagsController = require('./admin/flags');
|
||||
var categories = require('../categories');
|
||||
var flags = require('../flags');
|
||||
var analytics = require('../analytics');
|
||||
|
||||
var modsController = {};
|
||||
var modsController = {
|
||||
flags: {},
|
||||
};
|
||||
|
||||
modsController.flagged = function (req, res, next) {
|
||||
modsController.flags.list = function (req, res, next) {
|
||||
async.parallel({
|
||||
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
|
||||
moderatedCids: async.apply(user.getModeratedCids, req.uid),
|
||||
}, function (err, results) {
|
||||
if (err || !(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
if (!results.isAdminOrGlobalMod && results.moderatedCids.length) {
|
||||
res.locals.cids = results.moderatedCids;
|
||||
}
|
||||
|
||||
adminFlagsController.get(req, res, next);
|
||||
// Parse query string params for filters
|
||||
var hasFilter = false;
|
||||
var valid = ['assignee', 'state', 'reporterId', 'type', 'targetUid', 'cid', 'quick'];
|
||||
var filters = valid.reduce(function (memo, cur) {
|
||||
if (req.query.hasOwnProperty(cur)) {
|
||||
memo[cur] = req.query[cur];
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
hasFilter = !!Object.keys(filters).length;
|
||||
|
||||
if (res.locals.cids) {
|
||||
if (!filters.cid) {
|
||||
// If mod and no cid filter, add filter for their modded categories
|
||||
filters.cid = res.locals.cids;
|
||||
} else if (Array.isArray(filters.cid)) {
|
||||
// Remove cids they do not moderate
|
||||
filters.cid = filters.cid.filter(function (cid) {
|
||||
return res.locals.cids.indexOf(String(cid)) !== -1;
|
||||
});
|
||||
} else if (res.locals.cids.indexOf(String(filters.cid)) === -1) {
|
||||
filters.cid = res.locals.cids;
|
||||
hasFilter = false;
|
||||
}
|
||||
}
|
||||
|
||||
async.parallel({
|
||||
flags: async.apply(flags.list, filters, req.uid),
|
||||
analytics: async.apply(analytics.getDailyStatsForSet, 'analytics:flags', Date.now(), 30),
|
||||
categories: async.apply(categories.buildForSelect, req.uid),
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
// If res.locals.cids is populated, then slim down the categories list
|
||||
if (res.locals.cids) {
|
||||
data.categories = data.categories.filter(function (category) {
|
||||
return res.locals.cids.indexOf(String(category.cid)) !== -1;
|
||||
});
|
||||
}
|
||||
|
||||
// Minimal returned set for templates.js
|
||||
data.categories = data.categories.reduce(function (memo, cur) {
|
||||
if (!res.locals.cids) {
|
||||
memo[cur.cid] = cur.name;
|
||||
return memo;
|
||||
}
|
||||
|
||||
// If mod, remove categories they can't moderate
|
||||
if (res.locals.cids.indexOf(String(cur.cid)) !== -1) {
|
||||
memo[cur.cid] = cur.name;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
res.render('flags/list', {
|
||||
flags: data.flags,
|
||||
analytics: data.analytics,
|
||||
categories: data.categories,
|
||||
hasFilter: hasFilter,
|
||||
filters: filters,
|
||||
title: '[[pages:flags]]',
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
modsController.flags.detail = function (req, res, next) {
|
||||
async.parallel({
|
||||
isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, req.uid),
|
||||
moderatedCids: async.apply(user.getModeratedCids, req.uid),
|
||||
flagData: async.apply(flags.get, req.params.flagId),
|
||||
assignees: async.apply(user.getAdminsandGlobalModsandModerators),
|
||||
}, function (err, results) {
|
||||
if (err || !results.flagData) {
|
||||
return next(err || new Error('[[error:invalid-data]]'));
|
||||
} else if (!(results.isAdminOrGlobalMod || !!results.moderatedCids.length)) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
|
||||
res.render('flags/detail', Object.assign(results.flagData, {
|
||||
assignees: results.assignees,
|
||||
type_bool: ['post', 'user', 'empty'].reduce(function (memo, cur) {
|
||||
if (cur !== 'empty') {
|
||||
memo[cur] = results.flagData.type === cur && !!Object.keys(results.flagData.target).length;
|
||||
} else {
|
||||
memo[cur] = !Object.keys(results.flagData.target).length;
|
||||
}
|
||||
|
||||
return memo;
|
||||
}, {}),
|
||||
title: '[[pages:flag-details, ' + req.params.flagId + ']]',
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ searchController.search = function (req, res, next) {
|
||||
repliesFilter: req.query.repliesFilter,
|
||||
timeRange: req.query.timeRange,
|
||||
timeFilter: req.query.timeFilter,
|
||||
sortBy: req.query.sortBy,
|
||||
sortBy: req.query.sortBy || meta.config.searchDefaultSortBy || '',
|
||||
sortDirection: req.query.sortDirection,
|
||||
page: page,
|
||||
uid: req.uid,
|
||||
@@ -60,13 +60,14 @@ searchController.search = function (req, res, next) {
|
||||
|
||||
var searchData = results.search;
|
||||
searchData.categories = categoriesData;
|
||||
searchData.categoriesCount = results.categories.length;
|
||||
searchData.categoriesCount = Math.max(10, Math.min(20, categoriesData.length));
|
||||
searchData.pagination = pagination.create(page, searchData.pageCount, req.query);
|
||||
searchData.showAsPosts = !req.query.showAs || req.query.showAs === 'posts';
|
||||
searchData.showAsTopics = req.query.showAs === 'topics';
|
||||
searchData.title = '[[global:header.search]]';
|
||||
searchData.breadcrumbs = helpers.buildBreadcrumbs([{ text: '[[global:search]]' }]);
|
||||
searchData.expandSearch = !req.query.term;
|
||||
searchData.searchDefaultSortBy = meta.config.searchDefaultSortBy || '';
|
||||
|
||||
res.render('search', searchData);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
var validator = require('validator');
|
||||
|
||||
var user = require('../user');
|
||||
@@ -63,10 +61,6 @@ tagsController.getTag = function (req, res, next) {
|
||||
property: 'og:title',
|
||||
content: tag,
|
||||
},
|
||||
{
|
||||
property: 'og:url',
|
||||
content: nconf.get('url') + '/tags/' + tag,
|
||||
},
|
||||
];
|
||||
templateData.topics = topics;
|
||||
|
||||
|
||||
@@ -206,11 +206,6 @@ topicsController.get = function (req, res, callback) {
|
||||
property: 'og:type',
|
||||
content: 'article',
|
||||
},
|
||||
{
|
||||
property: 'og:url',
|
||||
content: nconf.get('url') + '/topic/' + topicData.slug + (req.params.post_index ? ('/' + req.params.post_index) : ''),
|
||||
noEscape: true,
|
||||
},
|
||||
{
|
||||
property: 'og:image',
|
||||
content: ogImageUrl,
|
||||
|
||||
@@ -40,7 +40,15 @@ unreadController.get = function (req, res, next) {
|
||||
settings = results.settings;
|
||||
var start = Math.max(0, (page - 1) * settings.topicsPerPage);
|
||||
var stop = start + settings.topicsPerPage - 1;
|
||||
topics.getUnreadTopics(cid, req.uid, start, stop, filter, next);
|
||||
var cutoff = req.session.unreadCutoff ? req.session.unreadCutoff : topics.unreadCutoff();
|
||||
topics.getUnreadTopics({
|
||||
cid: cid,
|
||||
uid: req.uid,
|
||||
start: start,
|
||||
stop: stop,
|
||||
filter: filter,
|
||||
cutoff: cutoff,
|
||||
}, next);
|
||||
},
|
||||
], function (err, data) {
|
||||
if (err) {
|
||||
|
||||
@@ -33,7 +33,7 @@ uploadsController.upload = function (req, res, filesIterator) {
|
||||
return res.status(500).json({ path: req.path, error: err.message });
|
||||
}
|
||||
|
||||
res.status(200).send(images);
|
||||
res.status(200).json(images);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -208,20 +208,19 @@ uploadsController.uploadFile = function (uid, uploadedFile, callback) {
|
||||
return callback(new Error('[[error:file-too-big, ' + meta.config.maximumFileSize + ']]'));
|
||||
}
|
||||
|
||||
if (meta.config.hasOwnProperty('allowedFileExtensions')) {
|
||||
var allowed = file.allowedExtensions();
|
||||
var extension = file.typeToExtension(uploadedFile.type);
|
||||
if (!extension || (allowed.length > 0 && allowed.indexOf(extension) === -1)) {
|
||||
return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]'));
|
||||
}
|
||||
var allowed = file.allowedExtensions();
|
||||
|
||||
var extension = path.extname(uploadedFile.name).toLowerCase();
|
||||
if (!extension || extension === '.' || (allowed.length > 0 && allowed.indexOf(extension) === -1)) {
|
||||
return callback(new Error('[[error:invalid-file-type, ' + allowed.join(', ') + ']]'));
|
||||
}
|
||||
|
||||
saveFileToLocal(uploadedFile, callback);
|
||||
};
|
||||
|
||||
function saveFileToLocal(uploadedFile, callback) {
|
||||
var extension = file.typeToExtension(uploadedFile.type);
|
||||
if (!extension) {
|
||||
var extension = path.extname(uploadedFile.name);
|
||||
if (!extension || extension === '.') {
|
||||
return callback(new Error('[[error:invalid-extension]]'));
|
||||
}
|
||||
var filename = uploadedFile.name || 'upload';
|
||||
|
||||
@@ -1,239 +1,244 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
(function (module) {
|
||||
var winston = require('winston');
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
var session = require('express-session');
|
||||
var _ = require('underscore');
|
||||
var semver = require('semver');
|
||||
var db;
|
||||
|
||||
_.mixin(require('underscore.deep'));
|
||||
var winston = require('winston');
|
||||
var async = require('async');
|
||||
var nconf = require('nconf');
|
||||
var session = require('express-session');
|
||||
var _ = require('underscore');
|
||||
var semver = require('semver');
|
||||
var db;
|
||||
|
||||
module.questions = [
|
||||
{
|
||||
name: 'mongo:host',
|
||||
description: 'Host IP or address of your MongoDB instance',
|
||||
default: nconf.get('mongo:host') || '127.0.0.1',
|
||||
},
|
||||
{
|
||||
name: 'mongo:port',
|
||||
description: 'Host port of your MongoDB instance',
|
||||
default: nconf.get('mongo:port') || 27017,
|
||||
},
|
||||
{
|
||||
name: 'mongo:username',
|
||||
description: 'MongoDB username',
|
||||
default: nconf.get('mongo:username') || '',
|
||||
},
|
||||
{
|
||||
name: 'mongo:password',
|
||||
description: 'Password of your MongoDB database',
|
||||
hidden: true,
|
||||
default: nconf.get('mongo:password') || '',
|
||||
before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; },
|
||||
},
|
||||
{
|
||||
name: 'mongo:database',
|
||||
description: 'MongoDB database name',
|
||||
default: nconf.get('mongo:database') || 'nodebb',
|
||||
},
|
||||
];
|
||||
_.mixin(require('underscore.deep'));
|
||||
|
||||
module.helpers = module.helpers || {};
|
||||
module.helpers.mongo = require('./mongo/helpers');
|
||||
var mongoModule = module.exports;
|
||||
|
||||
module.init = function (callback) {
|
||||
callback = callback || function () { };
|
||||
mongoModule.questions = [
|
||||
{
|
||||
name: 'mongo:host',
|
||||
description: 'Host IP or address of your MongoDB instance',
|
||||
default: nconf.get('mongo:host') || '127.0.0.1',
|
||||
},
|
||||
{
|
||||
name: 'mongo:port',
|
||||
description: 'Host port of your MongoDB instance',
|
||||
default: nconf.get('mongo:port') || 27017,
|
||||
},
|
||||
{
|
||||
name: 'mongo:username',
|
||||
description: 'MongoDB username',
|
||||
default: nconf.get('mongo:username') || '',
|
||||
},
|
||||
{
|
||||
name: 'mongo:password',
|
||||
description: 'Password of your MongoDB database',
|
||||
hidden: true,
|
||||
default: nconf.get('mongo:password') || '',
|
||||
before: function (value) { value = value || nconf.get('mongo:password') || ''; return value; },
|
||||
},
|
||||
{
|
||||
name: 'mongo:database',
|
||||
description: 'MongoDB database name',
|
||||
default: nconf.get('mongo:database') || 'nodebb',
|
||||
},
|
||||
];
|
||||
|
||||
var mongoClient = require('mongodb').MongoClient;
|
||||
mongoModule.helpers = mongoModule.helpers || {};
|
||||
mongoModule.helpers.mongo = require('./mongo/helpers');
|
||||
|
||||
var usernamePassword = '';
|
||||
if (nconf.get('mongo:username') && nconf.get('mongo:password')) {
|
||||
usernamePassword = nconf.get('mongo:username') + ':' + encodeURIComponent(nconf.get('mongo:password')) + '@';
|
||||
}
|
||||
mongoModule.init = function (callback) {
|
||||
callback = callback || function () { };
|
||||
|
||||
// Sensible defaults for Mongo, if not set
|
||||
if (!nconf.get('mongo:host')) {
|
||||
nconf.set('mongo:host', '127.0.0.1');
|
||||
}
|
||||
if (!nconf.get('mongo:port')) {
|
||||
nconf.set('mongo:port', 27017);
|
||||
}
|
||||
if (!nconf.get('mongo:database')) {
|
||||
nconf.set('mongo:database', 'nodebb');
|
||||
}
|
||||
var mongoClient = require('mongodb').MongoClient;
|
||||
|
||||
var hosts = nconf.get('mongo:host').split(',');
|
||||
var ports = nconf.get('mongo:port').toString().split(',');
|
||||
var servers = [];
|
||||
var usernamePassword = '';
|
||||
if (nconf.get('mongo:username') && nconf.get('mongo:password')) {
|
||||
usernamePassword = nconf.get('mongo:username') + ':' + encodeURIComponent(nconf.get('mongo:password')) + '@';
|
||||
}
|
||||
|
||||
for (var i = 0; i < hosts.length; i += 1) {
|
||||
servers.push(hosts[i] + ':' + ports[i]);
|
||||
}
|
||||
// Sensible defaults for Mongo, if not set
|
||||
if (!nconf.get('mongo:host')) {
|
||||
nconf.set('mongo:host', '127.0.0.1');
|
||||
}
|
||||
if (!nconf.get('mongo:port')) {
|
||||
nconf.set('mongo:port', 27017);
|
||||
}
|
||||
if (!nconf.get('mongo:database')) {
|
||||
nconf.set('mongo:database', 'nodebb');
|
||||
}
|
||||
|
||||
var connString = 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database');
|
||||
var hosts = nconf.get('mongo:host').split(',');
|
||||
var ports = nconf.get('mongo:port').toString().split(',');
|
||||
var servers = [];
|
||||
|
||||
var connOptions = {
|
||||
poolSize: 10,
|
||||
reconnectTries: 3600,
|
||||
reconnectInterval: 1000,
|
||||
autoReconnect: true,
|
||||
};
|
||||
for (var i = 0; i < hosts.length; i += 1) {
|
||||
servers.push(hosts[i] + ':' + ports[i]);
|
||||
}
|
||||
|
||||
connOptions = _.deepExtend(connOptions, nconf.get('mongo:options') || {});
|
||||
var connString = 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database');
|
||||
|
||||
mongoClient.connect(connString, connOptions, function (err, _db) {
|
||||
if (err) {
|
||||
winston.error('NodeBB could not connect to your Mongo database. Mongo returned the following error: ' + err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db = _db;
|
||||
|
||||
module.client = db;
|
||||
|
||||
require('./mongo/main')(db, module);
|
||||
require('./mongo/hash')(db, module);
|
||||
require('./mongo/sets')(db, module);
|
||||
require('./mongo/sorted')(db, module);
|
||||
require('./mongo/list')(db, module);
|
||||
|
||||
if (nconf.get('mongo:password') && nconf.get('mongo:username')) {
|
||||
db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) {
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
winston.warn('You have no mongo password setup!');
|
||||
callback();
|
||||
}
|
||||
});
|
||||
var connOptions = {
|
||||
poolSize: 10,
|
||||
reconnectTries: 3600,
|
||||
reconnectInterval: 1000,
|
||||
autoReconnect: true,
|
||||
};
|
||||
|
||||
module.initSessionStore = function (callback) {
|
||||
var meta = require('../meta');
|
||||
var sessionStore;
|
||||
connOptions = _.deepExtend(connOptions, nconf.get('mongo:options') || {});
|
||||
|
||||
var ttl = meta.getSessionTTLSeconds();
|
||||
mongoClient.connect(connString, connOptions, function (err, _db) {
|
||||
if (err) {
|
||||
winston.error('NodeBB could not connect to your Mongo database. Mongo returned the following error: ' + err.message);
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (nconf.get('redis')) {
|
||||
sessionStore = require('connect-redis')(session);
|
||||
var rdb = require('./redis');
|
||||
rdb.client = rdb.connect();
|
||||
db = _db;
|
||||
|
||||
module.sessionStore = new sessionStore({
|
||||
client: rdb.client,
|
||||
ttl: ttl,
|
||||
mongoModule.client = db;
|
||||
|
||||
require('./mongo/main')(db, mongoModule);
|
||||
require('./mongo/hash')(db, mongoModule);
|
||||
require('./mongo/sets')(db, mongoModule);
|
||||
require('./mongo/sorted')(db, mongoModule);
|
||||
require('./mongo/list')(db, mongoModule);
|
||||
|
||||
if (nconf.get('mongo:password') && nconf.get('mongo:username')) {
|
||||
db.authenticate(nconf.get('mongo:username'), nconf.get('mongo:password'), function (err) {
|
||||
callback(err);
|
||||
});
|
||||
} else if (nconf.get('mongo')) {
|
||||
sessionStore = require('connect-mongo')(session);
|
||||
module.sessionStore = new sessionStore({
|
||||
db: db,
|
||||
ttl: ttl,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
module.createIndices = function (callback) {
|
||||
function createIndex(collection, index, options, callback) {
|
||||
module.client.collection(collection).createIndex(index, options, callback);
|
||||
}
|
||||
|
||||
if (!module.client) {
|
||||
winston.warn('[database/createIndices] database not initialized');
|
||||
return callback();
|
||||
}
|
||||
|
||||
winston.info('[database] Checking database indices.');
|
||||
async.series([
|
||||
async.apply(createIndex, 'objects', { _key: 1, score: -1 }, { background: true }),
|
||||
async.apply(createIndex, 'objects', { _key: 1, value: -1 }, { background: true, unique: true, sparse: true }),
|
||||
async.apply(createIndex, 'objects', { expireAt: 1 }, { expireAfterSeconds: 0, background: true }),
|
||||
], function (err) {
|
||||
if (err) {
|
||||
winston.error('Error creating index ' + err.message);
|
||||
return callback(err);
|
||||
}
|
||||
winston.info('[database] Checking database indices done!');
|
||||
} else {
|
||||
winston.warn('You have no mongo password setup!');
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
module.checkCompatibility = function (callback) {
|
||||
var mongoPkg = require('mongodb/package.json');
|
||||
|
||||
if (semver.lt(mongoPkg.version, '2.0.0')) {
|
||||
return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
mongoModule.initSessionStore = function (callback) {
|
||||
var meta = require('../meta');
|
||||
var sessionStore;
|
||||
|
||||
var ttl = meta.getSessionTTLSeconds();
|
||||
|
||||
if (nconf.get('redis')) {
|
||||
sessionStore = require('connect-redis')(session);
|
||||
var rdb = require('./redis');
|
||||
rdb.client = rdb.connect();
|
||||
|
||||
mongoModule.sessionStore = new sessionStore({
|
||||
client: rdb.client,
|
||||
ttl: ttl,
|
||||
});
|
||||
} else if (nconf.get('mongo')) {
|
||||
sessionStore = require('connect-mongo')(session);
|
||||
mongoModule.sessionStore = new sessionStore({
|
||||
db: db,
|
||||
ttl: ttl,
|
||||
});
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
mongoModule.createIndices = function (callback) {
|
||||
function createIndex(collection, index, options, callback) {
|
||||
mongoModule.client.collection(collection).createIndex(index, options, callback);
|
||||
}
|
||||
|
||||
if (!mongoModule.client) {
|
||||
winston.warn('[database/createIndices] database not initialized');
|
||||
return callback();
|
||||
}
|
||||
|
||||
winston.info('[database] Checking database indices.');
|
||||
async.series([
|
||||
async.apply(createIndex, 'objects', { _key: 1, score: -1 }, { background: true }),
|
||||
async.apply(createIndex, 'objects', { _key: 1, value: -1 }, { background: true, unique: true, sparse: true }),
|
||||
async.apply(createIndex, 'objects', { expireAt: 1 }, { expireAfterSeconds: 0, background: true }),
|
||||
], function (err) {
|
||||
if (err) {
|
||||
winston.error('Error creating index ' + err.message);
|
||||
return callback(err);
|
||||
}
|
||||
winston.info('[database] Checking database indices done!');
|
||||
callback();
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
module.info = function (db, callback) {
|
||||
if (!db) {
|
||||
return callback();
|
||||
}
|
||||
async.parallel({
|
||||
serverStatus: function (next) {
|
||||
db.command({ serverStatus: 1 }, next);
|
||||
},
|
||||
stats: function (next) {
|
||||
db.command({ dbStats: 1 }, next);
|
||||
},
|
||||
listCollections: function (next) {
|
||||
db.listCollections().toArray(function (err, items) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
async.map(items, function (collection, next) {
|
||||
db.collection(collection.name).stats(next);
|
||||
}, next);
|
||||
});
|
||||
},
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var stats = results.stats;
|
||||
var scale = 1024 * 1024;
|
||||
mongoModule.checkCompatibility = function (callback) {
|
||||
var mongoPkg = require('mongodb/package.json');
|
||||
|
||||
results.listCollections = results.listCollections.map(function (collectionInfo) {
|
||||
return {
|
||||
name: collectionInfo.ns,
|
||||
count: collectionInfo.count,
|
||||
size: collectionInfo.size,
|
||||
avgObjSize: collectionInfo.avgObjSize,
|
||||
storageSize: collectionInfo.storageSize,
|
||||
totalIndexSize: collectionInfo.totalIndexSize,
|
||||
indexSizes: collectionInfo.indexSizes,
|
||||
};
|
||||
if (semver.lt(mongoPkg.version, '2.0.0')) {
|
||||
return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
|
||||
}
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
mongoModule.info = function (db, callback) {
|
||||
if (!db) {
|
||||
return callback();
|
||||
}
|
||||
async.parallel({
|
||||
serverStatus: function (next) {
|
||||
db.command({ serverStatus: 1 }, next);
|
||||
},
|
||||
stats: function (next) {
|
||||
db.command({ dbStats: 1 }, next);
|
||||
},
|
||||
listCollections: function (next) {
|
||||
db.listCollections().toArray(function (err, items) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
async.map(items, function (collection, next) {
|
||||
db.collection(collection.name).stats(next);
|
||||
}, next);
|
||||
});
|
||||
},
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
var stats = results.stats;
|
||||
var scale = 1024 * 1024 * 1024;
|
||||
|
||||
stats.mem = results.serverStatus.mem;
|
||||
stats.collectionData = results.listCollections;
|
||||
stats.network = results.serverStatus.network;
|
||||
stats.raw = JSON.stringify(stats, null, 4);
|
||||
|
||||
stats.avgObjSize = stats.avgObjSize.toFixed(2);
|
||||
stats.dataSize = (stats.dataSize / scale).toFixed(2);
|
||||
stats.storageSize = (stats.storageSize / scale).toFixed(2);
|
||||
stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
|
||||
stats.indexSize = (stats.indexSize / scale).toFixed(2);
|
||||
stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
|
||||
stats.host = results.serverStatus.host;
|
||||
stats.version = results.serverStatus.version;
|
||||
stats.uptime = results.serverStatus.uptime;
|
||||
stats.mongo = true;
|
||||
|
||||
callback(null, stats);
|
||||
results.listCollections = results.listCollections.map(function (collectionInfo) {
|
||||
return {
|
||||
name: collectionInfo.ns,
|
||||
count: collectionInfo.count,
|
||||
size: collectionInfo.size,
|
||||
avgObjSize: collectionInfo.avgObjSize,
|
||||
storageSize: collectionInfo.storageSize,
|
||||
totalIndexSize: collectionInfo.totalIndexSize,
|
||||
indexSizes: collectionInfo.indexSizes,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
module.close = function () {
|
||||
db.close();
|
||||
};
|
||||
}(exports));
|
||||
stats.mem = results.serverStatus.mem;
|
||||
stats.mem = results.serverStatus.mem;
|
||||
stats.mem.resident = (stats.mem.resident / 1024).toFixed(2);
|
||||
stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2);
|
||||
stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2);
|
||||
stats.collectionData = results.listCollections;
|
||||
stats.network = results.serverStatus.network;
|
||||
stats.raw = JSON.stringify(stats, null, 4);
|
||||
|
||||
stats.avgObjSize = stats.avgObjSize.toFixed(2);
|
||||
stats.dataSize = (stats.dataSize / scale).toFixed(2);
|
||||
stats.storageSize = (stats.storageSize / scale).toFixed(2);
|
||||
stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
|
||||
stats.indexSize = (stats.indexSize / scale).toFixed(2);
|
||||
stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
|
||||
stats.host = results.serverStatus.host;
|
||||
stats.version = results.serverStatus.version;
|
||||
stats.uptime = results.serverStatus.uptime;
|
||||
stats.mongo = true;
|
||||
|
||||
callback(null, stats);
|
||||
});
|
||||
};
|
||||
|
||||
mongoModule.close = function () {
|
||||
db.close();
|
||||
};
|
||||
|
||||
@@ -8,7 +8,9 @@ module.exports = function (db, module) {
|
||||
if (!key || !data) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('')) {
|
||||
delete data[''];
|
||||
}
|
||||
db.collection('objects').update({ _key: key }, { $set: data }, { upsert: true, w: 1 }, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
@@ -241,7 +241,7 @@ module.exports = function (db, module) {
|
||||
|
||||
module.sortedSetScore = function (key, value, callback) {
|
||||
if (!key) {
|
||||
return callback();
|
||||
return callback(null, null);
|
||||
}
|
||||
value = helpers.valueToString(value);
|
||||
db.collection('objects').findOne({ _key: key, value: value }, { fields: { _id: 0, score: 1 } }, function (err, result) {
|
||||
@@ -274,7 +274,7 @@ module.exports = function (db, module) {
|
||||
|
||||
module.sortedSetScores = function (key, values, callback) {
|
||||
if (!key) {
|
||||
return callback();
|
||||
return callback(null, null);
|
||||
}
|
||||
values = values.map(helpers.valueToString);
|
||||
db.collection('objects').find({ _key: key, value: { $in: values } }, { _id: 0, value: 1, score: 1 }).toArray(function (err, result) {
|
||||
|
||||
@@ -1,166 +1,164 @@
|
||||
'use strict';
|
||||
|
||||
(function (module) {
|
||||
var winston = require('winston');
|
||||
var nconf = require('nconf');
|
||||
var semver = require('semver');
|
||||
var session = require('express-session');
|
||||
var redis;
|
||||
var redisClient;
|
||||
var _ = require('underscore');
|
||||
var winston = require('winston');
|
||||
var nconf = require('nconf');
|
||||
var semver = require('semver');
|
||||
var session = require('express-session');
|
||||
var redis = require('redis');
|
||||
var redisClient;
|
||||
|
||||
module.questions = [
|
||||
{
|
||||
name: 'redis:host',
|
||||
description: 'Host IP or address of your Redis instance',
|
||||
default: nconf.get('redis:host') || '127.0.0.1',
|
||||
},
|
||||
{
|
||||
name: 'redis:port',
|
||||
description: 'Host port of your Redis instance',
|
||||
default: nconf.get('redis:port') || 6379,
|
||||
},
|
||||
{
|
||||
name: 'redis:password',
|
||||
description: 'Password of your Redis database',
|
||||
hidden: true,
|
||||
default: nconf.get('redis:password') || '',
|
||||
before: function (value) { value = value || nconf.get('redis:password') || ''; return value; },
|
||||
},
|
||||
{
|
||||
name: 'redis:database',
|
||||
description: 'Which database to use (0..n)',
|
||||
default: nconf.get('redis:database') || 0,
|
||||
},
|
||||
];
|
||||
_.mixin(require('underscore.deep'));
|
||||
|
||||
module.init = function (callback) {
|
||||
try {
|
||||
redis = require('redis');
|
||||
} catch (err) {
|
||||
winston.error('Unable to initialize Redis! Is Redis installed? Error :' + err.message);
|
||||
process.exit();
|
||||
}
|
||||
var redisModule = module.exports;
|
||||
|
||||
redisClient = module.connect();
|
||||
redisModule.questions = [
|
||||
{
|
||||
name: 'redis:host',
|
||||
description: 'Host IP or address of your Redis instance',
|
||||
default: nconf.get('redis:host') || '127.0.0.1',
|
||||
},
|
||||
{
|
||||
name: 'redis:port',
|
||||
description: 'Host port of your Redis instance',
|
||||
default: nconf.get('redis:port') || 6379,
|
||||
},
|
||||
{
|
||||
name: 'redis:password',
|
||||
description: 'Password of your Redis database',
|
||||
hidden: true,
|
||||
default: nconf.get('redis:password') || '',
|
||||
before: function (value) { value = value || nconf.get('redis:password') || ''; return value; },
|
||||
},
|
||||
{
|
||||
name: 'redis:database',
|
||||
description: 'Which database to use (0..n)',
|
||||
default: nconf.get('redis:database') || 0,
|
||||
},
|
||||
];
|
||||
|
||||
module.client = redisClient;
|
||||
redisModule.init = function (callback) {
|
||||
redisClient = redisModule.connect();
|
||||
|
||||
require('./redis/main')(redisClient, module);
|
||||
require('./redis/hash')(redisClient, module);
|
||||
require('./redis/sets')(redisClient, module);
|
||||
require('./redis/sorted')(redisClient, module);
|
||||
require('./redis/list')(redisClient, module);
|
||||
redisModule.client = redisClient;
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
require('./redis/main')(redisClient, redisModule);
|
||||
require('./redis/hash')(redisClient, redisModule);
|
||||
require('./redis/sets')(redisClient, redisModule);
|
||||
require('./redis/sorted')(redisClient, redisModule);
|
||||
require('./redis/list')(redisClient, redisModule);
|
||||
|
||||
module.initSessionStore = function (callback) {
|
||||
var meta = require('../meta');
|
||||
var sessionStore = require('connect-redis')(session);
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
module.sessionStore = new sessionStore({
|
||||
client: module.client,
|
||||
ttl: meta.getSessionTTLSeconds(),
|
||||
});
|
||||
redisModule.initSessionStore = function (callback) {
|
||||
var meta = require('../meta');
|
||||
var sessionStore = require('connect-redis')(session);
|
||||
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
redisModule.sessionStore = new sessionStore({
|
||||
client: redisModule.client,
|
||||
ttl: meta.getSessionTTLSeconds(),
|
||||
});
|
||||
|
||||
module.connect = function (options) {
|
||||
var redis_socket_or_host = nconf.get('redis:host');
|
||||
var cxn;
|
||||
if (typeof callback === 'function') {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
if (!redis) {
|
||||
redis = require('redis');
|
||||
}
|
||||
redisModule.connect = function (options) {
|
||||
var redis_socket_or_host = nconf.get('redis:host');
|
||||
var cxn;
|
||||
|
||||
options = options || {};
|
||||
if (nconf.get('redis:password')) {
|
||||
options.auth_pass = nconf.get('redis:password');
|
||||
}
|
||||
if (!redis) {
|
||||
redis = require('redis');
|
||||
}
|
||||
|
||||
if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) {
|
||||
/* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */
|
||||
cxn = redis.createClient(nconf.get('redis:host'), options);
|
||||
} else {
|
||||
/* Else, connect over tcp/ip */
|
||||
cxn = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host'), options);
|
||||
}
|
||||
options = options || {};
|
||||
|
||||
cxn.on('error', function (err) {
|
||||
winston.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
if (nconf.get('redis:password')) {
|
||||
options.auth_pass = nconf.get('redis:password');
|
||||
}
|
||||
|
||||
if (nconf.get('redis:password')) {
|
||||
cxn.auth(nconf.get('redis:password'));
|
||||
}
|
||||
options = _.deepExtend(options, nconf.get('redis:options') || {});
|
||||
|
||||
var dbIdx = parseInt(nconf.get('redis:database'), 10);
|
||||
if (dbIdx) {
|
||||
cxn.select(dbIdx, function (error) {
|
||||
if (error) {
|
||||
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + error.message);
|
||||
process.exit();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (redis_socket_or_host && redis_socket_or_host.indexOf('/') >= 0) {
|
||||
/* If redis.host contains a path name character, use the unix dom sock connection. ie, /tmp/redis.sock */
|
||||
cxn = redis.createClient(nconf.get('redis:host'), options);
|
||||
} else {
|
||||
/* Else, connect over tcp/ip */
|
||||
cxn = redis.createClient(nconf.get('redis:port'), nconf.get('redis:host'), options);
|
||||
}
|
||||
|
||||
return cxn;
|
||||
};
|
||||
cxn.on('error', function (err) {
|
||||
winston.error(err.stack);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
module.createIndices = function (callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
if (nconf.get('redis:password')) {
|
||||
cxn.auth(nconf.get('redis:password'));
|
||||
}
|
||||
|
||||
module.checkCompatibility = function (callback) {
|
||||
module.info(module.client, function (err, info) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
var dbIdx = parseInt(nconf.get('redis:database'), 10);
|
||||
if (dbIdx) {
|
||||
cxn.select(dbIdx, function (error) {
|
||||
if (error) {
|
||||
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + error.message);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
if (semver.lt(info.redis_version, '2.8.9')) {
|
||||
return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
module.close = function () {
|
||||
redisClient.quit();
|
||||
};
|
||||
return cxn;
|
||||
};
|
||||
|
||||
module.info = function (cxn, callback) {
|
||||
if (!cxn) {
|
||||
return callback();
|
||||
redisModule.createIndices = function (callback) {
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
redisModule.checkCompatibility = function (callback) {
|
||||
redisModule.info(redisModule.client, function (err, info) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
cxn.info(function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
||||
if (semver.lt(info.redis_version, '2.8.9')) {
|
||||
return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
redisModule.close = function () {
|
||||
redisClient.quit();
|
||||
};
|
||||
|
||||
redisModule.info = function (cxn, callback) {
|
||||
if (!cxn) {
|
||||
return callback();
|
||||
}
|
||||
cxn.info(function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var lines = data.toString().split('\r\n').sort();
|
||||
var redisData = {};
|
||||
lines.forEach(function (line) {
|
||||
var parts = line.split(':');
|
||||
if (parts[1]) {
|
||||
redisData[parts[0]] = parts[1];
|
||||
}
|
||||
|
||||
var lines = data.toString().split('\r\n').sort();
|
||||
var redisData = {};
|
||||
lines.forEach(function (line) {
|
||||
var parts = line.split(':');
|
||||
if (parts[1]) {
|
||||
redisData[parts[0]] = parts[1];
|
||||
}
|
||||
});
|
||||
|
||||
redisData.raw = JSON.stringify(redisData, null, 4);
|
||||
redisData.redis = true;
|
||||
|
||||
callback(null, redisData);
|
||||
});
|
||||
};
|
||||
redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2);
|
||||
redisData.raw = JSON.stringify(redisData, null, 4);
|
||||
redisData.redis = true;
|
||||
|
||||
module.helpers = module.helpers || {};
|
||||
module.helpers.redis = require('./redis/helpers');
|
||||
}(exports));
|
||||
callback(null, redisData);
|
||||
});
|
||||
};
|
||||
|
||||
redisModule.helpers = redisModule.helpers || {};
|
||||
redisModule.helpers.redis = require('./redis/helpers');
|
||||
|
||||
@@ -8,11 +8,17 @@ module.exports = function (redisClient, module) {
|
||||
if (!key || !data) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (data.hasOwnProperty('')) {
|
||||
delete data[''];
|
||||
}
|
||||
|
||||
Object.keys(data).forEach(function (key) {
|
||||
if (data[key] === undefined) {
|
||||
delete data[key];
|
||||
}
|
||||
});
|
||||
|
||||
redisClient.hmset(key, data, function (err) {
|
||||
callback(err);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var helpers = {};
|
||||
var helpers = module.exports;
|
||||
|
||||
helpers.multiKeys = function (redisClient, command, keys, callback) {
|
||||
callback = callback || function () {};
|
||||
@@ -15,7 +15,7 @@ helpers.multiKeysValue = function (redisClient, command, keys, value, callback)
|
||||
callback = callback || function () {};
|
||||
var multi = redisClient.multi();
|
||||
for (var i = 0; i < keys.length; i += 1) {
|
||||
multi[command](keys[i], value);
|
||||
multi[command](String(keys[i]), String(value));
|
||||
}
|
||||
multi.exec(callback);
|
||||
};
|
||||
@@ -24,7 +24,7 @@ helpers.multiKeyValues = function (redisClient, command, key, values, callback)
|
||||
callback = callback || function () {};
|
||||
var multi = redisClient.multi();
|
||||
for (var i = 0; i < values.length; i += 1) {
|
||||
multi[command](key, values[i]);
|
||||
multi[command](String(key), String(values[i]));
|
||||
}
|
||||
multi.exec(callback);
|
||||
};
|
||||
@@ -35,5 +35,3 @@ helpers.resultsToBool = function (results) {
|
||||
}
|
||||
return results;
|
||||
};
|
||||
|
||||
module.exports = helpers;
|
||||
|
||||
@@ -124,8 +124,12 @@ module.exports = function (redisClient, module) {
|
||||
};
|
||||
|
||||
module.sortedSetScore = function (key, value, callback) {
|
||||
if (!key || value === undefined) {
|
||||
return callback(null, null);
|
||||
}
|
||||
|
||||
redisClient.zscore(key, value, function (err, score) {
|
||||
callback(err, !err ? parseFloat(score) : undefined);
|
||||
callback(err, !err ? parseFloat(score) : null);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
module.exports = function (redisClient, module) {
|
||||
module.sortedSetAdd = function (key, score, value, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!key) {
|
||||
return setImmediate(callback);
|
||||
}
|
||||
if (Array.isArray(score) && Array.isArray(value)) {
|
||||
return sortedSetAddMulti(key, score, value, callback);
|
||||
}
|
||||
@@ -33,10 +36,15 @@ module.exports = function (redisClient, module) {
|
||||
|
||||
module.sortedSetsAdd = function (keys, score, value, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!Array.isArray(keys) || !keys.length) {
|
||||
return callback();
|
||||
}
|
||||
var multi = redisClient.multi();
|
||||
|
||||
for (var i = 0; i < keys.length; i += 1) {
|
||||
multi.zadd(keys[i], score, value);
|
||||
if (keys[i]) {
|
||||
multi.zadd(keys[i], score, value);
|
||||
}
|
||||
}
|
||||
|
||||
multi.exec(function (err) {
|
||||
|
||||
22
src/file.js
22
src/file.js
@@ -81,7 +81,7 @@ file.allowedExtensions = function () {
|
||||
if (!extension.startsWith('.')) {
|
||||
extension = '.' + extension;
|
||||
}
|
||||
return extension;
|
||||
return extension.toLowerCase();
|
||||
});
|
||||
|
||||
if (allowedExtensions.indexOf('.jpg') !== -1 && allowedExtensions.indexOf('.jpeg') === -1) {
|
||||
@@ -92,20 +92,28 @@ file.allowedExtensions = function () {
|
||||
};
|
||||
|
||||
file.exists = function (path, callback) {
|
||||
fs.stat(path, function (err, stat) {
|
||||
callback(!err && stat);
|
||||
fs.stat(path, function (err) {
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
return callback(null, true);
|
||||
});
|
||||
};
|
||||
|
||||
file.existsSync = function (path) {
|
||||
var exists = false;
|
||||
try {
|
||||
exists = fs.statSync(path);
|
||||
fs.statSync(path);
|
||||
} catch (err) {
|
||||
exists = false;
|
||||
if (err.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return !!exists;
|
||||
return true;
|
||||
};
|
||||
|
||||
file.link = function link(filePath, destPath, cb) {
|
||||
|
||||
686
src/flags.js
Normal file
686
src/flags.js
Normal file
@@ -0,0 +1,686 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var db = require('./database');
|
||||
var user = require('./user');
|
||||
var groups = require('./groups');
|
||||
var meta = require('./meta');
|
||||
var notifications = require('./notifications');
|
||||
var analytics = require('./analytics');
|
||||
var topics = require('./topics');
|
||||
var posts = require('./posts');
|
||||
var privileges = require('./privileges');
|
||||
var plugins = require('./plugins');
|
||||
var utils = require('../public/src/utils');
|
||||
var _ = require('underscore');
|
||||
var S = require('string');
|
||||
|
||||
var Flags = {};
|
||||
|
||||
Flags.get = function (flagId, callback) {
|
||||
async.waterfall([
|
||||
// First stage
|
||||
async.apply(async.parallel, {
|
||||
base: async.apply(db.getObject.bind(db), 'flag:' + flagId),
|
||||
history: async.apply(Flags.getHistory, flagId),
|
||||
notes: async.apply(Flags.getNotes, flagId),
|
||||
}),
|
||||
function (data, next) {
|
||||
// Second stage
|
||||
async.parallel({
|
||||
userObj: async.apply(user.getUserFields, data.base.uid, ['username', 'userslug', 'picture']),
|
||||
targetObj: async.apply(Flags.getTarget, data.base.type, data.base.targetId, data.base.uid),
|
||||
}, function (err, payload) {
|
||||
// Final object return construction
|
||||
next(err, Object.assign(data.base, {
|
||||
datetimeISO: new Date(parseInt(data.base.datetime, 10)).toISOString(),
|
||||
target_readable: data.base.type.charAt(0).toUpperCase() + data.base.type.slice(1) + ' ' + data.base.targetId,
|
||||
target: payload.targetObj,
|
||||
history: data.history,
|
||||
notes: data.notes,
|
||||
reporter: payload.userObj,
|
||||
}));
|
||||
});
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.list = function (filters, uid, callback) {
|
||||
if (typeof filters === 'function' && !uid && !callback) {
|
||||
callback = filters;
|
||||
filters = {};
|
||||
}
|
||||
|
||||
var sets = [];
|
||||
var orSets = [];
|
||||
var prepareSets = function (setPrefix, value) {
|
||||
if (!Array.isArray(value)) {
|
||||
sets.push(setPrefix + value);
|
||||
} else if (value.length) {
|
||||
value.forEach(function (x) {
|
||||
orSets.push(setPrefix + x);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (Object.keys(filters).length > 0) {
|
||||
for (var type in filters) {
|
||||
if (filters.hasOwnProperty(type)) {
|
||||
switch (type) {
|
||||
case 'type':
|
||||
prepareSets('flags:byType:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'state':
|
||||
prepareSets('flags:byState:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'reporterId':
|
||||
prepareSets('flags:byReporter:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
prepareSets('flags:byAssignee:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'targetUid':
|
||||
prepareSets('flags:byTargetUid:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'cid':
|
||||
prepareSets('flags:byCid:', filters[type]);
|
||||
break;
|
||||
|
||||
case 'quick':
|
||||
switch (filters.quick) {
|
||||
case 'mine':
|
||||
sets.push('flags:byAssignee:' + uid);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sets = (sets.length || orSets.length) ? sets : ['flags:datetime']; // No filter default
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (sets.length === 1) {
|
||||
db.getSortedSetRevRange(sets[0], 0, -1, next);
|
||||
} else if (sets.length > 1) {
|
||||
db.getSortedSetRevIntersect({ sets: sets, start: 0, stop: -1, aggregate: 'MAX' }, next);
|
||||
} else {
|
||||
next(null, []);
|
||||
}
|
||||
},
|
||||
function (flagIds, next) {
|
||||
// Find flags according to "or" rules, if any
|
||||
if (orSets.length) {
|
||||
db.getSortedSetRevUnion({ sets: orSets, start: 0, stop: -1, aggregate: 'MAX' }, function (err, _flagIds) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (sets.length) {
|
||||
// If flag ids are already present, return a subset of flags that are in both sets
|
||||
next(null, _.intersection(flagIds, _flagIds));
|
||||
} else {
|
||||
// Otherwise, return all flags returned via orSets
|
||||
next(null, _.union(flagIds, _flagIds));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setImmediate(next, null, flagIds);
|
||||
}
|
||||
},
|
||||
function (flagIds, next) {
|
||||
async.map(flagIds, function (flagId, next) {
|
||||
async.waterfall([
|
||||
async.apply(db.getObject, 'flag:' + flagId),
|
||||
function (flagObj, next) {
|
||||
user.getUserFields(flagObj.uid, ['username', 'picture'], function (err, userObj) {
|
||||
next(err, Object.assign(flagObj, {
|
||||
reporter: {
|
||||
username: userObj.username,
|
||||
picture: userObj.picture,
|
||||
'icon:bgColor': userObj['icon:bgColor'],
|
||||
'icon:text': userObj['icon:text'],
|
||||
},
|
||||
}));
|
||||
});
|
||||
},
|
||||
], function (err, flagObj) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
switch (flagObj.state) {
|
||||
case 'open':
|
||||
flagObj.labelClass = 'info';
|
||||
break;
|
||||
case 'wip':
|
||||
flagObj.labelClass = 'warning';
|
||||
break;
|
||||
case 'resolved':
|
||||
flagObj.labelClass = 'success';
|
||||
break;
|
||||
case 'rejected':
|
||||
flagObj.labelClass = 'danger';
|
||||
break;
|
||||
}
|
||||
|
||||
next(null, Object.assign(flagObj, {
|
||||
target_readable: flagObj.type.charAt(0).toUpperCase() + flagObj.type.slice(1) + ' ' + flagObj.targetId,
|
||||
datetimeISO: new Date(parseInt(flagObj.datetime, 10)).toISOString(),
|
||||
}));
|
||||
});
|
||||
}, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.validate = function (payload, callback) {
|
||||
async.parallel({
|
||||
targetExists: async.apply(Flags.targetExists, payload.type, payload.id),
|
||||
target: async.apply(Flags.getTarget, payload.type, payload.id, payload.uid),
|
||||
reporter: async.apply(user.getUserData, payload.uid),
|
||||
}, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (data.target.deleted) {
|
||||
return callback(new Error('[[error:post-deleted]]'));
|
||||
} else if (parseInt(data.reporter.banned, 10)) {
|
||||
return callback(new Error('[[error:user-banned]]'));
|
||||
}
|
||||
|
||||
switch (payload.type) {
|
||||
case 'post':
|
||||
privileges.posts.canEdit(payload.id, payload.uid, function (err, editable) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
// Check if reporter meets rep threshold (or can edit the target post, in which case threshold does not apply)
|
||||
if (!editable.flag && parseInt(data.reporter.reputation, 10) < minimumReputation) {
|
||||
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
privileges.users.canEdit(payload.uid, payload.id, function (err, editable) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
// Check if reporter meets rep threshold (or can edit the target user, in which case threshold does not apply)
|
||||
if (!editable && parseInt(data.reporter.reputation, 10) < minimumReputation) {
|
||||
return callback(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
callback();
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Flags.getNotes = function (flagId, callback) {
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':notes', 0, -1),
|
||||
function (notes, next) {
|
||||
var uids = [];
|
||||
var noteObj;
|
||||
notes = notes.map(function (note) {
|
||||
try {
|
||||
noteObj = JSON.parse(note.value);
|
||||
uids.push(noteObj[0]);
|
||||
return {
|
||||
uid: noteObj[0],
|
||||
content: noteObj[1],
|
||||
datetime: note.score,
|
||||
datetimeISO: new Date(parseInt(note.score, 10)).toISOString(),
|
||||
};
|
||||
} catch (e) {
|
||||
return next(e);
|
||||
}
|
||||
});
|
||||
next(null, notes, uids);
|
||||
},
|
||||
function (notes, uids, next) {
|
||||
user.getUsersFields(uids, ['username', 'userslug', 'picture'], function (err, users) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next(null, notes.map(function (note, idx) {
|
||||
note.user = users[idx];
|
||||
return note;
|
||||
}));
|
||||
});
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.create = function (type, id, uid, reason, timestamp, callback) {
|
||||
var targetUid;
|
||||
var targetCid;
|
||||
var doHistoryAppend = false;
|
||||
|
||||
// timestamp is optional
|
||||
if (typeof timestamp === 'function' && !callback) {
|
||||
callback = timestamp;
|
||||
timestamp = Date.now();
|
||||
doHistoryAppend = true;
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
// Sanity checks
|
||||
async.apply(Flags.exists, type, id, uid),
|
||||
async.apply(Flags.targetExists, type, id),
|
||||
|
||||
// Extra data for zset insertion
|
||||
async.apply(Flags.getTargetUid, type, id),
|
||||
async.apply(Flags.getTargetCid, type, id),
|
||||
], function (err, checks) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
targetUid = checks[2] || null;
|
||||
targetCid = checks[3] || null;
|
||||
|
||||
if (checks[0]) {
|
||||
return next(new Error('[[error:already-flagged]]'));
|
||||
} else if (!checks[1]) {
|
||||
return next(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
},
|
||||
async.apply(db.incrObjectField, 'global', 'nextFlagId'),
|
||||
function (flagId, next) {
|
||||
var tasks = [
|
||||
async.apply(db.setObject.bind(db), 'flag:' + flagId, {
|
||||
flagId: flagId,
|
||||
type: type,
|
||||
targetId: id,
|
||||
description: reason,
|
||||
uid: uid,
|
||||
datetime: timestamp,
|
||||
}),
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:datetime', timestamp, flagId), // by time, the default
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byReporter:' + uid, timestamp, flagId), // by reporter
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:byType:' + type, timestamp, flagId), // by flag type
|
||||
async.apply(db.sortedSetAdd.bind(db), 'flags:hash', flagId, [type, id, uid].join(':')), // save zset for duplicate checking
|
||||
async.apply(analytics.increment, 'flags'), // some fancy analytics
|
||||
];
|
||||
|
||||
if (targetUid) {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byTargetUid:' + targetUid, timestamp, flagId)); // by target uid
|
||||
}
|
||||
if (targetCid) {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byCid:' + targetCid, timestamp, flagId)); // by target cid
|
||||
}
|
||||
if (type === 'post') {
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byPid:' + id, timestamp, flagId)); // by target pid
|
||||
if (targetUid) {
|
||||
tasks.push(async.apply(db.sortedSetIncrBy.bind(db), 'users:flags', 1, targetUid));
|
||||
}
|
||||
}
|
||||
|
||||
async.parallel(tasks, function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (doHistoryAppend) {
|
||||
Flags.update(flagId, uid, { state: 'open' });
|
||||
}
|
||||
|
||||
next(null, flagId);
|
||||
});
|
||||
},
|
||||
async.apply(Flags.get),
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.exists = function (type, id, uid, callback) {
|
||||
db.isSortedSetMember('flags:hash', [type, id, uid].join(':'), callback);
|
||||
};
|
||||
|
||||
Flags.getTarget = function (type, id, uid, callback) {
|
||||
async.waterfall([
|
||||
async.apply(Flags.targetExists, type, id),
|
||||
function (exists, next) {
|
||||
if (exists) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
async.waterfall([
|
||||
async.apply(posts.getPostsByPids, [id], uid),
|
||||
function (posts, next) {
|
||||
topics.addPostData(posts, uid, next);
|
||||
},
|
||||
], function (err, posts) {
|
||||
next(err, posts[0]);
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user.getUsersData([id], function (err, users) {
|
||||
next(err, users ? users[0] : undefined);
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
next(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Target used to exist (otherwise flag creation'd fail), but no longer
|
||||
next(null, {});
|
||||
}
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.targetExists = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.exists(id, callback);
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
user.exists(id, callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.getTargetUid = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.getPostField(id, 'uid', callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
setImmediate(callback, null, id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.getTargetCid = function (type, id, callback) {
|
||||
switch (type) {
|
||||
case 'post':
|
||||
posts.getCidByPid(id, callback);
|
||||
break;
|
||||
|
||||
default:
|
||||
setImmediate(callback, null, id);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
Flags.update = function (flagId, uid, changeset, callback) {
|
||||
// Retrieve existing flag data to compare for history-saving purposes
|
||||
var fields = ['state', 'assignee'];
|
||||
var tasks = [];
|
||||
var now = changeset.datetime || Date.now();
|
||||
var notifyAssignee = function (assigneeId, next) {
|
||||
if (assigneeId === '') {
|
||||
// Do nothing
|
||||
return next();
|
||||
}
|
||||
// Notify assignee of this update
|
||||
notifications.create({
|
||||
type: 'my-flags',
|
||||
bodyShort: '[[notifications:flag_assigned_to_you, ' + flagId + ']]',
|
||||
bodyLong: '',
|
||||
path: '/flags/' + flagId,
|
||||
nid: 'flags:assign:' + flagId + ':uid:' + assigneeId,
|
||||
from: uid,
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
notifications.push(notification, [assigneeId], next);
|
||||
});
|
||||
};
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getObjectFields.bind(db), 'flag:' + flagId, fields),
|
||||
function (current, next) {
|
||||
for (var prop in changeset) {
|
||||
if (changeset.hasOwnProperty(prop)) {
|
||||
if (current[prop] === changeset[prop]) {
|
||||
delete changeset[prop];
|
||||
} else {
|
||||
// Add tasks as necessary
|
||||
switch (prop) {
|
||||
case 'state':
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byState:' + changeset[prop], now, flagId));
|
||||
tasks.push(async.apply(db.sortedSetRemove.bind(db), 'flags:byState:' + current[prop], flagId));
|
||||
break;
|
||||
|
||||
case 'assignee':
|
||||
tasks.push(async.apply(db.sortedSetAdd.bind(db), 'flags:byAssignee:' + changeset[prop], now, flagId));
|
||||
tasks.push(async.apply(notifyAssignee, changeset[prop]));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!Object.keys(changeset).length) {
|
||||
// No changes
|
||||
return next();
|
||||
}
|
||||
|
||||
// Save new object to db (upsert)
|
||||
tasks.push(async.apply(db.setObject, 'flag:' + flagId, changeset));
|
||||
|
||||
// Append history
|
||||
tasks.push(async.apply(Flags.appendHistory, flagId, uid, changeset));
|
||||
|
||||
// Fire plugin hook
|
||||
tasks.push(async.apply(plugins.fireHook, 'action:flag.update', { flagId: flagId, changeset: changeset, uid: uid }));
|
||||
|
||||
async.parallel(tasks, function (err) {
|
||||
return next(err);
|
||||
});
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.getHistory = function (flagId, callback) {
|
||||
var history;
|
||||
var uids = [];
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRangeWithScores.bind(db), 'flag:' + flagId + ':history', 0, -1),
|
||||
function (_history, next) {
|
||||
history = _history.map(function (entry) {
|
||||
try {
|
||||
entry.value = JSON.parse(entry.value);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
uids.push(entry.value[0]);
|
||||
|
||||
// Deserialise changeset
|
||||
var changeset = entry.value[1];
|
||||
if (changeset.hasOwnProperty('state')) {
|
||||
changeset.state = changeset.state === undefined ? '' : '[[flags:state-' + changeset.state + ']]';
|
||||
}
|
||||
|
||||
return {
|
||||
uid: entry.value[0],
|
||||
fields: changeset,
|
||||
datetime: entry.score,
|
||||
datetimeISO: new Date(parseInt(entry.score, 10)).toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
user.getUsersFields(uids, ['username', 'userslug', 'picture'], next);
|
||||
},
|
||||
], function (err, users) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Append user data to each history event
|
||||
history = history.map(function (event, idx) {
|
||||
event.user = users[idx];
|
||||
return event;
|
||||
});
|
||||
|
||||
callback(null, history);
|
||||
});
|
||||
};
|
||||
|
||||
Flags.appendHistory = function (flagId, uid, changeset, callback) {
|
||||
var payload;
|
||||
var datetime = changeset.datetime || Date.now();
|
||||
delete changeset.datetime;
|
||||
|
||||
try {
|
||||
payload = JSON.stringify([uid, changeset, datetime]);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
db.sortedSetAdd('flag:' + flagId + ':history', datetime, payload, callback);
|
||||
};
|
||||
|
||||
Flags.appendNote = function (flagId, uid, note, datetime, callback) {
|
||||
if (typeof datetime === 'function' && !callback) {
|
||||
callback = datetime;
|
||||
datetime = Date.now();
|
||||
}
|
||||
|
||||
var payload;
|
||||
try {
|
||||
payload = JSON.stringify([uid, note]);
|
||||
} catch (e) {
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.sortedSetAdd, 'flag:' + flagId + ':notes', datetime, payload),
|
||||
async.apply(Flags.appendHistory, flagId, uid, {
|
||||
notes: null,
|
||||
datetime: datetime,
|
||||
}),
|
||||
], callback);
|
||||
};
|
||||
|
||||
Flags.notify = function (flagObj, uid, callback) {
|
||||
// Notify administrators, mods, and other associated people
|
||||
if (!callback) {
|
||||
callback = function () {};
|
||||
}
|
||||
|
||||
switch (flagObj.type) {
|
||||
case 'post':
|
||||
async.parallel({
|
||||
post: function (next) {
|
||||
async.waterfall([
|
||||
async.apply(posts.getPostData, flagObj.targetId),
|
||||
async.apply(posts.parsePost),
|
||||
], next);
|
||||
},
|
||||
title: async.apply(topics.getTitleByPid, flagObj.targetId),
|
||||
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
|
||||
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
|
||||
moderators: function (next) {
|
||||
async.waterfall([
|
||||
async.apply(posts.getCidByPid, flagObj.targetId),
|
||||
function (cid, next) {
|
||||
groups.getMembers('cid:' + cid + ':privileges:mods', 0, -1, next);
|
||||
},
|
||||
], next);
|
||||
},
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var title = S(results.title).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
type: 'new-post-flag',
|
||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flagObj.reporter.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: flagObj.description,
|
||||
pid: flagObj.targetId,
|
||||
path: '/post/' + flagObj.targetId,
|
||||
nid: 'flag:post:' + flagObj.targetId + ':uid:' + uid,
|
||||
from: uid,
|
||||
mergeId: 'notifications:user_flagged_post_in|' + flagObj.targetId,
|
||||
topicTitle: results.title,
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:flag.create', {
|
||||
flag: flagObj,
|
||||
});
|
||||
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), callback);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
async.parallel({
|
||||
admins: async.apply(groups.getMembers, 'administrators', 0, -1),
|
||||
globalMods: async.apply(groups.getMembers, 'Global Moderators', 0, -1),
|
||||
}, function (err, results) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[notifications:user_flagged_user, ' + flagObj.reporter.username + ', ' + flagObj.target.username + ']]',
|
||||
bodyLong: flagObj.description,
|
||||
path: '/uid/' + flagObj.targetId,
|
||||
nid: 'flag:user:' + flagObj.targetId + ':uid:' + uid,
|
||||
from: uid,
|
||||
mergeId: 'notifications:user_flagged_user|' + flagObj.targetId,
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:flag.create', {
|
||||
flag: flagObj,
|
||||
});
|
||||
notifications.push(notification, results.admins.concat(results.globalMods), callback);
|
||||
});
|
||||
});
|
||||
break;
|
||||
|
||||
default:
|
||||
callback(new Error('[[error:invalid-data]]'));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = Flags;
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
var async = require('async');
|
||||
var fs = require('fs');
|
||||
var path = require('path');
|
||||
var Jimp = require('jimp');
|
||||
var mime = require('mime');
|
||||
var winston = require('winston');
|
||||
@@ -37,8 +38,9 @@ module.exports = function (Groups) {
|
||||
},
|
||||
function (_tempPath, next) {
|
||||
tempPath = _tempPath;
|
||||
|
||||
uploadsController.uploadGroupCover(uid, {
|
||||
name: 'groupCover',
|
||||
name: 'groupCover' + path.extname(tempPath),
|
||||
path: tempPath,
|
||||
type: type,
|
||||
}, next);
|
||||
@@ -52,7 +54,7 @@ module.exports = function (Groups) {
|
||||
},
|
||||
function (next) {
|
||||
uploadsController.uploadGroupCover(uid, {
|
||||
name: 'groupCoverThumb',
|
||||
name: 'groupCoverThumb' + path.extname(tempPath),
|
||||
path: tempPath,
|
||||
type: type,
|
||||
}, next);
|
||||
|
||||
@@ -69,7 +69,7 @@ module.exports = function (Groups) {
|
||||
async.series(tasks, next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:group.create', groupData);
|
||||
plugins.fireHook('action:group.create', { group: groupData });
|
||||
next(null, groupData);
|
||||
},
|
||||
], callback);
|
||||
|
||||
@@ -4,6 +4,7 @@ var async = require('async');
|
||||
var plugins = require('../plugins');
|
||||
var utils = require('../utils');
|
||||
var db = require('./../database');
|
||||
var batch = require('../batch');
|
||||
|
||||
module.exports = function (Groups) {
|
||||
Groups.destroy = function (groupName, callback) {
|
||||
@@ -16,8 +17,6 @@ module.exports = function (Groups) {
|
||||
}
|
||||
var groupObj = groupsData[0];
|
||||
|
||||
plugins.fireHook('action:group.destroy', groupObj);
|
||||
|
||||
async.parallel([
|
||||
async.apply(db.delete, 'group:' + groupName),
|
||||
async.apply(db.sortedSetRemove, 'groups:createtime', groupName),
|
||||
@@ -28,22 +27,24 @@ module.exports = function (Groups) {
|
||||
async.apply(db.delete, 'group:' + groupName + ':pending'),
|
||||
async.apply(db.delete, 'group:' + groupName + ':invited'),
|
||||
async.apply(db.delete, 'group:' + groupName + ':owners'),
|
||||
async.apply(db.delete, 'group:' + groupName + ':member:pids'),
|
||||
async.apply(db.deleteObjectField, 'groupslug:groupname', utils.slugify(groupName)),
|
||||
function (next) {
|
||||
db.getSortedSetRange('groups:createtime', 0, -1, function (err, groups) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
async.each(groups, function (group, next) {
|
||||
db.sortedSetRemove('group:' + group + ':members', groupName, next);
|
||||
}, next);
|
||||
});
|
||||
batch.processSortedSet('groups:createtime', function (groupNames, next) {
|
||||
var keys = groupNames.map(function (group) {
|
||||
return 'group:' + group + ':members';
|
||||
});
|
||||
db.sortedSetsRemove(keys, groupName, next);
|
||||
}, {
|
||||
batch: 500,
|
||||
}, next);
|
||||
},
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
Groups.resetCache();
|
||||
plugins.fireHook('action:group.destroy', { group: groupObj });
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,21 +7,61 @@ var privileges = require('../privileges');
|
||||
var posts = require('../posts');
|
||||
|
||||
module.exports = function (Groups) {
|
||||
Groups.onNewPostMade = function (postData, callback) {
|
||||
if (!parseInt(postData.uid, 10)) {
|
||||
return setImmediate(callback);
|
||||
}
|
||||
|
||||
var groupNames;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Groups.getUserGroupMembership('groups:visible:createtime', [postData.uid], next);
|
||||
},
|
||||
function (_groupNames, next) {
|
||||
groupNames = _groupNames[0];
|
||||
|
||||
var keys = groupNames.map(function (groupName) {
|
||||
return 'group:' + groupName + ':member:pids';
|
||||
});
|
||||
|
||||
db.sortedSetsAdd(keys, postData.timestamp, postData.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
async.each(groupNames, function (groupName, next) {
|
||||
truncateMemberPosts(groupName, next);
|
||||
}, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
function truncateMemberPosts(groupName, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRevRange('group:' + groupName + ':member:pids', 10, 10, next);
|
||||
},
|
||||
function (lastPid, next) {
|
||||
lastPid = lastPid[0];
|
||||
if (!parseInt(lastPid, 10)) {
|
||||
return callback();
|
||||
}
|
||||
db.sortedSetScore('group:' + groupName + ':member:pids', lastPid, next);
|
||||
},
|
||||
function (score, next) {
|
||||
db.sortedSetsRemoveRangeByScore(['group:' + groupName + ':member:pids'], '-inf', score, next);
|
||||
},
|
||||
], callback);
|
||||
}
|
||||
|
||||
Groups.getLatestMemberPosts = function (groupName, max, uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Groups.getMembers(groupName, 0, -1, next);
|
||||
},
|
||||
function (uids, next) {
|
||||
if (!Array.isArray(uids) || !uids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
var keys = uids.map(function (uid) {
|
||||
return 'uid:' + uid + ':posts';
|
||||
});
|
||||
db.getSortedSetRevRange(keys, 0, max - 1, next);
|
||||
db.getSortedSetRevRange('group:' + groupName + ':member:pids', 0, max - 1, next);
|
||||
},
|
||||
function (pids, next) {
|
||||
if (!Array.isArray(pids) || !pids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
privileges.posts.filter('read', pids, uid, next);
|
||||
},
|
||||
function (pids, next) {
|
||||
|
||||
@@ -22,6 +22,19 @@ module.exports = function (Groups) {
|
||||
};
|
||||
|
||||
Groups.getUserGroupsFromSet = function (set, uids, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Groups.getUserGroupMembership(set, uids, next);
|
||||
},
|
||||
function (memberOf, next) {
|
||||
async.map(memberOf, function (memberOf, next) {
|
||||
Groups.getGroupsData(memberOf, next);
|
||||
}, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Groups.getUserGroupMembership = function (set, uids, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRevRange(set, 0, -1, next);
|
||||
@@ -40,7 +53,7 @@ module.exports = function (Groups) {
|
||||
}
|
||||
});
|
||||
|
||||
Groups.getGroupsData(memberOf, next);
|
||||
next(null, memberOf);
|
||||
},
|
||||
], next);
|
||||
}, next);
|
||||
|
||||
@@ -507,12 +507,11 @@ install.setup = function (callback) {
|
||||
setCopyrightWidget,
|
||||
function (next) {
|
||||
var upgrade = require('./upgrade');
|
||||
upgrade.check(function (err, uptodate) {
|
||||
if (err) {
|
||||
upgrade.check(function (err) {
|
||||
if (err && err.message === 'schema-out-of-date') {
|
||||
upgrade.run(next);
|
||||
} else if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (!uptodate) {
|
||||
upgrade.upgrade(next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ Languages.list = function (callback) {
|
||||
|
||||
// filter out invalid ones
|
||||
languages = languages.filter(function (lang) {
|
||||
return lang.code && lang.name && lang.dir;
|
||||
return lang && lang.code && lang.name && lang.dir;
|
||||
});
|
||||
|
||||
listCache = languages;
|
||||
|
||||
@@ -102,7 +102,7 @@ Messaging.isNewSet = function (uid, roomId, timestamp, callback) {
|
||||
},
|
||||
function (messages, next) {
|
||||
if (messages && messages.length) {
|
||||
next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5));
|
||||
next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + Messaging.newMessageCutoff);
|
||||
} else {
|
||||
next(null, true);
|
||||
}
|
||||
|
||||
@@ -30,9 +30,11 @@ module.exports = function (Messaging) {
|
||||
if (!content) {
|
||||
return callback(new Error('[[error:invalid-chat-message]]'));
|
||||
}
|
||||
content = String(content);
|
||||
|
||||
if (content.length > (meta.config.maximumChatMessageLength || 1000)) {
|
||||
return callback(new Error('[[error:chat-message-too-long]]'));
|
||||
var maximumChatMessageLength = (meta.config.maximumChatMessageLength || 1000);
|
||||
if (content.length > maximumChatMessageLength) {
|
||||
return callback(new Error('[[error:chat-message-too-long, ' + maximumChatMessageLength + ']]'));
|
||||
}
|
||||
callback();
|
||||
};
|
||||
@@ -52,7 +54,7 @@ module.exports = function (Messaging) {
|
||||
function (_mid, next) {
|
||||
mid = _mid;
|
||||
message = {
|
||||
content: content,
|
||||
content: String(content),
|
||||
timestamp: timestamp,
|
||||
fromuid: fromuid,
|
||||
roomId: roomId,
|
||||
|
||||
@@ -9,6 +9,8 @@ var utils = require('../utils');
|
||||
var plugins = require('../plugins');
|
||||
|
||||
module.exports = function (Messaging) {
|
||||
Messaging.newMessageCutoff = 1000 * 60 * 3;
|
||||
|
||||
Messaging.getMessageField = function (mid, field, callback) {
|
||||
Messaging.getMessageFields(mid, [field], function (err, fields) {
|
||||
callback(err, fields ? fields[field] : null);
|
||||
@@ -81,7 +83,7 @@ module.exports = function (Messaging) {
|
||||
// Add a spacer in between messages with time gaps between them
|
||||
messages = messages.map(function (message, index) {
|
||||
// Compare timestamps with the previous message, and check if a spacer needs to be added
|
||||
if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index - 1].timestamp, 10) + (1000 * 60 * 5)) {
|
||||
if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index - 1].timestamp, 10) + Messaging.newMessageCutoff) {
|
||||
// If it's been 5 minutes, this is a new set of messages
|
||||
message.newSet = true;
|
||||
} else if (index > 0 && message.fromuid !== messages[index - 1].fromuid) {
|
||||
@@ -116,7 +118,7 @@ module.exports = function (Messaging) {
|
||||
}
|
||||
|
||||
if (
|
||||
(parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000 * 60 * 5)) ||
|
||||
(parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + Messaging.newMessageCutoff) ||
|
||||
(parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10))
|
||||
) {
|
||||
// If it's been 5 minutes, this is a new set of messages
|
||||
|
||||
@@ -79,6 +79,7 @@ module.exports = function (Messaging) {
|
||||
}
|
||||
|
||||
notifications.create({
|
||||
type: 'new-chat',
|
||||
bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
|
||||
bodyLong: messageObj.content,
|
||||
nid: 'chat_' + fromuid + '_' + roomId,
|
||||
|
||||
@@ -19,14 +19,17 @@ module.exports = function (Meta) {
|
||||
|
||||
winston.verbose('Checking dependencies for outdated modules');
|
||||
|
||||
async.every(modules, function (module, next) {
|
||||
async.each(modules, function (module, next) {
|
||||
fs.readFile(path.join(__dirname, '../../node_modules/', module, 'package.json'), {
|
||||
encoding: 'utf-8',
|
||||
}, function (err, pkgData) {
|
||||
// If a bundled plugin/theme is not present, skip the dep check (#3384)
|
||||
if (err && err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
|
||||
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
|
||||
return next(true);
|
||||
if (err) {
|
||||
// If a bundled plugin/theme is not present, skip the dep check (#3384)
|
||||
if (err.code === 'ENOENT' && (module === 'nodebb-rewards-essentials' || module.startsWith('nodebb-plugin') || module.startsWith('nodebb-theme'))) {
|
||||
winston.warn('[meta/dependencies] Bundled plugin ' + module + ' not found, skipping dependency check.');
|
||||
return next();
|
||||
}
|
||||
return next(err);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -34,20 +37,24 @@ module.exports = function (Meta) {
|
||||
} catch (e) {
|
||||
process.stdout.write('[' + 'missing'.red + '] ' + module.bold + ' is a required dependency but could not be found\n');
|
||||
depsMissing = true;
|
||||
return next(true);
|
||||
return next();
|
||||
}
|
||||
|
||||
var ok = !semver.validRange(pkg.dependencies[module]) || semver.satisfies(pkgData.version, pkg.dependencies[module]);
|
||||
|
||||
if (ok || (pkgData._resolved && pkgData._resolved.indexOf('//github.com') !== -1)) {
|
||||
next(true);
|
||||
next();
|
||||
} else {
|
||||
process.stdout.write('[' + 'outdated'.yellow + '] ' + module.bold + ' installed v' + pkgData.version + ', package.json requires ' + pkg.dependencies[module] + '\n');
|
||||
depsOutdated = true;
|
||||
next(true);
|
||||
next();
|
||||
}
|
||||
});
|
||||
}, function () {
|
||||
}, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (depsMissing) {
|
||||
callback(new Error('dependencies-missing'));
|
||||
} else if (depsOutdated) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var validator = require('validator');
|
||||
var cronJob = require('cron').CronJob;
|
||||
|
||||
var db = require('../database');
|
||||
var analytics = require('../analytics');
|
||||
@@ -9,17 +11,45 @@ var analytics = require('../analytics');
|
||||
module.exports = function (Meta) {
|
||||
Meta.errors = {};
|
||||
|
||||
var counters = {};
|
||||
|
||||
new cronJob('0 * * * * *', function () {
|
||||
Meta.errors.writeData();
|
||||
}, null, true);
|
||||
|
||||
Meta.errors.writeData = function () {
|
||||
var dbQueue = [];
|
||||
if (Object.keys(counters).length > 0) {
|
||||
for (var key in counters) {
|
||||
if (counters.hasOwnProperty(key)) {
|
||||
dbQueue.push(async.apply(db.sortedSetIncrBy, 'errors:404', counters[key], key));
|
||||
}
|
||||
}
|
||||
counters = {};
|
||||
async.series(dbQueue, function (err) {
|
||||
if (err) {
|
||||
winston.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Meta.errors.log404 = function (route, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!route) {
|
||||
return setImmediate(callback);
|
||||
}
|
||||
route = route.replace(/\/$/, ''); // remove trailing slashes
|
||||
analytics.increment('errors:404');
|
||||
db.sortedSetIncrBy('errors:404', 1, route, callback);
|
||||
counters[route] = counters[route] || 0;
|
||||
counters[route] += 1;
|
||||
setImmediate(callback);
|
||||
};
|
||||
|
||||
Meta.errors.get = function (escape, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRevRangeWithScores('errors:404', 0, -1, next);
|
||||
db.getSortedSetRevRangeWithScores('errors:404', 0, 199, next);
|
||||
},
|
||||
function (data, next) {
|
||||
data = data.map(function (nfObject) {
|
||||
|
||||
@@ -54,7 +54,6 @@ module.exports = function (Meta) {
|
||||
'public/src/client/unread.js',
|
||||
'public/src/client/topic.js',
|
||||
'public/src/client/topic/events.js',
|
||||
'public/src/client/topic/flag.js',
|
||||
'public/src/client/topic/fork.js',
|
||||
'public/src/client/topic/move.js',
|
||||
'public/src/client/topic/posts.js',
|
||||
@@ -78,6 +77,8 @@ module.exports = function (Meta) {
|
||||
'public/src/modules/taskbar.js',
|
||||
'public/src/modules/helpers.js',
|
||||
'public/src/modules/string.js',
|
||||
'public/src/modules/flags.js',
|
||||
'public/src/modules/storage.js',
|
||||
],
|
||||
|
||||
// modules listed below are built (/src/modules) so they can be defined anonymously
|
||||
|
||||
@@ -27,9 +27,7 @@ function getTranslationTree(callback) {
|
||||
});
|
||||
|
||||
// Filter out plugins with invalid paths
|
||||
async.filter(paths, file.exists, function (paths) {
|
||||
next(null, paths);
|
||||
});
|
||||
async.filter(paths, file.exists, next);
|
||||
},
|
||||
function (paths, next) {
|
||||
async.map(paths, Plugins.loadPluginInfo, next);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
var nconf = require('nconf');
|
||||
var fs = require('fs');
|
||||
var winston = require('winston');
|
||||
|
||||
module.exports = function (Meta) {
|
||||
Meta.logs = {
|
||||
path: path.join(nconf.get('base_dir'), 'logs', 'output.log'),
|
||||
path: path.join(__dirname, '..', '..', 'logs', 'output.log'),
|
||||
};
|
||||
|
||||
Meta.logs.get = function (callback) {
|
||||
|
||||
@@ -16,13 +16,21 @@ Minifier.js.minify = function (scripts, minify, callback) {
|
||||
});
|
||||
|
||||
async.filter(scripts, function (script, next) {
|
||||
file.exists(script, function (exists) {
|
||||
file.exists(script, function (err, exists) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
if (!exists) {
|
||||
console.warn('[minifier] file not found, ' + script);
|
||||
}
|
||||
next(exists);
|
||||
next(null, exists);
|
||||
});
|
||||
}, function (scripts) {
|
||||
}, function (err, scripts) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (minify) {
|
||||
minifyScripts(scripts, callback);
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,7 @@ var plugins = require('../plugins');
|
||||
module.exports = function (Meta) {
|
||||
Meta.tags = {};
|
||||
|
||||
Meta.tags.parse = function (meta, link, callback) {
|
||||
Meta.tags.parse = function (req, meta, link, callback) {
|
||||
async.parallel({
|
||||
tags: function (next) {
|
||||
var defaultTags = [{
|
||||
@@ -120,7 +120,23 @@ module.exports = function (Meta) {
|
||||
return tag;
|
||||
});
|
||||
|
||||
addDescription(meta);
|
||||
addIfNotExists(meta, 'property', 'og:title', Meta.config.title || 'NodeBB');
|
||||
|
||||
var ogUrl = nconf.get('url') + req.path;
|
||||
addIfNotExists(meta, 'property', 'og:url', ogUrl);
|
||||
|
||||
addIfNotExists(meta, 'name', 'description', Meta.config.description);
|
||||
addIfNotExists(meta, 'property', 'og:description', Meta.config.description);
|
||||
|
||||
var ogImage = Meta.config['og:image'] || Meta.config['brand:logo'] || '';
|
||||
if (ogImage && !ogImage.startsWith('http')) {
|
||||
ogImage = nconf.get('url') + ogImage;
|
||||
}
|
||||
addIfNotExists(meta, 'property', 'og:image', ogImage);
|
||||
if (ogImage) {
|
||||
addIfNotExists(meta, 'property', 'og:image:width', 200);
|
||||
addIfNotExists(meta, 'property', 'og:image:height', 200);
|
||||
}
|
||||
|
||||
link = results.links.concat(link || []);
|
||||
|
||||
@@ -131,19 +147,20 @@ module.exports = function (Meta) {
|
||||
});
|
||||
};
|
||||
|
||||
function addDescription(meta) {
|
||||
var hasDescription = false;
|
||||
function addIfNotExists(meta, keyName, tagName, value) {
|
||||
var exists = false;
|
||||
meta.forEach(function (tag) {
|
||||
if (tag.name === 'description') {
|
||||
hasDescription = true;
|
||||
if (tag[keyName] === tagName) {
|
||||
exists = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (!hasDescription && Meta.config.description) {
|
||||
meta.push({
|
||||
name: 'description',
|
||||
content: validator.escape(String(Meta.config.description)),
|
||||
});
|
||||
if (!exists && value) {
|
||||
var data = {
|
||||
content: validator.escape(String(value)),
|
||||
};
|
||||
data[keyName] = tagName;
|
||||
meta.push(data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,18 +27,28 @@ module.exports = function (Meta) {
|
||||
async.filter(files, function (file, next) {
|
||||
fs.stat(path.join(themePath, file), function (err, fileStat) {
|
||||
if (err) {
|
||||
return next(false);
|
||||
if (err.code === 'ENOENT') {
|
||||
return next(null, false);
|
||||
}
|
||||
return next(err);
|
||||
}
|
||||
|
||||
next((fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
|
||||
next(null, (fileStat.isDirectory() && file.slice(0, 13) === 'nodebb-theme-'));
|
||||
});
|
||||
}, function (themes) {
|
||||
}, function (err, themes) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
async.map(themes, function (theme, next) {
|
||||
var config = path.join(themePath, theme, 'theme.json');
|
||||
|
||||
fs.readFile(config, function (err, file) {
|
||||
if (err) {
|
||||
return next();
|
||||
if (err.code === 'ENOENT') {
|
||||
return next(null, null);
|
||||
}
|
||||
return next(err);
|
||||
}
|
||||
try {
|
||||
var configObj = JSON.parse(file.toString());
|
||||
|
||||
@@ -41,6 +41,7 @@ module.exports = function (middleware) {
|
||||
|
||||
middleware.renderHeader = function (req, res, data, callback) {
|
||||
var registrationType = meta.config.registrationType || 'normal';
|
||||
res.locals.config = res.locals.config || {};
|
||||
var templateValues = {
|
||||
title: meta.config.title || '',
|
||||
description: meta.config.description || '',
|
||||
@@ -97,7 +98,7 @@ module.exports = function (middleware) {
|
||||
db.get('uid:' + req.uid + ':confirm:email:sent', next);
|
||||
},
|
||||
navigation: async.apply(navigation.get),
|
||||
tags: async.apply(meta.tags.parse, res.locals.metaTags, res.locals.linkTags),
|
||||
tags: async.apply(meta.tags.parse, req, res.locals.metaTags, res.locals.linkTags),
|
||||
banned: async.apply(user.isBanned, req.uid),
|
||||
banReason: async.apply(user.getBannedReason, req.uid),
|
||||
}, next);
|
||||
@@ -105,7 +106,7 @@ module.exports = function (middleware) {
|
||||
function (results, next) {
|
||||
if (results.banned) {
|
||||
req.logout();
|
||||
return res.redirect('/?banned=' + (results.banReason || 'no-reason'));
|
||||
return res.redirect('/');
|
||||
}
|
||||
|
||||
results.user.isAdmin = results.isAdmin;
|
||||
@@ -133,6 +134,7 @@ module.exports = function (middleware) {
|
||||
templateValues.customJS = templateValues.useCustomJS ? meta.config.customJS : '';
|
||||
templateValues.maintenanceHeader = parseInt(meta.config.maintenanceMode, 10) === 1 && !results.isAdmin;
|
||||
templateValues.defaultLang = meta.config.defaultLang || 'en-GB';
|
||||
templateValues.userLang = res.locals.config.userLang;
|
||||
templateValues.privateUserInfo = parseInt(meta.config.privateUserInfo, 10) === 1;
|
||||
templateValues.privateTagListing = parseInt(meta.config.privateTagListing, 10) === 1;
|
||||
|
||||
|
||||
@@ -73,6 +73,29 @@ middleware.ensureSelfOrGlobalPrivilege = function (req, res, next) {
|
||||
], next);
|
||||
};
|
||||
|
||||
middleware.ensureSelfOrPrivileged = function (req, res, next) {
|
||||
/*
|
||||
The "self" part of this middleware hinges on you having used
|
||||
middleware.exposeUid prior to invoking this middleware.
|
||||
*/
|
||||
if (req.user) {
|
||||
if (parseInt(req.user.uid, 10) === parseInt(res.locals.uid, 10)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
user.isPrivileged(req.uid, function (err, ok) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
} else if (ok) {
|
||||
return next();
|
||||
}
|
||||
controllers.helpers.notAllowed(req, res);
|
||||
});
|
||||
} else {
|
||||
controllers.helpers.notAllowed(req, res);
|
||||
}
|
||||
};
|
||||
|
||||
middleware.pageView = function (req, res, next) {
|
||||
analytics.pageView({
|
||||
ip: req.ip,
|
||||
|
||||
@@ -15,42 +15,44 @@ var batch = require('./batch');
|
||||
var plugins = require('./plugins');
|
||||
var utils = require('./utils');
|
||||
|
||||
(function (Notifications) {
|
||||
Notifications.init = function () {
|
||||
winston.verbose('[notifications.init] Registering jobs.');
|
||||
new cron('*/30 * * * *', Notifications.prune, null, true);
|
||||
};
|
||||
var Notifications = module.exports;
|
||||
|
||||
Notifications.get = function (nid, callback) {
|
||||
Notifications.getMultiple([nid], function (err, notifications) {
|
||||
callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null);
|
||||
});
|
||||
};
|
||||
Notifications.startJobs = function () {
|
||||
winston.verbose('[notifications.init] Registering jobs.');
|
||||
new cron('*/30 * * * *', Notifications.prune, null, true);
|
||||
};
|
||||
|
||||
Notifications.getMultiple = function (nids, callback) {
|
||||
var keys = nids.map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
Notifications.get = function (nid, callback) {
|
||||
Notifications.getMultiple([nid], function (err, notifications) {
|
||||
callback(err, Array.isArray(notifications) && notifications.length ? notifications[0] : null);
|
||||
});
|
||||
};
|
||||
|
||||
db.getObjects(keys, function (err, notifications) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
Notifications.getMultiple = function (nids, callback) {
|
||||
if (!nids.length) {
|
||||
return setImmediate(callback, null, []);
|
||||
}
|
||||
var keys = nids.map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
notifications = notifications.filter(Boolean);
|
||||
if (!notifications.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
var notifications;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjects(keys, next);
|
||||
},
|
||||
function (_notifications, next) {
|
||||
notifications = _notifications;
|
||||
var userKeys = notifications.map(function (notification) {
|
||||
return notification.from;
|
||||
return notification && notification.from;
|
||||
});
|
||||
|
||||
User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], function (err, usersData) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
notifications.forEach(function (notification, index) {
|
||||
User.getUsersFields(userKeys, ['username', 'userslug', 'picture'], next);
|
||||
},
|
||||
function (usersData, next) {
|
||||
notifications.forEach(function (notification, index) {
|
||||
if (notification) {
|
||||
notification.datetimeISO = utils.toISOString(notification.datetime);
|
||||
|
||||
if (notification.bodyLong) {
|
||||
@@ -66,449 +68,449 @@ var utils = require('./utils');
|
||||
} else if (notification.image === 'brand:logo' || !notification.image) {
|
||||
notification.image = meta.config['brand:logo'] || nconf.get('relative_path') + '/logo.png';
|
||||
}
|
||||
});
|
||||
|
||||
callback(null, notifications);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.filterExists = function (nids, callback) {
|
||||
// Removes nids that have been pruned
|
||||
db.isSortedSetMembers('notifications', nids, function (err, exists) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
next(null, notifications);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Notifications.filterExists = function (nids, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.isSortedSetMembers('notifications', nids, next);
|
||||
},
|
||||
function (exists, next) {
|
||||
nids = nids.filter(function (notifId, idx) {
|
||||
return exists[idx];
|
||||
});
|
||||
|
||||
callback(null, nids);
|
||||
});
|
||||
};
|
||||
next(null, nids);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Notifications.findRelated = function (mergeIds, set, callback) {
|
||||
// A related notification is one in a zset that has the same mergeId
|
||||
var _nids;
|
||||
Notifications.findRelated = function (mergeIds, set, callback) {
|
||||
// A related notification is one in a zset that has the same mergeId
|
||||
var _nids;
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRange, set, 0, -1),
|
||||
function (nids, next) {
|
||||
_nids = nids;
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRevRange, set, 0, -1),
|
||||
function (nids, next) {
|
||||
_nids = nids;
|
||||
|
||||
var keys = nids.map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
db.getObjectsFields(keys, ['mergeId'], next);
|
||||
},
|
||||
], function (err, sets) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
sets = sets.map(function (set) {
|
||||
return set.mergeId;
|
||||
var keys = nids.map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
callback(null, _nids.filter(function (nid, idx) {
|
||||
return mergeIds.indexOf(sets[idx]) !== -1;
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.create = function (data, callback) {
|
||||
if (!data.nid) {
|
||||
return callback(new Error('no-notification-id'));
|
||||
db.getObjectsFields(keys, ['mergeId'], next);
|
||||
},
|
||||
], function (err, sets) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
data.importance = data.importance || 5;
|
||||
db.getObject('notifications:' + data.nid, function (err, oldNotification) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
||||
sets = sets.map(function (set) {
|
||||
return set.mergeId;
|
||||
});
|
||||
|
||||
callback(null, _nids.filter(function (nid, idx) {
|
||||
return mergeIds.indexOf(sets[idx]) !== -1;
|
||||
}));
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.create = function (data, callback) {
|
||||
if (!data.nid) {
|
||||
return callback(new Error('no-notification-id'));
|
||||
}
|
||||
data.importance = data.importance || 5;
|
||||
db.getObject('notifications:' + data.nid, function (err, oldNotification) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
if (oldNotification) {
|
||||
if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
|
||||
return callback(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldNotification) {
|
||||
if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
|
||||
return callback(null, null);
|
||||
}
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
data.datetime = now;
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('notifications', now, data.nid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.setObject('notifications:' + data.nid, data, next);
|
||||
},
|
||||
], function (err) {
|
||||
callback(err, data);
|
||||
});
|
||||
var now = Date.now();
|
||||
data.datetime = now;
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('notifications', now, data.nid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.setObject('notifications:' + data.nid, data, next);
|
||||
},
|
||||
], function (err) {
|
||||
callback(err, data);
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.push = function (notification, uids, callback) {
|
||||
callback = callback || function () {};
|
||||
Notifications.push = function (notification, uids, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
if (!notification || !notification.nid) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
if (!Array.isArray(uids)) {
|
||||
uids = [uids];
|
||||
}
|
||||
|
||||
uids = uids.filter(function (uid, index, array) {
|
||||
return parseInt(uid, 10) && array.indexOf(uid) === index;
|
||||
});
|
||||
|
||||
if (!uids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
batch.processArray(uids, function (uids, next) {
|
||||
pushToUids(uids, notification, next);
|
||||
}, { interval: 1000 }, function (err) {
|
||||
if (err) {
|
||||
winston.error(err.stack);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
function pushToUids(uids, notification, callback) {
|
||||
var oneWeekAgo = Date.now() - 604800000;
|
||||
var unreadKeys = [];
|
||||
var readKeys = [];
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next);
|
||||
},
|
||||
function (data, next) {
|
||||
if (!data || !data.notification || !data.uids || !data.uids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
uids = data.uids;
|
||||
notification = data.notification;
|
||||
|
||||
uids.forEach(function (uid) {
|
||||
unreadKeys.push('uid:' + uid + ':notifications:unread');
|
||||
readKeys.push('uid:' + uid + ':notifications:read');
|
||||
});
|
||||
|
||||
db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove(readKeys, notification.nid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next);
|
||||
},
|
||||
function (next) {
|
||||
var websockets = require('./socket.io');
|
||||
if (websockets.server) {
|
||||
uids.forEach(function (uid) {
|
||||
websockets.in('uid_' + uid).emit('event:new_notification', notification);
|
||||
});
|
||||
}
|
||||
|
||||
plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids });
|
||||
next();
|
||||
},
|
||||
], callback);
|
||||
if (!notification || !notification.nid) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
Notifications.pushGroup = function (notification, groupName, callback) {
|
||||
callback = callback || function () {};
|
||||
groups.getMembers(groupName, 0, -1, function (err, members) {
|
||||
if (err || !Array.isArray(members) || !members.length) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!Array.isArray(uids)) {
|
||||
uids = [uids];
|
||||
}
|
||||
|
||||
Notifications.push(notification, members, callback);
|
||||
});
|
||||
};
|
||||
uids = uids.filter(function (uid, index, array) {
|
||||
return parseInt(uid, 10) && array.indexOf(uid) === index;
|
||||
});
|
||||
|
||||
Notifications.pushGroups = function (notification, groupNames, callback) {
|
||||
callback = callback || function () {};
|
||||
groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
|
||||
if (!uids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
setTimeout(function () {
|
||||
batch.processArray(uids, function (uids, next) {
|
||||
pushToUids(uids, notification, next);
|
||||
}, { interval: 1000 }, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
winston.error(err.stack);
|
||||
}
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
callback();
|
||||
};
|
||||
|
||||
function pushToUids(uids, notification, callback) {
|
||||
var oneWeekAgo = Date.now() - 604800000;
|
||||
var unreadKeys = [];
|
||||
var readKeys = [];
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
plugins.fireHook('filter:notification.push', { notification: notification, uids: uids }, next);
|
||||
},
|
||||
function (data, next) {
|
||||
if (!data || !data.notification || !data.uids || !data.uids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
uids = data.uids;
|
||||
notification = data.notification;
|
||||
|
||||
uids.forEach(function (uid) {
|
||||
unreadKeys.push('uid:' + uid + ':notifications:unread');
|
||||
readKeys.push('uid:' + uid + ':notifications:read');
|
||||
});
|
||||
|
||||
db.sortedSetsAdd(unreadKeys, notification.datetime, notification.nid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove(readKeys, notification.nid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemoveRangeByScore(unreadKeys, '-inf', oneWeekAgo, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemoveRangeByScore(readKeys, '-inf', oneWeekAgo, next);
|
||||
},
|
||||
function (next) {
|
||||
var websockets = require('./socket.io');
|
||||
if (websockets.server) {
|
||||
uids.forEach(function (uid) {
|
||||
websockets.in('uid_' + uid).emit('event:new_notification', notification);
|
||||
});
|
||||
}
|
||||
|
||||
var members = _.unique(_.flatten(groupMembers));
|
||||
plugins.fireHook('action:notification.pushed', { notification: notification, uids: uids });
|
||||
next();
|
||||
},
|
||||
], callback);
|
||||
}
|
||||
|
||||
Notifications.push(notification, members, callback);
|
||||
});
|
||||
};
|
||||
Notifications.pushGroup = function (notification, groupName, callback) {
|
||||
callback = callback || function () {};
|
||||
groups.getMembers(groupName, 0, -1, function (err, members) {
|
||||
if (err || !Array.isArray(members) || !members.length) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
Notifications.rescind = function (nid, callback) {
|
||||
callback = callback || function () {};
|
||||
Notifications.push(notification, members, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.pushGroups = function (notification, groupNames, callback) {
|
||||
callback = callback || function () {};
|
||||
groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
var members = _.unique(_.flatten(groupMembers));
|
||||
Notifications.push(notification, members, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.rescind = function (nid, callback) {
|
||||
callback = callback || function () {};
|
||||
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetRemove, 'notifications', nid),
|
||||
async.apply(db.delete, 'notifications:' + nid),
|
||||
], function (err) {
|
||||
if (err) {
|
||||
winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
|
||||
} else {
|
||||
winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
|
||||
}
|
||||
|
||||
callback(err, nid);
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.markRead = function (nid, uid, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!parseInt(uid, 10) || !nid) {
|
||||
return callback();
|
||||
}
|
||||
Notifications.markReadMultiple([nid], uid, callback);
|
||||
};
|
||||
|
||||
Notifications.markUnread = function (nid, uid, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!parseInt(uid, 10) || !nid) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
db.getObject('notifications:' + nid, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err || new Error('[[error:no-notification]]'));
|
||||
}
|
||||
notification.datetime = notification.datetime || Date.now();
|
||||
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetRemove, 'notifications', nid),
|
||||
async.apply(db.delete, 'notifications:' + nid),
|
||||
async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
|
||||
async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.markReadMultiple = function (nids, uid, callback) {
|
||||
callback = callback || function () {};
|
||||
nids = nids.filter(Boolean);
|
||||
if (!Array.isArray(nids) || !nids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var notificationKeys = nids.map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getObjectsFields, notificationKeys, ['mergeId']),
|
||||
function (mergeIds, next) {
|
||||
// Isolate mergeIds and find related notifications
|
||||
mergeIds = mergeIds.map(function (set) {
|
||||
return set.mergeId;
|
||||
}).reduce(function (memo, mergeId, idx, arr) {
|
||||
if (mergeId && idx === arr.indexOf(mergeId)) {
|
||||
memo.push(mergeId);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next);
|
||||
},
|
||||
function (relatedNids, next) {
|
||||
notificationKeys = _.union(nids, relatedNids).map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next);
|
||||
},
|
||||
], function (err, notificationData) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Filter out notifications that didn't exist
|
||||
notificationData = notificationData.filter(function (notification) {
|
||||
return notification && notification.nid;
|
||||
});
|
||||
|
||||
// Extract nid
|
||||
nids = notificationData.map(function (notification) {
|
||||
return notification.nid;
|
||||
});
|
||||
|
||||
var datetimes = notificationData.map(function (notification) {
|
||||
return (notification && notification.datetime) || Date.now();
|
||||
});
|
||||
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next);
|
||||
},
|
||||
], function (err) {
|
||||
if (err) {
|
||||
winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
|
||||
} else {
|
||||
winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
|
||||
}
|
||||
|
||||
callback(err, nid);
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.markRead = function (nid, uid, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!parseInt(uid, 10) || !nid) {
|
||||
return callback();
|
||||
}
|
||||
Notifications.markReadMultiple([nid], uid, callback);
|
||||
};
|
||||
|
||||
Notifications.markUnread = function (nid, uid, callback) {
|
||||
callback = callback || function () {};
|
||||
if (!parseInt(uid, 10) || !nid) {
|
||||
return callback();
|
||||
Notifications.markAllRead = function (uid, callback) {
|
||||
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
db.getObject('notifications:' + nid, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return callback(err || new Error('[[error:no-notification]]'));
|
||||
}
|
||||
notification.datetime = notification.datetime || Date.now();
|
||||
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
|
||||
async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
|
||||
], callback);
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.markReadMultiple = function (nids, uid, callback) {
|
||||
callback = callback || function () {};
|
||||
nids = nids.filter(Boolean);
|
||||
if (!Array.isArray(nids) || !nids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
var notificationKeys = nids.map(function (nid) {
|
||||
Notifications.markReadMultiple(nids, uid, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.prune = function () {
|
||||
var week = 604800000;
|
||||
|
||||
var cutoffTime = Date.now() - week;
|
||||
|
||||
db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) {
|
||||
if (err) {
|
||||
return winston.error(err.message);
|
||||
}
|
||||
|
||||
if (!Array.isArray(nids) || !nids.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var keys = nids.map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getObjectsFields, notificationKeys, ['mergeId']),
|
||||
function (mergeIds, next) {
|
||||
// Isolate mergeIds and find related notifications
|
||||
mergeIds = mergeIds.map(function (set) {
|
||||
return set.mergeId;
|
||||
}).reduce(function (memo, mergeId, idx, arr) {
|
||||
if (mergeId && idx === arr.indexOf(mergeId)) {
|
||||
memo.push(mergeId);
|
||||
}
|
||||
return memo;
|
||||
}, []);
|
||||
|
||||
Notifications.findRelated(mergeIds, 'uid:' + uid + ':notifications:unread', next);
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetRemove('notifications', nids, next);
|
||||
},
|
||||
function (relatedNids, next) {
|
||||
notificationKeys = _.union(nids, relatedNids).map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
db.getObjectsFields(notificationKeys, ['nid', 'datetime'], next);
|
||||
function (next) {
|
||||
db.deleteAll(keys, next);
|
||||
},
|
||||
], function (err, notificationData) {
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
return winston.error('Encountered error pruning notifications: ' + err.message);
|
||||
}
|
||||
|
||||
// Filter out notifications that didn't exist
|
||||
notificationData = notificationData.filter(function (notification) {
|
||||
return notification && notification.nid;
|
||||
});
|
||||
|
||||
// Extract nid
|
||||
nids = notificationData.map(function (notification) {
|
||||
return notification.nid;
|
||||
});
|
||||
|
||||
var datetimes = notificationData.map(function (notification) {
|
||||
return (notification && notification.datetime) || Date.now();
|
||||
});
|
||||
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('uid:' + uid + ':notifications:read', datetimes, nids, next);
|
||||
},
|
||||
], function (err) {
|
||||
callback(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.markAllRead = function (uid, callback) {
|
||||
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
Notifications.merge = function (notifications, callback) {
|
||||
// When passed a set of notification objects, merge any that can be merged
|
||||
var mergeIds = [
|
||||
'notifications:upvoted_your_post_in',
|
||||
'notifications:user_started_following_you',
|
||||
'notifications:user_posted_to',
|
||||
'notifications:user_flagged_post_in',
|
||||
'notifications:user_flagged_user',
|
||||
'new_register',
|
||||
];
|
||||
var isolated;
|
||||
var differentiators;
|
||||
var differentiator;
|
||||
var modifyIndex;
|
||||
var set;
|
||||
|
||||
notifications = mergeIds.reduce(function (notifications, mergeId) {
|
||||
isolated = notifications.filter(function (notifObj) {
|
||||
if (!notifObj || !notifObj.hasOwnProperty('mergeId')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(nids) || !nids.length) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
Notifications.markReadMultiple(nids, uid, callback);
|
||||
return notifObj.mergeId.split('|')[0] === mergeId;
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.prune = function () {
|
||||
var week = 604800000;
|
||||
if (isolated.length <= 1) {
|
||||
return notifications; // Nothing to merge
|
||||
}
|
||||
|
||||
var cutoffTime = Date.now() - week;
|
||||
|
||||
db.getSortedSetRangeByScore('notifications', 0, 500, '-inf', cutoffTime, function (err, nids) {
|
||||
if (err) {
|
||||
return winston.error(err.message);
|
||||
// Each isolated mergeId may have multiple differentiators, so process each separately
|
||||
differentiators = isolated.reduce(function (cur, next) {
|
||||
differentiator = next.mergeId.split('|')[1] || 0;
|
||||
if (cur.indexOf(differentiator) === -1) {
|
||||
cur.push(differentiator);
|
||||
}
|
||||
|
||||
if (!Array.isArray(nids) || !nids.length) {
|
||||
return;
|
||||
}
|
||||
return cur;
|
||||
}, []);
|
||||
|
||||
var keys = nids.map(function (nid) {
|
||||
return 'notifications:' + nid;
|
||||
});
|
||||
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetRemove('notifications', nids, next);
|
||||
},
|
||||
function (next) {
|
||||
db.deleteAll(keys, next);
|
||||
},
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return winston.error('Encountered error pruning notifications: ' + err.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Notifications.merge = function (notifications, callback) {
|
||||
// When passed a set of notification objects, merge any that can be merged
|
||||
var mergeIds = [
|
||||
'notifications:upvoted_your_post_in',
|
||||
'notifications:user_started_following_you',
|
||||
'notifications:user_posted_to',
|
||||
'notifications:user_flagged_post_in',
|
||||
'new_register',
|
||||
];
|
||||
var isolated;
|
||||
var differentiators;
|
||||
var differentiator;
|
||||
var modifyIndex;
|
||||
var set;
|
||||
|
||||
notifications = mergeIds.reduce(function (notifications, mergeId) {
|
||||
isolated = notifications.filter(function (notifObj) {
|
||||
if (!notifObj || !notifObj.hasOwnProperty('mergeId')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return notifObj.mergeId.split('|')[0] === mergeId;
|
||||
});
|
||||
|
||||
if (isolated.length <= 1) {
|
||||
return notifications; // Nothing to merge
|
||||
}
|
||||
|
||||
// Each isolated mergeId may have multiple differentiators, so process each separately
|
||||
differentiators = isolated.reduce(function (cur, next) {
|
||||
differentiator = next.mergeId.split('|')[1] || 0;
|
||||
if (cur.indexOf(differentiator) === -1) {
|
||||
cur.push(differentiator);
|
||||
}
|
||||
|
||||
return cur;
|
||||
}, []);
|
||||
|
||||
differentiators.forEach(function (differentiator) {
|
||||
if (differentiator === 0 && differentiators.length === 1) {
|
||||
set = isolated;
|
||||
} else {
|
||||
set = isolated.filter(function (notifObj) {
|
||||
return notifObj.mergeId === (mergeId + '|' + differentiator);
|
||||
});
|
||||
}
|
||||
|
||||
modifyIndex = notifications.indexOf(set[0]);
|
||||
if (modifyIndex === -1 || set.length === 1) {
|
||||
return notifications;
|
||||
}
|
||||
|
||||
switch (mergeId) {
|
||||
// intentional fall-through
|
||||
case 'notifications:upvoted_your_post_in':
|
||||
case 'notifications:user_started_following_you':
|
||||
case 'notifications:user_posted_to':
|
||||
case 'notifications:user_flagged_post_in':
|
||||
var usernames = set.map(function (notifObj) {
|
||||
return notifObj && notifObj.user && notifObj.user.username;
|
||||
}).filter(function (username, idx, array) {
|
||||
return array.indexOf(username) === idx;
|
||||
});
|
||||
var numUsers = usernames.length;
|
||||
|
||||
var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
titleEscaped = titleEscaped ? (', ' + titleEscaped) : '';
|
||||
|
||||
if (numUsers === 2) {
|
||||
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]';
|
||||
} else if (numUsers > 2) {
|
||||
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]';
|
||||
}
|
||||
|
||||
notifications[modifyIndex].path = set[set.length - 1].path;
|
||||
break;
|
||||
|
||||
case 'new_register':
|
||||
notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]';
|
||||
break;
|
||||
}
|
||||
|
||||
// Filter out duplicates
|
||||
notifications = notifications.filter(function (notifObj, idx) {
|
||||
if (!notifObj || !notifObj.mergeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex);
|
||||
differentiators.forEach(function (differentiator) {
|
||||
if (differentiator === 0 && differentiators.length === 1) {
|
||||
set = isolated;
|
||||
} else {
|
||||
set = isolated.filter(function (notifObj) {
|
||||
return notifObj.mergeId === (mergeId + '|' + differentiator);
|
||||
});
|
||||
}
|
||||
|
||||
modifyIndex = notifications.indexOf(set[0]);
|
||||
if (modifyIndex === -1 || set.length === 1) {
|
||||
return notifications;
|
||||
}
|
||||
|
||||
switch (mergeId) {
|
||||
// intentional fall-through
|
||||
case 'notifications:upvoted_your_post_in':
|
||||
case 'notifications:user_started_following_you':
|
||||
case 'notifications:user_posted_to':
|
||||
case 'notifications:user_flagged_post_in':
|
||||
case 'notifications:user_flagged_user':
|
||||
var usernames = set.map(function (notifObj) {
|
||||
return notifObj && notifObj.user && notifObj.user.username;
|
||||
}).filter(function (username, idx, array) {
|
||||
return array.indexOf(username) === idx;
|
||||
});
|
||||
var numUsers = usernames.length;
|
||||
|
||||
var title = S(notifications[modifyIndex].topicTitle || '').decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
titleEscaped = titleEscaped ? (', ' + titleEscaped) : '';
|
||||
|
||||
if (numUsers === 2) {
|
||||
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_dual, ' + usernames.join(', ') + titleEscaped + ']]';
|
||||
} else if (numUsers > 2) {
|
||||
notifications[modifyIndex].bodyShort = '[[' + mergeId + '_multiple, ' + usernames[0] + ', ' + (numUsers - 1) + titleEscaped + ']]';
|
||||
}
|
||||
|
||||
notifications[modifyIndex].path = set[set.length - 1].path;
|
||||
break;
|
||||
|
||||
case 'new_register':
|
||||
notifications[modifyIndex].bodyShort = '[[notifications:' + mergeId + '_multiple, ' + set.length + ']]';
|
||||
break;
|
||||
}
|
||||
|
||||
// Filter out duplicates
|
||||
notifications = notifications.filter(function (notifObj, idx) {
|
||||
if (!notifObj || !notifObj.mergeId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !(notifObj.mergeId === (mergeId + (differentiator ? '|' + differentiator : '')) && idx !== modifyIndex);
|
||||
});
|
||||
|
||||
return notifications;
|
||||
}, notifications);
|
||||
|
||||
plugins.fireHook('filter:notifications.merge', {
|
||||
notifications: notifications,
|
||||
}, function (err, data) {
|
||||
callback(err, data.notifications);
|
||||
});
|
||||
};
|
||||
}(exports));
|
||||
|
||||
return notifications;
|
||||
}, notifications);
|
||||
|
||||
plugins.fireHook('filter:notifications.merge', {
|
||||
notifications: notifications,
|
||||
}, function (err, data) {
|
||||
callback(err, data.notifications);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var qs = require('querystring');
|
||||
var _ = require('underscore');
|
||||
|
||||
var pagination = {};
|
||||
|
||||
@@ -37,7 +38,7 @@ pagination.create = function (currentPage, pageCount, queryObj) {
|
||||
return a - b;
|
||||
});
|
||||
|
||||
queryObj = queryObj || {};
|
||||
queryObj = _.clone(queryObj || {});
|
||||
|
||||
delete queryObj._;
|
||||
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
};
|
||||
|
||||
module.compare = function (password, hash, callback) {
|
||||
if (!hash || !password) {
|
||||
return setImmediate(callback, null, false);
|
||||
}
|
||||
forkChild({ type: 'compare', password: password, hash: hash }, callback);
|
||||
};
|
||||
|
||||
|
||||
@@ -159,9 +159,7 @@ var middleware;
|
||||
});
|
||||
|
||||
// Filter out plugins with invalid paths
|
||||
async.filter(paths, file.exists, function (paths) {
|
||||
next(null, paths);
|
||||
});
|
||||
async.filter(paths, file.exists, next);
|
||||
},
|
||||
function (paths, next) {
|
||||
async.map(paths, Plugins.loadPluginInfo, next);
|
||||
@@ -341,11 +339,15 @@ var middleware;
|
||||
|
||||
async.filter(dirs, function (dir, callback) {
|
||||
fs.stat(dir, function (err, stats) {
|
||||
callback(!err && stats.isDirectory());
|
||||
if (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
return callback(null, false);
|
||||
}
|
||||
return callback(err);
|
||||
}
|
||||
callback(null, stats.isDirectory());
|
||||
});
|
||||
}, function (plugins) {
|
||||
next(null, plugins);
|
||||
});
|
||||
}, next);
|
||||
},
|
||||
|
||||
function (files, next) {
|
||||
|
||||
@@ -8,6 +8,7 @@ module.exports = function (Plugins) {
|
||||
'filter:user.custom_fields': null, // remove in v1.1.0
|
||||
'filter:post.save': 'filter:post.create',
|
||||
'filter:user.profileLinks': 'filter:user.profileMenu',
|
||||
'action:post.flag': 'action:flag.create',
|
||||
};
|
||||
/*
|
||||
`data` is an object consisting of (* is required):
|
||||
|
||||
@@ -49,8 +49,8 @@ module.exports = function (Plugins) {
|
||||
},
|
||||
function (next) {
|
||||
meta.reloadRequired = true;
|
||||
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', id);
|
||||
next();
|
||||
Plugins.fireHook(isActive ? 'action:plugin.deactivate' : 'action:plugin.activate', { id: id });
|
||||
setImmediate(next);
|
||||
},
|
||||
], function (err) {
|
||||
if (err) {
|
||||
@@ -67,8 +67,8 @@ module.exports = function (Plugins) {
|
||||
};
|
||||
|
||||
function toggleInstall(id, version, callback) {
|
||||
var type;
|
||||
var installed;
|
||||
var type;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Plugins.isInstalled(id, next);
|
||||
@@ -85,7 +85,7 @@ module.exports = function (Plugins) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
next();
|
||||
setImmediate(next);
|
||||
},
|
||||
function (next) {
|
||||
runNpmCommand(type, id, version || 'latest', next);
|
||||
@@ -94,8 +94,8 @@ module.exports = function (Plugins) {
|
||||
Plugins.get(id, next);
|
||||
},
|
||||
function (pluginData, next) {
|
||||
Plugins.fireHook('action:plugin.' + type, id);
|
||||
next(null, pluginData);
|
||||
Plugins.fireHook('action:plugin.' + type, { id: id, version: version });
|
||||
setImmediate(next, null, pluginData);
|
||||
},
|
||||
], callback);
|
||||
}
|
||||
|
||||
@@ -29,9 +29,7 @@ module.exports = function (Plugins) {
|
||||
return path.join(__dirname, '../../node_modules/', plugin);
|
||||
});
|
||||
|
||||
async.filter(plugins, file.exists, function (plugins) {
|
||||
next(null, plugins);
|
||||
});
|
||||
async.filter(plugins, file.exists, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
@@ -166,13 +164,13 @@ module.exports = function (Plugins) {
|
||||
var realPath = pluginData.staticDirs[mappedPath];
|
||||
var staticDir = path.join(pluginPath, realPath);
|
||||
|
||||
file.exists(staticDir, function (exists) {
|
||||
file.exists(staticDir, function (err, exists) {
|
||||
if (exists) {
|
||||
Plugins.staticDirs[pluginData.id + '/' + mappedPath] = staticDir;
|
||||
} else {
|
||||
winston.warn('[plugins/' + pluginData.id + '] Mapped path \'' + mappedPath + ' => ' + staticDir + '\' not found.');
|
||||
}
|
||||
callback();
|
||||
callback(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
456
src/posts.js
456
src/posts.js
@@ -10,259 +10,277 @@ var topics = require('./topics');
|
||||
var privileges = require('./privileges');
|
||||
var plugins = require('./plugins');
|
||||
|
||||
(function (Posts) {
|
||||
require('./posts/create')(Posts);
|
||||
require('./posts/delete')(Posts);
|
||||
require('./posts/edit')(Posts);
|
||||
require('./posts/parse')(Posts);
|
||||
require('./posts/user')(Posts);
|
||||
require('./posts/topics')(Posts);
|
||||
require('./posts/category')(Posts);
|
||||
require('./posts/summary')(Posts);
|
||||
require('./posts/recent')(Posts);
|
||||
require('./posts/flags')(Posts);
|
||||
require('./posts/tools')(Posts);
|
||||
require('./posts/votes')(Posts);
|
||||
require('./posts/bookmarks')(Posts);
|
||||
var Posts = module.exports;
|
||||
|
||||
Posts.exists = function (pid, callback) {
|
||||
db.isSortedSetMember('posts:pid', pid, callback);
|
||||
};
|
||||
require('./posts/create')(Posts);
|
||||
require('./posts/delete')(Posts);
|
||||
require('./posts/edit')(Posts);
|
||||
require('./posts/parse')(Posts);
|
||||
require('./posts/user')(Posts);
|
||||
require('./posts/topics')(Posts);
|
||||
require('./posts/category')(Posts);
|
||||
require('./posts/summary')(Posts);
|
||||
require('./posts/recent')(Posts);
|
||||
require('./posts/tools')(Posts);
|
||||
require('./posts/votes')(Posts);
|
||||
require('./posts/bookmarks')(Posts);
|
||||
|
||||
Posts.getPidsFromSet = function (set, start, stop, reverse, callback) {
|
||||
if (isNaN(start) || isNaN(stop)) {
|
||||
return callback(null, []);
|
||||
}
|
||||
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback);
|
||||
};
|
||||
Posts.exists = function (pid, callback) {
|
||||
db.isSortedSetMember('posts:pid', pid, callback);
|
||||
};
|
||||
|
||||
Posts.getPostsByPids = function (pids, uid, callback) {
|
||||
if (!Array.isArray(pids) || !pids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
Posts.getPidsFromSet = function (set, start, stop, reverse, callback) {
|
||||
if (isNaN(start) || isNaN(stop)) {
|
||||
return callback(null, []);
|
||||
}
|
||||
db[reverse ? 'getSortedSetRevRange' : 'getSortedSetRange'](set, start, stop, callback);
|
||||
};
|
||||
|
||||
var keys = [];
|
||||
Posts.getPostsByPids = function (pids, uid, callback) {
|
||||
if (!Array.isArray(pids) || !pids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
for (var x = 0, numPids = pids.length; x < numPids; x += 1) {
|
||||
keys.push('post:' + pids[x]);
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjects(keys, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
async.map(posts, function (post, next) {
|
||||
if (!post) {
|
||||
return next();
|
||||
}
|
||||
post.upvotes = parseInt(post.upvotes, 10) || 0;
|
||||
post.downvotes = parseInt(post.downvotes, 10) || 0;
|
||||
post.votes = post.upvotes - post.downvotes;
|
||||
post.timestampISO = utils.toISOString(post.timestamp);
|
||||
post.editedISO = parseInt(post.edited, 10) !== 0 ? utils.toISOString(post.edited) : '';
|
||||
Posts.parsePost(post, next);
|
||||
}, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
plugins.fireHook('filter:post.getPosts', { posts: posts, uid: uid }, next);
|
||||
},
|
||||
function (data, next) {
|
||||
if (!data || !Array.isArray(data.posts)) {
|
||||
return next(null, []);
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
var keys = pids.map(function (pid) {
|
||||
return 'post:' + pid;
|
||||
});
|
||||
db.getObjects(keys, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
async.map(posts, function (post, next) {
|
||||
if (!post) {
|
||||
return next();
|
||||
}
|
||||
data.posts = data.posts.filter(Boolean);
|
||||
next(null, data.posts);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.getPostSummariesFromSet = function (set, uid, start, stop, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRevRange(set, start, stop, next);
|
||||
},
|
||||
function (pids, next) {
|
||||
privileges.posts.filter('read', pids, uid, next);
|
||||
},
|
||||
function (pids, next) {
|
||||
Posts.getPostSummaryByPids(pids, uid, { stripTags: false }, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
next(null, { posts: posts, nextStart: stop + 1 });
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.getPostData = function (pid, callback) {
|
||||
db.getObject('post:' + pid, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
post.upvotes = parseInt(post.upvotes, 10) || 0;
|
||||
post.downvotes = parseInt(post.downvotes, 10) || 0;
|
||||
post.votes = post.upvotes - post.downvotes;
|
||||
post.timestampISO = utils.toISOString(post.timestamp);
|
||||
post.editedISO = parseInt(post.edited, 10) !== 0 ? utils.toISOString(post.edited) : '';
|
||||
Posts.parsePost(post, next);
|
||||
}, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
plugins.fireHook('filter:post.getPosts', { posts: posts, uid: uid }, next);
|
||||
},
|
||||
function (data, next) {
|
||||
if (!data || !Array.isArray(data.posts)) {
|
||||
return next(null, []);
|
||||
}
|
||||
data.posts = data.posts.filter(Boolean);
|
||||
next(null, data.posts);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
plugins.fireHook('filter:post.get', data, callback);
|
||||
});
|
||||
};
|
||||
Posts.getPostSummariesFromSet = function (set, uid, start, stop, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getSortedSetRevRange(set, start, stop, next);
|
||||
},
|
||||
function (pids, next) {
|
||||
privileges.posts.filter('read', pids, uid, next);
|
||||
},
|
||||
function (pids, next) {
|
||||
Posts.getPostSummaryByPids(pids, uid, { stripTags: false }, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
next(null, { posts: posts, nextStart: stop + 1 });
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.getPostField = function (pid, field, callback) {
|
||||
Posts.getPostFields(pid, [field], function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
Posts.getPostData = function (pid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObject('post:' + pid, next);
|
||||
},
|
||||
function (data, next) {
|
||||
plugins.fireHook('filter:post.getPostData', { post: data }, next);
|
||||
},
|
||||
function (data, next) {
|
||||
next(null, data.post);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
callback(null, data[field]);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.getPostFields = function (pid, fields, callback) {
|
||||
db.getObjectFields('post:' + pid, fields, function (err, data) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
Posts.getPostField = function (pid, field, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Posts.getPostFields(pid, [field], next);
|
||||
},
|
||||
function (data, next) {
|
||||
next(null, data[field]);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.getPostFields = function (pid, fields, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjectFields('post:' + pid, fields, next);
|
||||
},
|
||||
function (data, next) {
|
||||
data.pid = pid;
|
||||
|
||||
plugins.fireHook('filter:post.getFields', { posts: [data], fields: fields }, function (err, data) {
|
||||
callback(err, (data && Array.isArray(data.posts) && data.posts.length) ? data.posts[0] : null);
|
||||
});
|
||||
});
|
||||
};
|
||||
plugins.fireHook('filter:post.getFields', { posts: [data], fields: fields }, next);
|
||||
},
|
||||
function (data, next) {
|
||||
next(null, (data && Array.isArray(data.posts) && data.posts.length) ? data.posts[0] : null);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.getPostsFields = function (pids, fields, callback) {
|
||||
if (!Array.isArray(pids) || !pids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
Posts.getPostsFields = function (pids, fields, callback) {
|
||||
if (!Array.isArray(pids) || !pids.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
var keys = pids.map(function (pid) {
|
||||
return 'post:' + pid;
|
||||
});
|
||||
var keys = pids.map(function (pid) {
|
||||
return 'post:' + pid;
|
||||
});
|
||||
|
||||
db.getObjectsFields(keys, fields, function (err, posts) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
plugins.fireHook('filter:post.getFields', { posts: posts, fields: fields }, function (err, data) {
|
||||
callback(err, (data && Array.isArray(data.posts)) ? data.posts : null);
|
||||
});
|
||||
});
|
||||
};
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjectsFields(keys, fields, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
plugins.fireHook('filter:post.getFields', { posts: posts, fields: fields }, next);
|
||||
},
|
||||
function (data, next) {
|
||||
next(null, (data && Array.isArray(data.posts)) ? data.posts : null);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.setPostField = function (pid, field, value, callback) {
|
||||
db.setObjectField('post:' + pid, field, value, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
Posts.setPostField = function (pid, field, value, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.setObjectField('post:' + pid, field, value, next);
|
||||
},
|
||||
function (next) {
|
||||
var data = {
|
||||
pid: pid,
|
||||
};
|
||||
data[field] = value;
|
||||
plugins.fireHook('action:post.setFields', data);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
plugins.fireHook('action:post.setFields', { data: data });
|
||||
next();
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.setPostFields = function (pid, data, callback) {
|
||||
db.setObject('post:' + pid, data, function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
Posts.setPostFields = function (pid, data, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.setObject('post:' + pid, data, next);
|
||||
},
|
||||
function (next) {
|
||||
data.pid = pid;
|
||||
plugins.fireHook('action:post.setFields', data);
|
||||
callback();
|
||||
});
|
||||
};
|
||||
plugins.fireHook('action:post.setFields', { data: data });
|
||||
next();
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.getPidIndex = function (pid, tid, topicPostSort, callback) {
|
||||
var set = topicPostSort === 'most_votes' ? 'tid:' + tid + ':posts:votes' : 'tid:' + tid + ':posts';
|
||||
db.sortedSetRank(set, pid, function (err, index) {
|
||||
Posts.getPidIndex = function (pid, tid, topicPostSort, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
var set = topicPostSort === 'most_votes' ? 'tid:' + tid + ':posts:votes' : 'tid:' + tid + ':posts';
|
||||
db.sortedSetRank(set, pid, next);
|
||||
},
|
||||
function (index, next) {
|
||||
if (!utils.isNumber(index)) {
|
||||
return callback(err, 0);
|
||||
return next(null, 0);
|
||||
}
|
||||
callback(err, parseInt(index, 10) + 1);
|
||||
});
|
||||
};
|
||||
next(null, parseInt(index, 10) + 1);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.getPostIndices = function (posts, uid, callback) {
|
||||
if (!Array.isArray(posts) || !posts.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
Posts.getPostIndices = function (posts, uid, callback) {
|
||||
if (!Array.isArray(posts) || !posts.length) {
|
||||
return callback(null, []);
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.getSettings(uid, next);
|
||||
},
|
||||
function (settings, next) {
|
||||
var byVotes = settings.topicPostSort === 'most_votes';
|
||||
var sets = posts.map(function (post) {
|
||||
return byVotes ? 'tid:' + post.tid + ':posts:votes' : 'tid:' + post.tid + ':posts';
|
||||
});
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.getSettings(uid, next);
|
||||
},
|
||||
function (settings, next) {
|
||||
var byVotes = settings.topicPostSort === 'most_votes';
|
||||
var sets = posts.map(function (post) {
|
||||
return byVotes ? 'tid:' + post.tid + ':posts:votes' : 'tid:' + post.tid + ':posts';
|
||||
});
|
||||
|
||||
var uniqueSets = _.uniq(sets);
|
||||
var method = 'sortedSetsRanks';
|
||||
if (uniqueSets.length === 1) {
|
||||
method = 'sortedSetRanks';
|
||||
sets = uniqueSets[0];
|
||||
}
|
||||
var uniqueSets = _.uniq(sets);
|
||||
var method = 'sortedSetsRanks';
|
||||
if (uniqueSets.length === 1) {
|
||||
method = 'sortedSetRanks';
|
||||
sets = uniqueSets[0];
|
||||
}
|
||||
|
||||
var pids = posts.map(function (post) {
|
||||
return post.pid;
|
||||
});
|
||||
var pids = posts.map(function (post) {
|
||||
return post.pid;
|
||||
});
|
||||
|
||||
db[method](sets, pids, next);
|
||||
},
|
||||
function (indices, next) {
|
||||
for (var i = 0; i < indices.length; i += 1) {
|
||||
indices[i] = utils.isNumber(indices[i]) ? parseInt(indices[i], 10) + 1 : 0;
|
||||
}
|
||||
db[method](sets, pids, next);
|
||||
},
|
||||
function (indices, next) {
|
||||
for (var i = 0; i < indices.length; i += 1) {
|
||||
indices[i] = utils.isNumber(indices[i]) ? parseInt(indices[i], 10) + 1 : 0;
|
||||
}
|
||||
|
||||
next(null, indices);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
next(null, indices);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.updatePostVoteCount = function (postData, callback) {
|
||||
if (!postData || !postData.pid || !postData.tid) {
|
||||
return callback();
|
||||
}
|
||||
async.parallel([
|
||||
function (next) {
|
||||
if (postData.uid) {
|
||||
if (postData.votes > 0) {
|
||||
db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next);
|
||||
} else {
|
||||
db.sortedSetRemove('uid:' + postData.uid + ':posts:votes', postData.pid, next);
|
||||
}
|
||||
Posts.updatePostVoteCount = function (postData, callback) {
|
||||
if (!postData || !postData.pid || !postData.tid) {
|
||||
return callback();
|
||||
}
|
||||
async.parallel([
|
||||
function (next) {
|
||||
if (postData.uid) {
|
||||
if (postData.votes > 0) {
|
||||
db.sortedSetAdd('uid:' + postData.uid + ':posts:votes', postData.votes, postData.pid, next);
|
||||
} else {
|
||||
next();
|
||||
db.sortedSetRemove('uid:' + postData.uid + ':posts:votes', postData.pid, next);
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
topics.getTopicField(postData.tid, 'mainPid', next);
|
||||
},
|
||||
function (mainPid, next) {
|
||||
if (parseInt(mainPid, 10) === parseInt(postData.pid, 10)) {
|
||||
return next();
|
||||
}
|
||||
db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', postData.votes, postData.pid, next);
|
||||
},
|
||||
], next);
|
||||
},
|
||||
function (next) {
|
||||
Posts.setPostFields(postData.pid, { upvotes: postData.upvotes, downvotes: postData.downvotes }, next);
|
||||
},
|
||||
], function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.modifyPostByPrivilege = function (post, isAdminOrMod) {
|
||||
if (post.deleted && !(isAdminOrMod || post.selfPost)) {
|
||||
post.content = '[[topic:post_is_deleted]]';
|
||||
if (post.user) {
|
||||
post.user.signature = '';
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
topics.getTopicField(postData.tid, 'mainPid', next);
|
||||
},
|
||||
function (mainPid, next) {
|
||||
if (parseInt(mainPid, 10) === parseInt(postData.pid, 10)) {
|
||||
return next();
|
||||
}
|
||||
db.sortedSetAdd('tid:' + postData.tid + ':posts:votes', postData.votes, postData.pid, next);
|
||||
},
|
||||
], next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('posts:votes', postData.votes, postData.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
Posts.setPostFields(postData.pid, { upvotes: postData.upvotes, downvotes: postData.downvotes }, next);
|
||||
},
|
||||
], function (err) {
|
||||
callback(err);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.modifyPostByPrivilege = function (post, isAdminOrMod) {
|
||||
if (post.deleted && !(isAdminOrMod || post.selfPost)) {
|
||||
post.content = '[[topic:post_is_deleted]]';
|
||||
if (post.user) {
|
||||
post.user.signature = '';
|
||||
}
|
||||
};
|
||||
}(exports));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ var plugins = require('../plugins');
|
||||
var user = require('../user');
|
||||
var topics = require('../topics');
|
||||
var categories = require('../categories');
|
||||
var groups = require('../groups');
|
||||
var utils = require('../utils');
|
||||
|
||||
module.exports = function (Posts) {
|
||||
@@ -82,6 +83,9 @@ module.exports = function (Posts) {
|
||||
categories.onNewPostMade(topicData.cid, topicData.pinned, postData, next);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
groups.onNewPostMade(postData, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('posts:pid', timestamp, postData.pid, next);
|
||||
},
|
||||
@@ -101,13 +105,13 @@ module.exports = function (Posts) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
plugins.fireHook('filter:post.get', postData, next);
|
||||
plugins.fireHook('filter:post.get', { post: postData, uid: data.uid }, next);
|
||||
});
|
||||
},
|
||||
function (postData, next) {
|
||||
postData.isMain = isMain;
|
||||
plugins.fireHook('action:post.save', _.clone(postData));
|
||||
next(null, postData);
|
||||
function (data, next) {
|
||||
data.post.isMain = isMain;
|
||||
plugins.fireHook('action:post.save', { post: _.clone(data.post) });
|
||||
next(null, data.post);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ var _ = require('underscore');
|
||||
var db = require('../database');
|
||||
var topics = require('../topics');
|
||||
var user = require('../user');
|
||||
var groups = require('../groups');
|
||||
var notifications = require('../notifications');
|
||||
var plugins = require('../plugins');
|
||||
|
||||
@@ -27,6 +28,7 @@ module.exports = function (Posts) {
|
||||
topics.getTopicFields(_post.tid, ['tid', 'cid', 'pinned'], next);
|
||||
},
|
||||
function (topicData, next) {
|
||||
postData.cid = topicData.cid;
|
||||
async.parallel([
|
||||
function (next) {
|
||||
updateTopicTimestamp(topicData, next);
|
||||
@@ -40,7 +42,7 @@ module.exports = function (Posts) {
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:post.delete', pid);
|
||||
plugins.fireHook('action:post.delete', { post: _.clone(postData), uid: uid });
|
||||
next(null, postData);
|
||||
},
|
||||
], callback);
|
||||
@@ -77,7 +79,7 @@ module.exports = function (Posts) {
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:post.restore', _.clone(postData));
|
||||
plugins.fireHook('action:post.restore', { post: _.clone(postData), uid: uid });
|
||||
next(null, postData);
|
||||
},
|
||||
], callback);
|
||||
@@ -141,19 +143,22 @@ module.exports = function (Posts) {
|
||||
deletePostFromReplies(pid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
|
||||
deletePostFromGroups(pid, next);
|
||||
},
|
||||
function (next) {
|
||||
Posts.dismissFlag(pid, next);
|
||||
db.sortedSetsRemove(['posts:pid', 'posts:flagged'], pid, next);
|
||||
},
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
plugins.fireHook('action:post.purge', pid);
|
||||
db.delete('post:' + pid, next);
|
||||
next(err);
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
Posts.getPostData(pid, next);
|
||||
},
|
||||
function (postData, next) {
|
||||
plugins.fireHook('action:post.purge', { post: postData, uid: uid });
|
||||
db.delete('post:' + pid, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -293,4 +298,26 @@ module.exports = function (Posts) {
|
||||
], callback);
|
||||
});
|
||||
}
|
||||
|
||||
function deletePostFromGroups(pid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
Posts.getPostField(pid, 'uid', next);
|
||||
},
|
||||
function (uid, next) {
|
||||
if (!parseInt(uid, 10)) {
|
||||
return callback();
|
||||
}
|
||||
groups.getUserGroupMembership('groups:visible:createtime', [uid], next);
|
||||
},
|
||||
function (groupNames, next) {
|
||||
groupNames = groupNames[0];
|
||||
var keys = groupNames.map(function (groupName) {
|
||||
return 'group:' + groupName + ':member:pids';
|
||||
});
|
||||
|
||||
db.sortedSetsRemove(keys, pid, next);
|
||||
},
|
||||
], callback);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -65,7 +65,7 @@ module.exports = function (Posts) {
|
||||
|
||||
postData.cid = results.topic.cid;
|
||||
postData.topic = results.topic;
|
||||
plugins.fireHook('action:post.edit', _.clone(postData));
|
||||
plugins.fireHook('action:post.edit', { post: _.clone(postData), uid: data.uid });
|
||||
|
||||
cache.del(String(postData.pid));
|
||||
pubsub.publish('post:edit', String(postData.pid));
|
||||
@@ -85,7 +85,7 @@ module.exports = function (Posts) {
|
||||
|
||||
async.parallel({
|
||||
topic: function (next) {
|
||||
topics.getTopicFields(tid, ['cid', 'title'], next);
|
||||
topics.getTopicFields(tid, ['cid', 'title', 'timestamp'], next);
|
||||
},
|
||||
isMain: function (next) {
|
||||
Posts.isMain(data.pid, next);
|
||||
@@ -136,7 +136,8 @@ module.exports = function (Posts) {
|
||||
function (tags, next) {
|
||||
topicData.tags = data.tags;
|
||||
topicData.oldTitle = results.topic.title;
|
||||
plugins.fireHook('action:topic.edit', topicData);
|
||||
topicData.timestamp = results.topic.timestamp;
|
||||
plugins.fireHook('action:topic.edit', { topic: topicData, uid: data.uid });
|
||||
next(null, {
|
||||
tid: tid,
|
||||
cid: results.topic.cid,
|
||||
|
||||
@@ -1,418 +0,0 @@
|
||||
|
||||
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var winston = require('winston');
|
||||
var db = require('../database');
|
||||
var user = require('../user');
|
||||
var analytics = require('../analytics');
|
||||
|
||||
module.exports = function (Posts) {
|
||||
Posts.flag = function (post, uid, reason, callback) {
|
||||
if (!parseInt(uid, 10) || !reason) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
hasFlagged: async.apply(Posts.isFlaggedByUser, post.pid, uid),
|
||||
exists: async.apply(Posts.exists, post.pid),
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
if (!results.exists) {
|
||||
return next(new Error('[[error:no-post]]'));
|
||||
}
|
||||
|
||||
if (results.hasFlagged) {
|
||||
return next(new Error('[[error:already-flagged]]'));
|
||||
}
|
||||
|
||||
var now = Date.now();
|
||||
async.parallel([
|
||||
function (next) {
|
||||
db.sortedSetAdd('posts:flagged', now, post.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.incrObjectField('post:' + post.pid, 'flags', next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
|
||||
},
|
||||
function (next) {
|
||||
if (parseInt(post.uid, 10)) {
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
|
||||
async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
|
||||
async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid),
|
||||
], next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
], next);
|
||||
},
|
||||
function (data, next) {
|
||||
openNewFlag(post.pid, uid, next);
|
||||
},
|
||||
], function (err) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
analytics.increment('flags');
|
||||
callback();
|
||||
});
|
||||
};
|
||||
|
||||
function openNewFlag(pid, uid, callback) {
|
||||
db.sortedSetScore('posts:flags:count', pid, function (err, count) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (count === 1) { // Only update state on new flag
|
||||
Posts.updateFlagData(uid, pid, {
|
||||
state: 'open',
|
||||
}, callback);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Posts.isFlaggedByUser = function (pid, uid, callback) {
|
||||
db.isSortedSetMember('pid:' + pid + ':flag:uids', uid, callback);
|
||||
};
|
||||
|
||||
Posts.dismissFlag = function (pid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
|
||||
},
|
||||
function (postData, next) {
|
||||
if (!postData.pid) {
|
||||
return callback();
|
||||
}
|
||||
async.parallel([
|
||||
function (next) {
|
||||
if (parseInt(postData.uid, 10)) {
|
||||
if (parseInt(postData.flags, 10) > 0) {
|
||||
async.parallel([
|
||||
async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
|
||||
async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags),
|
||||
], next);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
},
|
||||
function (next) {
|
||||
db.sortedSetsRemove([
|
||||
'posts:flagged',
|
||||
'posts:flags:count',
|
||||
'uid:' + postData.uid + ':flag:pids',
|
||||
], pid, next);
|
||||
},
|
||||
function (next) {
|
||||
async.series([
|
||||
function (next) {
|
||||
db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function (err, uids) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
async.each(uids, function (uid, next) {
|
||||
var nid = 'post_flag:' + pid + ':uid:' + uid;
|
||||
async.parallel([
|
||||
async.apply(db.delete, 'notifications:' + nid),
|
||||
async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid),
|
||||
], next);
|
||||
}, next);
|
||||
});
|
||||
},
|
||||
async.apply(db.delete, 'pid:' + pid + ':flag:uids'),
|
||||
], next);
|
||||
},
|
||||
async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
|
||||
async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason'),
|
||||
async.apply(db.deleteObjectFields, 'post:' + pid, ['flag:state', 'flag:assignee', 'flag:notes', 'flag:history']),
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Posts.dismissAllFlags = function (callback) {
|
||||
db.getSortedSetRange('posts:flagged', 0, -1, function (err, pids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.eachSeries(pids, Posts.dismissFlag, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.dismissUserFlags = function (uid, callback) {
|
||||
db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function (err, pids) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
async.eachSeries(pids, Posts.dismissFlag, callback);
|
||||
});
|
||||
};
|
||||
|
||||
Posts.getFlags = function (set, cid, uid, start, stop, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
if (Array.isArray(set)) {
|
||||
db.getSortedSetRevIntersect({ sets: set, start: start, stop: -1, aggregate: 'MAX' }, next);
|
||||
} else {
|
||||
db.getSortedSetRevRange(set, start, -1, next);
|
||||
}
|
||||
},
|
||||
function (pids, next) {
|
||||
if (cid) {
|
||||
Posts.filterPidsByCid(pids, cid, next);
|
||||
} else {
|
||||
process.nextTick(next, null, pids);
|
||||
}
|
||||
},
|
||||
function (pids, next) {
|
||||
getFlaggedPostsWithReasons(pids, uid, next);
|
||||
},
|
||||
function (posts, next) {
|
||||
var count = posts.length;
|
||||
var end = stop - start + 1;
|
||||
next(null, { posts: posts.slice(0, stop === -1 ? undefined : end), count: count });
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
function getFlaggedPostsWithReasons(pids, uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
uidsReasons: function (next) {
|
||||
async.map(pids, function (pid, next) {
|
||||
db.getSortedSetRange('pid:' + pid + ':flag:uid:reason', 0, -1, next);
|
||||
}, next);
|
||||
},
|
||||
posts: function (next) {
|
||||
Posts.getPostSummaryByPids(pids, uid, { stripTags: false, extraFields: ['flags', 'flag:assignee', 'flag:state', 'flag:notes', 'flag:history'] }, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
async.map(results.uidsReasons, function (uidReasons, next) {
|
||||
async.map(uidReasons, function (uidReason, next) {
|
||||
var uid = uidReason.split(':')[0];
|
||||
var reason = uidReason.substr(uidReason.indexOf(':') + 1);
|
||||
user.getUserFields(uid, ['username', 'userslug', 'picture'], function (err, userData) {
|
||||
next(err, { user: userData, reason: reason });
|
||||
});
|
||||
}, next);
|
||||
}, function (err, reasons) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
results.posts.forEach(function (post, index) {
|
||||
if (post) {
|
||||
post.flagReasons = reasons[index];
|
||||
}
|
||||
});
|
||||
|
||||
next(null, results.posts);
|
||||
});
|
||||
},
|
||||
async.apply(Posts.expandFlagHistory),
|
||||
function (posts, next) {
|
||||
// Parse out flag data into its own object inside each post hash
|
||||
async.map(posts, function (postObj, next) {
|
||||
for (var prop in postObj) {
|
||||
if (postObj.hasOwnProperty(prop)) {
|
||||
postObj.flagData = postObj.flagData || {};
|
||||
|
||||
if (prop.startsWith('flag:')) {
|
||||
postObj.flagData[prop.slice(5)] = postObj[prop];
|
||||
|
||||
if (prop === 'flag:state') {
|
||||
switch (postObj[prop]) {
|
||||
case 'open':
|
||||
postObj.flagData.labelClass = 'info';
|
||||
break;
|
||||
case 'wip':
|
||||
postObj.flagData.labelClass = 'warning';
|
||||
break;
|
||||
case 'resolved':
|
||||
postObj.flagData.labelClass = 'success';
|
||||
break;
|
||||
case 'rejected':
|
||||
postObj.flagData.labelClass = 'danger';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
delete postObj[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (postObj.flagData.assignee) {
|
||||
user.getUserFields(parseInt(postObj.flagData.assignee, 10), ['username', 'picture'], function (err, userData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
postObj.flagData.assigneeUser = userData;
|
||||
next(null, postObj);
|
||||
});
|
||||
} else {
|
||||
setImmediate(next.bind(null, null, postObj));
|
||||
}
|
||||
}, next);
|
||||
},
|
||||
], callback);
|
||||
}
|
||||
|
||||
Posts.updateFlagData = function (uid, pid, flagObj, callback) {
|
||||
// Retrieve existing flag data to compare for history-saving purposes
|
||||
var changes = [];
|
||||
var changeset = {};
|
||||
var prop;
|
||||
|
||||
Posts.getPostData(pid, function (err, postData) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
// Track new additions
|
||||
for (prop in flagObj) {
|
||||
if (flagObj.hasOwnProperty(prop) && !postData.hasOwnProperty('flag:' + prop) && flagObj[prop].length) {
|
||||
changes.push(prop);
|
||||
}
|
||||
}
|
||||
|
||||
// Track changed items
|
||||
for (prop in postData) {
|
||||
if (
|
||||
postData.hasOwnProperty(prop) && prop.startsWith('flag:') &&
|
||||
flagObj.hasOwnProperty(prop.slice(5)) &&
|
||||
postData[prop] !== flagObj[prop.slice(5)]
|
||||
) {
|
||||
changes.push(prop.slice(5));
|
||||
}
|
||||
}
|
||||
|
||||
changeset = changes.reduce(function (memo, prop) {
|
||||
memo['flag:' + prop] = flagObj[prop];
|
||||
return memo;
|
||||
}, {});
|
||||
|
||||
// Append changes to history string
|
||||
if (changes.length) {
|
||||
try {
|
||||
var history = JSON.parse(postData['flag:history'] || '[]');
|
||||
|
||||
changes.forEach(function (property) {
|
||||
switch (property) {
|
||||
case 'assignee': // intentional fall-through
|
||||
case 'state':
|
||||
history.unshift({
|
||||
uid: uid,
|
||||
type: property,
|
||||
value: flagObj[property],
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
break;
|
||||
|
||||
case 'notes':
|
||||
history.unshift({
|
||||
uid: uid,
|
||||
type: property,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
changeset['flag:history'] = JSON.stringify(history);
|
||||
} catch (e) {
|
||||
winston.warn('[posts/updateFlagData] Unable to deserialise post flag history, likely malformed data');
|
||||
}
|
||||
}
|
||||
|
||||
// Save flag data into post hash
|
||||
if (changes.length) {
|
||||
Posts.setPostFields(pid, changeset, callback);
|
||||
} else {
|
||||
setImmediate(callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Posts.expandFlagHistory = function (posts, callback) {
|
||||
// Expand flag history
|
||||
async.map(posts, function (post, next) {
|
||||
var history;
|
||||
try {
|
||||
history = JSON.parse(post['flag:history'] || '[]');
|
||||
} catch (e) {
|
||||
winston.warn('[posts/getFlags] Unable to deserialise post flag history, likely malformed data');
|
||||
return callback(e);
|
||||
}
|
||||
|
||||
async.map(history, function (event, next) {
|
||||
event.timestampISO = new Date(event.timestamp).toISOString();
|
||||
|
||||
async.parallel([
|
||||
function (next) {
|
||||
user.getUserFields(event.uid, ['username', 'picture'], function (err, userData) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
event.user = userData;
|
||||
next();
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
if (event.type === 'assignee') {
|
||||
user.getUserField(parseInt(event.value, 10), 'username', function (err, username) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
event.label = username || 'Unknown user';
|
||||
next(null);
|
||||
});
|
||||
} else if (event.type === 'state') {
|
||||
event.label = '[[topic:flag_manage_state_' + event.value + ']]';
|
||||
setImmediate(next);
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
], function (err) {
|
||||
next(err, event);
|
||||
});
|
||||
}, function (err, history) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
post['flag:history'] = history;
|
||||
next(null, post);
|
||||
});
|
||||
}, callback);
|
||||
};
|
||||
};
|
||||
@@ -8,12 +8,8 @@ var helpers = {};
|
||||
|
||||
helpers.some = function (tasks, callback) {
|
||||
async.some(tasks, function (task, next) {
|
||||
task(function (err, result) {
|
||||
next(!err && result);
|
||||
});
|
||||
}, function (result) {
|
||||
callback(null, result);
|
||||
});
|
||||
task(next);
|
||||
}, callback);
|
||||
};
|
||||
|
||||
helpers.isUserAllowedTo = function (privilege, uid, cid, callback) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var user = require('../user');
|
||||
var groups = require('../groups');
|
||||
var plugins = require('../plugins');
|
||||
|
||||
@@ -157,4 +158,49 @@ module.exports = function (privileges) {
|
||||
callback(null, canEdit);
|
||||
});
|
||||
};
|
||||
|
||||
privileges.users.canBanUser = function (callerUid, uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
isAdmin: function (next) {
|
||||
privileges.users.isAdministrator(callerUid, next);
|
||||
},
|
||||
isGlobalMod: function (next) {
|
||||
privileges.users.isGlobalModerator(callerUid, next);
|
||||
},
|
||||
isTargetAdmin: function (next) {
|
||||
privileges.users.isAdministrator(uid, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
results.canBan = !results.isTargetAdmin && (results.isAdmin || results.isGlobalMod);
|
||||
results.callerUid = callerUid;
|
||||
results.uid = uid;
|
||||
plugins.fireHook('filter:user.canBanUser', results, next);
|
||||
},
|
||||
function (data, next) {
|
||||
next(null, data.canBan);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
privileges.users.hasBanPrivilege = function (uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalMod, next) {
|
||||
plugins.fireHook('filter:user.hasBanPrivilege', {
|
||||
uid: uid,
|
||||
isAdminOrGlobalMod: isAdminOrGlobalMod,
|
||||
canBan: isAdminOrGlobalMod,
|
||||
}, next);
|
||||
},
|
||||
function (data, next) {
|
||||
next(null, data.canBan);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -33,12 +33,14 @@ rewards.checkConditionAndRewardUser = function (uid, condition, method, callback
|
||||
|
||||
async.filter(rewards, function (reward, next) {
|
||||
if (!reward) {
|
||||
return next(false);
|
||||
return next(null, false);
|
||||
}
|
||||
|
||||
checkCondition(reward, method, next);
|
||||
}, function (eligible) {
|
||||
if (!eligible) {
|
||||
checkCondition(reward, method, function (result) {
|
||||
next(null, result);
|
||||
});
|
||||
}, function (err, eligible) {
|
||||
if (err || !eligible) {
|
||||
return next(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,6 @@ function addRoutes(router, middleware, controllers) {
|
||||
router.get('/manage/categories/:category_id/analytics', middlewares, controllers.admin.categories.getAnalytics);
|
||||
|
||||
router.get('/manage/tags', middlewares, controllers.admin.tags.get);
|
||||
router.get('/manage/flags', middlewares, controllers.admin.flags.get);
|
||||
router.get('/manage/ip-blacklist', middlewares, controllers.admin.blacklist.get);
|
||||
|
||||
router.get('/manage/users', middlewares, controllers.admin.users.sortByJoinDate);
|
||||
|
||||
@@ -3,10 +3,15 @@
|
||||
var helpers = {};
|
||||
|
||||
helpers.setupPageRoute = function (router, name, middleware, middlewares, controller) {
|
||||
middlewares = middlewares.concat([middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks]);
|
||||
middlewares = [middleware.maintenanceMode, middleware.registrationComplete, middleware.pageView, middleware.pluginHooks].concat(middlewares);
|
||||
|
||||
router.get(name, middleware.busyCheck, middleware.buildHeader, middlewares, controller);
|
||||
router.get('/api' + name, middlewares, controller);
|
||||
};
|
||||
|
||||
helpers.setupAdminPageRoute = function (router, name, middleware, middlewares, controller) {
|
||||
router.get(name, middleware.admin.buildHeader, middlewares, controller);
|
||||
router.get('/api' + name, middlewares, controller);
|
||||
};
|
||||
|
||||
module.exports = helpers;
|
||||
|
||||
@@ -34,13 +34,11 @@ function mainRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/search', middleware, [], controllers.search.search);
|
||||
setupPageRoute(app, '/reset/:code?', middleware, [], controllers.reset);
|
||||
setupPageRoute(app, '/tos', middleware, [], controllers.termsOfUse);
|
||||
|
||||
app.get('/ping', controllers.ping);
|
||||
app.get('/sping', controllers.ping);
|
||||
}
|
||||
|
||||
function modRoutes(app, middleware, controllers) {
|
||||
setupPageRoute(app, '/posts/flags', middleware, [], controllers.mods.flagged);
|
||||
setupPageRoute(app, '/flags', middleware, [], controllers.mods.flags.list);
|
||||
setupPageRoute(app, '/flags/:flagId', middleware, [], controllers.mods.flags.detail);
|
||||
}
|
||||
|
||||
function globalModRoutes(app, middleware, controllers) {
|
||||
|
||||
@@ -146,7 +146,7 @@ function getMatchedPosts(pids, data, callback) {
|
||||
topicFields.push('postcount');
|
||||
}
|
||||
|
||||
if (data.sortBy) {
|
||||
if (data.sortBy && data.sortBy !== 'relevance') {
|
||||
if (data.sortBy.startsWith('category')) {
|
||||
topicFields.push('cid');
|
||||
} else if (data.sortBy.startsWith('topic.')) {
|
||||
@@ -326,7 +326,7 @@ function filterByTags(posts, hasTags) {
|
||||
}
|
||||
|
||||
function sortPosts(posts, data) {
|
||||
if (!posts.length || !data.sortBy) {
|
||||
if (!posts.length || !data.sortBy || data.sortBy === 'relevance') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,31 +19,25 @@ var sitemap = {
|
||||
};
|
||||
|
||||
sitemap.render = function (callback) {
|
||||
var numTopics = parseInt(meta.config.sitemapTopics, 10) || 500;
|
||||
var topicsPerPage = parseInt(meta.config.sitemapTopics, 10) || 500;
|
||||
var returnData = {
|
||||
url: nconf.get('url'),
|
||||
topics: [],
|
||||
};
|
||||
var numPages;
|
||||
|
||||
async.waterfall([
|
||||
async.apply(db.getSortedSetRange, 'topics:recent', 0, -1),
|
||||
function (tids, next) {
|
||||
privileges.topics.filterTids('read', tids, 0, next);
|
||||
function (next) {
|
||||
db.getObjectField('global', 'topicCount', next);
|
||||
},
|
||||
], function (err, tids) {
|
||||
if (err) {
|
||||
numPages = 1;
|
||||
} else {
|
||||
numPages = Math.ceil(tids.length / numTopics);
|
||||
}
|
||||
function (topicCount, next) {
|
||||
var numPages = Math.max(0, topicCount / topicsPerPage);
|
||||
for (var x = 1; x <= numPages; x += 1) {
|
||||
returnData.topics.push(x);
|
||||
}
|
||||
|
||||
for (var x = 1; x <= numPages; x += 1) {
|
||||
returnData.topics.push(x);
|
||||
}
|
||||
|
||||
callback(null, returnData);
|
||||
});
|
||||
next(null, returnData);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
sitemap.getPages = function (callback) {
|
||||
|
||||
@@ -173,6 +173,7 @@ SocketAdmin.config.setMultiple = function (socket, data, callback) {
|
||||
logger.monitorConfig({ io: index.server }, setting);
|
||||
}
|
||||
}
|
||||
plugins.fireHook('action:config.set', { settings: data });
|
||||
setImmediate(next);
|
||||
},
|
||||
], callback);
|
||||
|
||||
@@ -7,7 +7,9 @@ var groups = require('../../groups');
|
||||
var categories = require('../../categories');
|
||||
var privileges = require('../../privileges');
|
||||
var plugins = require('../../plugins');
|
||||
var Categories = {};
|
||||
var events = require('../../events');
|
||||
|
||||
var Categories = module.exports;
|
||||
|
||||
Categories.create = function (socket, data, callback) {
|
||||
if (!data) {
|
||||
@@ -42,7 +44,26 @@ Categories.getNames = function (socket, data, callback) {
|
||||
};
|
||||
|
||||
Categories.purge = function (socket, cid, callback) {
|
||||
categories.purge(cid, socket.uid, callback);
|
||||
var name;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
categories.getCategoryField(cid, 'name', next);
|
||||
},
|
||||
function (_name, next) {
|
||||
name = _name;
|
||||
categories.purge(cid, socket.uid, next);
|
||||
},
|
||||
function (next) {
|
||||
events.log({
|
||||
type: 'category-purge',
|
||||
uid: socket.uid,
|
||||
ip: socket.ip,
|
||||
cid: cid,
|
||||
name: name,
|
||||
});
|
||||
setImmediate(next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Categories.update = function (socket, data, callback) {
|
||||
@@ -102,5 +123,3 @@ Categories.copySettingsFrom = function (socket, data, callback) {
|
||||
Categories.copyPrivilegesFrom = function (socket, data, callback) {
|
||||
categories.copyPrivilegesFrom(data.fromCid, data.toCid, callback);
|
||||
};
|
||||
|
||||
module.exports = Categories;
|
||||
|
||||
@@ -69,14 +69,6 @@ User.resetLockouts = function (socket, uids, callback) {
|
||||
async.each(uids, user.auth.resetLockout, callback);
|
||||
};
|
||||
|
||||
User.resetFlags = function (socket, uids, callback) {
|
||||
if (!Array.isArray(uids)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
user.resetFlags(uids, callback);
|
||||
};
|
||||
|
||||
User.validateEmail = function (socket, uids, callback) {
|
||||
if (!Array.isArray(uids)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
|
||||
102
src/socket.io/flags.js
Normal file
102
src/socket.io/flags.js
Normal file
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var user = require('../user');
|
||||
var flags = require('../flags');
|
||||
|
||||
var SocketFlags = {};
|
||||
|
||||
SocketFlags.create = function (socket, data, callback) {
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:not-logged-in]]'));
|
||||
}
|
||||
|
||||
if (!data || !data.type || !data.id || !data.reason) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
async.apply(flags.validate, {
|
||||
uid: socket.uid,
|
||||
type: data.type,
|
||||
id: data.id,
|
||||
}),
|
||||
function (next) {
|
||||
// If we got here, then no errors occurred
|
||||
flags.create(data.type, data.id, socket.uid, data.reason, next);
|
||||
},
|
||||
], function (err, flagObj) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
||||
flags.notify(flagObj, socket.uid);
|
||||
callback(null, flagObj);
|
||||
});
|
||||
};
|
||||
|
||||
SocketFlags.update = function (socket, data, callback) {
|
||||
if (!data || !(data.flagId && data.data)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid),
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
// Translate form data into object
|
||||
payload = data.data.reduce(function (memo, cur) {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, payload);
|
||||
|
||||
flags.update(data.flagId, socket.uid, payload, next);
|
||||
},
|
||||
async.apply(flags.getHistory, data.flagId),
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketFlags.appendNote = function (socket, data, callback) {
|
||||
if (!data || !(data.flagId && data.note)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid),
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
flags.appendNote(data.flagId, socket.uid, data.note, next);
|
||||
},
|
||||
function (next) {
|
||||
async.parallel({
|
||||
notes: async.apply(flags.getNotes, data.flagId),
|
||||
history: async.apply(flags.getHistory, data.flagId),
|
||||
}, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
module.exports = SocketFlags;
|
||||
@@ -106,6 +106,7 @@ SocketHelpers.sendNotificationToPostOwner = function (pid, fromuid, command, not
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
type: command,
|
||||
bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: results.postObj.content,
|
||||
pid: pid,
|
||||
|
||||
@@ -151,7 +151,7 @@ function onMessage(socket, payload) {
|
||||
|
||||
function requireModules() {
|
||||
var modules = ['admin', 'categories', 'groups', 'meta', 'modules',
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist',
|
||||
'notifications', 'plugins', 'posts', 'topics', 'user', 'blacklist', 'flags',
|
||||
];
|
||||
|
||||
modules.forEach(function (module) {
|
||||
@@ -219,7 +219,11 @@ function addRedisAdapter(io) {
|
||||
var redis = require('../database/redis');
|
||||
var pub = redis.connect();
|
||||
var sub = redis.connect();
|
||||
io.adapter(redisAdapter({ pubClient: pub, subClient: sub }));
|
||||
io.adapter(redisAdapter({
|
||||
key: 'db:' + nconf.get('redis:database') + ':adapter_key',
|
||||
pubClient: pub,
|
||||
subClient: sub,
|
||||
}));
|
||||
} else if (nconf.get('isCluster') === 'true') {
|
||||
winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.');
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
var async = require('async');
|
||||
var validator = require('validator');
|
||||
|
||||
var db = require('../database');
|
||||
var meta = require('../meta');
|
||||
var notifications = require('../notifications');
|
||||
var plugins = require('../plugins');
|
||||
@@ -36,6 +37,17 @@ SocketModules.chats.getRaw = function (socket, data, callback) {
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketModules.chats.isDnD = function (socket, uid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.getObjectField('user:' + uid, 'status', next);
|
||||
},
|
||||
function (status, next) {
|
||||
next(null, status === 'dnd');
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketModules.chats.newRoom = function (socket, data, callback) {
|
||||
if (!data) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
@@ -133,6 +145,7 @@ SocketModules.chats.loadRoom = function (socket, data, callback) {
|
||||
results.roomData.groupChat = results.roomData.hasOwnProperty('groupChat') ? results.roomData.groupChat : results.users.length > 2;
|
||||
results.roomData.isOwner = parseInt(results.roomData.owner, 10) === socket.uid;
|
||||
results.roomData.maximumUsersInChatRoom = parseInt(meta.config.maximumUsersInChatRoom, 10) || 0;
|
||||
results.roomData.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000;
|
||||
results.roomData.showUserInput = !results.roomData.maximumUsersInChatRoom || results.roomData.maximumUsersInChatRoom > 2;
|
||||
next(null, results.roomData);
|
||||
},
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var user = require('../user');
|
||||
var notifications = require('../notifications');
|
||||
var utils = require('../utils');
|
||||
|
||||
var SocketNotifs = {};
|
||||
var SocketNotifs = module.exports;
|
||||
|
||||
SocketNotifs.get = function (socket, data, callback) {
|
||||
if (data && Array.isArray(data.nids) && socket.uid) {
|
||||
@@ -15,25 +12,6 @@ SocketNotifs.get = function (socket, data, callback) {
|
||||
}
|
||||
};
|
||||
|
||||
SocketNotifs.loadMore = function (socket, data, callback) {
|
||||
if (!data || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
var start = parseInt(data.after, 10);
|
||||
var stop = start + 20;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.notifications.getAll(socket.uid, start, stop, next);
|
||||
},
|
||||
function (notifications, next) {
|
||||
next(null, { notifications: notifications, nextStart: stop });
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketNotifs.getCount = function (socket, data, callback) {
|
||||
user.notifications.getUnreadCount(socket.uid, callback);
|
||||
};
|
||||
@@ -57,5 +35,3 @@ SocketNotifs.markUnread = function (socket, nid, callback) {
|
||||
SocketNotifs.markAllRead = function (socket, data, callback) {
|
||||
notifications.markAllRead(socket.uid, callback);
|
||||
};
|
||||
|
||||
module.exports = SocketNotifs;
|
||||
|
||||
@@ -20,7 +20,6 @@ require('./posts/move')(SocketPosts);
|
||||
require('./posts/votes')(SocketPosts);
|
||||
require('./posts/bookmarks')(SocketPosts);
|
||||
require('./posts/tools')(SocketPosts);
|
||||
require('./posts/flag')(SocketPosts);
|
||||
|
||||
SocketPosts.reply = function (socket, data, callback) {
|
||||
if (!data || !data.tid || !data.content) {
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var S = require('string');
|
||||
|
||||
var user = require('../../user');
|
||||
var groups = require('../../groups');
|
||||
var posts = require('../../posts');
|
||||
var topics = require('../../topics');
|
||||
var privileges = require('../../privileges');
|
||||
var notifications = require('../../notifications');
|
||||
var plugins = require('../../plugins');
|
||||
var meta = require('../../meta');
|
||||
var utils = require('../../utils');
|
||||
|
||||
module.exports = function (SocketPosts) {
|
||||
SocketPosts.flag = function (socket, data, callback) {
|
||||
if (!socket.uid) {
|
||||
return callback(new Error('[[error:not-logged-in]]'));
|
||||
}
|
||||
|
||||
if (!data || !data.pid || !data.reason) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var flaggingUser = {};
|
||||
var post;
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
posts.getPostFields(data.pid, ['pid', 'tid', 'uid', 'content', 'deleted'], next);
|
||||
},
|
||||
function (postData, next) {
|
||||
if (parseInt(postData.deleted, 10) === 1) {
|
||||
return next(new Error('[[error:post-deleted]]'));
|
||||
}
|
||||
|
||||
post = postData;
|
||||
topics.getTopicFields(post.tid, ['title', 'cid'], next);
|
||||
},
|
||||
function (topicData, next) {
|
||||
post.topic = topicData;
|
||||
|
||||
async.parallel({
|
||||
isAdminOrMod: function (next) {
|
||||
privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next);
|
||||
},
|
||||
userData: function (next) {
|
||||
user.getUserFields(socket.uid, ['username', 'reputation', 'banned'], next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (user, next) {
|
||||
var minimumReputation = utils.isNumber(meta.config['privileges:flag']) ? parseInt(meta.config['privileges:flag'], 10) : 1;
|
||||
if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < minimumReputation) {
|
||||
return next(new Error('[[error:not-enough-reputation-to-flag]]'));
|
||||
}
|
||||
|
||||
if (parseInt(user.banned, 10) === 1) {
|
||||
return next(new Error('[[error:user-banned]]'));
|
||||
}
|
||||
|
||||
flaggingUser = user.userData;
|
||||
flaggingUser.uid = socket.uid;
|
||||
|
||||
posts.flag(post, socket.uid, data.reason, next);
|
||||
},
|
||||
function (next) {
|
||||
async.parallel({
|
||||
post: function (next) {
|
||||
posts.parsePost(post, next);
|
||||
},
|
||||
admins: function (next) {
|
||||
groups.getMembers('administrators', 0, -1, next);
|
||||
},
|
||||
globalMods: function (next) {
|
||||
groups.getMembers('Global Moderators', 0, -1, next);
|
||||
},
|
||||
moderators: function (next) {
|
||||
groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next);
|
||||
},
|
||||
}, next);
|
||||
},
|
||||
function (results, next) {
|
||||
var title = S(post.topic.title).decodeHTMLEntities().s;
|
||||
var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ',');
|
||||
|
||||
notifications.create({
|
||||
bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: post.content,
|
||||
pid: data.pid,
|
||||
path: '/post/' + data.pid,
|
||||
nid: 'post_flag:' + data.pid + ':uid:' + socket.uid,
|
||||
from: socket.uid,
|
||||
mergeId: 'notifications:user_flagged_post_in|' + data.pid,
|
||||
topicTitle: post.topic.title,
|
||||
}, function (err, notification) {
|
||||
if (err || !notification) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
plugins.fireHook('action:post.flag', { post: post, reason: data.reason, flaggingUser: flaggingUser });
|
||||
notifications.push(notification, results.admins.concat(results.moderators).concat(results.globalMods), next);
|
||||
});
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.dismissFlag = function (socket, pid, callback) {
|
||||
if (!pid || !socket.uid) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(socket.uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalModerator, next) {
|
||||
if (!isAdminOrGlobalModerator) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
posts.dismissFlag(pid, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.dismissAllFlags = function (socket, data, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(socket.uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalModerator, next) {
|
||||
if (!isAdminOrGlobalModerator) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
posts.dismissAllFlags(next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
SocketPosts.updateFlag = function (socket, data, callback) {
|
||||
if (!data || !(data.pid && data.data)) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel([
|
||||
async.apply(user.isAdminOrGlobalMod, socket.uid),
|
||||
async.apply(user.isModeratorOfAnyCategory, socket.uid),
|
||||
], function (err, results) {
|
||||
next(err, results[0] || results[1]);
|
||||
});
|
||||
},
|
||||
function (allowed, next) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[no-privileges]]'));
|
||||
}
|
||||
|
||||
// Translate form data into object
|
||||
payload = data.data.reduce(function (memo, cur) {
|
||||
memo[cur.name] = cur.value;
|
||||
return memo;
|
||||
}, payload);
|
||||
|
||||
posts.updateFlagData(socket.uid, data.pid, payload, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
};
|
||||
@@ -14,10 +14,14 @@ helpers.postCommand = function (socket, command, eventName, notification, data,
|
||||
return callback(new Error('[[error:not-logged-in]]'));
|
||||
}
|
||||
|
||||
if (!data || !data.pid || !data.room_id) {
|
||||
if (!data || !data.pid) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
if (!data.room_id) {
|
||||
return callback(new Error('[[error:invalid-room-id, ' + data.room_id + ' ]]'));
|
||||
}
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
async.parallel({
|
||||
|
||||
@@ -138,39 +138,46 @@ module.exports = function (SocketPosts) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
var postData;
|
||||
var topicData;
|
||||
var isMainAndLast = false;
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
isMainAndLastPost(data.pid, next);
|
||||
},
|
||||
function (results, next) {
|
||||
if (results.isMain && !results.isLast) {
|
||||
return callback(new Error('[[error:cant-purge-main-post]]'));
|
||||
return next(new Error('[[error:cant-purge-main-post]]'));
|
||||
}
|
||||
if (results.isMain && results.isLast) {
|
||||
return deleteTopicOf(data.pid, socket, next);
|
||||
}
|
||||
setImmediate(next);
|
||||
isMainAndLast = results.isMain && results.isLast;
|
||||
|
||||
posts.getPostFields(data.pid, ['toPid', 'tid'], next);
|
||||
},
|
||||
function (next) {
|
||||
posts.getPostField(data.pid, 'toPid', next);
|
||||
},
|
||||
function (toPid, next) {
|
||||
postData = { pid: data.pid, toPid: toPid };
|
||||
function (_postData, next) {
|
||||
postData = _postData;
|
||||
postData.pid = data.pid;
|
||||
posts.tools.purge(socket.uid, data.pid, next);
|
||||
},
|
||||
function (next) {
|
||||
websockets.in('topic_' + data.tid).emit('event:post_purged', postData);
|
||||
topics.getTopicField(data.tid, 'title', next);
|
||||
topics.getTopicFields(data.tid, ['title', 'cid'], next);
|
||||
},
|
||||
function (title, next) {
|
||||
function (_topicData, next) {
|
||||
topicData = _topicData;
|
||||
events.log({
|
||||
type: 'post-purge',
|
||||
uid: socket.uid,
|
||||
pid: data.pid,
|
||||
ip: socket.ip,
|
||||
title: String(title),
|
||||
title: String(topicData.title),
|
||||
}, next);
|
||||
},
|
||||
function (next) {
|
||||
if (isMainAndLast) {
|
||||
socketTopics.doTopicAction('purge', 'event:topic_purged', socket, { tids: [postData.tid], cid: topicData.cid }, next);
|
||||
} else {
|
||||
setImmediate(next);
|
||||
}
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
|
||||
@@ -40,19 +40,19 @@ module.exports = function (SocketTopics) {
|
||||
var reverse = data.topicPostSort === 'newest_to_oldest' || data.topicPostSort === 'most_votes';
|
||||
var start = Math.max(0, parseInt(data.after, 10));
|
||||
|
||||
var infScrollPostsPerPage = 10;
|
||||
var infScrollPostsPerPage = Math.max(0, Math.min(meta.config.postsPerPage || 20, parseInt(data.postsPerPage, 10) || meta.config.postsPerPage || 20) - 1);
|
||||
|
||||
if (data.direction > 0) {
|
||||
if (reverse) {
|
||||
start = results.topic.postcount - start;
|
||||
}
|
||||
} else if (reverse) {
|
||||
start = results.topic.postcount - start - infScrollPostsPerPage - 1;
|
||||
start = results.topic.postcount - start - infScrollPostsPerPage;
|
||||
} else {
|
||||
start = start - infScrollPostsPerPage - 1;
|
||||
start -= infScrollPostsPerPage;
|
||||
}
|
||||
|
||||
var stop = start + (infScrollPostsPerPage - 1);
|
||||
var stop = start + (infScrollPostsPerPage);
|
||||
|
||||
start = Math.max(0, start);
|
||||
stop = Math.max(0, stop);
|
||||
@@ -93,9 +93,9 @@ module.exports = function (SocketTopics) {
|
||||
}
|
||||
|
||||
var start = parseInt(data.after, 10);
|
||||
var stop = start + 9;
|
||||
var stop = start + Math.max(0, Math.min(meta.config.topicsPerPage || 20, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage || 20) - 1);
|
||||
|
||||
topics.getUnreadTopics(data.cid, socket.uid, start, stop, data.filter, callback);
|
||||
topics.getUnreadTopics({ cid: data.cid, uid: socket.uid, start: start, stop: stop, filter: data.filter }, callback);
|
||||
};
|
||||
|
||||
SocketTopics.loadMoreRecentTopics = function (socket, data, callback) {
|
||||
@@ -104,7 +104,7 @@ module.exports = function (SocketTopics) {
|
||||
}
|
||||
|
||||
var start = parseInt(data.after, 10);
|
||||
var stop = start + 9;
|
||||
var stop = start + Math.max(0, Math.min(meta.config.topicsPerPage || 20, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage || 20) - 1);
|
||||
|
||||
topics.getRecentTopics(data.cid, socket.uid, start, stop, data.filter, callback);
|
||||
};
|
||||
@@ -115,7 +115,7 @@ module.exports = function (SocketTopics) {
|
||||
}
|
||||
|
||||
var start = parseInt(data.after, 10);
|
||||
var stop = start + 9;
|
||||
var stop = start + Math.max(0, Math.min(meta.config.topicsPerPage || 20, parseInt(data.topicsPerPage, 10) || meta.config.topicsPerPage || 20) - 1);
|
||||
|
||||
topics.getTopicsFromSet(data.set, socket.uid, start, stop, callback);
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ module.exports = function (SocketTopics) {
|
||||
SocketTopics.markCategoryTopicsRead = function (socket, cid, callback) {
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
topics.getUnreadTids(cid, socket.uid, '', next);
|
||||
topics.getUnreadTids({ cid: cid, uid: socket.uid, filter: '' }, next);
|
||||
},
|
||||
function (tids, next) {
|
||||
SocketTopics.markAsRead(socket, tids, next);
|
||||
|
||||
@@ -171,6 +171,7 @@ SocketUser.follow = function (socket, data, callback) {
|
||||
function (_userData, next) {
|
||||
userData = _userData;
|
||||
notifications.create({
|
||||
type: 'follow',
|
||||
bodyShort: '[[notifications:user_started_following_you, ' + userData.username + ']]',
|
||||
nid: 'follow:' + data.uid + ':uid:' + socket.uid,
|
||||
from: socket.uid,
|
||||
@@ -256,6 +257,7 @@ SocketUser.getUnreadCounts = function (socket, data, callback) {
|
||||
async.parallel({
|
||||
unreadTopicCount: async.apply(topics.getTotalUnread, socket.uid),
|
||||
unreadNewTopicCount: async.apply(topics.getTotalUnread, socket.uid, 'new'),
|
||||
unreadWatchedTopicCount: async.apply(topics.getTotalUnread, socket.uid, 'watched'),
|
||||
unreadChatCount: async.apply(messaging.getUnreadCount, socket.uid),
|
||||
unreadNotificationCount: async.apply(user.notifications.getUnreadCount, socket.uid),
|
||||
}, callback);
|
||||
@@ -315,7 +317,7 @@ SocketUser.getUserByEmail = function (socket, email, callback) {
|
||||
};
|
||||
|
||||
SocketUser.setModerationNote = function (socket, data, callback) {
|
||||
if (!socket.uid || !data || !data.uid) {
|
||||
if (!socket.uid || !data || !data.uid || !data.note) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
@@ -334,11 +336,13 @@ SocketUser.setModerationNote = function (socket, data, callback) {
|
||||
if (!allowed) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
if (data.note) {
|
||||
user.setUserField(data.uid, 'moderationNote', data.note, next);
|
||||
} else {
|
||||
db.deleteObjectField('user:' + data.uid, 'moderationNote', next);
|
||||
}
|
||||
|
||||
var note = {
|
||||
uid: socket.uid,
|
||||
note: data.note,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
db.sortedSetAdd('uid:' + data.uid + ':moderation:notes', note.timestamp, JSON.stringify(note), next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
|
||||
var user = require('../../user');
|
||||
var meta = require('../../meta');
|
||||
var websockets = require('../index');
|
||||
var events = require('../../events');
|
||||
|
||||
var privileges = require('../../privileges');
|
||||
var plugins = require('../../plugins');
|
||||
var emailer = require('../../emailer');
|
||||
var translator = require('../../translator');
|
||||
|
||||
module.exports = function (SocketUser) {
|
||||
SocketUser.banUsers = function (socket, data, callback) {
|
||||
@@ -35,6 +39,9 @@ module.exports = function (SocketUser) {
|
||||
});
|
||||
next();
|
||||
},
|
||||
function (next) {
|
||||
user.auth.revokeAllSessions(uid, next);
|
||||
},
|
||||
], next);
|
||||
}, callback);
|
||||
};
|
||||
@@ -72,10 +79,10 @@ module.exports = function (SocketUser) {
|
||||
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
user.isAdminOrGlobalMod(uid, next);
|
||||
privileges.users.hasBanPrivilege(uid, next);
|
||||
},
|
||||
function (isAdminOrGlobalMod, next) {
|
||||
if (!isAdminOrGlobalMod) {
|
||||
function (hasBanPrivilege, next) {
|
||||
if (!hasBanPrivilege) {
|
||||
return next(new Error('[[error:no-privileges]]'));
|
||||
}
|
||||
async.each(uids, method, next);
|
||||
@@ -92,10 +99,38 @@ module.exports = function (SocketUser) {
|
||||
if (isAdmin) {
|
||||
return next(new Error('[[error:cant-ban-other-admins]]'));
|
||||
}
|
||||
|
||||
user.getUserField(uid, 'username', next);
|
||||
},
|
||||
function (username, next) {
|
||||
var siteTitle = meta.config.title || 'NodeBB';
|
||||
var data = {
|
||||
subject: '[[email:banned.subject, ' + siteTitle + ']]',
|
||||
site_title: siteTitle,
|
||||
username: username,
|
||||
until: until ? new Date(until).toString() : false,
|
||||
reason: reason,
|
||||
};
|
||||
|
||||
emailer.send('banned', uid, data, next);
|
||||
},
|
||||
function (next) {
|
||||
user.ban(uid, until, reason, next);
|
||||
},
|
||||
function (next) {
|
||||
websockets.in('uid_' + uid).emit('event:banned');
|
||||
if (!reason) {
|
||||
return translator.translate('[[user:info.banned-no-reason]]', function (translated) {
|
||||
next(false, translated);
|
||||
});
|
||||
}
|
||||
|
||||
next(false, reason);
|
||||
},
|
||||
function (_reason, next) {
|
||||
websockets.in('uid_' + uid).emit('event:banned', {
|
||||
until: until,
|
||||
reason: _reason,
|
||||
});
|
||||
next();
|
||||
},
|
||||
], callback);
|
||||
|
||||
@@ -99,6 +99,9 @@ module.exports = function (SocketUser) {
|
||||
picture: userData.uploadedpicture === userData.picture ? '' : userData.picture, // if current picture is uploaded picture, reset to user icon
|
||||
}, next);
|
||||
},
|
||||
function (next) {
|
||||
plugins.fireHook('action:user.removeUploadedPicture', { callerUid: socket.uid, uid: data.uid }, next);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ module.exports = function (SocketUser) {
|
||||
data.email = oldUserData.email;
|
||||
}
|
||||
|
||||
user.updateProfile(data.uid, data, next);
|
||||
user.updateProfile(socket.uid, data, next);
|
||||
},
|
||||
function (userData, next) {
|
||||
function log(type, eventData) {
|
||||
|
||||
14
src/start.js
14
src/start.js
@@ -48,7 +48,7 @@ start.start = function () {
|
||||
require('./socket.io').init(webserver.server);
|
||||
|
||||
if (nconf.get('isPrimary') === 'true' && !nconf.get('jobsDisabled')) {
|
||||
require('./notifications').init();
|
||||
require('./notifications').startJobs();
|
||||
require('./user').startJobs();
|
||||
}
|
||||
|
||||
@@ -58,16 +58,16 @@ start.start = function () {
|
||||
if (err) {
|
||||
switch (err.message) {
|
||||
case 'schema-out-of-date':
|
||||
winston.warn('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:');
|
||||
winston.warn(' ./nodebb upgrade');
|
||||
winston.error('Your NodeBB schema is out-of-date. Please run the following command to bring your dataset up to spec:');
|
||||
winston.error(' ./nodebb upgrade');
|
||||
break;
|
||||
case 'dependencies-out-of-date':
|
||||
winston.warn('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:');
|
||||
winston.warn(' ./nodebb upgrade');
|
||||
winston.error('One or more of NodeBB\'s dependent packages are out-of-date. Please run the following command to update them:');
|
||||
winston.error(' ./nodebb upgrade');
|
||||
break;
|
||||
case 'dependencies-missing':
|
||||
winston.warn('One or more of NodeBB\'s dependent packages are missing. Please run the following command to update them:');
|
||||
winston.warn(' ./nodebb upgrade');
|
||||
winston.error('One or more of NodeBB\'s dependent packages are missing. Please run the following command to update them:');
|
||||
winston.error(' ./nodebb upgrade');
|
||||
break;
|
||||
default:
|
||||
winston.error(err);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async');
|
||||
var _ = require('underscore');
|
||||
var validator = require('validator');
|
||||
var S = require('string');
|
||||
var db = require('../database');
|
||||
@@ -81,7 +82,7 @@ module.exports = function (Topics) {
|
||||
], next);
|
||||
},
|
||||
function (results, next) {
|
||||
plugins.fireHook('action:topic.save', topicData);
|
||||
plugins.fireHook('action:topic.save', { topic: _.clone(topicData) });
|
||||
next(null, topicData.tid);
|
||||
},
|
||||
], callback);
|
||||
@@ -177,7 +178,7 @@ module.exports = function (Topics) {
|
||||
data.postData.index = 0;
|
||||
|
||||
analytics.increment(['topics', 'topics:byCid:' + data.topicData.cid]);
|
||||
plugins.fireHook('action:topic.post', data.topicData);
|
||||
plugins.fireHook('action:topic.post', { topic: data.topicData, post: data.postData });
|
||||
|
||||
if (parseInt(uid, 10)) {
|
||||
user.notifications.sendTopicNotificationToFollowers(uid, data.topicData, data.postData);
|
||||
@@ -272,7 +273,7 @@ module.exports = function (Topics) {
|
||||
|
||||
Topics.notifyFollowers(postData, uid);
|
||||
analytics.increment(['posts', 'posts:byCid:' + cid]);
|
||||
plugins.fireHook('action:topic.reply', postData);
|
||||
plugins.fireHook('action:topic.reply', { post: _.clone(postData) });
|
||||
|
||||
next(null, postData);
|
||||
},
|
||||
|
||||
@@ -154,7 +154,10 @@ module.exports = function (Topics) {
|
||||
});
|
||||
},
|
||||
function (next) {
|
||||
plugins.fireHook('action:topic.purge', tid);
|
||||
Topics.getTopicData(tid, next);
|
||||
},
|
||||
function (topicData, next) {
|
||||
plugins.fireHook('action:topic.purge', { topic: topicData, uid: uid });
|
||||
db.delete('topic:' + tid, next);
|
||||
},
|
||||
], callback);
|
||||
|
||||
@@ -162,27 +162,31 @@ module.exports = function (Topics) {
|
||||
};
|
||||
|
||||
Topics.filterWatchedTids = function (tids, uid, callback) {
|
||||
db.sortedSetScores('uid:' + uid + ':followed_tids', tids, function (err, scores) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
tids = tids.filter(function (tid, index) {
|
||||
return tid && !!scores[index];
|
||||
});
|
||||
callback(null, tids);
|
||||
});
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.sortedSetScores('uid:' + uid + ':followed_tids', tids, next);
|
||||
},
|
||||
function (scores, next) {
|
||||
tids = tids.filter(function (tid, index) {
|
||||
return tid && !!scores[index];
|
||||
});
|
||||
next(null, tids);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Topics.filterNotIgnoredTids = function (tids, uid, callback) {
|
||||
db.sortedSetScores('uid:' + uid + ':ignored_tids', tids, function (err, scores) {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
tids = tids.filter(function (tid, index) {
|
||||
return tid && !scores[index];
|
||||
});
|
||||
callback(null, tids);
|
||||
});
|
||||
async.waterfall([
|
||||
function (next) {
|
||||
db.sortedSetScores('uid:' + uid + ':ignored_tids', tids, next);
|
||||
},
|
||||
function (scores, next) {
|
||||
tids = tids.filter(function (tid, index) {
|
||||
return tid && !scores[index];
|
||||
});
|
||||
next(null, tids);
|
||||
},
|
||||
], callback);
|
||||
};
|
||||
|
||||
Topics.notifyFollowers = function (postData, exceptUid, callback) {
|
||||
@@ -224,6 +228,7 @@ module.exports = function (Topics) {
|
||||
postData.content = posts.relativeToAbsolute(postData.content);
|
||||
|
||||
notifications.create({
|
||||
type: 'new-reply',
|
||||
bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]',
|
||||
bodyLong: postData.content,
|
||||
pid: postData.pid,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user