diff --git a/src/emailer.js b/src/emailer.js index d8477a658b..d6ead0a771 100644 --- a/src/emailer.js +++ b/src/emailer.js @@ -218,7 +218,7 @@ Emailer.send = function (template, uid, params, callback) { }); }, ], function (err) { - return callback(err); + callback(err); }); }; diff --git a/src/posts/queue.js b/src/posts/queue.js index d861792b30..e48ea5d5c9 100644 --- a/src/posts/queue.js +++ b/src/posts/queue.js @@ -2,16 +2,16 @@ const _ = require('lodash'); -var db = require('../database'); -var user = require('../user'); -var meta = require('../meta'); -var groups = require('../groups'); -var topics = require('../topics'); -var categories = require('../categories'); -var notifications = require('../notifications'); -var privileges = require('../privileges'); -var plugins = require('../plugins'); -var socketHelpers = require('../socket.io/helpers'); +const db = require('../database'); +const user = require('../user'); +const meta = require('../meta'); +const groups = require('../groups'); +const topics = require('../topics'); +const categories = require('../categories'); +const notifications = require('../notifications'); +const privileges = require('../privileges'); +const plugins = require('../plugins'); +const socketHelpers = require('../socket.io/helpers'); module.exports = function (Posts) { Posts.shouldQueue = async function (uid, data) { diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 7fd30e750a..8cbd7464c0 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -1,22 +1,20 @@ 'use strict'; -var async = require('async'); -var winston = require('winston'); -var _ = require('lodash'); +const _ = require('lodash'); -var db = require('../database'); -var websockets = require('./index'); -var user = require('../user'); -var posts = require('../posts'); -var topics = require('../topics'); -var categories = require('../categories'); -var privileges = require('../privileges'); -var notifications = require('../notifications'); -var plugins = require('../plugins'); -var utils = require('../utils'); -var batch = require('../batch'); +const db = require('../database'); +const websockets = require('./index'); +const user = require('../user'); +const posts = require('../posts'); +const topics = require('../topics'); +const categories = require('../categories'); +const privileges = require('../privileges'); +const notifications = require('../notifications'); +const plugins = require('../plugins'); +const utils = require('../utils'); +const batch = require('../batch'); -var SocketHelpers = module.exports; +const SocketHelpers = module.exports; SocketHelpers.setDefaultPostData = function (data, socket) { data.uid = socket.uid; @@ -25,81 +23,50 @@ SocketHelpers.setDefaultPostData = function (data, socket) { data.fromQueue = false; }; -SocketHelpers.notifyNew = function (uid, type, result) { - async.waterfall([ - function (next) { - user.getUidsFromSet('users:online', 0, -1, next); - }, - function (uids, next) { - uids = uids.filter(toUid => parseInt(toUid, 10) !== uid); - batch.processArray(uids, function (uids, next) { - notifyUids(uid, uids, type, result, next); - }, { - interval: 1000, - }, next); - }, - ], function (err) { - if (err) { - return winston.error(err.stack); - } +SocketHelpers.notifyNew = async function (uid, type, result) { + let uids = await user.getUidsFromSet('users:online', 0, -1); + uids = uids.filter(toUid => parseInt(toUid, 10) !== uid); + await batch.processArray(uids, async function (uids) { + await notifyUids(uid, uids, type, result); + }, { + interval: 1000, }); }; -function notifyUids(uid, uids, type, result, callback) { - let watchStateUids; - let categoryWatchStates; - let topicFollowState; +async function notifyUids(uid, uids, type, result) { const post = result.posts[0]; const tid = post.topic.tid; const cid = post.topic.cid; - async.waterfall([ - function (next) { - privileges.topics.filterUids('topics:read', tid, uids, next); - }, - function (uids, next) { - watchStateUids = uids; - getWatchStates(watchStateUids, tid, cid, next); - }, - function (watchStates, next) { - categoryWatchStates = _.zipObject(watchStateUids, watchStates.categoryWatchStates); - topicFollowState = _.zipObject(watchStateUids, watchStates.topicFollowed); - const uids = filterTidCidIgnorers(watchStateUids, watchStates); - user.blocks.filterUids(uid, uids, next); - }, - function (uids, next) { - user.blocks.filterUids(post.topic.uid, uids, next); - }, - function (uids, next) { - plugins.fireHook('filter:sockets.sendNewPostToUids', { uidsTo: uids, uidFrom: uid, type: type }, next); - }, - function (data, next) { - post.ip = undefined; + uids = await privileges.topics.filterUids('topics:read', tid, uids); + const watchStateUids = uids; - data.uidsTo.forEach(function (toUid) { - post.categoryWatchState = categoryWatchStates[toUid]; - post.topic.isFollowing = topicFollowState[toUid]; - websockets.in('uid_' + toUid).emit('event:new_post', result); - if (result.topic && type === 'newTopic') { - websockets.in('uid_' + toUid).emit('event:new_topic', result.topic); - } - }); - setImmediate(next); - }, - ], callback); + const watchStates = await getWatchStates(watchStateUids, tid, cid); + + const categoryWatchStates = _.zipObject(watchStateUids, watchStates.categoryWatchStates); + const topicFollowState = _.zipObject(watchStateUids, watchStates.topicFollowed); + uids = filterTidCidIgnorers(watchStateUids, watchStates); + uids = await user.blocks.filterUids(uid, uids); + uids = await user.blocks.filterUids(post.topic.uid, uids); + const data = await plugins.fireHook('filter:sockets.sendNewPostToUids', { uidsTo: uids, uidFrom: uid, type: type }); + + post.ip = undefined; + + data.uidsTo.forEach(function (toUid) { + post.categoryWatchState = categoryWatchStates[toUid]; + post.topic.isFollowing = topicFollowState[toUid]; + websockets.in('uid_' + toUid).emit('event:new_post', result); + if (result.topic && type === 'newTopic') { + websockets.in('uid_' + toUid).emit('event:new_topic', result.topic); + } + }); } -function getWatchStates(uids, tid, cid, callback) { - async.parallel({ - topicFollowed: function (next) { - db.isSetMembers('tid:' + tid + ':followers', uids, next); - }, - topicIgnored: function (next) { - db.isSetMembers('tid:' + tid + ':ignorers', uids, next); - }, - categoryWatchStates: function (next) { - categories.getUidsWatchStates(cid, uids, next); - }, - }, callback); +async function getWatchStates(uids, tid, cid) { + return await utils.promiseParallel({ + topicFollowed: db.isSetMembers('tid:' + tid + ':followers', uids), + topicIgnored: db.isSetMembers('tid:' + tid + ':ignorers', uids), + categoryWatchStates: categories.getUidsWatchStates(cid, uids), + }); } function filterTidCidIgnorers(uids, watchStates) { @@ -109,112 +76,87 @@ function filterTidCidIgnorers(uids, watchStates) { }); } -SocketHelpers.sendNotificationToPostOwner = function (pid, fromuid, command, notification) { +SocketHelpers.sendNotificationToPostOwner = async function (pid, fromuid, command, notification) { if (!pid || !fromuid || !notification) { return; } fromuid = parseInt(fromuid, 10); - var postData; - async.waterfall([ - function (next) { - posts.getPostFields(pid, ['tid', 'uid', 'content'], next); - }, - function (_postData, next) { - postData = _postData; - async.parallel({ - canRead: async.apply(privileges.posts.can, 'topics:read', pid, postData.uid), - isIgnoring: async.apply(topics.isIgnoring, [postData.tid], postData.uid), - }, next); - }, - function (results, next) { - if (!results.canRead || results.isIgnoring[0] || !postData.uid || fromuid === postData.uid) { - return; - } - async.parallel({ - username: async.apply(user.getUserField, fromuid, 'username'), - topicTitle: async.apply(topics.getTopicField, postData.tid, 'title'), - postObj: async.apply(posts.parsePost, postData), - }, next); - }, - function (results, next) { - var title = utils.decodeHTMLEntities(results.topicTitle); - var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + const postData = await posts.getPostFields(pid, ['tid', 'uid', 'content']); + const [canRead, isIgnoring] = await Promise.all([ + privileges.posts.can('topics:read', pid, postData.uid), + topics.isIgnoring([postData.tid], postData.uid), + ]); + if (!canRead || isIgnoring[0] || !postData.uid || fromuid === postData.uid) { + return; + } + const [username, topicTitle, postObj] = await Promise.all([ + user.getUserField(fromuid, 'username'), + topics.getTopicField(postData.tid, 'title'), + posts.parsePost(postData), + ]); - notifications.create({ - type: command, - bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]', - bodyLong: results.postObj.content, - pid: pid, - tid: postData.tid, - path: '/post/' + pid, - nid: command + ':post:' + pid + ':uid:' + fromuid, - from: fromuid, - mergeId: notification + '|' + pid, - topicTitle: results.topicTitle, - }, next); - }, - ], function (err, notification) { - if (err) { - return winston.error(err); - } - if (notification) { - notifications.push(notification, [postData.uid]); - } + const title = utils.decodeHTMLEntities(topicTitle); + const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + + const notifObj = await notifications.create({ + type: command, + bodyShort: '[[' + notification + ', ' + username + ', ' + titleEscaped + ']]', + bodyLong: postObj.content, + pid: pid, + tid: postData.tid, + path: '/post/' + pid, + nid: command + ':post:' + pid + ':uid:' + fromuid, + from: fromuid, + mergeId: notification + '|' + pid, + topicTitle: topicTitle, }); + + notifications.push(notifObj, [postData.uid]); }; -SocketHelpers.sendNotificationToTopicOwner = function (tid, fromuid, command, notification) { +SocketHelpers.sendNotificationToTopicOwner = async function (tid, fromuid, command, notification) { if (!tid || !fromuid || !notification) { return; } fromuid = parseInt(fromuid, 10); - var ownerUid; - async.waterfall([ - function (next) { - async.parallel({ - username: async.apply(user.getUserField, fromuid, 'username'), - topicData: async.apply(topics.getTopicFields, tid, ['uid', 'slug', 'title']), - }, next); - }, - function (results, next) { - if (fromuid === results.topicData.uid) { - return; - } - ownerUid = results.topicData.uid; - var title = utils.decodeHTMLEntities(results.topicData.title); - var titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + const [username, topicData] = await Promise.all([ + user.getUserField(fromuid, 'username'), + topics.getTopicFields(tid, ['uid', 'slug', 'title']), + ]); - notifications.create({ - bodyShort: '[[' + notification + ', ' + results.username + ', ' + titleEscaped + ']]', - path: '/topic/' + results.topicData.slug, - nid: command + ':tid:' + tid + ':uid:' + fromuid, - from: fromuid, - }, next); - }, - ], function (err, notification) { - if (err) { - return winston.error(err); - } - if (notification && ownerUid) { - notifications.push(notification, [ownerUid]); - } + if (fromuid === topicData.uid) { + return; + } + const ownerUid = topicData.uid; + const title = utils.decodeHTMLEntities(topicData.title); + const titleEscaped = title.replace(/%/g, '%').replace(/,/g, ','); + + const notifObj = await notifications.create({ + bodyShort: '[[' + notification + ', ' + username + ', ' + titleEscaped + ']]', + path: '/topic/' + topicData.slug, + nid: command + ':tid:' + tid + ':uid:' + fromuid, + from: fromuid, }); + + if (ownerUid) { + notifications.push(notifObj, [ownerUid]); + } }; -SocketHelpers.upvote = function (data, notification) { +SocketHelpers.upvote = async function (data, notification) { if (!data || !data.post || !data.post.uid || !data.post.votes || !data.post.pid || !data.fromuid) { return; } - var votes = data.post.votes; - var touid = data.post.uid; - var fromuid = data.fromuid; - var pid = data.post.pid; + const votes = data.post.votes; + const touid = data.post.uid; + const fromuid = data.fromuid; + const pid = data.post.pid; - var shouldNotify = { + const shouldNotify = { all: function () { return votes > 0; }, @@ -234,52 +176,24 @@ SocketHelpers.upvote = function (data, notification) { return false; }, }; + const settings = await user.getSettings(touid); + const should = shouldNotify[settings.upvoteNotifFreq] || shouldNotify.all; - async.waterfall([ - function (next) { - user.getSettings(touid, next); - }, - function (settings, next) { - var should = shouldNotify[settings.upvoteNotifFreq] || shouldNotify.all; - - if (should()) { - SocketHelpers.sendNotificationToPostOwner(pid, fromuid, 'upvote', notification); - } - - next(); - }, - ], function (err) { - if (err) { - winston.error(err); - } - }); + if (should()) { + SocketHelpers.sendNotificationToPostOwner(pid, fromuid, 'upvote', notification); + } }; -SocketHelpers.rescindUpvoteNotification = function (pid, fromuid) { - var uid; - async.waterfall([ - function (next) { - notifications.rescind('upvote:post:' + pid + ':uid:' + fromuid, next); - }, - function (next) { - posts.getPostField(pid, 'uid', next); - }, - function (_uid, next) { - uid = _uid; - user.notifications.getUnreadCount(uid, next); - }, - function (count, next) { - websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); - next(); - }, - ], function (err) { - if (err) { - winston.error(err); - } - }); +SocketHelpers.rescindUpvoteNotification = async function (pid, fromuid) { + await notifications.rescind('upvote:post:' + pid + ':uid:' + fromuid); + const uid = await posts.getPostField(pid, 'uid'); + const count = await user.notifications.getUnreadCount(uid); + websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); }; SocketHelpers.emitToTopicAndCategory = function (event, data) { websockets.in('topic_' + data.tid).emit(event, data); websockets.in('category_' + data.cid).emit(event, data); }; + +require('../promisify')(SocketHelpers); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 1589fe1dad..8a76eb74d8 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -1,19 +1,18 @@ 'use strict'; -var async = require('async'); -var validator = require('validator'); +const validator = require('validator'); -var db = require('../database'); -var meta = require('../meta'); -var notifications = require('../notifications'); -var plugins = require('../plugins'); -var Messaging = require('../messaging'); -var utils = require('../utils'); -var server = require('./'); -var user = require('../user'); -var privileges = require('../privileges'); +const db = require('../database'); +const meta = require('../meta'); +const notifications = require('../notifications'); +const plugins = require('../plugins'); +const Messaging = require('../messaging'); +const utils = require('../utils'); +const server = require('./'); +const user = require('../user'); +const privileges = require('../privileges'); -var SocketModules = module.exports; +const SocketModules = module.exports; SocketModules.chats = {}; SocketModules.sounds = {}; @@ -21,403 +20,276 @@ SocketModules.settings = {}; /* Chat */ -SocketModules.chats.getRaw = function (socket, data, callback) { +SocketModules.chats.getRaw = async function (socket, data) { if (!data || !data.hasOwnProperty('mid')) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - async.waterfall([ - function (next) { - Messaging.getMessageField(data.mid, 'roomId', next); - }, - function (roomId, next) { - async.parallel({ - isAdmin: function (next) { - user.isAdministrator(socket.uid, next); - }, - hasMessage: function (next) { - db.isSortedSetMember('uid:' + socket.uid + ':chat:room:' + roomId + ':mids', data.mid, next); - }, - inRoom: function (next) { - Messaging.isUserInRoom(socket.uid, roomId, next); - }, - }, next); - }, - function (results, next) { - if (!results.isAdmin && (!results.inRoom || !results.hasMessage)) { - return next(new Error('[[error:not-allowed]]')); - } + const roomId = await Messaging.getMessageField(data.mid, 'roomId'); + const [isAdmin, hasMessage, inRoom] = await Promise.all([ + user.isAdministrator(socket.uid), + db.isSortedSetMember('uid:' + socket.uid + ':chat:room:' + roomId + ':mids', data.mid), + Messaging.isUserInRoom(socket.uid, roomId), + ]); - Messaging.getMessageField(data.mid, 'content', next); - }, - ], callback); + if (!isAdmin && (!inRoom || !hasMessage)) { + throw new Error('[[error:not-allowed]]'); + } + + return await Messaging.getMessageField(data.mid, 'content'); }; -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.isDnD = async function (socket, uid) { + const status = await db.getObjectField('user:' + uid, 'status'); + return status === 'dnd'; }; -SocketModules.chats.newRoom = function (socket, data, callback) { +SocketModules.chats.newRoom = async function (socket, data) { if (!data) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } if (rateLimitExceeded(socket)) { - return callback(new Error('[[error:too-many-messages]]')); + throw new Error('[[error:too-many-messages]]'); } - async.waterfall([ - function (next) { - privileges.global.can('chat', socket.uid, next); - }, - function (canChat, next) { - if (!canChat) { - return next(new Error('[[error:no-privileges]]')); - } - Messaging.canMessageUser(socket.uid, data.touid, next); - }, - function (next) { - Messaging.newRoom(socket.uid, [data.touid], next); - }, - ], callback); + const canChat = await privileges.global.can('chat', socket.uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + await Messaging.canMessageUser(socket.uid, data.touid); + return await Messaging.newRoom(socket.uid, [data.touid]); }; -SocketModules.chats.send = function (socket, data, callback) { +SocketModules.chats.send = async function (socket, data) { if (!data || !data.roomId || !socket.uid) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } if (rateLimitExceeded(socket)) { - return callback(new Error('[[error:too-many-messages]]')); + throw new Error('[[error:too-many-messages]]'); } + const canChat = await privileges.global.can('chat', socket.uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } + const results = await plugins.fireHook('filter:messaging.send', { + data: data, + uid: socket.uid, + }); + data = results.data; - async.waterfall([ - function (next) { - privileges.global.can('chat', socket.uid, next); - }, - function (canChat, next) { - if (!canChat) { - return next(new Error('[[error:no-privileges]]')); - } - - plugins.fireHook('filter:messaging.send', { - data: data, - uid: socket.uid, - }, function (err, results) { - data = results.data; - next(err); - }); - }, - function (next) { - Messaging.canMessageRoom(socket.uid, data.roomId, next); - }, - function (next) { - Messaging.sendMessage({ - uid: socket.uid, - roomId: data.roomId, - content: data.message, - timestamp: Date.now(), - ip: socket.ip, - }, next); - }, - function (message, next) { - Messaging.notifyUsersInRoom(socket.uid, data.roomId, message); - user.updateOnlineUsers(socket.uid); - next(null, message); - }, - ], callback); + await Messaging.canMessageRoom(socket.uid, data.roomId); + const message = await Messaging.sendMessage({ + uid: socket.uid, + roomId: data.roomId, + content: data.message, + timestamp: Date.now(), + ip: socket.ip, + }); + Messaging.notifyUsersInRoom(socket.uid, data.roomId, message); + user.updateOnlineUsers(socket.uid); + return message; }; function rateLimitExceeded(socket) { - var now = Date.now(); + const now = Date.now(); socket.lastChatMessageTime = socket.lastChatMessageTime || 0; if (now - socket.lastChatMessageTime < meta.config.chatMessageDelay) { return true; } socket.lastChatMessageTime = now; - return false; } -SocketModules.chats.loadRoom = function (socket, data, callback) { +SocketModules.chats.loadRoom = async function (socket, data) { if (!data || !data.roomId) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - Messaging.loadRoom(socket.uid, data, callback); + return await Messaging.loadRoom(socket.uid, data); }; -SocketModules.chats.getUsersInRoom = function (socket, data, callback) { +SocketModules.chats.getUsersInRoom = async function (socket, data) { if (!data || !data.roomId) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); + } + const [userData, isOwner] = await Promise.all([ + Messaging.getUsersInRoom(data.roomId, 0, -1), + Messaging.isRoomOwner(socket.uid, data.roomId), + ]); + + userData.forEach((user) => { + user.canKick = (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)) && isOwner; + }); + return userData; +}; + +SocketModules.chats.addUserToRoom = async function (socket, data) { + if (!data || !data.roomId || !data.username) { + throw new Error('[[error:invalid-data]]'); } - async.parallel({ - users: async.apply(Messaging.getUsersInRoom, data.roomId, 0, -1), - isOwner: async.apply(Messaging.isRoomOwner, socket.uid, data.roomId), - }, function (err, payload) { - if (err) { - return callback(err); - } + const canChat = await privileges.global.can('chat', socket.uid); + if (!canChat) { + throw new Error('[[error:no-privileges]]'); + } - payload.users = payload.users.map((user) => { - user.canKick = (parseInt(user.uid, 10) !== parseInt(socket.uid, 10)) && payload.isOwner; - return user; - }); + const userCount = await Messaging.getUserCountInRoom(data.roomId); + const maxUsers = meta.config.maximumUsersInChatRoom; + if (maxUsers && userCount >= maxUsers) { + throw new Error('[[error:cant-add-more-users-to-chat-room]]'); + } - callback(null, payload.users); + const uid = await user.getUidByUsername(data.username); + if (!uid) { + throw new Error('[[error:no-user]]'); + } + if (socket.uid === parseInt(uid, 10)) { + throw new Error('[[error:cant-chat-with-yourself]]'); + } + const [settings, isAdminOrGlobalMod, isFollowing] = await Promise.all([ + user.getSettings(uid), + user.isAdminOrGlobalMod(socket.uid), + user.isFollowing(uid, socket.uid), + ]); + + if (settings.restrictChat && !isAdminOrGlobalMod && !isFollowing) { + throw new Error('[[error:chat-restricted]]'); + } + + await Messaging.addUsersToRoom(socket.uid, [uid], data.roomId); +}; + +SocketModules.chats.removeUserFromRoom = async function (socket, data) { + if (!data || !data.roomId) { + throw new Error('[[error:invalid-data]]'); + } + const exists = await user.exists(data.uid); + if (!exists) { + throw new Error('[[error:no-user]]'); + } + + await Messaging.removeUsersFromRoom(socket.uid, [data.uid], data.roomId); +}; + +SocketModules.chats.leave = async function (socket, roomid) { + if (!socket.uid || !roomid) { + throw new Error('[[error:invalid-data]]'); + } + + await Messaging.leaveRoom([socket.uid], roomid); +}; + +SocketModules.chats.edit = async function (socket, data) { + if (!data || !data.roomId || !data.message) { + throw new Error('[[error:invalid-data]]'); + } + await Messaging.canEdit(data.mid, socket.uid); + await Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message); +}; + +SocketModules.chats.delete = async function (socket, data) { + if (!data || !data.roomId || !data.messageId) { + throw new Error('[[error:invalid-data]]'); + } + await Messaging.canDelete(data.messageId, socket.uid); + await Messaging.deleteMessage(data.messageId); +}; + +SocketModules.chats.restore = async function (socket, data) { + if (!data || !data.roomId || !data.messageId) { + throw new Error('[[error:invalid-data]]'); + } + await Messaging.canDelete(data.messageId, socket.uid); + await Messaging.restoreMessage(data.messageId); +}; + +SocketModules.chats.canMessage = async function (socket, roomId) { + await Messaging.canMessageRoom(socket.uid, roomId); +}; + +SocketModules.chats.markRead = async function (socket, roomId) { + if (!socket.uid || !roomId) { + throw new Error('[[error:invalid-data]]'); + } + const [uidsInRoom] = await Promise.all([ + Messaging.getUidsInRoom(roomId, 0, -1), + Messaging.markRead(socket.uid, roomId), + ]); + + Messaging.pushUnreadCount(socket.uid); + server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId }); + + if (!uidsInRoom.includes(String(socket.uid))) { + return; + } + + // Mark notification read + const nids = uidsInRoom.filter(uid => parseInt(uid, 10) !== socket.uid) + .map(uid => 'chat_' + uid + '_' + roomId); + + await notifications.markReadMultiple(nids, socket.uid); + await user.notifications.pushCount(socket.uid); +}; + +SocketModules.chats.markAllRead = async function (socket) { + await Messaging.markAllRead(socket.uid); + Messaging.pushUnreadCount(socket.uid); +}; + +SocketModules.chats.renameRoom = async function (socket, data) { + if (!data || !data.roomId || !data.newName) { + throw new Error('[[error:invalid-data]]'); + } + await Messaging.renameRoom(socket.uid, data.roomId, data.newName); + const uids = await Messaging.getUidsInRoom(data.roomId, 0, -1); + const eventData = { roomId: data.roomId, newName: validator.escape(String(data.newName)) }; + uids.forEach(function (uid) { + server.in('uid_' + uid).emit('event:chats.roomRename', eventData); }); }; -SocketModules.chats.addUserToRoom = function (socket, data, callback) { - if (!data || !data.roomId || !data.username) { - return callback(new Error('[[error:invalid-data]]')); - } - var uid; - async.waterfall([ - function (next) { - privileges.global.can('chat', socket.uid, next); - }, - function (canChat, next) { - if (!canChat) { - return next(new Error('[[error:no-privileges]]')); - } - - Messaging.getUserCountInRoom(data.roomId, next); - }, - function (userCount, next) { - var maxUsers = meta.config.maximumUsersInChatRoom; - if (maxUsers && userCount >= maxUsers) { - return next(new Error('[[error:cant-add-more-users-to-chat-room]]')); - } - next(); - }, - function (next) { - user.getUidByUsername(data.username, next); - }, - function (_uid, next) { - uid = _uid; - if (!uid) { - return next(new Error('[[error:no-user]]')); - } - if (socket.uid === parseInt(uid, 10)) { - return next(new Error('[[error:cant-chat-with-yourself]]')); - } - async.parallel({ - settings: async.apply(user.getSettings, uid), - isAdminOrGlobalMod: async.apply(user.isAdminOrGlobalMod, socket.uid), - isFollowing: async.apply(user.isFollowing, uid, socket.uid), - }, next); - }, - function (results, next) { - if (results.settings.restrictChat && !results.isAdminOrGlobalMod && !results.isFollowing) { - return next(new Error('[[error:chat-restricted]]')); - } - - Messaging.addUsersToRoom(socket.uid, [uid], data.roomId, next); - }, - ], callback); -}; - -SocketModules.chats.removeUserFromRoom = function (socket, data, callback) { - if (!data || !data.roomId) { - return callback(new Error('[[error:invalid-data]]')); - } - - async.waterfall([ - function (next) { - user.exists(data.uid, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:no-user]]')); - } - - Messaging.removeUsersFromRoom(socket.uid, [data.uid], data.roomId, next); - }, - ], callback); -}; - -SocketModules.chats.leave = function (socket, roomid, callback) { - if (!socket.uid || !roomid) { - return callback(new Error('[[error:invalid-data]]')); - } - - Messaging.leaveRoom([socket.uid], roomid, callback); -}; - - -SocketModules.chats.edit = function (socket, data, callback) { - if (!data || !data.roomId || !data.message) { - return callback(new Error('[[error:invalid-data]]')); - } - - async.waterfall([ - function (next) { - Messaging.canEdit(data.mid, socket.uid, next); - }, - function (next) { - Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, next); - }, - ], callback); -}; - -SocketModules.chats.delete = function (socket, data, callback) { - if (!data || !data.roomId || !data.messageId) { - return callback(new Error('[[error:invalid-data]]')); - } - - async.waterfall([ - function (next) { - Messaging.canDelete(data.messageId, socket.uid, next); - }, - function (next) { - Messaging.deleteMessage(data.messageId, next); - }, - ], callback); -}; - -SocketModules.chats.restore = function (socket, data, callback) { - if (!data || !data.roomId || !data.messageId) { - return callback(new Error('[[error:invalid-data]]')); - } - - async.waterfall([ - function (next) { - Messaging.canDelete(data.messageId, socket.uid, next); - }, - function (next) { - Messaging.restoreMessage(data.messageId, next); - }, - ], callback); -}; - -SocketModules.chats.canMessage = function (socket, roomId, callback) { - Messaging.canMessageRoom(socket.uid, roomId, callback); -}; - -SocketModules.chats.markRead = function (socket, roomId, callback) { - if (!socket.uid || !roomId) { - return callback(new Error('[[error:invalid-data]]')); - } - async.waterfall([ - function (next) { - async.parallel({ - uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1), - markRead: async.apply(Messaging.markRead, socket.uid, roomId), - }, next); - }, - function (results, next) { - Messaging.pushUnreadCount(socket.uid); - server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId }); - - if (!results.uidsInRoom.includes(String(socket.uid))) { - return callback(); - } - - // Mark notification read - var nids = results.uidsInRoom.filter(function (uid) { - return parseInt(uid, 10) !== socket.uid; - }).map(function (uid) { - return 'chat_' + uid + '_' + roomId; - }); - - notifications.markReadMultiple(nids, socket.uid, function () { - user.notifications.pushCount(socket.uid); - }); - - next(); - }, - ], callback); -}; - -SocketModules.chats.markAllRead = function (socket, data, callback) { - async.waterfall([ - function (next) { - Messaging.markAllRead(socket.uid, next); - }, - function (next) { - Messaging.pushUnreadCount(socket.uid); - next(); - }, - ], callback); -}; - -SocketModules.chats.renameRoom = function (socket, data, callback) { - if (!data || !data.roomId || !data.newName) { - return callback(new Error('[[error:invalid-data]]')); - } - - async.waterfall([ - function (next) { - Messaging.renameRoom(socket.uid, data.roomId, data.newName, next); - }, - function (next) { - Messaging.getUidsInRoom(data.roomId, 0, -1, next); - }, - function (uids, next) { - var eventData = { roomId: data.roomId, newName: validator.escape(String(data.newName)) }; - uids.forEach(function (uid) { - server.in('uid_' + uid).emit('event:chats.roomRename', eventData); - }); - next(); - }, - ], callback); -}; - -SocketModules.chats.getRecentChats = function (socket, data, callback) { +SocketModules.chats.getRecentChats = async function (socket, data) { if (!data || !utils.isNumber(data.after) || !utils.isNumber(data.uid)) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - var start = parseInt(data.after, 10); - var stop = start + 9; - Messaging.getRecentChats(socket.uid, data.uid, start, stop, callback); + const start = parseInt(data.after, 10); + const stop = start + 9; + return await Messaging.getRecentChats(socket.uid, data.uid, start, stop); }; -SocketModules.chats.hasPrivateChat = function (socket, uid, callback) { +SocketModules.chats.hasPrivateChat = async function (socket, uid) { if (socket.uid <= 0 || uid <= 0) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - Messaging.hasPrivateChat(socket.uid, uid, callback); + return await Messaging.hasPrivateChat(socket.uid, uid); }; -SocketModules.chats.getMessages = function (socket, data, callback) { +SocketModules.chats.getMessages = async function (socket, data) { if (!socket.uid || !data || !data.uid || !data.roomId) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - var params = { + return await Messaging.getMessages({ callerUid: socket.uid, uid: data.uid, roomId: data.roomId, start: parseInt(data.start, 10) || 0, count: 50, - }; - - Messaging.getMessages(params, callback); + }); }; -SocketModules.chats.getIP = function (socket, mid, callback) { - async.waterfall([ - function (next) { - user.isAdminOrGlobalMod(socket.uid, next); - }, - function (allowed, next) { - if (!allowed) { - return next(new Error('[[error:no-privilege]]')); - } - Messaging.getMessageField(mid, 'ip', next); - }, - ], callback); +SocketModules.chats.getIP = async function (socket, mid) { + const allowed = await user.isAdminOrGlobalMod(socket.uid); + if (!allowed) { + throw new Error('[[error:no-privilege]]'); + } + return await Messaging.getMessageField(mid, 'ip'); }; /* Sounds */ -SocketModules.sounds.getUserSoundMap = function getUserSoundMap(socket, data, callback) { - meta.sounds.getUserSoundMap(socket.uid, callback); +SocketModules.sounds.getUserSoundMap = async function getUserSoundMap(socket) { + return await meta.sounds.getUserSoundMap(socket.uid); }; + +require('../promisify')(SocketModules); diff --git a/src/socket.io/notifications.js b/src/socket.io/notifications.js index 3a56ff8a55..0b555823a6 100644 --- a/src/socket.io/notifications.js +++ b/src/socket.io/notifications.js @@ -1,63 +1,41 @@ 'use strict'; -var async = require('async'); +const user = require('../user'); +const notifications = require('../notifications'); +const SocketNotifs = module.exports; -var user = require('../user'); -var notifications = require('../notifications'); -var SocketNotifs = module.exports; - -SocketNotifs.get = function (socket, data, callback) { +SocketNotifs.get = async function (socket, data) { if (data && Array.isArray(data.nids) && socket.uid) { - user.notifications.getNotifications(data.nids, socket.uid, callback); - } else { - user.notifications.get(socket.uid, callback); + return await user.notifications.getNotifications(data.nids, socket.uid); } + return await user.notifications.get(socket.uid); }; -SocketNotifs.getCount = function (socket, data, callback) { - user.notifications.getUnreadCount(socket.uid, callback); +SocketNotifs.getCount = async function (socket) { + return await user.notifications.getUnreadCount(socket.uid); }; -SocketNotifs.deleteAll = function (socket, data, callback) { +SocketNotifs.deleteAll = async function (socket) { if (!socket.uid) { - return callback(new Error('[[error:no-privileges]]')); + throw new Error('[[error:no-privileges]]'); } - user.notifications.deleteAll(socket.uid, callback); + await user.notifications.deleteAll(socket.uid); }; -SocketNotifs.markRead = function (socket, nid, callback) { - async.waterfall([ - function (next) { - notifications.markRead(nid, socket.uid, next); - }, - function (next) { - user.notifications.pushCount(socket.uid); - next(); - }, - ], callback); +SocketNotifs.markRead = async function (socket, nid) { + await notifications.markRead(nid, socket.uid); + user.notifications.pushCount(socket.uid); }; -SocketNotifs.markUnread = function (socket, nid, callback) { - async.waterfall([ - function (next) { - notifications.markUnread(nid, socket.uid, next); - }, - function (next) { - user.notifications.pushCount(socket.uid); - next(); - }, - ], callback); +SocketNotifs.markUnread = async function (socket, nid) { + await notifications.markUnread(nid, socket.uid); + user.notifications.pushCount(socket.uid); }; -SocketNotifs.markAllRead = function (socket, data, callback) { - async.waterfall([ - function (next) { - notifications.markAllRead(socket.uid, next); - }, - function (next) { - user.notifications.pushCount(socket.uid); - next(); - }, - ], callback); +SocketNotifs.markAllRead = async function (socket) { + await notifications.markAllRead(socket.uid); + user.notifications.pushCount(socket.uid); }; + +require('../promisify')(SocketNotifs); diff --git a/src/socket.io/user.js b/src/socket.io/user.js index 959dcc7fb5..a57885a76f 100644 --- a/src/socket.io/user.js +++ b/src/socket.io/user.js @@ -1,20 +1,24 @@ 'use strict'; -var async = require('async'); +const async = require('async'); -var user = require('../user'); -var topics = require('../topics'); -var notifications = require('../notifications'); -var messaging = require('../messaging'); -var plugins = require('../plugins'); -var meta = require('../meta'); -var events = require('../events'); -var emailer = require('../emailer'); -var db = require('../database'); -var userController = require('../controllers/user'); -var privileges = require('../privileges'); +const util = require('util'); +const sleep = util.promisify(setTimeout); -var SocketUser = module.exports; +const user = require('../user'); +const topics = require('../topics'); +const notifications = require('../notifications'); +const messaging = require('../messaging'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const events = require('../events'); +const emailer = require('../emailer'); +const db = require('../database'); +const userController = require('../controllers/user'); +const privileges = require('../privileges'); +const utils = require('../utils'); + +const SocketUser = module.exports; require('./user/profile')(SocketUser); require('./user/search')(SocketUser); @@ -23,375 +27,296 @@ require('./user/picture')(SocketUser); require('./user/ban')(SocketUser); require('./user/registration')(SocketUser); -SocketUser.exists = function (socket, data, callback) { +SocketUser.exists = async function (socket, data) { if (!data || !data.username) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - meta.userOrGroupExists(data.username, callback); + return await meta.userOrGroupExists(data.username); }; -SocketUser.deleteAccount = function (socket, data, callback) { +SocketUser.deleteAccount = async function (socket, data) { if (!socket.uid) { - return callback(new Error('[[error:no-privileges]]')); + throw new Error('[[error:no-privileges]]'); } + const hasPassword = await user.hasPassword(socket.uid); + if (hasPassword) { + const ok = await user.isPasswordCorrect(socket.uid, data.password, socket.ip); + if (!ok) { + throw new Error('[[error:invalid-password]]'); + } + } + const isAdmin = await user.isAdministrator(socket.uid); + if (isAdmin) { + throw new Error('[[error:cant-delete-admin]]'); + } + const userData = await user.deleteAccount(socket.uid); + require('./index').server.sockets.emit('event:user_status_change', { uid: socket.uid, status: 'offline' }); - async.waterfall([ - function (next) { - user.hasPassword(socket.uid, next); - }, - function (hasPassword, next) { - if (!hasPassword) { - return next(); - } - user.isPasswordCorrect(socket.uid, data.password, socket.ip, function (err, ok) { - next(err || (!ok ? new Error('[[error:invalid-password]]') : undefined)); - }); - }, - function (next) { - user.isAdministrator(socket.uid, next); - }, - function (isAdmin, next) { - if (isAdmin) { - return next(new Error('[[error:cant-delete-admin]]')); - } - user.deleteAccount(socket.uid, next); - }, - function (userData, next) { - require('./index').server.sockets.emit('event:user_status_change', { uid: socket.uid, status: 'offline' }); - - events.log({ - type: 'user-delete', - uid: socket.uid, - targetUid: socket.uid, - ip: socket.ip, - username: userData.username, - email: userData.email, - }); - next(); - }, - ], callback); + await events.log({ + type: 'user-delete', + uid: socket.uid, + targetUid: socket.uid, + ip: socket.ip, + username: userData.username, + email: userData.email, + }); }; -SocketUser.emailExists = function (socket, data, callback) { +SocketUser.emailExists = async function (socket, data) { if (!data || !data.email) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - user.email.exists(data.email, callback); + return await user.email.exists(data.email); }; -SocketUser.emailConfirm = function (socket, data, callback) { +SocketUser.emailConfirm = async function (socket) { if (!socket.uid) { - return callback(new Error('[[error:no-privileges]]')); + throw new Error('[[error:no-privileges]]'); } if (!meta.config.requireEmailConfirmation) { - return callback(new Error('[[error:email-confirmations-are-disabled]]')); + throw new Error('[[error:email-confirmations-are-disabled]]'); } - user.email.sendValidationEmail(socket.uid, callback); + return await user.email.sendValidationEmail(socket.uid); }; // Password Reset SocketUser.reset = {}; -SocketUser.reset.send = function (socket, email, callback) { +SocketUser.reset.send = async function (socket, email) { if (!email) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } if (meta.config['password:disableEdit']) { - return callback(new Error('[[error:no-privileges]]')); + throw new Error('[[error:no-privileges]]'); } - - user.reset.send(email, function (err) { - events.log({ + async function logEvent(text) { + await events.log({ type: 'password-reset', - text: err ? err.message : '[[success:success]]', + text: text, ip: socket.ip, uid: socket.uid, email: email, }); - + } + try { + await user.reset.send(email); + await logEvent('[[success:success]]'); + await sleep(2500); + } catch (err) { + await logEvent(err.message); const internalErrors = ['[[error:invalid-email]]', '[[error:reset-rate-limited]]']; - if (err && internalErrors.includes(err.message)) { - err = null; + if (!internalErrors.includes(err.message)) { + throw err; } + } +}; - setTimeout(callback.bind(err), 2500); +SocketUser.reset.commit = async function (socket, data) { + if (!data || !data.code || !data.password) { + throw new Error('[[error:invalid-data]]'); + } + const [uid] = await Promise.all([ + db.getObjectField('reset:uid', data.code), + user.reset.commit(data.code, data.password), + plugins.fireHook('action:password.reset', { uid: socket.uid }), + ]); + + await events.log({ + type: 'password-reset', + uid: uid, + ip: socket.ip, + }); + + const username = await user.getUserField(uid, 'username'); + const now = new Date(); + const parsedDate = now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate(); + emailer.send('reset_notify', uid, { + username: username, + date: parsedDate, + subject: '[[email:reset.notify.subject]]', }); }; -SocketUser.reset.commit = function (socket, data, callback) { - if (!data || !data.code || !data.password) { - return callback(new Error('[[error:invalid-data]]')); - } - var uid; - async.waterfall([ - function (next) { - async.parallel({ - uid: async.apply(db.getObjectField, 'reset:uid', data.code), - reset: async.apply(user.reset.commit, data.code, data.password), - hook: async.apply(plugins.fireHook, 'action:password.reset', { uid: socket.uid }), - }, next); - }, - function (results, next) { - uid = results.uid; - events.log({ - type: 'password-reset', - uid: uid, - ip: socket.ip, - }); - - user.getUserField(uid, 'username', next); - }, - function (username, next) { - var now = new Date(); - var parsedDate = now.getFullYear() + '/' + (now.getMonth() + 1) + '/' + now.getDate(); - emailer.send('reset_notify', uid, { - username: username, - date: parsedDate, - subject: '[[email:reset.notify.subject]]', - }); - - next(); - }, - ], callback); -}; - -SocketUser.isFollowing = function (socket, data, callback) { +SocketUser.isFollowing = async function (socket, data) { if (!socket.uid || !data.uid) { - return callback(null, false); + return false; } - user.isFollowing(socket.uid, data.uid, callback); + return await user.isFollowing(socket.uid, data.uid); }; -SocketUser.follow = function (socket, data, callback) { +SocketUser.follow = async function (socket, data) { if (!socket.uid || !data) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - var userData; - async.waterfall([ - function (next) { - toggleFollow('follow', socket.uid, data.uid, next); - }, - function (next) { - user.getUserFields(socket.uid, ['username', 'userslug'], next); - }, - 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, - path: '/uid/' + data.uid + '/followers', - mergeId: 'notifications:user_started_following_you', - }, next); - }, - function (notification, next) { - if (!notification) { - return next(); - } - notification.user = userData; - notifications.push(notification, [data.uid], next); - }, - ], callback); + + await toggleFollow('follow', socket.uid, data.uid); + const userData = await user.getUserFields(socket.uid, ['username', 'userslug']); + const notifObj = await notifications.create({ + type: 'follow', + bodyShort: '[[notifications:user_started_following_you, ' + userData.username + ']]', + nid: 'follow:' + data.uid + ':uid:' + socket.uid, + from: socket.uid, + path: '/uid/' + data.uid + '/followers', + mergeId: 'notifications:user_started_following_you', + }); + if (!notifObj) { + return; + } + notifObj.user = userData; + await notifications.push(notifObj, [data.uid]); }; -SocketUser.unfollow = function (socket, data, callback) { +SocketUser.unfollow = async function (socket, data) { if (!socket.uid || !data) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - toggleFollow('unfollow', socket.uid, data.uid, callback); + await toggleFollow('unfollow', socket.uid, data.uid); }; -function toggleFollow(method, uid, theiruid, callback) { - async.waterfall([ - function (next) { - user[method](uid, theiruid, next); - }, - function (next) { - plugins.fireHook('action:user.' + method, { - fromUid: uid, - toUid: theiruid, - }); - next(); - }, - ], callback); +async function toggleFollow(method, uid, theiruid) { + await user[method](uid, theiruid); + plugins.fireHook('action:user.' + method, { + fromUid: uid, + toUid: theiruid, + }); } -SocketUser.saveSettings = function (socket, data, callback) { +SocketUser.saveSettings = async function (socket, data) { if (!socket.uid || !data || !data.settings) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - - async.waterfall([ - function (next) { - privileges.users.canEdit(socket.uid, data.uid, next); - }, - function (allowed, next) { - if (!allowed) { - return next(new Error('[[error:no-privileges]]')); - } - user.saveSettings(data.uid, data.settings, next); - }, - ], callback); + const canEdit = await privileges.users.canEdit(socket.uid, data.uid); + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + return await user.saveSettings(data.uid, data.settings); }; -SocketUser.setTopicSort = function (socket, sort, callback) { - user.setSetting(socket.uid, 'topicPostSort', sort, callback); +SocketUser.setTopicSort = async function (socket, sort) { + await user.setSetting(socket.uid, 'topicPostSort', sort); }; -SocketUser.setCategorySort = function (socket, sort, callback) { - user.setSetting(socket.uid, 'categoryTopicSort', sort, callback); +SocketUser.setCategorySort = async function (socket, sort) { + await user.setSetting(socket.uid, 'categoryTopicSort', sort); }; -SocketUser.getUnreadCount = function (socket, data, callback) { +SocketUser.getUnreadCount = async function (socket) { if (!socket.uid) { - return callback(null, 0); + return 0; } - topics.getTotalUnread(socket.uid, '', callback); + return await topics.getTotalUnread(socket.uid, ''); }; -SocketUser.getUnreadChatCount = function (socket, data, callback) { +SocketUser.getUnreadChatCount = async function (socket) { if (!socket.uid) { - return callback(null, 0); + return 0; } - messaging.getUnreadCount(socket.uid, callback); + return await messaging.getUnreadCount(socket.uid); }; -SocketUser.getUnreadCounts = function (socket, data, callback) { +SocketUser.getUnreadCounts = async function (socket) { if (!socket.uid) { - return callback(null, {}); + return {}; } - async.parallel({ - unreadCounts: async.apply(topics.getUnreadTids, { uid: socket.uid, count: true }), - unreadChatCount: async.apply(messaging.getUnreadCount, socket.uid), - unreadNotificationCount: async.apply(user.notifications.getUnreadCount, socket.uid), - }, function (err, results) { - if (err) { - return callback(err); + const results = await utils.promiseParallel({ + unreadCounts: topics.getUnreadTids({ uid: socket.uid, count: true }), + unreadChatCount: messaging.getUnreadCount(socket.uid), + unreadNotificationCount: user.notifications.getUnreadCount(socket.uid), + }); + results.unreadTopicCount = results.unreadCounts['']; + results.unreadNewTopicCount = results.unreadCounts.new; + results.unreadWatchedTopicCount = results.unreadCounts.watched; + results.unreadUnrepliedTopicCount = results.unreadCounts.unreplied; + return results; +}; + +SocketUser.invite = async function (socket, email) { + if (!email || !socket.uid) { + throw new Error('[[error:invalid-data]]'); + } + + const registrationType = meta.config.registrationType; + if (registrationType !== 'invite-only' && registrationType !== 'admin-invite-only') { + throw new Error('[[error:forum-not-invite-only]]'); + } + + const isAdmin = await user.isAdministrator(socket.uid); + if (registrationType === 'admin-invite-only' && !isAdmin) { + throw new Error('[[error:no-privileges]]'); + } + + const max = meta.config.maximumInvites; + email = email.split(',').map(email => email.trim()).filter(Boolean); + + await async.eachSeries(email, async function (email) { + let invites = 0; + if (max) { + invites = await user.getInvitesNumber(socket.uid); } - results.unreadTopicCount = results.unreadCounts['']; - results.unreadNewTopicCount = results.unreadCounts.new; - results.unreadWatchedTopicCount = results.unreadCounts.watched; - results.unreadUnrepliedTopicCount = results.unreadCounts.unreplied; - callback(null, results); + if (!isAdmin && max && invites >= max) { + throw new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]'); + } + + await user.sendInvitationEmail(socket.uid, email); }); }; -SocketUser.invite = function (socket, email, callback) { - if (!email || !socket.uid) { - return callback(new Error('[[error:invalid-data]]')); - } - - var registrationType = meta.config.registrationType; - - if (registrationType !== 'invite-only' && registrationType !== 'admin-invite-only') { - return callback(new Error('[[error:forum-not-invite-only]]')); - } - - async.waterfall([ - function (next) { - user.isAdministrator(socket.uid, next); - }, - function (isAdmin, next) { - if (registrationType === 'admin-invite-only' && !isAdmin) { - return next(new Error('[[error:no-privileges]]')); - } - var max = meta.config.maximumInvites; - email = email.split(',').map(email => email.trim()).filter(Boolean); - async.eachSeries(email, function (email, next) { - async.waterfall([ - function (next) { - if (max) { - user.getInvitesNumber(socket.uid, next); - } else { - next(null, 0); - } - }, - function (invites, next) { - if (!isAdmin && max && invites >= max) { - return next(new Error('[[error:invite-maximum-met, ' + invites + ', ' + max + ']]')); - } - - user.sendInvitationEmail(socket.uid, email, next); - }, - ], next); - }, next); - }, - ], callback); +SocketUser.getUserByUID = async function (socket, uid) { + return await userController.getUserDataByField(socket.uid, 'uid', uid); }; -SocketUser.getUserByUID = function (socket, uid, callback) { - userController.getUserDataByField(socket.uid, 'uid', uid, callback); +SocketUser.getUserByUsername = async function (socket, username) { + return await userController.getUserDataByField(socket.uid, 'username', username); }; -SocketUser.getUserByUsername = function (socket, username, callback) { - userController.getUserDataByField(socket.uid, 'username', username, callback); +SocketUser.getUserByEmail = async function (socket, email) { + return await userController.getUserDataByField(socket.uid, 'email', email); }; -SocketUser.getUserByEmail = function (socket, email, callback) { - userController.getUserDataByField(socket.uid, 'email', email, callback); -}; - -SocketUser.setModerationNote = function (socket, data, callback) { +SocketUser.setModerationNote = async function (socket, data) { if (!socket.uid || !data || !data.uid || !data.note) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } const noteData = { uid: socket.uid, note: data.note, timestamp: Date.now(), }; - async.waterfall([ - function (next) { - privileges.users.canEdit(socket.uid, data.uid, next); - }, - function (allowed, next) { - if (allowed) { - return setImmediate(next, null, allowed); - } - - user.isModeratorOfAnyCategory(socket.uid, next); - }, - function (allowed, next) { - if (!allowed) { - return next(new Error('[[error:no-privileges]]')); - } - - db.sortedSetAdd('uid:' + data.uid + ':moderation:notes', noteData.timestamp, noteData.timestamp, next); - }, - function (next) { - db.setObject('uid:' + data.uid + ':moderation:note:' + noteData.timestamp, noteData, next); - }, - ], callback); + let canEdit = await privileges.users.canEdit(socket.uid, data.uid); + if (!canEdit) { + canEdit = await user.isModeratorOfAnyCategory(socket.uid); + } + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + await db.sortedSetAdd('uid:' + data.uid + ':moderation:notes', noteData.timestamp, noteData.timestamp); + await db.setObject('uid:' + data.uid + ':moderation:note:' + noteData.timestamp, noteData); }; -SocketUser.deleteUpload = function (socket, data, callback) { +SocketUser.deleteUpload = async function (socket, data) { if (!data || !data.name || !data.uid) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - user.deleteUpload(socket.uid, data.uid, data.name, callback); + await user.deleteUpload(socket.uid, data.uid, data.name); }; SocketUser.gdpr = {}; -SocketUser.gdpr.consent = function (socket, data, callback) { - user.setUserField(socket.uid, 'gdpr_consent', 1, callback); +SocketUser.gdpr.consent = async function (socket) { + await user.setUserField(socket.uid, 'gdpr_consent', 1); }; -SocketUser.gdpr.check = function (socket, data, callback) { - async.waterfall([ - async.apply(user.isAdministrator, socket.uid), - function (isAdmin, next) { - if (!isAdmin) { - data.uid = socket.uid; - } - - db.getObjectField('user:' + data.uid, 'gdpr_consent', next); - }, - ], callback); +SocketUser.gdpr.check = async function (socket, data) { + const isAdmin = await user.isAdministrator(socket.uid); + if (!isAdmin) { + data.uid = socket.uid; + } + return await db.getObjectField('user:' + data.uid, 'gdpr_consent'); }; + +require('../promisify')(SocketUser); diff --git a/src/socket.io/user/ban.js b/src/socket.io/user/ban.js index ca70884000..c4509e7a77 100644 --- a/src/socket.io/user/ban.js +++ b/src/socket.io/user/ban.js @@ -1,149 +1,101 @@ 'use strict'; -var async = require('async'); -var winston = require('winston'); +const winston = require('winston'); -var db = require('../../database'); -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'); -var utils = require('../../../public/src/utils'); +const db = require('../../database'); +const user = require('../../user'); +const meta = require('../../meta'); +const websockets = require('../index'); +const events = require('../../events'); +const privileges = require('../../privileges'); +const plugins = require('../../plugins'); +const emailer = require('../../emailer'); +const translator = require('../../translator'); +const utils = require('../../../public/src/utils'); module.exports = function (SocketUser) { - SocketUser.banUsers = function (socket, data, callback) { + SocketUser.banUsers = async function (socket, data) { if (!data || !Array.isArray(data.uids)) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - toggleBan(socket.uid, data.uids, function (uid, next) { - async.waterfall([ - function (next) { - banUser(socket.uid, uid, data.until || 0, data.reason || '', next); - }, - function (next) { - events.log({ - type: 'user-ban', - uid: socket.uid, - targetUid: uid, - ip: socket.ip, - reason: data.reason || undefined, - }, next); - }, - function (next) { - plugins.fireHook('action:user.banned', { - callerUid: socket.uid, - ip: socket.ip, - uid: uid, - until: data.until > 0 ? data.until : undefined, - reason: data.reason || undefined, - }); - next(); - }, - function (next) { - user.auth.revokeAllSessions(uid, next); - }, - ], next); - }, callback); + await toggleBan(socket.uid, data.uids, async function (uid) { + await banUser(socket.uid, uid, data.until || 0, data.reason || ''); + await events.log({ + type: 'user-ban', + uid: socket.uid, + targetUid: uid, + ip: socket.ip, + reason: data.reason || undefined, + }); + plugins.fireHook('action:user.banned', { + callerUid: socket.uid, + ip: socket.ip, + uid: uid, + until: data.until > 0 ? data.until : undefined, + reason: data.reason || undefined, + }); + await user.auth.revokeAllSessions(uid); + }); }; - SocketUser.unbanUsers = function (socket, uids, callback) { - toggleBan(socket.uid, uids, function (uid, next) { - async.waterfall([ - function (next) { - user.bans.unban(uid, next); - }, - function (next) { - events.log({ - type: 'user-unban', - uid: socket.uid, - targetUid: uid, - ip: socket.ip, - }, next); - }, - function (next) { - plugins.fireHook('action:user.unbanned', { - callerUid: socket.uid, - ip: socket.ip, - uid: uid, - }); - next(); - }, - ], next); - }, callback); + SocketUser.unbanUsers = async function (socket, uids) { + await toggleBan(socket.uid, uids, async function (uid) { + await user.bans.unban(uid); + await events.log({ + type: 'user-unban', + uid: socket.uid, + targetUid: uid, + ip: socket.ip, + }); + plugins.fireHook('action:user.unbanned', { + callerUid: socket.uid, + ip: socket.ip, + uid: uid, + }); + }); }; - function toggleBan(uid, uids, method, callback) { + async function toggleBan(uid, uids, method) { if (!Array.isArray(uids)) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); + } + const hasBanPrivilege = await privileges.users.hasBanPrivilege(uid); + if (!hasBanPrivilege) { + throw new Error('[[error:no-privileges]]'); } - async.waterfall([ - function (next) { - privileges.users.hasBanPrivilege(uid, next); - }, - function (hasBanPrivilege, next) { - if (!hasBanPrivilege) { - return next(new Error('[[error:no-privileges]]')); - } - async.each(uids, method, next); - }, - ], callback); + await Promise.all(uids.map(uid => method(uid))); } - function banUser(callerUid, uid, until, reason, callback) { - async.waterfall([ - function (next) { - user.isAdministrator(uid, next); - }, - function (isAdmin, next) { - if (isAdmin) { - return next(new Error('[[error:cant-ban-other-admins]]')); - } + async function banUser(callerUid, uid, until, reason) { + const isAdmin = await user.isAdministrator(uid); + if (isAdmin) { + throw new Error('[[error:cant-ban-other-admins]]'); + } + const username = await user.getUserField(uid, 'username'); + const siteTitle = meta.config.title || 'NodeBB'; + const data = { + subject: '[[email:banned.subject, ' + siteTitle + ']]', + username: username, + until: until ? utils.toISOString(until) : false, + reason: reason, + }; + try { + await emailer.send('banned', uid, data); + } catch (err) { + winston.error('[emailer.send] ' + err.message); + } + const banData = await user.bans.ban(uid, until, reason); + await db.setObjectField('uid:' + uid + ':ban:' + banData.timestamp, 'fromUid', callerUid); - user.getUserField(uid, 'username', next); - }, - function (username, next) { - var siteTitle = meta.config.title || 'NodeBB'; - var data = { - subject: '[[email:banned.subject, ' + siteTitle + ']]', - username: username, - until: until ? utils.toISOString(until) : false, - reason: reason, - }; + if (!reason) { + reason = await translator.translate('[[user:info.banned-no-reason]]'); + } - emailer.send('banned', uid, data, function (err) { - if (err) { - winston.error('[emailer.send] ' + err.message); - } - next(); - }); - }, - function (next) { - user.bans.ban(uid, until, reason, next); - }, - function (banData, next) { - db.setObjectField('uid:' + uid + ':ban:' + banData.timestamp, 'fromUid', callerUid, next); - }, - function (next) { - if (reason) { - return next(null, reason); - } - translator.translate('[[user:info.banned-no-reason]]', function (translated) { - next(null, translated); - }); - }, - function (_reason, next) { - websockets.in('uid_' + uid).emit('event:banned', { - until: until, - reason: _reason, - }); - next(); - }, - ], callback); + websockets.in('uid_' + uid).emit('event:banned', { + until: until, + reason: reason, + }); } }; diff --git a/src/socket.io/user/picture.js b/src/socket.io/user/picture.js index 8f8c0577c3..9e305cc5ba 100644 --- a/src/socket.io/user/picture.js +++ b/src/socket.io/user/picture.js @@ -1,110 +1,82 @@ 'use strict'; -var async = require('async'); -var path = require('path'); -var nconf = require('nconf'); +const path = require('path'); +const nconf = require('nconf'); -var user = require('../../user'); -var plugins = require('../../plugins'); -var file = require('../../file'); +const user = require('../../user'); +const plugins = require('../../plugins'); +const file = require('../../file'); module.exports = function (SocketUser) { - SocketUser.changePicture = function (socket, data, callback) { + SocketUser.changePicture = async function (socket, data) { if (!socket.uid) { - return callback(new Error('[[error:invalid-uid]]')); + throw new Error('[[error:invalid-uid]]'); } if (!data) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - var type = data.type; + const type = data.type; + let picture = ''; + await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid); + if (type === 'default') { + picture = ''; + } else if (type === 'uploaded') { + picture = await user.getUserField(data.uid, 'uploadedpicture'); + } else { + const returnData = await plugins.fireHook('filter:user.getPicture', { + uid: socket.uid, + type: type, + picture: undefined, + }); + picture = returnData && returnData.picture; + } - async.waterfall([ - function (next) { - user.isAdminOrGlobalModOrSelf(socket.uid, data.uid, next); - }, - function (next) { - switch (type) { - case 'default': - next(null, ''); - break; - case 'uploaded': - user.getUserField(data.uid, 'uploadedpicture', next); - break; - default: - plugins.fireHook('filter:user.getPicture', { - uid: socket.uid, - type: type, - picture: undefined, - }, function (err, returnData) { - next(err, returnData && returnData.picture); - }); - break; - } - }, - function (picture, next) { - user.setUserField(data.uid, 'picture', picture, next); - }, - ], callback); + await user.setUserField(data.uid, 'picture', picture); }; - SocketUser.removeUploadedPicture = function (socket, data, callback) { + SocketUser.removeUploadedPicture = async function (socket, data) { if (!socket.uid || !data || !data.uid) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - - async.waterfall([ - function (next) { - user.isAdminOrSelf(socket.uid, data.uid, next); - }, - function (next) { - user.getUserFields(data.uid, ['uploadedpicture', 'picture'], next); - }, - function (userData, next) { - if (userData.uploadedpicture && !userData.uploadedpicture.startsWith('http')) { - var pathToFile = path.join(nconf.get('base_dir'), 'public', userData.uploadedpicture); - if (pathToFile.startsWith(nconf.get('upload_path'))) { - file.delete(pathToFile); - } - } - - user.setUserFields(data.uid, { - uploadedpicture: '', - 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); + await user.isAdminOrSelf(socket.uid, data.uid); + const userData = await user.getUserFields(data.uid, ['uploadedpicture', 'picture']); + if (userData.uploadedpicture && !userData.uploadedpicture.startsWith('http')) { + const pathToFile = path.join(nconf.get('base_dir'), 'public', userData.uploadedpicture); + if (pathToFile.startsWith(nconf.get('upload_path'))) { + file.delete(pathToFile); + } + } + await user.setUserFields(data.uid, { + uploadedpicture: '', + // if current picture is uploaded picture, reset to user icon + picture: userData.uploadedpicture === userData.picture ? '' : userData.picture, + }); + plugins.fireHook('action:user.removeUploadedPicture', { callerUid: socket.uid, uid: data.uid }); }; - SocketUser.getProfilePictures = function (socket, data, callback) { + SocketUser.getProfilePictures = async function (socket, data) { if (!data || !data.uid) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - async.waterfall([ - function (next) { - async.parallel({ - list: async.apply(plugins.fireHook, 'filter:user.listPictures', { - uid: data.uid, - pictures: [], - }), - uploaded: async.apply(user.getUserField, data.uid, 'uploadedpicture'), - }, next); - }, - function (data, next) { - if (data.uploaded) { - data.list.pictures.push({ - type: 'uploaded', - url: data.uploaded, - text: '[[user:uploaded_picture]]', - }); - } - next(null, data.list.pictures); - }, - ], callback); + const [list, uploaded] = await Promise.all([ + plugins.fireHook('filter:user.listPictures', { + uid: data.uid, + pictures: [], + }), + user.getUserField(data.uid, 'uploadedpicture'), + ]); + + if (uploaded) { + list.pictures.push({ + type: 'uploaded', + url: data.uploaded, + text: '[[user:uploaded_picture]]', + }); + } + + return list.pictures; }; }; diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index b3eb7de9be..403621bc86 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -1,229 +1,143 @@ 'use strict'; -var async = require('async'); - -var user = require('../../user'); -var meta = require('../../meta'); -var events = require('../../events'); -var privileges = require('../../privileges'); +const user = require('../../user'); +const meta = require('../../meta'); +const events = require('../../events'); +const privileges = require('../../privileges'); module.exports = function (SocketUser) { - SocketUser.changeUsernameEmail = function (socket, data, callback) { + SocketUser.changeUsernameEmail = async function (socket, data) { if (!data || !data.uid || !socket.uid) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - - async.waterfall([ - function (next) { - isPrivilegedOrSelfAndPasswordMatch(socket, data, next); - }, - function (next) { - SocketUser.updateProfile(socket, data, next); - }, - ], callback); + await isPrivilegedOrSelfAndPasswordMatch(socket, data); + return await SocketUser.updateProfile(socket, data); }; - SocketUser.updateCover = function (socket, data, callback) { + SocketUser.updateCover = async function (socket, data) { if (!socket.uid) { - return callback(new Error('[[error:no-privileges]]')); + throw new Error('[[error:no-privileges]]'); } - async.waterfall([ - function (next) { - user.isAdminOrGlobalModOrSelf(socket.uid, data.uid, next); - }, - function (next) { - user.checkMinReputation(socket.uid, data.uid, 'min:rep:cover-picture', next); - }, - function (next) { - user.updateCoverPicture(data, next); - }, - ], callback); + await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid); + await user.checkMinReputation(socket.uid, data.uid, 'min:rep:cover-picture'); + return await user.updateCoverPicture(data); }; - SocketUser.uploadCroppedPicture = function (socket, data, callback) { + SocketUser.uploadCroppedPicture = async function (socket, data) { if (!socket.uid) { - return callback(new Error('[[error:no-privileges]]')); + throw new Error('[[error:no-privileges]]'); } - async.waterfall([ - function (next) { - user.isAdminOrGlobalModOrSelf(socket.uid, data.uid, next); - }, - function (next) { - user.checkMinReputation(socket.uid, data.uid, 'min:rep:profile-picture', next); - }, - function (next) { - user.uploadCroppedPicture(data, next); - }, - ], callback); + await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid); + await user.checkMinReputation(socket.uid, data.uid, 'min:rep:profile-picture'); + return await user.uploadCroppedPicture(data); }; - SocketUser.removeCover = function (socket, data, callback) { + SocketUser.removeCover = async function (socket, data) { if (!socket.uid) { - return callback(new Error('[[error:no-privileges]]')); + throw new Error('[[error:no-privileges]]'); } - - async.waterfall([ - function (next) { - user.isAdminOrGlobalModOrSelf(socket.uid, data.uid, next); - }, - function (next) { - user.removeCoverPicture(data, next); - }, - ], callback); + await user.isAdminOrGlobalModOrSelf(socket.uid, data.uid); + await user.removeCoverPicture(data); }; - function isPrivilegedOrSelfAndPasswordMatch(socket, data, callback) { + async function isPrivilegedOrSelfAndPasswordMatch(socket, data) { const uid = socket.uid; const isSelf = parseInt(uid, 10) === parseInt(data.uid, 10); - async.waterfall([ - function (next) { - async.parallel({ - isAdmin: async.apply(user.isAdministrator, uid), - isTargetAdmin: async.apply(user.isAdministrator, data.uid), - isGlobalMod: async.apply(user.isGlobalModerator, uid), - }, next); - }, - function (results, next) { - if (results.isTargetAdmin && !results.isAdmin) { - return next(new Error('[[error:no-privileges]]')); - } + const [isAdmin, isTargetAdmin, isGlobalMod] = await Promise.all([ + user.isAdministrator(uid), + user.isAdministrator(data.uid), + user.isGlobalModerator(uid), + ]); - if (!isSelf && !(results.isAdmin || results.isGlobalMod)) { - return next(new Error('[[error:no-privileges]]')); - } + if ((isTargetAdmin && !isAdmin) || (!isSelf && !(isAdmin || isGlobalMod))) { + throw new Error('[[error:no-privileges]]'); + } + const [hasPassword, passwordMatch] = await Promise.all([ + user.hasPassword(data.uid), + data.password ? user.isPasswordCorrect(data.uid, data.password, socket.ip) : false, + ]); - async.parallel({ - hasPassword: async.apply(user.hasPassword, data.uid), - passwordMatch: function (next) { - if (data.password) { - user.isPasswordCorrect(data.uid, data.password, socket.ip, next); - } else { - next(null, false); - } - }, - }, next); - }, function (results, next) { - if (isSelf && results.hasPassword && !results.passwordMatch) { - return next(new Error('[[error:invalid-password]]')); - } - - next(); - }, - ], callback); + if (isSelf && hasPassword && !passwordMatch) { + throw new Error('[[error:invalid-password]]'); + } } - SocketUser.changePassword = function (socket, data, callback) { + SocketUser.changePassword = async function (socket, data) { if (!socket.uid) { - return callback(new Error('[[error:invalid-uid]]')); + throw new Error('[[error:invalid-uid]]'); } if (!data || !data.uid) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - async.waterfall([ - function (next) { - user.changePassword(socket.uid, Object.assign(data, { ip: socket.ip }), next); - }, - function (next) { - events.log({ - type: 'password-change', - uid: socket.uid, - targetUid: data.uid, - ip: socket.ip, - }); - next(); - }, - ], callback); - }; - - SocketUser.updateProfile = function (socket, data, callback) { - if (!socket.uid) { - return callback(new Error('[[error:invalid-uid]]')); - } - - if (!data || !data.uid) { - return callback(new Error('[[error:invalid-data]]')); - } - - var oldUserData; - async.waterfall([ - function (next) { - user.getUserFields(data.uid, ['email', 'username'], next); - }, - function (_oldUserData, next) { - oldUserData = _oldUserData; - if (!oldUserData || !oldUserData.username) { - return next(new Error('[[error:invalid-data]]')); - } - - async.parallel({ - isAdminOrGlobalMod: function (next) { - user.isAdminOrGlobalMod(socket.uid, next); - }, - canEdit: function (next) { - privileges.users.canEdit(socket.uid, data.uid, next); - }, - }, next); - }, - function (results, next) { - if (!results.canEdit) { - return next(new Error('[[error:no-privileges]]')); - } - - if (!results.isAdminOrGlobalMod && meta.config['username:disableEdit']) { - data.username = oldUserData.username; - } - - if (!results.isAdminOrGlobalMod && meta.config['email:disableEdit']) { - data.email = oldUserData.email; - } - - user.updateProfile(socket.uid, data, next); - }, - function (userData, next) { - function log(type, eventData) { - eventData.type = type; - eventData.uid = socket.uid; - eventData.targetUid = data.uid; - eventData.ip = socket.ip; - - events.log(eventData); - } - - if (userData.email !== oldUserData.email) { - log('email-change', { oldEmail: oldUserData.email, newEmail: userData.email }); - } - - if (userData.username !== oldUserData.username) { - log('username-change', { oldUsername: oldUserData.username, newUsername: userData.username }); - } - - next(null, userData); - }, - ], callback); - }; - - SocketUser.toggleBlock = function (socket, data, callback) { - let isBlocked; - - async.waterfall([ - function (next) { - async.parallel({ - can: function (next) { - user.blocks.can(socket.uid, data.blockerUid, data.blockeeUid, next); - }, - is: function (next) { - user.blocks.is(data.blockeeUid, data.blockerUid, next); - }, - }, next); - }, - function (results, next) { - isBlocked = results.is; - user.blocks[isBlocked ? 'remove' : 'add'](data.blockeeUid, data.blockerUid, next); - }, - ], function (err) { - callback(err, !isBlocked); + await user.changePassword(socket.uid, Object.assign(data, { ip: socket.ip })); + await events.log({ + type: 'password-change', + uid: socket.uid, + targetUid: data.uid, + ip: socket.ip, }); }; + + SocketUser.updateProfile = async function (socket, data) { + if (!socket.uid) { + throw new Error('[[error:invalid-uid]]'); + } + + if (!data || !data.uid) { + throw new Error('[[error:invalid-data]]'); + } + + const oldUserData = await user.getUserFields(data.uid, ['email', 'username']); + if (!oldUserData || !oldUserData.username) { + throw new Error('[[error:invalid-data]]'); + } + + const [isAdminOrGlobalMod, canEdit] = await Promise.all([ + user.isAdminOrGlobalMod(socket.uid), + privileges.users.canEdit(socket.uid, data.uid), + ]); + + if (!canEdit) { + throw new Error('[[error:no-privileges]]'); + } + + if (!isAdminOrGlobalMod && meta.config['username:disableEdit']) { + data.username = oldUserData.username; + } + + if (!isAdminOrGlobalMod && meta.config['email:disableEdit']) { + data.email = oldUserData.email; + } + + const userData = await user.updateProfile(socket.uid, data); + + async function log(type, eventData) { + eventData.type = type; + eventData.uid = socket.uid; + eventData.targetUid = data.uid; + eventData.ip = socket.ip; + await events.log(eventData); + } + + if (userData.email !== oldUserData.email) { + await log('email-change', { oldEmail: oldUserData.email, newEmail: userData.email }); + } + + if (userData.username !== oldUserData.username) { + await log('username-change', { oldUsername: oldUserData.username, newUsername: userData.username }); + } + return userData; + }; + + SocketUser.toggleBlock = async function (socket, data) { + const [is] = await Promise.all([ + user.blocks.is(data.blockeeUid, data.blockerUid), + user.blocks.can(socket.uid, data.blockerUid, data.blockeeUid), + ]); + const isBlocked = is; + await user.blocks[isBlocked ? 'remove' : 'add'](data.blockeeUid, data.blockerUid); + return !isBlocked; + }; }; diff --git a/src/socket.io/user/registration.js b/src/socket.io/user/registration.js index aee1382eb2..b8e4f39d98 100644 --- a/src/socket.io/user/registration.js +++ b/src/socket.io/user/registration.js @@ -1,70 +1,43 @@ 'use strict'; -var async = require('async'); -var user = require('../../user'); -var events = require('../../events'); +const user = require('../../user'); +const events = require('../../events'); module.exports = function (SocketUser) { - SocketUser.acceptRegistration = function (socket, data, callback) { - async.waterfall([ - function (next) { - user.isAdminOrGlobalMod(socket.uid, next); - }, - function (isAdminOrGlobalMod, next) { - if (!isAdminOrGlobalMod) { - return next(new Error('[[error:no-privileges]]')); - } - - user.acceptRegistration(data.username, next); - }, - function (uid, next) { - events.log({ - type: 'registration-approved', - uid: socket.uid, - ip: socket.ip, - targetUid: uid, - }); - next(null, uid); - }, - ], callback); + SocketUser.acceptRegistration = async function (socket, data) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + const uid = await user.acceptRegistration(data.username); + await events.log({ + type: 'registration-approved', + uid: socket.uid, + ip: socket.ip, + targetUid: uid, + }); + return uid; }; - SocketUser.rejectRegistration = function (socket, data, callback) { - async.waterfall([ - function (next) { - user.isAdminOrGlobalMod(socket.uid, next); - }, - function (isAdminOrGlobalMod, next) { - if (!isAdminOrGlobalMod) { - return next(new Error('[[error:no-privileges]]')); - } - - user.rejectRegistration(data.username, next); - }, - function (next) { - events.log({ - type: 'registration-rejected', - uid: socket.uid, - ip: socket.ip, - username: data.username, - }); - next(); - }, - ], callback); + SocketUser.rejectRegistration = async function (socket, data) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + await user.rejectRegistration(data.username); + await events.log({ + type: 'registration-rejected', + uid: socket.uid, + ip: socket.ip, + username: data.username, + }); }; - SocketUser.deleteInvitation = function (socket, data, callback) { - async.waterfall([ - function (next) { - user.isAdminOrGlobalMod(socket.uid, next); - }, - function (isAdminOrGlobalMod, next) { - if (!isAdminOrGlobalMod) { - return next(new Error('[[error:no-privileges]]')); - } - - user.deleteInvitation(data.invitedBy, data.email, next); - }, - ], callback); + SocketUser.deleteInvitation = async function (socket, data) { + const isAdminOrGlobalMod = await user.isAdminOrGlobalMod(socket.uid); + if (!isAdminOrGlobalMod) { + throw new Error('[[error:no-privileges]]'); + } + await user.deleteInvitation(data.invitedBy, data.email); }; }; diff --git a/src/socket.io/user/search.js b/src/socket.io/user/search.js index 5d1b52b068..deb2fecfd6 100644 --- a/src/socket.io/user/search.js +++ b/src/socket.io/user/search.js @@ -1,42 +1,31 @@ 'use strict'; -var async = require('async'); - -var user = require('../../user'); -var pagination = require('../../pagination'); -var privileges = require('../../privileges'); +const user = require('../../user'); +const pagination = require('../../pagination'); +const privileges = require('../../privileges'); module.exports = function (SocketUser) { - SocketUser.search = function (socket, data, callback) { + SocketUser.search = async function (socket, data) { if (!data) { - return callback(new Error('[[error:invalid-data]]')); + throw new Error('[[error:invalid-data]]'); } - - async.waterfall([ - function (next) { - privileges.global.can('search:users', socket.uid, next); - }, - function (allowed, next) { - if (!allowed) { - return next(new Error('[[error:no-privileges]]')); - } - user.search({ - query: data.query, - page: data.page, - searchBy: data.searchBy, - sortBy: data.sortBy, - onlineOnly: data.onlineOnly, - bannedOnly: data.bannedOnly, - flaggedOnly: data.flaggedOnly, - paginate: data.paginate, - uid: socket.uid, - }, next); - }, - function (result, next) { - result.pagination = pagination.create(data.page, result.pageCount); - result['route_users:' + data.sortBy] = true; - next(null, result); - }, - ], callback); + const allowed = await privileges.global.can('search:users', socket.uid); + if (!allowed) { + throw new Error('[[error:no-privileges]]'); + } + const result = await user.search({ + query: data.query, + page: data.page, + searchBy: data.searchBy, + sortBy: data.sortBy, + onlineOnly: data.onlineOnly, + bannedOnly: data.bannedOnly, + flaggedOnly: data.flaggedOnly, + paginate: data.paginate, + uid: socket.uid, + }); + result.pagination = pagination.create(data.page, result.pageCount); + result['route_users:' + data.sortBy] = true; + return result; }; }; diff --git a/src/socket.io/user/status.js b/src/socket.io/user/status.js index 249bbb134a..a00c1a6e9b 100644 --- a/src/socket.io/user/status.js +++ b/src/socket.io/user/status.js @@ -1,59 +1,40 @@ 'use strict'; -var async = require('async'); - -var user = require('../../user'); -var websockets = require('../index'); +const user = require('../../user'); +const websockets = require('../index'); module.exports = function (SocketUser) { - SocketUser.checkStatus = function (socket, uid, callback) { + SocketUser.checkStatus = async function (socket, uid) { if (!socket.uid) { - return callback(new Error('[[error:invalid-uid]]')); + throw new Error('[[error:invalid-uid]]'); } - async.waterfall([ - function (next) { - user.getUserFields(uid, ['lastonline', 'status'], next); - }, - function (userData, next) { - next(null, user.getStatus(userData)); - }, - ], callback); + const userData = await user.getUserFields(uid, ['lastonline', 'status']); + return user.getStatus(userData); }; - SocketUser.setStatus = function (socket, status, callback) { + SocketUser.setStatus = async function (socket, status) { if (socket.uid <= 0) { - return callback(new Error('[[error:invalid-uid]]')); + throw new Error('[[error:invalid-uid]]'); } - var allowedStatus = ['online', 'offline', 'dnd', 'away']; + const allowedStatus = ['online', 'offline', 'dnd', 'away']; if (!allowedStatus.includes(status)) { - return callback(new Error('[[error:invalid-user-status]]')); + throw new Error('[[error:invalid-user-status]]'); } - var data = { status: status }; + const userData = { status: status }; if (status !== 'offline') { - data.lastonline = Date.now(); + userData.lastonline = Date.now(); } - - async.waterfall([ - function (next) { - user.setUserFields(socket.uid, data, next); - }, - function (next) { - if (status !== 'offline') { - user.updateOnlineUsers(socket.uid, next); - } else { - next(); - } - }, - function (next) { - var data = { - uid: socket.uid, - status: status, - }; - websockets.server.emit('event:user_status_change', data); - next(null, data); - }, - ], callback); + await user.setUserFields(socket.uid, userData); + if (status !== 'offline') { + await user.updateOnlineUsers(socket.uid); + } + const eventData = { + uid: socket.uid, + status: status, + }; + websockets.server.emit('event:user_status_change', eventData); + return eventData; }; }; diff --git a/src/user/blocks.js b/src/user/blocks.js index c8fd75ed95..2c41e61192 100644 --- a/src/user/blocks.js +++ b/src/user/blocks.js @@ -39,7 +39,7 @@ module.exports = function (User) { throw new Error('[[error:cannot-block-privileged]]'); } if (parseInt(callerUid, 10) !== parseInt(blockerUid, 10) && !isCallerAdminOrMod) { - throw new Error(); + throw new Error('[[error:no-privileges]]'); } }; diff --git a/src/user/notifications.js b/src/user/notifications.js index 7ee213c43c..4e0cedbac5 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -221,7 +221,7 @@ UserNotifications.sendNameChangeNotification = async function (uid, username) { }; UserNotifications.pushCount = async function (uid) { - var websockets = require('./../socket.io'); + const websockets = require('./../socket.io'); const count = await UserNotifications.getUnreadCount(uid); websockets.in('uid_' + uid).emit('event:notifications.updateCount', count); }; diff --git a/src/user/profile.js b/src/user/profile.js index 211862703e..589e539f54 100644 --- a/src/user/profile.js +++ b/src/user/profile.js @@ -1,13 +1,13 @@ 'use strict'; -var async = require('async'); +const async = require('async'); -var utils = require('../utils'); -var meta = require('../meta'); -var db = require('../database'); -var groups = require('../groups'); -var plugins = require('../plugins'); +const utils = require('../utils'); +const meta = require('../meta'); +const db = require('../database'); +const groups = require('../groups'); +const plugins = require('../plugins'); module.exports = function (User) { User.updateProfile = async function (uid, data) { @@ -136,7 +136,7 @@ module.exports = function (User) { } User.checkMinReputation = async function (callerUid, uid, setting) { - var isSelf = parseInt(callerUid, 10) === parseInt(uid, 10); + const isSelf = parseInt(callerUid, 10) === parseInt(uid, 10); if (!isSelf || meta.config['reputation:disabled']) { return; } diff --git a/src/user/settings.js b/src/user/settings.js index d75e14c265..c9223c8cd2 100644 --- a/src/user/settings.js +++ b/src/user/settings.js @@ -77,7 +77,7 @@ module.exports = function (User) { } User.saveSettings = async function (uid, data) { - var maxPostsPerPage = meta.config.maxPostsPerPage || 20; + const maxPostsPerPage = meta.config.maxPostsPerPage || 20; if (!data.postsPerPage || parseInt(data.postsPerPage, 10) <= 1 || parseInt(data.postsPerPage, 10) > maxPostsPerPage) { throw new Error('[[error:invalid-pagination-value, 2, ' + maxPostsPerPage + ']]'); } diff --git a/src/user/uploads.js b/src/user/uploads.js index 43e4d49508..9ba5efc9ca 100644 --- a/src/user/uploads.js +++ b/src/user/uploads.js @@ -1,12 +1,12 @@ 'use strict'; -var path = require('path'); -var nconf = require('nconf'); -var winston = require('winston'); +const path = require('path'); +const nconf = require('nconf'); +const winston = require('winston'); -var db = require('../database'); -var file = require('../file'); -var batch = require('../batch'); +const db = require('../database'); +const file = require('../file'); +const batch = require('../batch'); module.exports = function (User) { User.deleteUpload = async function (callerUid, uid, uploadName) {