diff --git a/src/messaging/create.js b/src/messaging/create.js index 59f1bc6fe3..7a126e1e60 100644 --- a/src/messaging/create.js +++ b/src/messaging/create.js @@ -1,120 +1,78 @@ 'use strict'; -var async = require('async'); - var meta = require('../meta'); var plugins = require('../plugins'); var db = require('../database'); var user = require('../user'); module.exports = function (Messaging) { - Messaging.sendMessage = function (data, callback) { - async.waterfall([ - function (next) { - Messaging.checkContent(data.content, next); - }, - function (next) { - Messaging.isUserInRoom(data.uid, data.roomId, next); - }, - function (inRoom, next) { - if (!inRoom) { - return next(new Error('[[error:not-allowed]]')); - } - - Messaging.addMessage(data, next); - }, - ], callback); + Messaging.sendMessage = async (data) => { + await Messaging.checkContent(data.content); + const inRoom = await Messaging.isUserInRoom(data.uid, data.roomId); + if (!inRoom) { + throw new Error('[[error:not-allowed]]'); + } + + return await Messaging.addMessage(data); }; - Messaging.checkContent = function (content, callback) { + Messaging.checkContent = async (content) => { if (!content) { - return callback(new Error('[[error:invalid-chat-message]]')); + throw new Error('[[error:invalid-chat-message]]'); } - plugins.fireHook('filter:messaging.checkContent', { content: content }, function (err, data) { - if (err) { - return callback(err); - } - - content = String(data.content).trim(); - if (!content) { - return callback(new Error('[[error:invalid-chat-message]]')); - } - - var maximumChatMessageLength = (meta.config.maximumChatMessageLength || 1000); - if (content.length > maximumChatMessageLength) { - return callback(new Error('[[error:chat-message-too-long, ' + maximumChatMessageLength + ']]')); - } - callback(); - }); + const maximumChatMessageLength = (meta.config.maximumChatMessageLength || 1000); + const data = await plugins.fireHook('filter:messaging.checkContent', { content: content }); + content = String(data.content).trim(); + if (!content) { + throw new Error('[[error:invalid-chat-message]]'); + } + if (content.length > maximumChatMessageLength) { + throw new Error('[[error:chat-message-too-long, ' + maximumChatMessageLength + ']]'); + } }; - Messaging.addMessage = function (data, callback) { - var mid; - var message; - var isNewSet; + Messaging.addMessage = async (data) => { + const mid = await db.incrObjectField('global', 'nextMid'); const timestamp = data.timestamp || new Date().getTime(); + let message = { + content: String(data.content), + timestamp: timestamp, + fromuid: data.uid, + roomId: data.roomId, + deleted: 0, + system: data.system || 0, + }; - async.waterfall([ - function (next) { - Messaging.checkContent(data.content, next); - }, - function (next) { - db.incrObjectField('global', 'nextMid', next); - }, - function (_mid, next) { - mid = _mid; - message = { - content: String(data.content), - timestamp: timestamp, - fromuid: data.uid, - roomId: data.roomId, - deleted: 0, - system: data.system || 0, - }; - if (data.ip) { - message.ip = data.ip; - } - - plugins.fireHook('filter:messaging.save', message, next); - }, - function (message, next) { - db.setObject('message:' + mid, message, next); - }, - function (next) { - Messaging.isNewSet(data.uid, data.roomId, timestamp, next); - }, - function (_isNewSet, next) { - isNewSet = _isNewSet; - db.getSortedSetRange('chat:room:' + data.roomId + ':uids', 0, -1, next); - }, - function (uids, next) { - user.blocks.filterUids(data.uid, uids, next); - }, - function (uids, next) { - async.parallel([ - async.apply(Messaging.addRoomToUsers, data.roomId, uids, timestamp), - async.apply(Messaging.addMessageToUsers, data.roomId, uids, mid, timestamp), - async.apply(Messaging.markUnread, uids, data.roomId), - ], next); - }, - function (results, next) { - async.parallel({ - markRead: async.apply(Messaging.markRead, data.uid, data.roomId), - messages: async.apply(Messaging.getMessagesData, [mid], data.uid, data.roomId, true), - }, next); - }, - function (results, next) { - if (!results.messages || !results.messages[0]) { - return next(null, null); - } - - results.messages[0].newSet = isNewSet; - results.messages[0].mid = mid; - results.messages[0].roomId = data.roomId; - next(null, results.messages[0]); - }, - ], callback); + if (data.ip) { + message.ip = data.ip; + } + + message = await plugins.fireHook('filter:messaging.save', message); + await db.setObject('message:' + mid, message); + const isNewSet = await Messaging.isNewSet(data.uid, data.roomId, timestamp); + let uids = await db.getSortedSetRange('chat:room:' + data.roomId + ':uids', 0, -1); + uids = await user.blocks.filterUids(data.uid, uids); + + await Promise.all([ + Messaging.addRoomToUsers(data.roomId, uids, timestamp), + Messaging.addMessageToUsers(data.roomId, uids, mid, timestamp), + Messaging.markUnread(uids, data.roomId), + ]); + + const [, messages] = await Promise.all([ + await Messaging.markRead(data.uid, data.roomId), + await Messaging.getMessagesData([mid], data.uid, data.roomId, true), + ]); + + if (!messages || !messages[0]) { + return null; + } + + messages[0].newSet = isNewSet; + messages[0].mid = mid; + messages[0].roomId = data.roomId; + return messages[0]; }; Messaging.addSystemMessage = async (content, uid, roomId) => { @@ -127,19 +85,20 @@ module.exports = function (Messaging) { Messaging.notifyUsersInRoom(uid, roomId, message); }; - Messaging.addRoomToUsers = function (roomId, uids, timestamp, callback) { + Messaging.addRoomToUsers = async (roomId, uids, timestamp) => { if (!uids.length) { - return callback(); + return; } + const keys = uids.map(uid => 'uid:' + uid + ':chat:rooms'); - db.sortedSetsAdd(keys, timestamp, roomId, callback); + await db.sortedSetsAdd(keys, timestamp, roomId); }; - Messaging.addMessageToUsers = function (roomId, uids, mid, timestamp, callback) { + Messaging.addMessageToUsers = async (roomId, uids, mid, timestamp) => { if (!uids.length) { - return callback(); + return; } const keys = uids.map(uid => 'uid:' + uid + ':chat:room:' + roomId + ':mids'); - db.sortedSetsAdd(keys, timestamp, mid, callback); + await db.sortedSetsAdd(keys, timestamp, mid); }; }; diff --git a/src/messaging/data.js b/src/messaging/data.js index 6781e2f773..982e51b746 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -1,7 +1,5 @@ 'use strict'; -var async = require('async'); - var db = require('../database'); var user = require('../user'); var utils = require('../utils'); @@ -12,157 +10,126 @@ const intFields = ['timestamp', 'edited', 'fromuid', 'roomId', 'deleted', 'syste module.exports = function (Messaging) { Messaging.newMessageCutoff = 1000 * 60 * 3; - Messaging.getMessagesFields = function (mids, fields, callback) { + Messaging.getMessagesFields = async (mids, fields) => { if (!Array.isArray(mids) || !mids.length) { - return callback(null, []); + return []; } - async.waterfall([ - function (next) { - const keys = mids.map(mid => 'message:' + mid); - if (fields.length) { - db.getObjectsFields(keys, fields, next); - } else { - db.getObjects(keys, next); - } - }, - function (messages, next) { - messages.forEach(message => modifyMessage(message, fields)); - next(null, messages); - }, - ], callback); + const keys = mids.map(mid => 'message:' + mid); + let messages; + if (fields.length) { + messages = await db.getObjectsFields(keys, fields); + } else { + messages = await db.getObjects(keys); + } + + messages.forEach(message => modifyMessage(message, fields)); + return messages; }; - Messaging.getMessageField = function (mid, field, callback) { - Messaging.getMessageFields(mid, [field], function (err, fields) { - callback(err, fields ? fields[field] : null); - }); + Messaging.getMessageField = async (mid, field) => { + const fields = await Messaging.getMessageFields(mid, [field]); + return fields ? fields[field] : null; }; - Messaging.getMessageFields = function (mid, fields, callback) { - Messaging.getMessagesFields([mid], fields, function (err, messages) { - callback(err, messages ? messages[0] : null); - }); + Messaging.getMessageFields = async (mid, fields) => { + const messages = await Messaging.getMessagesFields([mid], fields); + return messages ? messages[0] : null; }; - Messaging.setMessageField = function (mid, field, content, callback) { - db.setObjectField('message:' + mid, field, content, callback); + Messaging.setMessageField = async (mid, field, content) => { + await db.setObjectField('message:' + mid, field, content); }; - Messaging.setMessageFields = function (mid, data, callback) { - db.setObject('message:' + mid, data, callback); + Messaging.setMessageFields = async (mid, data) => { + await db.setObject('message:' + mid, data); }; - Messaging.getMessagesData = function (mids, uid, roomId, isNew, callback) { - var messages; - - async.waterfall([ - function (next) { - Messaging.getMessagesFields(mids, [], next); - }, - async.apply(user.blocks.filter, uid, 'fromuid'), - function (_messages, next) { - messages = _messages.map(function (msg, idx) { - if (msg) { - msg.messageId = parseInt(mids[idx], 10); - msg.ip = undefined; - } - return msg; - }).filter(Boolean); - - const uids = messages.map(msg => msg && msg.fromuid); - - user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'banned'], next); - }, - function (users, next) { - messages.forEach(function (message, index) { - message.fromUser = users[index]; - message.fromUser.banned = !!message.fromUser.banned; - message.fromUser.deleted = message.fromuid !== message.fromUser.uid && message.fromUser.uid === 0; - - var self = message.fromuid === parseInt(uid, 10); - message.self = self ? 1 : 0; - - message.newSet = false; - message.roomId = String(message.roomId || roomId); - message.deleted = !!message.deleted; - message.system = !!message.system; - }); - - async.map(messages, function (message, next) { - if (message.system) { - return setImmediate(next, null, message); - } - - Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function (err, result) { - if (err) { - return next(err); - } - message.content = result; - message.cleanedContent = utils.stripHTMLTags(utils.decodeHTMLEntities(result)); - next(null, message); - }); - }, next); - }, - function (messages, next) { - if (messages.length > 1) { - // Add a spacer in between messages with time gaps between them - messages = messages.map(function (message, index) { - // Compare timestamps with the previous message, and check if a spacer needs to be added - if (index > 0 && message.timestamp > messages[index - 1].timestamp + Messaging.newMessageCutoff) { - // If it's been 5 minutes, this is a new set of messages - message.newSet = true; - } else if (index > 0 && message.fromuid !== messages[index - 1].fromuid) { - // If the previous message was from the other person, this is also a new set - message.newSet = true; - } - - return message; - }); - - next(undefined, messages); - } else if (messages.length === 1) { - // For single messages, we don't know the context, so look up the previous message and compare - var key = 'uid:' + uid + ':chat:room:' + roomId + ':mids'; - async.waterfall([ - async.apply(db.sortedSetRank, key, messages[0].messageId), - function (index, next) { - // Continue only if this isn't the first message in sorted set - if (index > 0) { - db.getSortedSetRange(key, index - 1, index - 1, next); - } else { - messages[0].newSet = true; - return next(undefined, messages); - } - }, - function (mid, next) { - Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next); - }, - function (fields, next) { - if ((messages[0].timestamp > fields.timestamp + Messaging.newMessageCutoff) || - (messages[0].fromuid !== fields.fromuid)) { - // If it's been 5 minutes, this is a new set of messages - messages[0].newSet = true; - } - next(null, messages); - }, - ], next); - } else { - next(null, []); + Messaging.getMessagesData = async (mids, uid, roomId, isNew) => { + let messages = await Messaging.getMessagesFields(mids, []); + messages = await user.blocks.filter(uid, 'fromuid', messages); + messages = messages + .map(function (msg, idx) { + if (msg) { + msg.messageId = parseInt(mids[idx], 10); + msg.ip = undefined; } - }, - function (messages, next) { - plugins.fireHook('filter:messaging.getMessages', { - messages: messages, - uid: uid, - roomId: roomId, - isNew: isNew, - mids: mids, - }, function (err, data) { - next(err, data && data.messages); - }); - }, - ], callback); + return msg; + }) + .filter(Boolean); + + const users = await user.getUsersFields( + messages.map(msg => msg && msg.fromuid), + ['uid', 'username', 'userslug', 'picture', 'status', 'banned'] + ); + + messages.forEach(function (message, index) { + message.fromUser = users[index]; + message.fromUser.banned = !!message.fromUser.banned; + message.fromUser.deleted = message.fromuid !== message.fromUser.uid && message.fromUser.uid === 0; + + var self = message.fromuid === parseInt(uid, 10); + message.self = self ? 1 : 0; + + message.newSet = false; + message.roomId = String(message.roomId || roomId); + message.deleted = !!message.deleted; + message.system = !!message.system; + }); + + messages = await Promise.all(messages.map(async (message) => { + if (message.system) { + return message; + } + + const result = await Messaging.parse(message.content, message.fromuid, uid, roomId, isNew); + message.content = result; + message.cleanedContent = utils.stripHTMLTags(utils.decodeHTMLEntities(result)); + return message; + })); + + if (messages.length > 1) { + // Add a spacer in between messages with time gaps between them + messages = messages.map(function (message, index) { + // Compare timestamps with the previous message, and check if a spacer needs to be added + if (index > 0 && message.timestamp > messages[index - 1].timestamp + Messaging.newMessageCutoff) { + // If it's been 5 minutes, this is a new set of messages + message.newSet = true; + } else if (index > 0 && message.fromuid !== messages[index - 1].fromuid) { + // If the previous message was from the other person, this is also a new set + message.newSet = true; + } + + return message; + }); + } else if (messages.length === 1) { + // For single messages, we don't know the context, so look up the previous message and compare + var key = 'uid:' + uid + ':chat:room:' + roomId + ':mids'; + const index = await db.sortedSetRank(key, messages[0].messageId); + if (index > 0) { + const mid = await db.getSortedSetRange(key, index - 1, index - 1); + const fields = await Messaging.getMessageFields(mid, ['fromuid', 'timestamp']); + if ((messages[0].timestamp > fields.timestamp + Messaging.newMessageCutoff) || + (messages[0].fromuid !== fields.fromuid)) { + // If it's been 5 minutes, this is a new set of messages + messages[0].newSet = true; + } + } else { + messages[0].newSet = true; + } + } else { + messages = []; + } + + const data = await plugins.fireHook('filter:messaging.getMessages', { + messages: messages, + uid: uid, + roomId: roomId, + isNew: isNew, + mids: mids, + }); + + return data && data.messages; }; }; diff --git a/src/messaging/delete.js b/src/messaging/delete.js index a31c6288cc..b7c1bbbd1b 100644 --- a/src/messaging/delete.js +++ b/src/messaging/delete.js @@ -1,31 +1,16 @@ 'use strict'; -var async = require('async'); - module.exports = function (Messaging) { - Messaging.deleteMessage = function (mid, roomId, callback) { - async.waterfall([ - async.apply(Messaging.getMessageField, mid, 'deleted'), - function (deleted, next) { - if (deleted) { - return next(new Error('[[error:chat-deleted-already]]')); - } - - Messaging.setMessageField(mid, 'deleted', 1, next); - }, - ], callback); - }; + Messaging.deleteMessage = async mid => await doDeleteRestore(mid, 1); + Messaging.restoreMessage = async mid => await doDeleteRestore(mid, 0); - Messaging.restoreMessage = function (mid, roomId, callback) { - async.waterfall([ - async.apply(Messaging.getMessageField, mid, 'deleted'), - function (deleted, next) { - if (!deleted) { - return next(new Error('[[error:chat-restored-already]]')); - } + async function doDeleteRestore(mid, state) { + const field = state ? 'deleted' : 'restored'; + const cur = await Messaging.getMessageField(mid, 'deleted'); + if (cur === state) { + throw new Error('[[error:chat-' + field + '-already]]'); + } - Messaging.setMessageField(mid, 'deleted', 0, next); - }, - ], callback); - }; + return await Messaging.setMessageField(mid, 'deleted', state); + } }; diff --git a/src/messaging/edit.js b/src/messaging/edit.js index 5189a29f5d..50af113f43 100644 --- a/src/messaging/edit.js +++ b/src/messaging/edit.js @@ -1,7 +1,5 @@ 'use strict'; -var async = require('async'); - var meta = require('../meta'); var user = require('../user'); @@ -9,52 +7,34 @@ var sockets = require('../socket.io'); module.exports = function (Messaging) { - Messaging.editMessage = function (uid, mid, roomId, content, callback) { - var uids; - async.waterfall([ - function (next) { - Messaging.getMessageField(mid, 'content', next); - }, - function (raw, next) { - if (raw === content) { - return callback(); - } - if (!String(content).trim()) { - return callback(new Error('[[error:invalid-chat-message]]')); - } - Messaging.setMessageFields(mid, { - content: content, - edited: Date.now(), - }, next); - }, - function (next) { - Messaging.getUidsInRoom(roomId, 0, -1, next); - }, - function (_uids, next) { - uids = _uids; - Messaging.getMessagesData([mid], uid, roomId, true, next); - }, - function (messages, next) { - uids.forEach(function (uid) { - sockets.in('uid_' + uid).emit('event:chats.edit', { - messages: messages, - }); - }); - next(); - }, - ], callback); - }; + Messaging.editMessage = async (uid, mid, roomId, content) => { + const raw = await Messaging.getMessageField(mid, 'content'); + if (raw === content) { + return; + } + if (!String(content).trim()) { + throw new Error('[[error:invalid-chat-message]]'); + } + await Messaging.setMessageFields(mid, { + content: content, + edited: Date.now(), + }); - Messaging.canEdit = function (messageId, uid, callback) { - canEditDelete(messageId, uid, 'edit', callback); - }; + // Propagate this change to users in the room + const [uids, messages] = await Promise.all([ + Messaging.getUidsInRoom(roomId, 0, -1), + Messaging.getMessagesData([mid], uid, roomId, true), + ]); - Messaging.canDelete = function (messageId, uid, callback) { - canEditDelete(messageId, uid, 'delete', callback); + uids.forEach(function (uid) { + sockets.in('uid_' + uid).emit('event:chats.edit', { + messages: messages, + }); + }); }; - function canEditDelete(messageId, uid, type, callback) { - var durationConfig = ''; + const canEditDelete = async (messageId, uid, type) => { + let durationConfig = ''; if (type === 'edit') { durationConfig = 'chatEditDuration'; } else if (type === 'delete') { @@ -62,47 +42,39 @@ module.exports = function (Messaging) { } if (meta.config.disableChat) { - return callback(new Error('[[error:chat-disabled]]')); + throw new Error('[[error:chat-disabled]]'); } else if (meta.config.disableChatMessageEditing) { - return callback(new Error('[[error:chat-message-editing-disabled]]')); + throw new Error('[[error:chat-message-editing-disabled]]'); } - async.waterfall([ - function (next) { - user.getUserFields(uid, ['banned', 'email:confirmed'], next); - }, - function (userData, next) { - if (userData.banned) { - return callback(new Error('[[error:user-banned]]')); - } + const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']); + if (userData.banned) { + throw new Error('[[error:user-banned]]'); + } + if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { + throw new Error('[[error:email-not-confirmed]]'); + } - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - return callback(new Error('[[error:email-not-confirmed]]')); - } - async.parallel({ - isAdmin: function (next) { - user.isAdministrator(uid, next); - }, - messageData: function (next) { - Messaging.getMessageFields(messageId, ['fromuid', 'timestamp'], next); - }, - }, next); - }, - function (results, next) { - if (results.isAdmin) { - return callback(); - } - var chatConfigDuration = meta.config[durationConfig]; - if (chatConfigDuration && Date.now() - results.messageData.timestamp > chatConfigDuration * 1000) { - return callback(new Error('[[error:chat-' + type + '-duration-expired, ' + meta.config[durationConfig] + ']]')); - } + const [isAdmin, messageData] = await Promise.all([ + user.isAdministrator(uid), + Messaging.getMessageFields(messageId, ['fromuid', 'timestamp']), + ]); - if (results.messageData.fromuid === parseInt(uid, 10)) { - return callback(); - } + if (isAdmin) { + return; + } + var chatConfigDuration = meta.config[durationConfig]; + if (chatConfigDuration && Date.now() - messageData.timestamp > chatConfigDuration * 1000) { + throw new Error('[[error:chat-' + type + '-duration-expired, ' + meta.config[durationConfig] + ']]'); + } + + if (messageData.fromuid === parseInt(uid, 10)) { + return; + } + + throw new Error('[[error:cant-' + type + '-chat-message]]'); + }; - next(new Error('[[error:cant-' + type + '-chat-message]]')); - }, - ], callback); - } + Messaging.canEdit = async (messageId, uid) => await canEditDelete(messageId, uid, 'edit'); + Messaging.canDelete = async (messageId, uid) => await canEditDelete(messageId, uid, 'delete'); }; diff --git a/src/messaging/index.js b/src/messaging/index.js index bce2afe297..2bcad1445b 100644 --- a/src/messaging/index.js +++ b/src/messaging/index.js @@ -1,16 +1,15 @@ 'use strict'; -var async = require('async'); -var validator = require('validator'); +const validator = require('validator'); -var db = require('../database'); -var user = require('../user'); -var plugins = require('../plugins'); -var meta = require('../meta'); -var utils = require('../utils'); +const db = require('../database'); +const user = require('../user'); +const plugins = require('../plugins'); +const meta = require('../meta'); +const utils = require('../utils'); -var Messaging = module.exports; +const Messaging = module.exports; require('./data')(Messaging); require('./create')(Messaging); @@ -21,179 +20,126 @@ require('./unread')(Messaging); require('./notifications')(Messaging); -Messaging.getMessages = function (params, callback) { - var uid = params.uid; - var roomId = params.roomId; - var isNew = params.isNew || false; - var start = params.hasOwnProperty('start') ? params.start : 0; - var stop = parseInt(start, 10) + ((params.count || 50) - 1); - - var indices = {}; - async.waterfall([ - function (next) { - canGet('filter:messaging.canGetMessages', params.callerUid, params.uid, next); - }, - function (canGet, next) { - if (!canGet) { - return callback(null, null); - } - db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', start, stop, next); - }, - function (mids, next) { - if (!mids.length) { - return callback(null, []); - } +Messaging.getMessages = async (params) => { + const isNew = params.isNew || false; + const start = params.hasOwnProperty('start') ? params.start : 0; + const stop = parseInt(start, 10) + ((params.count || 50) - 1); - mids.forEach(function (mid, index) { - indices[mid] = start + index; - }); + const indices = {}; + const ok = await canGet('filter:messaging.canGetMessages', params.callerUid, params.uid); + if (!ok) { + return; + } - mids.reverse(); + const mids = await db.getSortedSetRevRange('uid:' + params.uid + ':chat:room:' + params.roomId + ':mids', start, stop); + if (!mids.length) { + return []; + } + mids.forEach(function (mid, index) { + indices[mid] = start + index; + }); + mids.reverse(); - Messaging.getMessagesData(mids, uid, roomId, isNew, next); - }, - function (messageData, next) { - messageData.forEach(function (messageData) { - messageData.index = indices[messageData.messageId.toString()]; - }); + let messageData = await Messaging.getMessagesData(mids, params.uid, params.roomId, isNew); + messageData.forEach(function (messageData) { + messageData.index = indices[messageData.messageId.toString()]; + }); - // Filter out deleted messages unless you're the sender of said message - messageData = messageData.filter(function (messageData) { - return (!messageData.deleted || messageData.fromuid === parseInt(params.uid, 10)); - }); + // Filter out deleted messages unless you're the sender of said message + messageData = messageData.filter(function (messageData) { + return (!messageData.deleted || messageData.fromuid === parseInt(params.uid, 10)); + }); - next(null, messageData); - }, - ], callback); + return messageData; }; -function canGet(hook, callerUid, uid, callback) { - plugins.fireHook(hook, { +async function canGet(hook, callerUid, uid) { + const data = await plugins.fireHook(hook, { callerUid: callerUid, uid: uid, canGet: parseInt(callerUid, 10) === parseInt(uid, 10), - }, function (err, data) { - callback(err, data ? data.canGet : false); }); + + return data ? data.canGet : false; } -Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) { - plugins.fireHook('filter:parse.raw', message, function (err, parsed) { - if (err) { - return callback(err); - } +Messaging.parse = async (message, fromuid, uid, roomId, isNew) => { + const parsed = await plugins.fireHook('filter:parse.raw', message); + let messageData = { + message: message, + parsed: parsed, + fromuid: fromuid, + uid: uid, + roomId: roomId, + isNew: isNew, + parsedMessage: parsed, + }; - var messageData = { - message: message, - parsed: parsed, - fromuid: fromuid, - uid: uid, - roomId: roomId, - isNew: isNew, - parsedMessage: parsed, - }; - - plugins.fireHook('filter:messaging.parse', messageData, function (err, messageData) { - callback(err, messageData ? messageData.parsedMessage : ''); - }); - }); + messageData = await plugins.fireHook('filter:messaging.parse', messageData); + return messageData ? messageData.parsedMessage : ''; }; -Messaging.isNewSet = function (uid, roomId, timestamp, callback) { - var setKey = 'uid:' + uid + ':chat:room:' + roomId + ':mids'; - - async.waterfall([ - function (next) { - db.getSortedSetRevRangeWithScores(setKey, 0, 0, next); - }, - function (messages, next) { - if (messages && messages.length) { - next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + Messaging.newMessageCutoff); - } else { - next(null, true); - } - }, - ], callback); +Messaging.isNewSet = async (uid, roomId, timestamp) => { + const setKey = 'uid:' + uid + ':chat:room:' + roomId + ':mids'; + const messages = await db.getSortedSetRevRangeWithScores(setKey, 0, 0); + if (messages && messages.length) { + return parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + Messaging.newMessageCutoff; + } + return true; }; +Messaging.getRecentChats = async (callerUid, uid, start, stop) => { + const ok = await canGet('filter:messaging.canGetRecentChats', callerUid, uid); + if (!ok) { + return null; + } -Messaging.getRecentChats = function (callerUid, uid, start, stop, callback) { - async.waterfall([ - function (next) { - canGet('filter:messaging.canGetRecentChats', callerUid, uid, next); - }, - function (canGet, next) { - if (!canGet) { - return callback(null, null); - } - db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, next); - }, - function (roomIds, next) { - async.parallel({ - roomData: function (next) { - Messaging.getRoomsData(roomIds, next); - }, - unread: function (next) { - db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next); - }, - users: function (next) { - async.map(roomIds, function (roomId, next) { - db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 9, function (err, uids) { - if (err) { - return next(err); - } - uids = uids.filter(function (value) { - return value && parseInt(value, 10) !== parseInt(uid, 10); - }); - user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline'], next); - }); - }, next); - }, - teasers: function (next) { - async.map(roomIds, function (roomId, next) { - Messaging.getTeaser(uid, roomId, next); - }, next); - }, - }, next); - }, - function (results, next) { - results.roomData.forEach(function (room, index) { - if (room) { - room.users = results.users[index]; - room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; - room.unread = results.unread[index]; - room.teaser = results.teasers[index]; - - room.users.forEach(function (userData) { - if (userData && parseInt(userData.uid, 10)) { - userData.status = user.getStatus(userData); - } - }); - room.users = room.users.filter(function (user) { - return user && parseInt(user.uid, 10); - }); - room.lastUser = room.users[0]; - - room.usernames = Messaging.generateUsernames(room.users, uid); + const roomIds = await db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop); + const results = await utils.promiseParallel({ + roomData: Messaging.getRoomsData(roomIds), + unread: db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds), + users: Promise.all(roomIds.map(async (roomId) => { + let uids = await db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 9); + uids = uids.filter(function (value) { + return value && parseInt(value, 10) !== parseInt(uid, 10); + }); + return await user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline']); + })), + teasers: Promise.all(roomIds.map(async roomId => Messaging.getTeaser(uid, roomId))), + }); + + results.roomData.forEach(function (room, index) { + if (room) { + room.users = results.users[index]; + room.groupChat = room.hasOwnProperty('groupChat') ? room.groupChat : room.users.length > 2; + room.unread = results.unread[index]; + room.teaser = results.teasers[index]; + + room.users.forEach(function (userData) { + if (userData && parseInt(userData.uid, 10)) { + userData.status = user.getStatus(userData); } }); + room.users = room.users.filter(function (user) { + return user && parseInt(user.uid, 10); + }); + room.lastUser = room.users[0]; + + room.usernames = Messaging.generateUsernames(room.users, uid); + } + }); - results.roomData = results.roomData.filter(Boolean); - - next(null, { rooms: results.roomData, nextStart: stop + 1 }); - }, - function (ref, next) { - plugins.fireHook('filter:messaging.getRecentChats', { - rooms: ref.rooms, - nextStart: ref.nextStart, - uid: uid, - callerUid: callerUid, - }, next); - }, - ], callback); + results.roomData = results.roomData.filter(Boolean); + const ref = { rooms: results.roomData, nextStart: stop + 1 }; + return await plugins.fireHook('filter:messaging.getRecentChats', { + rooms: ref.rooms, + nextStart: ref.nextStart, + uid: uid, + callerUid: callerUid, + }); }; -Messaging.generateUsernames = function (users, excludeUid) { +Messaging.generateUsernames = (users, excludeUid) => { users = users.filter(function (user) { return user && parseInt(user.uid, 10) !== excludeUid; }); @@ -202,211 +148,149 @@ Messaging.generateUsernames = function (users, excludeUid) { }).join(', '); }; -Messaging.getTeaser = function (uid, roomId, callback) { - var teaser; - async.waterfall([ - function (next) { - Messaging.getLatestUndeletedMessage(uid, roomId, next); - }, - function (mid, next) { - if (!mid) { - return callback(null, null); - } - Messaging.getMessageFields(mid, ['fromuid', 'content', 'timestamp'], next); - }, - function (_teaser, next) { - teaser = _teaser; - if (!teaser.fromuid) { - return callback(null, null); - } - user.blocks.is(teaser.fromuid, uid, next); - }, - function (blocked, next) { - if (blocked) { - return callback(null, null); - } - if (teaser.content) { - teaser.content = utils.stripHTMLTags(utils.decodeHTMLEntities(teaser.content)); - teaser.content = validator.escape(String(teaser.content)); - } +Messaging.getTeaser = async (uid, roomId) => { + const mid = await Messaging.getLatestUndeletedMessage(uid, roomId); + if (!mid) { + return null; + } + const teaser = await Messaging.getMessageFields(mid, ['fromuid', 'content', 'timestamp']); + if (!teaser.fromuid) { + return null; + } + const blocked = await user.blocks.is(teaser.fromuid, uid); + if (blocked) { + return null; + } - teaser.timestampISO = utils.toISOString(teaser.timestamp); - user.getUserFields(teaser.fromuid, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline'], next); - }, - function (user, next) { - teaser.user = user; - plugins.fireHook('filter:messaging.getTeaser', { teaser: teaser }, function (err, data) { - next(err, data.teaser); - }); - }, - ], callback); + teaser.user = await user.getUserFields(teaser.fromuid, ['uid', 'username', 'userslug', 'picture', 'status', 'lastonline']); + if (teaser.content) { + teaser.content = utils.stripHTMLTags(utils.decodeHTMLEntities(teaser.content)); + teaser.content = validator.escape(String(teaser.content)); + } + + const payload = await plugins.fireHook('filter:messaging.getTeaser', { teaser: teaser }); + return payload.teaser; }; -Messaging.getLatestUndeletedMessage = function (uid, roomId, callback) { - var done = false; - var latestMid = null; - var index = 0; - var mids; - async.doWhilst( - function (next) { - async.waterfall([ - function (_next) { - db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', index, index, _next); - }, - function (_mids, _next) { - mids = _mids; - if (!mids.length) { - done = true; - return next(); - } - Messaging.getMessageFields(mids[0], ['deleted', 'system'], _next); - }, - function (states, _next) { - done = !states.deleted && !states.system; - if (done) { - latestMid = mids[0]; - } - index += 1; - _next(); - }, - ], next); - }, - function (next) { - next(null, !done); - }, - function (err) { - callback(err, parseInt(latestMid, 10)); +Messaging.getLatestUndeletedMessage = async (uid, roomId) => { + let done = false; + let latestMid = null; + let index = 0; + let mids; + + while (!done) { + /* eslint-disable no-await-in-loop */ + mids = await db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', index, index); + if (mids.length) { + const states = await Messaging.getMessageFields(mids[0], ['deleted', 'system']); + done = !states.deleted && !states.system; + if (done) { + latestMid = mids[0]; + } + index += 1; + } else { + done = true; } - ); + } + + return latestMid; }; -Messaging.canMessageUser = function (uid, toUid, callback) { +Messaging.canMessageUser = async (uid, toUid) => { if (meta.config.disableChat || uid <= 0 || uid === toUid) { - return callback(new Error('[[error:chat-disabled]]')); + throw new Error('[[error:chat-disabled]]'); } if (parseInt(uid, 10) === parseInt(toUid, 10)) { - return callback(new Error('[[error:cant-chat-with-yourself')); + throw new Error('[[error:cant-chat-with-yourself'); } - async.waterfall([ - function (next) { - user.exists(toUid, next); - }, - function (exists, next) { - if (!exists) { - return callback(new Error('[[error:no-user]]')); - } - user.getUserFields(uid, ['banned', 'email:confirmed'], next); - }, - function (userData, next) { - if (userData.banned) { - return callback(new Error('[[error:user-banned]]')); - } + const exists = await user.exists(toUid); + if (!exists) { + throw new Error('[[error:no-user]]'); + } - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - return callback(new Error('[[error:email-not-confirmed-chat]]')); - } + const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']); + if (userData.banned) { + throw new Error('[[error:user-banned]]'); + } - async.parallel({ - settings: async.apply(user.getSettings, toUid), - isAdmin: async.apply(user.isAdministrator, uid), - isModerator: async.apply(user.isModeratorOfAnyCategory, uid), - isFollowing: async.apply(user.isFollowing, toUid, uid), - }, next); - }, - function (results, next) { - if (results.settings.restrictChat && !results.isAdmin && !results.isModerator && !results.isFollowing) { - return next(new Error('[[error:chat-restricted]]')); - } + if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { + throw new Error('[[error:email-not-confirmed-chat]]'); + } - plugins.fireHook('static:messaging.canMessageUser', { - uid: uid, - toUid: toUid, - }, function (err) { - next(err); - }); - }, - ], callback); + const results = await utils.promiseParallel({ + settings: user.getSettings(toUid), + isAdmin: user.isAdministrator(uid), + isModerator: user.isModeratorOfAnyCategory(uid), + isFollowing: user.isFollowing(toUid, uid), + }); + + if (results.settings.restrictChat && !results.isAdmin && !results.isModerator && !results.isFollowing) { + throw new Error('[[error:chat-restricted]]'); + } + + await plugins.fireHook('static:messaging.canMessageUser', { + uid: uid, + toUid: toUid, + }); }; -Messaging.canMessageRoom = function (uid, roomId, callback) { +Messaging.canMessageRoom = async (uid, roomId) => { if (meta.config.disableChat || uid <= 0) { - return callback(new Error('[[error:chat-disabled]]')); + throw new Error('[[error:chat-disabled]]'); } - async.waterfall([ - function (next) { - Messaging.isUserInRoom(uid, roomId, next); - }, - function (inRoom, next) { - if (!inRoom) { - return next(new Error('[[error:not-in-room]]')); - } + const inRoom = await Messaging.isUserInRoom(uid, roomId); + if (!inRoom) { + throw new Error('[[error:not-in-room]]'); + } - user.getUserFields(uid, ['banned', 'email:confirmed'], next); - }, - function (userData, next) { - if (userData.banned) { - return next(new Error('[[error:user-banned]]')); - } + const userData = await user.getUserFields(uid, ['banned', 'email:confirmed']); + if (userData.banned) { + throw new Error('[[error:user-banned]]'); + } - if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { - return next(new Error('[[error:email-not-confirmed-chat]]')); - } + if (meta.config.requireEmailConfirmation && !userData['email:confirmed']) { + throw new Error('[[error:email-not-confirmed-chat]]'); + } - plugins.fireHook('static:messaging.canMessageRoom', { - uid: uid, - roomId: roomId, - }, function (err) { - next(err); - }); - }, - ], callback); + await plugins.fireHook('static:messaging.canMessageRoom', { + uid: uid, + roomId: roomId, + }); }; -Messaging.hasPrivateChat = function (uid, withUid, callback) { +Messaging.hasPrivateChat = async (uid, withUid) => { if (parseInt(uid, 10) === parseInt(withUid, 10)) { - return callback(null, 0); + return 0; } - async.waterfall([ - function (next) { - async.parallel({ - myRooms: async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':chat:rooms', 0, -1), - theirRooms: async.apply(db.getSortedSetRevRange, 'uid:' + withUid + ':chat:rooms', 0, -1), - }, next); - }, - function (results, next) { - var roomIds = results.myRooms.filter(function (roomId) { - return roomId && results.theirRooms.includes(roomId); - }); - if (!roomIds.length) { - return callback(); - } + const results = await utils.promiseParallel({ + myRooms: db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', 0, -1), + theirRooms: db.getSortedSetRevRange('uid:' + withUid + ':chat:rooms', 0, -1), + }); + const roomIds = results.myRooms.filter(function (roomId) { + return roomId && results.theirRooms.includes(roomId); + }); - var index = 0; - var roomId = 0; - async.whilst(function (next) { - next(null, index < roomIds.length && !roomId); - }, function (next) { - Messaging.getUserCountInRoom(roomIds[index], function (err, count) { - if (err) { - return next(err); - } - if (count === 2) { - roomId = roomIds[index]; - next(null, roomId); - } else { - index += 1; - next(); - } - }); - }, function (err) { - next(err, roomId); - }); - }, - ], callback); + if (!roomIds.length) { + return 0; + } + + var index = 0; + var roomId = 0; + while (index < roomIds.length && !roomId) { + /* eslint-disable no-await-in-loop */ + const count = await Messaging.getUserCountInRoom(roomIds[index]); + if (count === 2) { + roomId = roomIds[index]; + } else { + index += 1; + } + } + + return roomId; }; Messaging.async = require('../promisify')(Messaging); diff --git a/src/messaging/notifications.js b/src/messaging/notifications.js index 340ddcf356..28f76fbc62 100644 --- a/src/messaging/notifications.js +++ b/src/messaging/notifications.js @@ -1,7 +1,5 @@ 'use strict'; -var async = require('async'); - var user = require('../user'); var notifications = require('../notifications'); var sockets = require('../socket.io'); @@ -12,88 +10,67 @@ module.exports = function (Messaging) { Messaging.notificationSendDelay = 1000 * 60; - Messaging.notifyUsersInRoom = function (fromUid, roomId, messageObj) { - async.waterfall([ - function (next) { - Messaging.getUidsInRoom(roomId, 0, -1, next); - }, - function (uids, next) { - user.blocks.filterUids(fromUid, uids, next); - }, - function (uids, next) { - var data = { - roomId: roomId, - fromUid: fromUid, - message: messageObj, - uids: uids, - }; - - plugins.fireHook('filter:messaging.notify', data, next); - }, - function (data, next) { - if (!data || !data.uids || !data.uids.length) { - return next(); - } + Messaging.notifyUsersInRoom = async (fromUid, roomId, messageObj) => { + let uids = await Messaging.getUidsInRoom(roomId, 0, -1); + uids = await user.blocks.filterUids(fromUid, uids); - var uids = data.uids; + let data = { + roomId: roomId, + fromUid: fromUid, + message: messageObj, + uids: uids, + }; + data = await plugins.fireHook('filter:messaging.notify', data); + if (!data || !data.uids || !data.uids.length) { + return; + } - uids.forEach(function (uid) { - data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0; - Messaging.pushUnreadCount(uid); - sockets.in('uid_' + uid).emit('event:chats.receive', data); - }); + uids = data.uids; + uids.forEach(function (uid) { + data.self = parseInt(uid, 10) === parseInt(fromUid, 10) ? 1 : 0; + Messaging.pushUnreadCount(uid); + sockets.in('uid_' + uid).emit('event:chats.receive', data); + }); - // Delayed notifications - var queueObj = Messaging.notifyQueue[fromUid + ':' + roomId]; - if (queueObj) { - queueObj.message.content += '\n' + messageObj.content; - clearTimeout(queueObj.timeout); - } else { - queueObj = { - message: messageObj, - }; - Messaging.notifyQueue[fromUid + ':' + roomId] = queueObj; - } + // Delayed notifications + var queueObj = Messaging.notifyQueue[fromUid + ':' + roomId]; + if (queueObj) { + queueObj.message.content += '\n' + messageObj.content; + clearTimeout(queueObj.timeout); + } else { + queueObj = { + message: messageObj, + }; + Messaging.notifyQueue[fromUid + ':' + roomId] = queueObj; + } - queueObj.timeout = setTimeout(function () { - sendNotifications(fromUid, uids, roomId, queueObj.message); - }, Messaging.notificationSendDelay); - next(); - }, - ]); + queueObj.timeout = setTimeout(function () { + sendNotifications(fromUid, uids, roomId, queueObj.message); + }, Messaging.notificationSendDelay); }; - function sendNotifications(fromuid, uids, roomId, messageObj) { - async.waterfall([ - function (next) { - user.isOnline(uids, next); - }, - function (isOnline, next) { - uids = uids.filter(function (uid, index) { - return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10); - }); - - if (!uids.length) { - return; - } + async function sendNotifications(fromuid, uids, roomId, messageObj) { + const isOnline = await user.isOnline(uids); + uids = uids.filter(function (uid, index) { + return !isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10); + }); + if (!uids.length) { + return; + } - notifications.create({ - type: 'new-chat', - subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]', - bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', - bodyLong: messageObj.content, - nid: 'chat_' + fromuid + '_' + roomId, - from: fromuid, - path: '/chats/' + messageObj.roomId, - }, next); - }, - ], function (err, notification) { - if (!err) { - delete Messaging.notifyQueue[fromuid + ':' + roomId]; - if (notification) { - notifications.push(notification, uids); - } - } + const notification = await notifications.create({ + type: 'new-chat', + subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]', + bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]', + bodyLong: messageObj.content, + nid: 'chat_' + fromuid + '_' + roomId, + from: fromuid, + path: '/chats/' + messageObj.roomId, }); + + delete Messaging.notifyQueue[fromuid + ':' + roomId]; + if (notification) { + notifications.push(notification, uids); + } } }; diff --git a/src/messaging/unread.js b/src/messaging/unread.js index bedbed7021..436b88d24c 100644 --- a/src/messaging/unread.js +++ b/src/messaging/unread.js @@ -1,53 +1,37 @@ 'use strict'; -var async = require('async'); - var db = require('../database'); var sockets = require('../socket.io'); module.exports = function (Messaging) { - Messaging.getUnreadCount = function (uid, callback) { + Messaging.getUnreadCount = async (uid) => { if (parseInt(uid, 10) <= 0) { - return setImmediate(callback, null, 0); + return 0; } - db.sortedSetCard('uid:' + uid + ':chat:rooms:unread', callback); + + return await db.sortedSetCard('uid:' + uid + ':chat:rooms:unread'); }; - Messaging.pushUnreadCount = function (uid) { + Messaging.pushUnreadCount = async (uid) => { if (parseInt(uid, 10) <= 0) { return; } - Messaging.getUnreadCount(uid, function (err, unreadCount) { - if (err) { - return; - } - sockets.in('uid_' + uid).emit('event:unread.updateChatCount', unreadCount); - }); - }; - - Messaging.markRead = function (uid, roomId, callback) { - db.sortedSetRemove('uid:' + uid + ':chat:rooms:unread', roomId, callback); + const unreadCount = await Messaging.getUnreadCount(uid); + sockets.in('uid_' + uid).emit('event:unread.updateChatCount', unreadCount); }; - Messaging.markAllRead = function (uid, callback) { - db.delete('uid:' + uid + ':chat:rooms:unread', callback); - }; + Messaging.markRead = async (uid, roomId) => db.sortedSetRemove('uid:' + uid + ':chat:rooms:unread', roomId); + Messaging.markAllRead = async uid => db.delete('uid:' + uid + ':chat:rooms:unread'); - Messaging.markUnread = function (uids, roomId, callback) { - async.waterfall([ - function (next) { - Messaging.roomExists(roomId, next); - }, - function (exists, next) { - if (!exists) { - return next(new Error('[[error:chat-room-does-not-exist]]')); - } - var keys = uids.map(function (uid) { - return 'uid:' + uid + ':chat:rooms:unread'; - }); + Messaging.markUnread = async (uids, roomId) => { + const exists = await Messaging.roomExists(roomId); + if (!exists) { + throw new Error('[[error:chat-room-does-not-exist]]'); + } + var keys = uids.map(function (uid) { + return 'uid:' + uid + ':chat:rooms:unread'; + }); - db.sortedSetsAdd(keys, Date.now(), roomId, next); - }, - ], callback); + return await db.sortedSetsAdd(keys, Date.now(), roomId); }; }; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 9094b9811d..1589fe1dad 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -278,7 +278,7 @@ SocketModules.chats.delete = function (socket, data, callback) { Messaging.canDelete(data.messageId, socket.uid, next); }, function (next) { - Messaging.deleteMessage(data.messageId, data.roomId, next); + Messaging.deleteMessage(data.messageId, next); }, ], callback); }; @@ -293,7 +293,7 @@ SocketModules.chats.restore = function (socket, data, callback) { Messaging.canDelete(data.messageId, socket.uid, next); }, function (next) { - Messaging.restoreMessage(data.messageId, data.roomId, next); + Messaging.restoreMessage(data.messageId, next); }, ], callback); };