From e2896ef6b9f4ca601a26b6b64d8474bacdb167ad Mon Sep 17 00:00:00 2001 From: barisusakli Date: Fri, 25 Sep 2015 15:09:25 -0400 Subject: [PATCH] socketio/posts socketio/topics refactor --- public/src/client/topic/postTools.js | 2 +- src/socket.io/posts.js | 405 +----------------------- src/socket.io/posts/edit.js | 83 +++++ src/socket.io/posts/favourites.js | 157 ++++++++++ src/socket.io/posts/flag.js | 91 ++++++ src/socket.io/posts/move.js | 37 +++ src/socket.io/posts/tools.js | 103 ++++++ src/socket.io/topics.js | 413 +------------------------ src/socket.io/topics/infinitescroll.js | 109 +++++++ src/socket.io/topics/move.js | 73 +++++ src/socket.io/topics/tags.js | 34 ++ src/socket.io/topics/tools.js | 74 +++++ src/socket.io/topics/unread.js | 112 +++++++ 13 files changed, 893 insertions(+), 800 deletions(-) create mode 100644 src/socket.io/posts/edit.js create mode 100644 src/socket.io/posts/favourites.js create mode 100644 src/socket.io/posts/flag.js create mode 100644 src/socket.io/posts/move.js create mode 100644 src/socket.io/posts/tools.js create mode 100644 src/socket.io/topics/infinitescroll.js create mode 100644 src/socket.io/topics/move.js create mode 100644 src/socket.io/topics/tags.js create mode 100644 src/socket.io/topics/tools.js create mode 100644 src/socket.io/topics/unread.js diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 8a21575e85..f018075ba9 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -321,7 +321,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator } function movePost(post, pid, tid) { - socket.emit('topics.movePost', {pid: pid, tid: tid}, function(err) { + socket.emit('posts.movePost', {pid: pid, tid: tid}, function(err) { $('#move-post-modal').addClass('hide'); if (err) { diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 9d4a3a8d44..9e81c09993 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -1,27 +1,30 @@ "use strict"; var async = require('async'), - nconf = require('nconf'), + winston = require('winston'), - db = require('../database'), + posts = require('../posts'), plugins = require('../plugins'), privileges = require('../privileges'), meta = require('../meta'), topics = require('../topics'), - favourites = require('../favourites'), notifications = require('../notifications'), - groups = require('../groups'), user = require('../user'), websockets = require('./index'), socketTopics = require('./topics'), - events = require('../events'), utils = require('../../public/src/utils'), SocketPosts = {}; +require('./posts/edit')(SocketPosts); +require('./posts/move')(SocketPosts); +require('./posts/favourites')(SocketPosts); +require('./posts/tools')(SocketPosts); +require('./posts/flag')(SocketPosts); + SocketPosts.reply = function(socket, data, callback) { if(!data || !data.tid || !data.content) { return callback(new Error('[[error:invalid-data]]')); @@ -85,125 +88,6 @@ SocketPosts.notifyOnlineUsers = function(uid, result) { }); }; -SocketPosts.getVoters = function(socket, data, callback) { - if (!data || !data.pid || !data.cid) { - return callback(new Error('[[error:invalid-data]]')); - } - - async.waterfall([ - function (next) { - privileges.categories.isAdminOrMod(data.cid, socket.uid, next); - }, - function (isAdminOrMod, next) { - if (!isAdminOrMod) { - return next(new Error('[[error:no-privileges]]')); - } - - async.parallel({ - upvoteUids: function(next) { - db.getSetMembers('pid:' + data.pid + ':upvote', next); - }, - downvoteUids: function(next) { - db.getSetMembers('pid:' + data.pid + ':downvote', next); - } - }, next); - }, - function (results, next) { - async.parallel({ - upvoters: function(next) { - user.getMultipleUserFields(results.upvoteUids, ['username', 'userslug', 'picture'], next); - }, - upvoteCount: function(next) { - next(null, results.upvoteUids.length); - }, - downvoters: function(next) { - user.getMultipleUserFields(results.downvoteUids, ['username', 'userslug', 'picture'], next); - }, - downvoteCount: function(next) { - next(null, results.downvoteUids.length); - } - }, next); - } - ], callback); -}; - -SocketPosts.upvote = function(socket, data, callback) { - favouriteCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback); -}; - -SocketPosts.downvote = function(socket, data, callback) { - favouriteCommand(socket, 'downvote', 'voted', '', data, callback); -}; - -SocketPosts.unvote = function(socket, data, callback) { - favouriteCommand(socket, 'unvote', 'voted', '', data, callback); -}; - -SocketPosts.favourite = function(socket, data, callback) { - favouriteCommand(socket, 'favourite', 'favourited', 'notifications:favourited_your_post_in', data, callback); -}; - -SocketPosts.unfavourite = function(socket, data, callback) { - favouriteCommand(socket, 'unfavourite', 'favourited', '', data, callback); -}; - -function favouriteCommand(socket, command, eventName, notification, data, callback) { - if(!data || !data.pid || !data.room_id) { - return callback(new Error('[[error:invalid-data]]')); - } - async.parallel({ - exists: function(next) { - posts.exists(data.pid, next); - }, - deleted: function(next) { - posts.getPostField(data.pid, 'deleted', next); - } - }, function(err, results) { - if (err || !results.exists) { - return callback(err || new Error('[[error:invalid-pid]]')); - } - - if (parseInt(results.deleted, 10) === 1) { - return callback(new Error('[[error:post-deleted]]')); - } - - /* - hooks: - filter.post.upvote - filter.post.downvote - filter.post.unvote - filter.post.favourite - filter.post.unfavourite - */ - plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function(err, filteredData) { - if (err) { - return callback(err); - } - - executeFavouriteCommand(socket, command, eventName, notification, filteredData.data, callback); - }); - }); -} - -function executeFavouriteCommand(socket, command, eventName, notification, data, callback) { - favourites[command](data.pid, socket.uid, function(err, result) { - if (err) { - return callback(err); - } - - socket.emit('posts.' + command, result); - - if (result && eventName) { - websockets.in(data.room_id).emit('event:' + eventName, result); - } - - if (notification) { - SocketPosts.sendNotificationToPostOwner(data.pid, socket.uid, notification); - } - callback(); - }); -} - SocketPosts.sendNotificationToPostOwner = function(pid, fromuid, notification) { if(!pid || !fromuid || !notification) { return; @@ -261,165 +145,6 @@ SocketPosts.getRawPost = function(socket, pid, callback) { ], callback); }; -SocketPosts.edit = function(socket, data, callback) { - if (!socket.uid) { - return callback(new Error('[[error:not-logged-in]]')); - } else if (!data || !data.pid || !data.content) { - return callback(new Error('[[error:invalid-data]]')); - } else if (data.title && data.title.length < parseInt(meta.config.minimumTitleLength, 10)) { - return callback(new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]')); - } else if (data.title && data.title.length > parseInt(meta.config.maximumTitleLength, 10)) { - return callback(new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]')); - } else if (data.tags && data.tags.length < parseInt(meta.config.minimumTagsPerTopic, 10)) { - return callback(new Error('[[error:not-enough-tags, ' + meta.config.minimumTagsPerTopic + ']]')); - } else if (data.tags && data.tags.length > parseInt(meta.config.maximumTagsPerTopic, 10)) { - return callback(new Error('[[error:too-many-tags, ' + meta.config.maximumTagsPerTopic + ']]')); - } else if (!data.content || data.content.length < parseInt(meta.config.minimumPostLength, 10)) { - return callback(new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]')); - } else if (data.content.length > parseInt(meta.config.maximumPostLength, 10)) { - return callback(new Error('[[error:content-too-long, ' + meta.config.maximumPostLength + ']]')); - } - - posts.edit({ - uid: socket.uid, - handle: data.handle, - pid: data.pid, - title: data.title, - content: data.content, - topic_thumb: data.topic_thumb, - tags: data.tags - }, function(err, result) { - if (err) { - return callback(err); - } - - if (result.topic.renamed) { - events.log({ - type: 'topic-rename', - uid: socket.uid, - ip: socket.ip, - oldTitle: result.topic.oldTitle, - newTitle: result.topic.title - }); - } - - if (parseInt(result.post.deleted) !== 1) { - websockets.in('topic_' + result.topic.tid).emit('event:post_edited', result); - return callback(null, result.post); - } - - socket.emit('event:post_edited', result); - callback(null, result.post); - - async.parallel({ - admins: async.apply(groups.getMembers, 'administrators', 0, -1), - moderators: async.apply(groups.getMembers, 'cid:' + result.topic.cid + ':privileges:mods', 0, -1), - uidsInTopic: async.apply(websockets.getUidsInRoom, 'topic_' + result.topic.tid) - }, function(err, results) { - if (err) { - return winston.error(err); - } - - var uids = results.uidsInTopic.filter(function(uid) { - return results.admins.indexOf(uid) !== -1 || results.moderators.indexOf(uid) !== -1; - }); - - uids.forEach(function(uid) { - websockets.in('uid_' + uid).emit('event:post_edited', result); - }); - }); - }); -}; - -SocketPosts.delete = function(socket, data, callback) { - doPostAction('delete', 'event:post_deleted', socket, data, callback); -}; - -SocketPosts.restore = function(socket, data, callback) { - doPostAction('restore', 'event:post_restored', socket, data, callback); -}; - -function doPostAction(command, eventName, socket, data, callback) { - if (!data) { - return callback(new Error('[[error:invalid-data]]')); - } - - posts.tools[command](socket.uid, data.pid, function(err, postData) { - if (err) { - return callback(err); - } - - websockets.in('topic_' + data.tid).emit(eventName, postData); - - events.log({ - type: 'post-' + command, - uid: socket.uid, - pid: data.pid, - ip: socket.ip - }); - - callback(); - }); -} - -SocketPosts.purge = function(socket, data, callback) { - function purgePost() { - posts.tools.purge(socket.uid, data.pid, function(err) { - if (err) { - return callback(err); - } - - websockets.in('topic_' + data.tid).emit('event:post_purged', data.pid); - - events.log({ - type: 'post-purge', - uid: socket.uid, - pid: data.pid, - ip: socket.ip - }); - - callback(); - }); - } - - if (!data || !parseInt(data.pid, 10)) { - return callback(new Error('[[error:invalid-data]]')); - } - - isMainAndLastPost(data.pid, function(err, results) { - if (err) { - return callback(err); - } - - if (!results.isMain) { - return purgePost(); - } - - if (!results.isLast) { - return callback(new Error('[[error:cant-purge-main-post]]')); - } - - posts.getTopicFields(data.pid, ['tid', 'cid'], function(err, topic) { - if (err) { - return callback(err); - } - socketTopics.doTopicAction('delete', 'event:topic_deleted', socket, {tids: [topic.tid], cid: topic.cid}, callback); - }); - }); -}; - -function isMainAndLastPost(pid, callback) { - async.parallel({ - isMain: function(next) { - posts.isMain(pid, next); - }, - isLast: function(next) { - posts.getTopicFields(pid, ['postcount'], function(err, topic) { - next(err, topic ? parseInt(topic.postcount, 10) === 1 : false); - }); - } - }, callback); -} SocketPosts.getPrivileges = function(socket, pids, callback) { privileges.posts.get(pids, socket.uid, function(err, privileges) { @@ -434,112 +159,6 @@ SocketPosts.getPrivileges = function(socket, pids, callback) { }); }; -SocketPosts.getUpvoters = function(socket, pids, callback) { - if (!Array.isArray(pids)) { - return callback(new Error('[[error:invalid-data]]')); - } - favourites.getUpvotedUidsByPids(pids, function(err, data) { - if (err || !Array.isArray(data) || !data.length) { - return callback(err, []); - } - - async.map(data, function(uids, next) { - var otherCount = 0; - if (uids.length > 6) { - otherCount = uids.length - 5; - uids = uids.slice(0, 5); - } - user.getUsernamesByUids(uids, function(err, usernames) { - next(err, { - otherCount: otherCount, - usernames: usernames - }); - }); - }, callback); - }); -}; - -SocketPosts.flag = function(socket, pid, callback) { - if (!socket.uid) { - return callback(new Error('[[error:not-logged-in]]')); - } - - var message = '', - flaggingUser = {}, - post; - - async.waterfall([ - function(next) { - posts.getPostFields(pid, ['pid', 'tid', 'uid', 'content', 'deleted'], function(err, postData) { - if (parseInt(postData.deleted, 10) === 1) { - return next(new Error('[[error:post-deleted]]')); - } - - post = postData; - next(); - }); - }, - function(next) { - topics.getTopicFields(post.tid, ['title', 'cid'], function(err, topicData) { - post.topic = topicData; - next(); - }); - }, - function(next) { - async.parallel({ - isAdminOrMod: function(next) { - privileges.categories.isAdminOrMod(post.topic.cid, next); - }, - userData: function(next) { - user.getUserFields(socket.uid, ['username', 'reputation'], next); - } - }, next); - }, - function(user, next) { - if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < parseInt(meta.config['privileges:flag'] || 1, 10)) { - return next(new Error('[[error:not-enough-reputation-to-flag]]')); - } - - flaggingUser = user.userData; - flaggingUser.uid = socket.uid; - - next(); - }, - function(next) { - posts.flag(post, socket.uid, next); - }, - function(next) { - message = '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + post.topic.title + ']]'; - posts.parsePost(post, next); - }, - function(post, next) { - async.parallel({ - admins: function(next) { - groups.getMembers('administrators', 0, -1, next); - }, - moderators: function(next) { - groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next); - } - }, next); - }, - function(results, next) { - notifications.create({ - bodyShort: message, - bodyLong: post.content, - pid: pid, - nid: 'post_flag:' + pid + ':uid:' + socket.uid, - from: socket.uid - }, function(err, notification) { - if (err || !notification) { - return next(err); - } - - plugins.fireHook('action:post.flag', {post: post, flaggingUser: flaggingUser}); - notifications.push(notification, results.admins.concat(results.moderators), next); - }); - } - ], callback); -}; SocketPosts.loadMoreFavourites = function(socket, data, callback) { loadMorePosts('uid:' + data.uid + ':favourites', socket.uid, data, callback); @@ -560,14 +179,6 @@ function loadMorePosts(set, uid, data, callback) { posts.getPostSummariesFromSet(set, uid, start, stop, callback); } -SocketPosts.getRecentPosts = function(socket, data, callback) { - if (!data || !data.count) { - return callback(new Error('[[error:invalid-data]]')); - } - - posts.getRecentPosts(socket.uid, 0, data.count - 1, data.term, callback); -}; - SocketPosts.getCategory = function(socket, pid, callback) { posts.getCidByPid(pid, callback); }; diff --git a/src/socket.io/posts/edit.js b/src/socket.io/posts/edit.js new file mode 100644 index 0000000000..742ce915d0 --- /dev/null +++ b/src/socket.io/posts/edit.js @@ -0,0 +1,83 @@ +'use strict'; + +var async = require('async'); +var winston = require('winston'); + +var posts = require('../../posts'); +var groups = require('../../groups'); +var events = require('../../events'); +var meta = require('../../meta'); +var websockets = require('../index'); + +module.exports = function(SocketPosts) { + + SocketPosts.edit = function(socket, data, callback) { + if (!socket.uid) { + return callback(new Error('[[error:not-logged-in]]')); + } else if (!data || !data.pid || !data.content) { + return callback(new Error('[[error:invalid-data]]')); + } else if (data.title && data.title.length < parseInt(meta.config.minimumTitleLength, 10)) { + return callback(new Error('[[error:title-too-short, ' + meta.config.minimumTitleLength + ']]')); + } else if (data.title && data.title.length > parseInt(meta.config.maximumTitleLength, 10)) { + return callback(new Error('[[error:title-too-long, ' + meta.config.maximumTitleLength + ']]')); + } else if (data.tags && data.tags.length < parseInt(meta.config.minimumTagsPerTopic, 10)) { + return callback(new Error('[[error:not-enough-tags, ' + meta.config.minimumTagsPerTopic + ']]')); + } else if (data.tags && data.tags.length > parseInt(meta.config.maximumTagsPerTopic, 10)) { + return callback(new Error('[[error:too-many-tags, ' + meta.config.maximumTagsPerTopic + ']]')); + } else if (!data.content || data.content.length < parseInt(meta.config.minimumPostLength, 10)) { + return callback(new Error('[[error:content-too-short, ' + meta.config.minimumPostLength + ']]')); + } else if (data.content.length > parseInt(meta.config.maximumPostLength, 10)) { + return callback(new Error('[[error:content-too-long, ' + meta.config.maximumPostLength + ']]')); + } + + posts.edit({ + uid: socket.uid, + handle: data.handle, + pid: data.pid, + title: data.title, + content: data.content, + topic_thumb: data.topic_thumb, + tags: data.tags + }, function(err, result) { + if (err) { + return callback(err); + } + + if (result.topic.renamed) { + events.log({ + type: 'topic-rename', + uid: socket.uid, + ip: socket.ip, + oldTitle: result.topic.oldTitle, + newTitle: result.topic.title + }); + } + + if (parseInt(result.post.deleted) !== 1) { + websockets.in('topic_' + result.topic.tid).emit('event:post_edited', result); + return callback(null, result.post); + } + + socket.emit('event:post_edited', result); + callback(null, result.post); + + async.parallel({ + admins: async.apply(groups.getMembers, 'administrators', 0, -1), + moderators: async.apply(groups.getMembers, 'cid:' + result.topic.cid + ':privileges:mods', 0, -1), + uidsInTopic: async.apply(websockets.getUidsInRoom, 'topic_' + result.topic.tid) + }, function(err, results) { + if (err) { + return winston.error(err); + } + + var uids = results.uidsInTopic.filter(function(uid) { + return (results.admins.indexOf(uid) !== -1 || results.moderators.indexOf(uid) !== -1) && parseInt(uid, 10) !== socket.uid; + }); + + uids.forEach(function(uid) { + websockets.in('uid_' + uid).emit('event:post_edited', result); + }); + }); + }); + }; +}; \ No newline at end of file diff --git a/src/socket.io/posts/favourites.js b/src/socket.io/posts/favourites.js new file mode 100644 index 0000000000..f29ff25f50 --- /dev/null +++ b/src/socket.io/posts/favourites.js @@ -0,0 +1,157 @@ +'use strict'; + +var async = require('async'); + +var db = require('../../database'); +var user = require('../../user'); +var posts = require('../../posts'); +var favourites = require('../../favourites'); +var plugins = require('../../plugins'); +var websockets = require('../index'); +var privileges = require('../../privileges'); + +module.exports = function(SocketPosts) { + SocketPosts.getVoters = function(socket, data, callback) { + if (!data || !data.pid || !data.cid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + privileges.categories.isAdminOrMod(data.cid, socket.uid, next); + }, + function (isAdminOrMod, next) { + if (!isAdminOrMod) { + return next(new Error('[[error:no-privileges]]')); + } + + async.parallel({ + upvoteUids: function(next) { + db.getSetMembers('pid:' + data.pid + ':upvote', next); + }, + downvoteUids: function(next) { + db.getSetMembers('pid:' + data.pid + ':downvote', next); + } + }, next); + }, + function (results, next) { + async.parallel({ + upvoters: function(next) { + user.getMultipleUserFields(results.upvoteUids, ['username', 'userslug', 'picture'], next); + }, + upvoteCount: function(next) { + next(null, results.upvoteUids.length); + }, + downvoters: function(next) { + user.getMultipleUserFields(results.downvoteUids, ['username', 'userslug', 'picture'], next); + }, + downvoteCount: function(next) { + next(null, results.downvoteUids.length); + } + }, next); + } + ], callback); + }; + + SocketPosts.getUpvoters = function(socket, pids, callback) { + if (!Array.isArray(pids)) { + return callback(new Error('[[error:invalid-data]]')); + } + favourites.getUpvotedUidsByPids(pids, function(err, data) { + if (err || !Array.isArray(data) || !data.length) { + return callback(err, []); + } + + async.map(data, function(uids, next) { + var otherCount = 0; + if (uids.length > 6) { + otherCount = uids.length - 5; + uids = uids.slice(0, 5); + } + user.getUsernamesByUids(uids, function(err, usernames) { + next(err, { + otherCount: otherCount, + usernames: usernames + }); + }); + }, callback); + }); + }; + + SocketPosts.upvote = function(socket, data, callback) { + favouriteCommand(socket, 'upvote', 'voted', 'notifications:upvoted_your_post_in', data, callback); + }; + + SocketPosts.downvote = function(socket, data, callback) { + favouriteCommand(socket, 'downvote', 'voted', '', data, callback); + }; + + SocketPosts.unvote = function(socket, data, callback) { + favouriteCommand(socket, 'unvote', 'voted', '', data, callback); + }; + + SocketPosts.favourite = function(socket, data, callback) { + favouriteCommand(socket, 'favourite', 'favourited', 'notifications:favourited_your_post_in', data, callback); + }; + + SocketPosts.unfavourite = function(socket, data, callback) { + favouriteCommand(socket, 'unfavourite', 'favourited', '', data, callback); + }; + + function favouriteCommand(socket, command, eventName, notification, data, callback) { + if(!data || !data.pid || !data.room_id) { + return callback(new Error('[[error:invalid-data]]')); + } + async.parallel({ + exists: function(next) { + posts.exists(data.pid, next); + }, + deleted: function(next) { + posts.getPostField(data.pid, 'deleted', next); + } + }, function(err, results) { + if (err || !results.exists) { + return callback(err || new Error('[[error:invalid-pid]]')); + } + + if (parseInt(results.deleted, 10) === 1) { + return callback(new Error('[[error:post-deleted]]')); + } + + /* + hooks: + filter.post.upvote + filter.post.downvote + filter.post.unvote + filter.post.favourite + filter.post.unfavourite + */ + plugins.fireHook('filter:post.' + command, {data: data, uid: socket.uid}, function(err, filteredData) { + if (err) { + return callback(err); + } + + executeFavouriteCommand(socket, command, eventName, notification, filteredData.data, callback); + }); + }); + } + + function executeFavouriteCommand(socket, command, eventName, notification, data, callback) { + favourites[command](data.pid, socket.uid, function(err, result) { + if (err) { + return callback(err); + } + + socket.emit('posts.' + command, result); + + if (result && eventName) { + websockets.in(data.room_id).emit('event:' + eventName, result); + } + + if (notification) { + SocketPosts.sendNotificationToPostOwner(data.pid, socket.uid, notification); + } + callback(); + }); + } +}; \ No newline at end of file diff --git a/src/socket.io/posts/flag.js b/src/socket.io/posts/flag.js new file mode 100644 index 0000000000..201ec350a5 --- /dev/null +++ b/src/socket.io/posts/flag.js @@ -0,0 +1,91 @@ +'use strict'; + +var async = require('async'); + +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'); + +module.exports = function(SocketPosts) { + + SocketPosts.flag = function(socket, pid, callback) { + if (!socket.uid) { + return callback(new Error('[[error:not-logged-in]]')); + } + + var flaggingUser = {}, + post; + + async.waterfall([ + function (next) { + posts.getPostFields(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; + next(); + }, + function (next) { + async.parallel({ + isAdminOrMod: function(next) { + privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next); + }, + userData: function(next) { + user.getUserFields(socket.uid, ['username', 'reputation'], next); + } + }, next); + }, + function (user, next) { + if (!user.isAdminOrMod && parseInt(user.userData.reputation, 10) < parseInt(meta.config['privileges:flag'] || 1, 10)) { + return next(new Error('[[error:not-enough-reputation-to-flag]]')); + } + + flaggingUser = user.userData; + flaggingUser.uid = socket.uid; + + posts.flag(post, socket.uid, next); + }, + function (next) { + async.parallel({ + post: function(next) { + posts.parsePost(post, next); + }, + admins: function(next) { + groups.getMembers('administrators', 0, -1, next); + }, + moderators: function(next) { + groups.getMembers('cid:' + post.topic.cid + ':privileges:mods', 0, -1, next); + } + }, next); + }, + function (results, next) { + notifications.create({ + bodyShort: '[[notifications:user_flagged_post_in, ' + flaggingUser.username + ', ' + post.topic.title + ']]', + bodyLong: post.content, + pid: pid, + nid: 'post_flag:' + pid + ':uid:' + socket.uid, + from: socket.uid + }, function(err, notification) { + if (err || !notification) { + return next(err); + } + + plugins.fireHook('action:post.flag', {post: post, flaggingUser: flaggingUser}); + notifications.push(notification, results.admins.concat(results.moderators), next); + }); + } + ], callback); + }; +}; \ No newline at end of file diff --git a/src/socket.io/posts/move.js b/src/socket.io/posts/move.js new file mode 100644 index 0000000000..4aa114b89f --- /dev/null +++ b/src/socket.io/posts/move.js @@ -0,0 +1,37 @@ +'use strict'; + +var async = require('async'); +var privileges = require('../../privileges'); +var topics = require('../../topics'); + + +module.exports = function(SocketPosts) { + + SocketPosts.movePost = function(socket, data, callback) { + if (!socket.uid) { + return callback(new Error('[[error:not-logged-in]]')); + } + + if (!data || !data.pid || !data.tid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + privileges.posts.canMove(data.pid, socket.uid, next); + }, + function (canMove, next) { + if (!canMove){ + return next(new Error('[[error:no-privileges]]')); + } + + topics.movePostToTopic(data.pid, data.tid, next); + }, + function (next) { + SocketPosts.sendNotificationToPostOwner(data.pid, socket.uid, 'notifications:moved_your_post'); + next(); + } + ], callback); + }; + +}; \ No newline at end of file diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js new file mode 100644 index 0000000000..e5e04b8f5a --- /dev/null +++ b/src/socket.io/posts/tools.js @@ -0,0 +1,103 @@ +'use strict'; + +var async = require('async'); + +var posts = require('../../posts'); +var events = require('../../events'); +var websockets = require('../index'); +var socketTopics = require('../topics'); + + +module.exports = function(SocketPosts) { + + SocketPosts.delete = function(socket, data, callback) { + doPostAction('delete', 'event:post_deleted', socket, data, callback); + }; + + SocketPosts.restore = function(socket, data, callback) { + doPostAction('restore', 'event:post_restored', socket, data, callback); + }; + + function doPostAction(command, eventName, socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + + posts.tools[command](socket.uid, data.pid, function(err, postData) { + if (err) { + return callback(err); + } + + websockets.in('topic_' + data.tid).emit(eventName, postData); + + events.log({ + type: 'post-' + command, + uid: socket.uid, + pid: data.pid, + ip: socket.ip + }); + + callback(); + }); + } + + SocketPosts.purge = function(socket, data, callback) { + function purgePost() { + posts.tools.purge(socket.uid, data.pid, function(err) { + if (err) { + return callback(err); + } + + websockets.in('topic_' + data.tid).emit('event:post_purged', data.pid); + + events.log({ + type: 'post-purge', + uid: socket.uid, + pid: data.pid, + ip: socket.ip + }); + + callback(); + }); + } + + if (!data || !parseInt(data.pid, 10)) { + return callback(new Error('[[error:invalid-data]]')); + } + + isMainAndLastPost(data.pid, function(err, results) { + if (err) { + return callback(err); + } + + if (!results.isMain) { + return purgePost(); + } + + if (!results.isLast) { + return callback(new Error('[[error:cant-purge-main-post]]')); + } + + posts.getTopicFields(data.pid, ['tid', 'cid'], function(err, topic) { + if (err) { + return callback(err); + } + socketTopics.doTopicAction('delete', 'event:topic_deleted', socket, {tids: [topic.tid], cid: topic.cid}, callback); + }); + }); + }; + + function isMainAndLastPost(pid, callback) { + async.parallel({ + isMain: function(next) { + posts.isMain(pid, next); + }, + isLast: function(next) { + posts.getTopicFields(pid, ['postcount'], function(err, topic) { + next(err, topic ? parseInt(topic.postcount, 10) === 1 : false); + }); + } + }, callback); + } + +}; \ No newline at end of file diff --git a/src/socket.io/topics.js b/src/socket.io/topics.js index ab3a4be5d3..6b33ba7414 100644 --- a/src/socket.io/topics.js +++ b/src/socket.io/topics.js @@ -6,23 +6,22 @@ var nconf = require('nconf'), winston = require('winston'), topics = require('../topics'), - categories = require('../categories'), privileges = require('../privileges'), plugins = require('../plugins'), notifications = require('../notifications'), websockets = require('./index'), user = require('../user'), - db = require('../database'), - meta = require('../meta'), - events = require('../events'), - utils = require('../../public/src/utils'), - SocketTopics = {}; +require('./topics/unread')(SocketTopics); +require('./topics/move')(SocketTopics); +require('./topics/tools')(SocketTopics); +require('./topics/infinitescroll')(SocketTopics); +require('./topics/tags')(SocketTopics); SocketTopics.post = function(socket, data, callback) { - if(!data) { + if (!data) { return callback(new Error('[[error:invalid-data]]')); } @@ -102,175 +101,11 @@ SocketTopics.bookmark = function(socket, data, callback) { topics.setUserBookmark(data.tid, socket.uid, data.index, callback); }; -SocketTopics.markAsRead = function(socket, tids, callback) { - if (!Array.isArray(tids) || !socket.uid) { - return callback(new Error('[[error:invalid-data]]')); - } - - if (!tids.length) { - return callback(); - } - tids = tids.filter(function(tid) { - return tid && utils.isNumber(tid); - }); - - topics.markAsRead(tids, socket.uid, function(err) { - if (err) { - return callback(err); - } - - topics.pushUnreadCount(socket.uid); - - for (var i=0; i 0) { - return next(); - } - topics.getMainPost(data.tid, socket.uid, next); - }, - posts: function(next) { - topics.getTopicPosts(data.tid, set, start, stop, socket.uid, reverse, next); - }, - privileges: function(next) { - next(null, results.privileges); - }, - 'reputation:disabled': function(next) { - next(null, parseInt(meta.config['reputation:disabled'], 10) === 1); - }, - 'downvote:disabled': function(next) { - next(null, parseInt(meta.config['downvote:disabled'], 10) === 1); - } - }, function(err, results) { - if (results.mainPost) { - results.posts = [results.mainPost].concat(results.posts); - } - - callback(err, results); - }); - }); -}; - -SocketTopics.loadMoreUnreadTopics = function(socket, data, callback) { - if (!data || !data.after) { - return callback(new Error('[[error:invalid-data]]')); - } - - var start = parseInt(data.after, 10), - stop = start + 9; - - topics.getUnreadTopics(data.cid, socket.uid, start, stop, callback); -}; - -SocketTopics.loadMoreFromSet = function(socket, data, callback) { - if (!data || !data.after || !data.set) { - return callback(new Error('[[error:invalid-data]]')); - } - - var start = parseInt(data.after, 10), - stop = start + 9; - - topics.getTopicsFromSet(data.set, socket.uid, start, stop, callback); -}; - -SocketTopics.loadTopics = function(socket, data, callback) { - if (!data || !data.set || !utils.isNumber(data.start) || !utils.isNumber(data.stop)) { - return callback(new Error('[[error:invalid-data]]')); - } - - topics.getTopicsFromSet(data.set, socket.uid, data.start, data.stop, callback); -}; - -SocketTopics.getPageCount = function(socket, tid, callback) { - topics.getPageCount(tid, socket.uid, callback); -}; - -SocketTopics.searchTags = function(socket, data, callback) { - topics.searchTags(data, callback); -}; - SocketTopics.search = function(socket, data, callback) { topics.search(data.tid, data.term, callback); }; -SocketTopics.searchAndLoadTags = function(socket, data, callback) { - if (!data) { - return callback(new Error('[[error:invalid-data]]')); - } - topics.searchAndLoadTags(data, callback); -}; - -SocketTopics.loadMoreTags = function(socket, data, callback) { - if(!data || !utils.isNumber(data.after)) { - return callback(new Error('[[error:invalid-data]]')); - } - - var start = parseInt(data.after, 10), - stop = start + 99; - - topics.getTags(start, stop, function(err, tags) { - if (err) { - return callback(err); - } - - callback(null, {tags: tags, nextStart: stop + 1}); - }); -}; - SocketTopics.isModerator = function(socket, tid, callback) { topics.getTopicField(tid, 'cid', function(err, cid) { if (err) { diff --git a/src/socket.io/topics/infinitescroll.js b/src/socket.io/topics/infinitescroll.js new file mode 100644 index 0000000000..6c24399f7c --- /dev/null +++ b/src/socket.io/topics/infinitescroll.js @@ -0,0 +1,109 @@ +'use strict'; + +var async = require('async'); +var user = require('../../user'); +var topics = require('../../topics'); +var privileges = require('../../privileges'); +var meta = require('../../meta'); +var utils = require('../../../public/src/utils'); + +module.exports = function(SocketTopics) { + + SocketTopics.loadMore = function(socket, data, callback) { + if (!data || !data.tid || !utils.isNumber(data.after) || parseInt(data.after, 10) < 0) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.parallel({ + settings: function(next) { + user.getSettings(socket.uid, next); + }, + privileges: function(next) { + privileges.topics.get(data.tid, socket.uid, next); + }, + topic: function(next) { + topics.getTopicFields(data.tid, ['postcount', 'deleted'], next); + } + }, function(err, results) { + if (err) { + return callback(err); + } + + if (!results.privileges.read || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) { + return callback(new Error('[[error:no-privileges]]')); + } + + var set = 'tid:' + data.tid + ':posts'; + var reverse = results.settings.topicPostSort === 'newest_to_oldest' || results.settings.topicPostSort === 'most_votes'; + var start = Math.max(0, parseInt(data.after, 10)); + + var infScrollPostsPerPage = 10; + + if (data.direction === -1) { + start = start - (reverse ? -infScrollPostsPerPage : infScrollPostsPerPage); + } + + if (reverse) { + start = results.topic.postcount - 1 - start; + if (results.settings.topicPostSort === 'most_votes') { + set = 'tid:' + data.tid + ':posts:votes'; + } + } + + var stop = start + (infScrollPostsPerPage - 1); + + start = Math.max(0, start); + stop = Math.max(0, stop); + + async.parallel({ + mainPost: function(next) { + if (start > 0) { + return next(); + } + topics.getMainPost(data.tid, socket.uid, next); + }, + posts: function(next) { + topics.getTopicPosts(data.tid, set, start, stop, socket.uid, reverse, next); + }, + privileges: function(next) { + next(null, results.privileges); + }, + 'reputation:disabled': function(next) { + next(null, parseInt(meta.config['reputation:disabled'], 10) === 1); + }, + 'downvote:disabled': function(next) { + next(null, parseInt(meta.config['downvote:disabled'], 10) === 1); + } + }, function(err, results) { + if (results.mainPost) { + results.posts = [results.mainPost].concat(results.posts); + } + + callback(err, results); + }); + }); + }; + + SocketTopics.loadMoreUnreadTopics = function(socket, data, callback) { + if (!data || !data.after) { + return callback(new Error('[[error:invalid-data]]')); + } + + var start = parseInt(data.after, 10), + stop = start + 9; + + topics.getUnreadTopics(data.cid, socket.uid, start, stop, callback); + }; + + SocketTopics.loadMoreFromSet = function(socket, data, callback) { + if (!data || !data.after || !data.set) { + return callback(new Error('[[error:invalid-data]]')); + } + + var start = parseInt(data.after, 10), + stop = start + 9; + + topics.getTopicsFromSet(data.set, socket.uid, start, stop, callback); + }; + +}; \ No newline at end of file diff --git a/src/socket.io/topics/move.js b/src/socket.io/topics/move.js new file mode 100644 index 0000000000..61dc8e19af --- /dev/null +++ b/src/socket.io/topics/move.js @@ -0,0 +1,73 @@ +'use strict'; + +var async = require('async'); +var topics = require('../../topics'); +var categories = require('../../categories'); +var privileges = require('../../privileges'); + +module.exports = function(SocketTopics) { + + SocketTopics.move = function(socket, data, callback) { + if (!data || !Array.isArray(data.tids) || !data.cid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.eachLimit(data.tids, 10, function(tid, next) { + var topicData; + async.waterfall([ + function(next) { + privileges.topics.isAdminOrMod(tid, socket.uid, next); + }, + function(canMove, next) { + if (!canMove) { + return next(new Error('[[error:no-privileges]]')); + } + next(); + }, + function(next) { + topics.getTopicFields(tid, ['cid', 'slug'], next); + }, + function(_topicData, next) { + topicData = _topicData; + topicData.tid = tid; + topics.tools.move(tid, data.cid, socket.uid, next); + } + ], function(err) { + if (err) { + return next(err); + } + + SocketTopics.emitToTopicAndCategory('event:topic_moved', topicData); + + SocketTopics.sendNotificationToTopicOwner(tid, socket.uid, 'notifications:moved_your_topic'); + + next(); + }); + }, callback); + }; + + + SocketTopics.moveAll = function(socket, data, callback) { + if (!data || !data.cid || !data.currentCid) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + privileges.categories.canMoveAllTopics(data.currentCid, data.cid, data.uid, next); + }, + function (canMove, next) { + if (!canMove) { + return callback(new Error('[[error:no-privileges]]')); + } + + categories.getTopicIds('cid:' + data.currentCid + ':tids', true, 0, -1, next); + }, + function (tids, next) { + async.eachLimit(tids, 50, function(tid, next) { + topics.tools.move(tid, data.cid, socket.uid, next); + }, next); + } + ], callback); + }; +}; \ No newline at end of file diff --git a/src/socket.io/topics/tags.js b/src/socket.io/topics/tags.js new file mode 100644 index 0000000000..535fdc3027 --- /dev/null +++ b/src/socket.io/topics/tags.js @@ -0,0 +1,34 @@ +'use strict'; + +var topics = require('../../topics'); +var utils = require('../../../public/src/utils'); + +module.exports = function(SocketTopics) { + SocketTopics.searchTags = function(socket, data, callback) { + topics.searchTags(data, callback); + }; + + SocketTopics.searchAndLoadTags = function(socket, data, callback) { + if (!data) { + return callback(new Error('[[error:invalid-data]]')); + } + topics.searchAndLoadTags(data, callback); + }; + + SocketTopics.loadMoreTags = function(socket, data, callback) { + if (!data || !utils.isNumber(data.after)) { + return callback(new Error('[[error:invalid-data]]')); + } + + var start = parseInt(data.after, 10), + stop = start + 99; + + topics.getTags(start, stop, function(err, tags) { + if (err) { + return callback(err); + } + + callback(null, {tags: tags, nextStart: stop + 1}); + }); + }; +}; diff --git a/src/socket.io/topics/tools.js b/src/socket.io/topics/tools.js new file mode 100644 index 0000000000..698f94d3c1 --- /dev/null +++ b/src/socket.io/topics/tools.js @@ -0,0 +1,74 @@ +'use strict'; + +var async = require('async'); +var topics = require('../../topics'); +var events = require('../../events'); + +module.exports = function(SocketTopics) { + + + SocketTopics.delete = function(socket, data, callback) { + SocketTopics.doTopicAction('delete', 'event:topic_deleted', socket, data, callback); + }; + + SocketTopics.restore = function(socket, data, callback) { + SocketTopics.doTopicAction('restore', 'event:topic_restored', socket, data, callback); + }; + + SocketTopics.purge = function(socket, data, callback) { + SocketTopics.doTopicAction('purge', 'event:topic_purged', socket, data, callback); + }; + + SocketTopics.lock = function(socket, data, callback) { + SocketTopics.doTopicAction('lock', 'event:topic_locked', socket, data, callback); + }; + + SocketTopics.unlock = function(socket, data, callback) { + SocketTopics.doTopicAction('unlock', 'event:topic_unlocked', socket, data, callback); + }; + + SocketTopics.pin = function(socket, data, callback) { + SocketTopics.doTopicAction('pin', 'event:topic_pinned', socket, data, callback); + }; + + SocketTopics.unpin = function(socket, data, callback) { + SocketTopics.doTopicAction('unpin', 'event:topic_unpinned', socket, data, callback); + }; + + SocketTopics.doTopicAction = function(action, event, socket, data, callback) { + callback = callback || function() {}; + if (!socket.uid) { + return; + } + + if (!data || !Array.isArray(data.tids) || !data.cid) { + return callback(new Error('[[error:invalid-tid]]')); + } + + if (typeof topics.tools[action] !== 'function') { + return callback(); + } + + async.each(data.tids, function(tid, next) { + topics.tools[action](tid, socket.uid, function(err, data) { + if (err) { + return next(err); + } + + SocketTopics.emitToTopicAndCategory(event, data); + + if (action === 'delete' || action === 'restore' || action === 'purge') { + events.log({ + type: 'topic-' + action, + uid: socket.uid, + ip: socket.ip, + tid: tid + }); + } + + next(); + }); + }, callback); + }; + +}; \ No newline at end of file diff --git a/src/socket.io/topics/unread.js b/src/socket.io/topics/unread.js new file mode 100644 index 0000000000..a0ae8b3925 --- /dev/null +++ b/src/socket.io/topics/unread.js @@ -0,0 +1,112 @@ +'use strict'; + +var async = require('async'), + + db = require('../../database'), + user = require('../../user'), + topics = require('../../topics'), + utils = require('../../../public/src/utils'); + +module.exports = function(SocketTopics) { + + SocketTopics.markAsRead = function(socket, tids, callback) { + if (!Array.isArray(tids) || !socket.uid) { + return callback(new Error('[[error:invalid-data]]')); + } + + if (!tids.length) { + return callback(); + } + tids = tids.filter(function(tid) { + return tid && utils.isNumber(tid); + }); + + topics.markAsRead(tids, socket.uid, function(err) { + if (err) { + return callback(err); + } + + topics.pushUnreadCount(socket.uid); + + for (var i=0; i