mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-10 09:32:59 +01:00
... due to incorrect class and id assignment of the chat modal. Regression was caused by an earlier commit that moved the typing span elsewhere.
421 lines
12 KiB
JavaScript
421 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
var db = require('./database'),
|
|
async = require('async'),
|
|
nconf = require('nconf'),
|
|
winston = require('winston'),
|
|
user = require('./user'),
|
|
plugins = require('./plugins'),
|
|
meta = require('./meta'),
|
|
utils = require('../public/src/utils'),
|
|
notifications = require('./notifications'),
|
|
userNotifications = require('./user/notifications'),
|
|
emailer = require('./emailer'),
|
|
sockets = require('./socket.io');
|
|
|
|
(function(Messaging) {
|
|
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
|
|
|
|
var terms = {
|
|
day: 86400000,
|
|
week: 604800000,
|
|
month: 2592000000,
|
|
threemonths: 7776000000
|
|
};
|
|
|
|
function sortUids(fromuid, touid) {
|
|
return [fromuid, touid].sort();
|
|
}
|
|
|
|
Messaging.addMessage = function(fromuid, touid, content, timestamp, callback) {
|
|
var uids = sortUids(fromuid, touid);
|
|
|
|
if (typeof timestamp === 'function') {
|
|
callback = timestamp;
|
|
timestamp = Date.now();
|
|
} else {
|
|
timestamp = timestamp || Date.now();
|
|
}
|
|
|
|
db.incrObjectField('global', 'nextMid', function(err, mid) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
var message = {
|
|
content: content,
|
|
timestamp: timestamp,
|
|
fromuid: fromuid,
|
|
touid: touid
|
|
};
|
|
|
|
async.waterfall([
|
|
function(next) {
|
|
plugins.fireHook('filter:messaging.save', message, next);
|
|
},
|
|
function(message, next) {
|
|
db.setObject('message:' + mid, message, next);
|
|
}
|
|
], function(err) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
async.parallel([
|
|
async.apply(db.sortedSetAdd, 'messages:uid:' + uids[0] + ':to:' + uids[1], timestamp, mid),
|
|
async.apply(Messaging.updateChatTime, fromuid, touid),
|
|
async.apply(Messaging.updateChatTime, touid, fromuid),
|
|
async.apply(Messaging.markRead, fromuid, touid),
|
|
async.apply(Messaging.markUnread, touid, fromuid),
|
|
], function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
async.waterfall([
|
|
function(next) {
|
|
getMessages([mid], fromuid, touid, true, next);
|
|
},
|
|
function(messages, next) {
|
|
Messaging.isNewSet(fromuid, touid, mid, function(err, isNewSet) {
|
|
if (err) {
|
|
return next(err);
|
|
}
|
|
|
|
if (!messages || !messages[0]) {
|
|
return next(null, null);
|
|
}
|
|
|
|
messages[0].newSet = isNewSet;
|
|
messages[0].mid = mid;
|
|
next(null, messages[0]);
|
|
});
|
|
}
|
|
], callback);
|
|
});
|
|
});
|
|
});
|
|
};
|
|
|
|
Messaging.getMessages = function(fromuid, touid, since, isNew, callback) {
|
|
var uids = sortUids(fromuid, touid);
|
|
|
|
var count = parseInt(meta.config.chatMessageInboxSize, 10) || 250;
|
|
var min = Date.now() - (terms[since] || terms.day);
|
|
if (since === 'recent') {
|
|
count = 49;
|
|
min = 0;
|
|
}
|
|
|
|
db.getSortedSetRevRangeByScore('messages:uid:' + uids[0] + ':to:' + uids[1], 0, count, '+inf', min, function(err, mids) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (!Array.isArray(mids) || !mids.length) {
|
|
return callback(null, []);
|
|
}
|
|
|
|
mids.reverse();
|
|
|
|
getMessages(mids, fromuid, touid, isNew, callback);
|
|
});
|
|
|
|
notifications.markRead('chat_' + touid + '_' + fromuid, fromuid, function(err) {
|
|
if (err) {
|
|
winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message);
|
|
}
|
|
|
|
userNotifications.pushCount(fromuid);
|
|
});
|
|
};
|
|
|
|
function getMessages(mids, fromuid, touid, isNew, callback) {
|
|
user.getMultipleUserFields([fromuid, touid], ['uid', 'username', 'userslug', 'picture'], function(err, userData) {
|
|
if(err) {
|
|
return callback(err);
|
|
}
|
|
|
|
var keys = mids.map(function(mid) {
|
|
return 'message:' + mid;
|
|
});
|
|
|
|
async.waterfall([
|
|
async.apply(db.getObjects, keys),
|
|
function(messages, next) {
|
|
messages = messages.filter(Boolean);
|
|
async.map(messages, function(message, next) {
|
|
var self = parseInt(message.fromuid, 10) === parseInt(fromuid, 10);
|
|
message.fromUser = self ? userData[0] : userData[1];
|
|
message.toUser = self ? userData[1] : userData[0];
|
|
message.timestampISO = utils.toISOString(message.timestamp);
|
|
message.self = self ? 1 : 0;
|
|
message.newSet = false;
|
|
|
|
Messaging.parse(message.content, message.fromuid, fromuid, userData[1], userData[0], isNew, function(result) {
|
|
message.content = result;
|
|
next(null, message);
|
|
});
|
|
}, next);
|
|
},
|
|
function(messages, next) {
|
|
// Add a spacer in between messages with time gaps between them
|
|
messages = messages.map(function(message, index) {
|
|
// Compare timestamps with the previous message, and check if a spacer needs to be added
|
|
if (index > 0 && parseInt(message.timestamp, 10) > parseInt(messages[index-1].timestamp, 10) + (1000*60*5)) {
|
|
// If 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);
|
|
}
|
|
], callback);
|
|
});
|
|
}
|
|
|
|
Messaging.parse = function (message, fromuid, myuid, toUserData, myUserData, isNew, callback) {
|
|
plugins.fireHook('filter:parse.raw', message, function(err, parsed) {
|
|
if (err) {
|
|
return callback(message);
|
|
}
|
|
|
|
var messageData = {
|
|
message: message,
|
|
parsed: parsed,
|
|
fromuid: fromuid,
|
|
myuid: myuid,
|
|
toUserData: toUserData,
|
|
myUserData: myUserData,
|
|
isNew: isNew,
|
|
parsedMessage: parsed
|
|
};
|
|
|
|
plugins.fireHook('filter:messaging.parse', messageData, function(err, messageData) {
|
|
callback(messageData.parsedMessage);
|
|
});
|
|
});
|
|
};
|
|
|
|
Messaging.isNewSet = function(fromuid, touid, mid, callback) {
|
|
var uids = sortUids(fromuid, touid),
|
|
setKey = 'messages:uid:' + uids[0] + ':to:' + uids[1];
|
|
|
|
async.waterfall([
|
|
async.apply(db.sortedSetRank, setKey, mid),
|
|
function(index, next) {
|
|
if (index > 0) {
|
|
db.getSortedSetRange(setKey, index-1, index, next);
|
|
} else {
|
|
next(null, true);
|
|
}
|
|
},
|
|
function(mids, next) {
|
|
if (typeof mids !== 'boolean' && mids && mids.length) {
|
|
db.getObjects(['message:' + mids[0], 'message:' + mids[1]], next);
|
|
} else {
|
|
next(null, mids);
|
|
}
|
|
},
|
|
function(messages, next) {
|
|
if (typeof messages !== 'boolean' && messages && messages.length) {
|
|
next(null, parseInt(messages[1].timestamp, 10) > parseInt(messages[0].timestamp, 10) + (1000*60*5));
|
|
} else {
|
|
next(null, messages);
|
|
}
|
|
}
|
|
], callback);
|
|
};
|
|
|
|
Messaging.updateChatTime = function(uid, toUid, callback) {
|
|
callback = callback || function() {};
|
|
db.sortedSetAdd('uid:' + uid + ':chats', Date.now(), toUid, callback);
|
|
};
|
|
|
|
Messaging.getRecentChats = function(uid, start, stop, callback) {
|
|
db.getSortedSetRevRange('uid:' + uid + ':chats', start, stop, function(err, uids) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
async.parallel({
|
|
unread: function(next) {
|
|
db.isSortedSetMembers('uid:' + uid + ':chats:unread', uids, next);
|
|
},
|
|
users: function(next) {
|
|
user.getMultipleUserFields(uids, ['uid', 'username', 'picture', 'status'] , next);
|
|
}
|
|
}, function(err, results) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
results.users.forEach(function(user, index) {
|
|
if (user && !parseInt(user.uid, 10)) {
|
|
Messaging.markRead(uid, uids[index]);
|
|
}
|
|
});
|
|
|
|
results.users = results.users.filter(function(user) {
|
|
return user && parseInt(user.uid, 10);
|
|
});
|
|
|
|
if (!results.users.length) {
|
|
return callback(null, {users: [], nextStart: stop + 1});
|
|
}
|
|
|
|
results.users.forEach(function(user, index) {
|
|
if (user) {
|
|
user.unread = results.unread[index];
|
|
user.status = sockets.isUserOnline(user.uid) ? user.status : 'offline';
|
|
}
|
|
});
|
|
|
|
callback(null, {users: results.users, nextStart: stop + 1});
|
|
});
|
|
});
|
|
};
|
|
|
|
Messaging.getUnreadCount = function(uid, callback) {
|
|
db.sortedSetCard('uid:' + uid + ':chats:unread', callback);
|
|
};
|
|
|
|
Messaging.pushUnreadCount = function(uid) {
|
|
Messaging.getUnreadCount(uid, function(err, unreadCount) {
|
|
if (err) {
|
|
return;
|
|
}
|
|
sockets.in('uid_' + uid).emit('event:unread.updateChatCount', null, unreadCount);
|
|
});
|
|
};
|
|
|
|
Messaging.markRead = function(uid, toUid, callback) {
|
|
db.sortedSetRemove('uid:' + uid + ':chats:unread', toUid, callback);
|
|
};
|
|
|
|
Messaging.markUnread = function(uid, toUid, callback) {
|
|
db.sortedSetAdd('uid:' + uid + ':chats:unread', Date.now(), toUid, callback);
|
|
};
|
|
|
|
Messaging.notifyUser = function(fromuid, touid, messageObj) {
|
|
// Immediate notifications
|
|
// Recipient
|
|
Messaging.pushUnreadCount(touid);
|
|
sockets.in('uid_' + touid).emit('event:chats.receive', {
|
|
withUid: fromuid,
|
|
message: messageObj,
|
|
self: 0
|
|
});
|
|
// Sender
|
|
Messaging.pushUnreadCount(fromuid);
|
|
sockets.in('uid_' + fromuid).emit('event:chats.receive', {
|
|
withUid: touid,
|
|
message: messageObj,
|
|
self: 1
|
|
});
|
|
|
|
// Delayed notifications
|
|
var queueObj = Messaging.notifyQueue[fromuid + ':' + touid];
|
|
if (queueObj) {
|
|
queueObj.message.content += '\n' + messageObj.content;
|
|
clearTimeout(queueObj.timeout);
|
|
} else {
|
|
queueObj = Messaging.notifyQueue[fromuid + ':' + touid] = {
|
|
message: messageObj
|
|
};
|
|
}
|
|
|
|
queueObj.timeout = setTimeout(function() {
|
|
sendNotifications(fromuid, touid, queueObj.message, function(err) {
|
|
if (!err) {
|
|
delete Messaging.notifyQueue[fromuid + ':' + touid];
|
|
}
|
|
});
|
|
}, 1000*60); // wait 60s before sending
|
|
};
|
|
|
|
Messaging.canMessage = function(fromUid, toUid, callback) {
|
|
if (parseInt(meta.config.disableChat) === 1) {
|
|
return callback(new Error('[[error:chat-disabled]]'));
|
|
} else if (toUid === fromUid) {
|
|
return callback(new Error('[[error:cant-chat-with-yourself]]'));
|
|
} else if (fromUid === 0) {
|
|
return callback(new Error('[[error:not-logged-in]]'));
|
|
}
|
|
|
|
async.waterfall([
|
|
function(next) {
|
|
user.getUserFields(fromUid, ['banned', 'email:confirmed'], function(err, userData) {
|
|
if (err) {
|
|
return callback(err);
|
|
}
|
|
|
|
if (parseInt(userData.banned, 10) === 1) {
|
|
return callback(new Error('[[error:user-banned]]'));
|
|
}
|
|
|
|
if (parseInt(meta.config.requireEmailConfirmation, 10) === 1 && parseInt(userData['email:confirmed'], 10) !== 1) {
|
|
return callback(new Error('[[error:email-not-confirmed-chat]]'));
|
|
}
|
|
|
|
next();
|
|
});
|
|
},
|
|
function(next) {
|
|
user.getSettings(toUid, next);
|
|
},
|
|
function(settings, next) {
|
|
if (!settings.restrictChat) {
|
|
return callback(null, true);
|
|
}
|
|
|
|
user.isAdministrator(fromUid, next);
|
|
},
|
|
function(isAdmin, next) {
|
|
if (isAdmin) {
|
|
return callback(null, true);
|
|
}
|
|
user.isFollowing(toUid, fromUid, next);
|
|
}
|
|
], callback);
|
|
};
|
|
|
|
function sendNotifications(fromuid, touid, messageObj, callback) {
|
|
if (sockets.isUserOnline(touid)) {
|
|
return callback();
|
|
}
|
|
|
|
notifications.create({
|
|
bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
|
|
bodyLong: messageObj.content,
|
|
nid: 'chat_' + fromuid + '_' + touid,
|
|
from: fromuid,
|
|
path: '/chats/' + messageObj.fromUser.username
|
|
}, function(err, notification) {
|
|
if (!err && notification) {
|
|
notifications.push(notification, [touid], callback);
|
|
}
|
|
});
|
|
|
|
user.getSettings(messageObj.toUser.uid, function(err, settings) {
|
|
if (settings.sendChatNotifications && !parseInt(meta.config.disableEmailSubscriptions, 10)) {
|
|
emailer.send('notif_chat', touid, {
|
|
subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.username + ']]',
|
|
username: messageObj.toUser.username,
|
|
userslug: utils.slugify(messageObj.toUser.username),
|
|
summary: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
|
|
message: messageObj,
|
|
site_title: meta.config.title || 'NodeBB',
|
|
url: nconf.get('url'),
|
|
fromUserslug: utils.slugify(messageObj.fromUser.username)
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
}(exports));
|