Merge pull request #3968 from NodeBB/group-chat

Group chat closes #1798
v1.18.x
Barış Soner Uşaklı 9 years ago
commit a5385d81fd

@ -1,5 +1,5 @@
"use strict"; "use strict";
/*global io, templates, ajaxify, utils, bootbox, overrides, socket, config, Visibility*/ /*global templates, translator, ajaxify, utils, bootbox, overrides, socket, config, Visibility*/
var app = app || {}; var app = app || {};
@ -159,7 +159,7 @@ app.cacheBuster = null;
} }
app.currentRoom = ''; app.currentRoom = '';
}); });
} };
function highlightNavigationLink() { function highlightNavigationLink() {
var path = window.location.pathname; var path = window.location.pathname;
@ -239,11 +239,7 @@ app.cacheBuster = null;
} }
}; };
app.openChat = function (username, touid) { app.openChat = function (roomId) {
if (username === app.user.username) {
return app.alertError('[[error:cant-chat-with-yourself]]');
}
if (!app.user.uid) { if (!app.user.uid) {
return app.alertError('[[error:not-logged-in]]'); return app.alertError('[[error:not-logged-in]]');
} }
@ -255,14 +251,35 @@ app.cacheBuster = null;
chat.focusInput(chatModal); chat.focusInput(chatModal);
} }
if (!chat.modalExists(touid)) { if (chat.modalExists(roomId)) {
loadAndCenter(chat.getModal(roomId));
} else {
socket.emit('modules.chats.getUsersInRoom', {roomId: roomId}, function(err, users) {
if (err) {
return app.alertError(err.message);
}
users = users.filter(function(user) {
return user && parseInt(user.uid, 10) !== parseInt(app.user.uid, 10);
});
chat.createModal({ chat.createModal({
username: username, roomId: roomId,
touid: touid users: users
}, loadAndCenter); }, loadAndCenter);
} else { });
loadAndCenter(chat.getModal(touid)); }
});
};
app.newChat = function (touid) {
if (!app.user.uid) {
return app.alertError('[[error:not-logged-in]]');
}
socket.emit('modules.chats.newRoom', {touid: touid}, function(err, roomId) {
if (err) {
return app.alertError(err.message);
} }
app.openChat(roomId);
}); });
}; };
@ -408,7 +425,7 @@ app.cacheBuster = null;
function handleStatusChange() { function handleStatusChange() {
$('[component="header/usercontrol"] [data-status]').off('click').on('click', function(e) { $('[component="header/usercontrol"] [data-status]').off('click').on('click', function(e) {
var status = $(this).attr('data-status'); var status = $(this).attr('data-status');
socket.emit('user.setStatus', status, function(err, data) { socket.emit('user.setStatus', status, function(err) {
if(err) { if(err) {
return app.alertError(err.message); return app.alertError(err.message);
} }

@ -30,7 +30,7 @@ define('forum/account/header', [
}); });
components.get('account/chat').on('click', function() { components.get('account/chat').on('click', function() {
app.openChat($('.account-username').html(), theirid); app.newChat(theirid);
}); });
components.get('account/ban').on('click', banAccount); components.get('account/ban').on('click', banAccount);

@ -1,6 +1,6 @@
'use strict'; 'use strict';
/* globals define, config, app, ajaxify, utils, socket, templates */ /* globals define, config, app, ajaxify, utils, socket, templates, Mousetrap, bootbox */
define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', 'translator'], function(components, S, sounds, infinitescroll, translator) { define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll', 'translator'], function(components, S, sounds, infinitescroll, translator) {
var Chats = { var Chats = {
@ -18,6 +18,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
} }
Chats.addEventListeners(); Chats.addEventListeners();
Chats.createTagsInput(ajaxify.data.roomId, ajaxify.data.users);
if (env === 'md' || env === 'lg') { if (env === 'md' || env === 'lg') {
Chats.resizeMainWindow(); Chats.resizeMainWindow();
@ -28,39 +29,30 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.initialised = true; Chats.initialised = true;
if (ajaxify.data.hasOwnProperty('meta') && ajaxify.data.meta.hasOwnProperty('uid')) { if (ajaxify.data.hasOwnProperty('roomId')) {
// This is an active chat, focus on the input box
components.get('chat/input').focus(); components.get('chat/input').focus();
} }
}; };
Chats.getRecipientUid = function() {
return parseInt($('.expanded-chat').attr('data-uid'), 10);
};
Chats.isCurrentChat = function(uid) {
return Chats.getRecipientUid() === parseInt(uid, 10);
};
Chats.addEventListeners = function() { Chats.addEventListeners = function() {
components.get('chat/recent').on('click', 'li', function(e) { $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function() {
Chats.switchChat(parseInt($(this).attr('data-uid'), 10), $(this).attr('data-username')); Chats.switchChat($(this).attr('data-roomid'));
}); });
Chats.addSendHandlers(Chats.getRecipientUid(), $('.chat-input'), $('.expanded-chat button[data-action="send"]')); Chats.addSendHandlers(ajaxify.data.roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]'));
$('[data-action="pop-out"]').on('click', function() { $('[data-action="pop-out"]').on('click', function() {
var username = $('.expanded-chat').attr('data-username'),
uid = Chats.getRecipientUid(), var text = components.get('chat/input').val();
text = components.get('chat/input').val(); var roomId = ajaxify.data.roomId;
if (app.previousUrl && app.previousUrl.match(/chats/)) { if (app.previousUrl && app.previousUrl.match(/chats/)) {
ajaxify.go('chats', function() { ajaxify.go('chats', function() {
app.openChat(username, uid); app.openChat(roomId);
}, true); }, true);
} else { } else {
window.history.go(-1); window.history.go(-1);
app.openChat(username, uid); app.openChat(roomId);
} }
$(window).one('action:chat.loaded', function() { $(window).one('action:chat.loaded', function() {
@ -72,14 +64,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
.on('click', '[data-action="edit"]', function() { .on('click', '[data-action="edit"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid'); var messageId = $(this).parents('[data-mid]').attr('data-mid');
var inputEl = components.get('chat/input'); var inputEl = components.get('chat/input');
Chats.prepEdit(inputEl, messageId); Chats.prepEdit(inputEl, messageId, ajaxify.data.roomId);
}) })
.on('click', '[data-action="delete"]', function() { .on('click', '[data-action="delete"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid'); var messageId = $(this).parents('[data-mid]').attr('data-mid');
Chats.delete(messageId); Chats.delete(messageId, ajaxify.data.roomId);
}); });
$('.recent-chats').on('scroll', function() { $('[component="chat/recent"]').on('scroll', function() {
var $this = $(this); var $this = $(this);
var bottom = ($this[0].scrollHeight - $this.height()) * 0.9; var bottom = ($this[0].scrollHeight - $this.height()) * 0.9;
if ($this.scrollTop() > bottom) { if ($this.scrollTop() > bottom) {
@ -87,7 +79,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
} }
}); });
Chats.addSinceHandler(Chats.getRecipientUid(), $('.expanded-chat .chat-content'), $('.expanded-chat [data-since]')); Chats.addSinceHandler(ajaxify.data.roomId, $('.expanded-chat .chat-content'), $('.expanded-chat [data-since]'));
}; };
Chats.addHotkeys = function() { Chats.addHotkeys = function() {
@ -96,7 +88,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
prev = activeContact.prev(); prev = activeContact.prev();
if (prev.length) { if (prev.length) {
Chats.switchChat(parseInt(prev.attr('data-uid'), 10), prev.attr('data-username')); Chats.switchChat(prev.attr('data-roomid'));
} }
}); });
Mousetrap.bind('ctrl+down', function() { Mousetrap.bind('ctrl+down', function() {
@ -104,7 +96,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
next = activeContact.next(); next = activeContact.next();
if (next.length) { if (next.length) {
Chats.switchChat(parseInt(next.attr('data-uid'), 10), next.attr('data-username')); Chats.switchChat(next.attr('data-roomid'));
} }
}); });
Mousetrap.bind('up', function(e) { Mousetrap.bind('up', function(e) {
@ -114,13 +106,16 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
var lastMid = message.attr('data-mid'); var lastMid = message.attr('data-mid');
var inputEl = components.get('chat/input'); var inputEl = components.get('chat/input');
Chats.prepEdit(inputEl, lastMid); Chats.prepEdit(inputEl, lastMid, ajaxify.data.roomId);
} }
}); });
}; };
Chats.prepEdit = function(inputEl, messageId) { Chats.prepEdit = function(inputEl, messageId, roomId) {
socket.emit('modules.chats.getRaw', { mid: messageId }, function(err, raw) { socket.emit('modules.chats.getRaw', { mid: messageId, roomId: roomId }, function(err, raw) {
if (err) {
return app.alertError(err.message);
}
// Populate the input field with the raw message content // Populate the input field with the raw message content
if (inputEl.val().length === 0) { if (inputEl.val().length === 0) {
// By setting the `data-mid` attribute, I tell the chat code that I am editing a // By setting the `data-mid` attribute, I tell the chat code that I am editing a
@ -131,42 +126,44 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}); });
}; };
Chats.delete = function(messageId) { Chats.delete = function(messageId, roomId) {
translator.translate('[[modules:chat.delete_message_confirm]]', function(translated) { translator.translate('[[modules:chat.delete_message_confirm]]', function(translated) {
bootbox.confirm(translated, function(ok) { bootbox.confirm(translated, function(ok) {
if (ok) { if (!ok) {
return;
}
socket.emit('modules.chats.delete', { socket.emit('modules.chats.delete', {
messageId: messageId messageId: messageId,
roomId: roomId
}, function(err) { }, function(err) {
if (err) { if (err) {
return app.alertError(err.message); return app.alertError(err.message);
} }
// Remove message from list
components.get('chat/message', messageId).slideUp('slow', function() { components.get('chat/message', messageId).slideUp('slow', function() {
$(this).remove(); $(this).remove();
}); });
}); });
}
}); });
}); });
}; };
Chats.addSinceHandler = function(toUid, chatContentEl, sinceEl) { Chats.addSinceHandler = function(roomId, chatContentEl, sinceEl) {
sinceEl.on('click', function() { sinceEl.on('click', function() {
var since = $(this).attr('data-since'); var since = $(this).attr('data-since');
sinceEl.removeClass('selected'); sinceEl.removeClass('selected');
$(this).addClass('selected'); $(this).addClass('selected');
Chats.loadChatSince(toUid, chatContentEl, since); Chats.loadChatSince(roomId, chatContentEl, since);
return false; return false;
}); });
}; };
Chats.addSendHandlers = function(toUid, inputEl, sendEl) { Chats.addSendHandlers = function(roomId, inputEl, sendEl) {
inputEl.off('keypress').on('keypress', function(e) { inputEl.off('keypress').on('keypress', function(e) {
if (e.which === 13 && !e.shiftKey) { if (e.which === 13 && !e.shiftKey) {
Chats.sendMessage(toUid, inputEl); Chats.sendMessage(roomId, inputEl);
return false; return false;
} }
}); });
@ -177,51 +174,66 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return; return;
} }
Chats.notifyTyping(toUid, val); Chats.notifyTyping(roomId, val);
$(this).attr('data-typing', val); $(this).attr('data-typing', val);
}); });
sendEl.off('click').on('click', function(e) { sendEl.off('click').on('click', function() {
Chats.sendMessage(toUid, inputEl); Chats.sendMessage(roomId, inputEl);
inputEl.focus(); inputEl.focus();
return false; return false;
}); });
}; };
Chats.switchChat = function(uid, username) { Chats.createTagsInput = function(roomId, users) {
if (!$('#content [component="chat/messages"]').length) { var tagEl = $('.users-tag-input');
return ajaxify.go('chats/' + utils.slugify(username));
tagEl.tagsinput({
confirmKeys: [13, 44],
trimValue: true
});
if (users && users.length) {
users.forEach(function(user) {
tagEl.tagsinput('add', user.username);
});
} }
var contactEl = $('.chats-list [data-uid="' + uid + '"]'); tagEl.on('itemAdded', function(event) {
if (event.item === app.user.username) {
return;
}
socket.emit('modules.chats.addUserToRoom', {roomId: roomId, username: event.item}, function(err) {
if (err && err.message === '[[error:no-user]]') {
tagEl.tagsinput('remove', event.item);
}
});
});
Chats.loadChatSince(uid, $('.chat-content'), 'recent'); tagEl.on('beforeItemRemove', function(event) {
Chats.addSendHandlers(uid, components.get('chat/input'), $('[data-action="send"]')); event.cancel = !ajaxify.data.owner;
});
contactEl tagEl.on('itemRemoved', function(event) {
.removeClass('unread') socket.emit('modules.chats.removeUserFromRoom', {roomId: roomId, username: event.item});
.addClass('bg-primary') });
.siblings().removeClass('bg-primary');
components.get('chat/title').text(username); var input = $('.users-tag-container').find('.bootstrap-tagsinput input');
components.get('chat/messages').attr('data-uid', uid).attr('data-username', username);
components.get('breadcrumb/current').text(username);
components.get('chat/input').focus();
if (window.history && window.history.pushState) { require(['autocomplete'], function(autocomplete) {
var url = 'chats/' + utils.slugify(username); autocomplete.user(input);
});
};
window.history.pushState({ Chats.switchChat = function(roomid) {
url: url ajaxify.go('chats/' + roomid);
}, url, RELATIVE_PATH + '/' + url);
}
}; };
Chats.loadChatSince = function(toUid, chatContentEl, since) { Chats.loadChatSince = function(roomId, chatContentEl, since) {
if (!toUid) { if (!roomId) {
return; return;
} }
socket.emit('modules.chats.get', {touid: toUid, since: since}, function(err, messages) { socket.emit('modules.chats.get', {roomId: roomId, since: since}, function(err, messages) {
if (err) { if (err) {
return app.alertError(err.message); return app.alertError(err.message);
} }
@ -235,13 +247,10 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.addGlobalEventListeners = function() { Chats.addGlobalEventListeners = function() {
$(window).on('resize', Chats.resizeMainWindow); $(window).on('resize', Chats.resizeMainWindow);
$(window).on('mousemove keypress click', function() { $(window).on('mousemove keypress click', function() {
if (newMessage) { if (newMessage && ajaxify.data.roomId) {
var recipientUid = Chats.getRecipientUid(); socket.emit('modules.chats.markRead', ajaxify.data.roomId);
if (recipientUid) {
socket.emit('modules.chats.markRead', recipientUid);
newMessage = false; newMessage = false;
} }
}
}); });
}; };
@ -268,42 +277,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.addSocketListeners = function() { Chats.addSocketListeners = function() {
socket.on('event:chats.receive', function(data) { socket.on('event:chats.receive', function(data) {
if (Chats.isCurrentChat(data.withUid)) { if (parseInt(data.roomId, 10) === parseInt(ajaxify.data.roomId, 10)) {
newMessage = data.self === 0; newMessage = data.self === 0;
data.message.self = data.self; data.message.self = data.self;
Chats.appendChatMessage($('.expanded-chat .chat-content'), data.message); Chats.appendChatMessage($('.expanded-chat .chat-content'), data.message);
} else {
var contactEl = $('[component="chat/recent"] li[data-uid="' + data.withUid + '"]'),
userKey = parseInt(data.withUid, 10) === parseInt(data.message.fromuid, 10) ? 'fromUser' : 'toUser';
// Spawn a new contact if required
templates.parse('partials/chat_contacts', {
contacts: [{
uid: data.message[userKey].uid,
username: data.message[userKey].username,
status: data.message[userKey].status,
picture: data.message[userKey].picture,
'icon:text': data.message[userKey]['icon:text'],
'icon:bgColor': data.message[userKey]['icon:bgColor'],
teaser: {
content: data.message.cleanedContent,
timestampISO: new Date(Date.now()).toISOString()
}
}]
}, function(html) {
translator.translate(html, function(translatedHTML) {
if (contactEl.length) {
contactEl.replaceWith(translatedHTML);
} else {
$('[component="chat/recent"]').prepend(translatedHTML);
}
// Mark that contact list entry unread
$('.chats-list li[data-uid="' + data.withUid + '"]').addClass('unread').find('.timeago').timeago();
app.alternatingTitle('[[modules:chat.user_has_messaged_you, ' + data.message.fromUser.username + ']]');
});
});
} }
}); });
@ -320,13 +298,12 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}); });
socket.on('event:chats.edit', function(data) { socket.on('event:chats.edit', function(data) {
var message;
data.messages.forEach(function(message) { data.messages.forEach(function(message) {
templates.parse('partials/chat_message', { templates.parse('partials/chat_message', {
messages: message messages: message
}, function(html) { }, function(html) {
body = components.get('chat/message', message.messageId); var body = components.get('chat/message', message.messageId);
if (body.length) { if (body.length) {
body.replaceWith(html); body.replaceWith(html);
components.get('chat/message', message.messageId).find('.timeago').timeago(); components.get('chat/message', message.messageId).find('.timeago').timeago();
@ -351,14 +328,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.setActive(); Chats.setActive();
}; };
Chats.notifyTyping = function(toUid, typing) { Chats.notifyTyping = function(roomId, typing) {
socket.emit('modules.chats.user' + (typing ? 'Start' : 'Stop') + 'Typing', { socket.emit('modules.chats.user' + (typing ? 'Start' : 'Stop') + 'Typing', {
touid: toUid, roomId: roomId,
fromUid: app.user.uid fromUid: app.user.uid
}); });
}; };
Chats.sendMessage = function(toUid, inputEl) { Chats.sendMessage = function(roomId, inputEl) {
var msg = inputEl.val(), var msg = inputEl.val(),
mid = inputEl.attr('data-mid'); mid = inputEl.attr('data-mid');
@ -375,7 +352,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
if (!mid) { if (!mid) {
socket.emit('modules.chats.send', { socket.emit('modules.chats.send', {
touid: toUid, roomId: roomId,
message: msg message: msg
}, function(err) { }, function(err) {
if (err) { if (err) {
@ -386,10 +363,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
} }
sounds.play('chat-outgoing'); sounds.play('chat-outgoing');
Chats.notifyTyping(toUid, false); Chats.notifyTyping(roomId, false);
}); });
} else { } else {
socket.emit('modules.chats.edit', { socket.emit('modules.chats.edit', {
roomId: roomId,
mid: mid, mid: mid,
message: msg message: msg
}, function(err) { }, function(err) {
@ -397,7 +375,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return app.alertError(err.message); return app.alertError(err.message);
} }
Chats.notifyTyping(toUid, false); Chats.notifyTyping(roomId, false);
}); });
} }
}; };
@ -411,13 +389,12 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}; };
Chats.setActive = function() { Chats.setActive = function() {
var recipientUid = Chats.getRecipientUid(); if (ajaxify.data.roomId) {
if (recipientUid) { socket.emit('modules.chats.markRead', ajaxify.data.roomId);
socket.emit('modules.chats.markRead', recipientUid);
$('.expanded-chat input').focus(); $('.expanded-chat input').focus();
} }
$('.chats-list li').removeClass('bg-primary'); $('.chats-list li').removeClass('bg-primary');
$('.chats-list li[data-uid="' + recipientUid + '"]').addClass('bg-primary'); $('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-primary');
}; };
Chats.parseMessage = function(data, callback) { Chats.parseMessage = function(data, callback) {
@ -429,7 +406,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}; };
function loadMoreRecentChats() { function loadMoreRecentChats() {
var recentChats = $('.recent-chats'); var recentChats = $('[component="chat/recent"]');
if (recentChats.attr('loading')) { if (recentChats.attr('loading')) {
return; return;
} }
@ -441,8 +418,8 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return app.alertError(err.message); return app.alertError(err.message);
} }
if (data && data.users.length) { if (data && data.rooms.length) {
onRecentChatsLoaded(data.users, function() { onRecentChatsLoaded(data, function() {
recentChats.removeAttr('loading'); recentChats.removeAttr('loading');
recentChats.attr('data-nextstart', data.nextStart); recentChats.attr('data-nextstart', data.nextStart);
}); });
@ -452,17 +429,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}); });
} }
function onRecentChatsLoaded(users, callback) { function onRecentChatsLoaded(data, callback) {
users = users.filter(function(user) { if (!data.rooms.length) {
return !$('.recent-chats li[data-uid=' + user.uid + ']').length;
});
if (!users.length) {
return callback(); return callback();
} }
app.parseAndTranslate('chats', 'chats', {chats: users}, function(html) { app.parseAndTranslate('chats', 'rooms', data, function(html) {
$('.recent-chats').append(html); $('[component="chat/recent"]').append(html);
html.find('.timeago').timeago();
callback(); callback();
}); });
} }

@ -405,12 +405,10 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
}); });
} }
function openChat(button) { function openChat(button) {
var post = button.parents('[data-pid]'); var post = button.parents('[data-pid]');
app.openChat(post.attr('data-username'), post.attr('data-uid')); app.newChat(post.attr('data-uid'));
button.parents('.btn-group').find('.dropdown-toggle').click(); button.parents('.btn-group').find('.dropdown-toggle').click();
return false; return false;
} }

@ -10,6 +10,9 @@ define('autocomplete', function() {
app.loadJQueryUI(function() { app.loadJQueryUI(function() {
input.autocomplete({ input.autocomplete({
delay: 100, delay: 100,
open: function() {
$(this).autocomplete('widget').css('z-index', 20000);
},
select: onselect, select: onselect,
source: function(request, response) { source: function(request, response) {
socket.emit('user.search', {query: request.term}, function(err, result) { socket.emit('user.search', {query: request.term}, function(err, result) {

@ -1,5 +1,5 @@
"use strict"; "use strict";
/* globals app, config, define, socket, templates, utils, ajaxify */ /* globals app, define, socket, templates, utils, ajaxify */
define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'translator'], function(components, taskbar, S, sounds, Chats, translator) { define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'translator'], function(components, taskbar, S, sounds, Chats, translator) {
@ -20,14 +20,12 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
socket.on('event:chats.receive', function(data) { socket.on('event:chats.receive', function(data) {
var username = data.message.fromUser.username; var username = data.message.fromUser.username;
var isSelf = parseInt(data.message.fromUser.uid, 10) === parseInt(app.user.uid, 10); var isSelf = data.self === 1;
data.message.self = data.self; data.message.self = data.self;
if (isSelf) {
username = data.message.toUser.username;
}
newMessage = data.self === 0; newMessage = data.self === 0;
if (module.modalExists(data.withUid)) { if (module.modalExists(data.roomId)) {
var modal = module.getModal(data.withUid); var modal = module.getModal(data.roomId);
Chats.appendChatMessage(modal.find('.chat-content'), data.message); Chats.appendChatMessage(modal.find('.chat-content'), data.message);
@ -49,8 +47,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
} }
} else { } else {
module.createModal({ module.createModal({
username: username, roomId: data.roomId,
touid: data.withUid,
silent: true silent: true
}, function(modal) { }, function(modal) {
module.toggleNew(modal.attr('UUID'), true, true); module.toggleNew(modal.attr('UUID'), true, true);
@ -90,15 +87,16 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
module.loadChatsDropdown = function(chatsListEl) { module.loadChatsDropdown = function(chatsListEl) {
var dropdownEl; var dropdownEl;
socket.emit('modules.chats.getRecentChats', {after: 0}, function(err, chats) { socket.emit('modules.chats.getRecentChats', {after: 0}, function(err, data) {
if (err) { if (err) {
return app.alertError(err.message); return app.alertError(err.message);
} }
chats = chats.users;
var rooms = data.rooms;
chatsListEl.empty(); chatsListEl.empty();
if (!chats.length) { if (!rooms.length) {
translator.translate('[[modules:chat.no_active]]', function(str) { translator.translate('[[modules:chat.no_active]]', function(str) {
$('<li />') $('<li />')
.addClass('no_active') .addClass('no_active')
@ -108,23 +106,29 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
return; return;
} }
chats.forEach(function(userObj) { rooms.forEach(function(roomObj) {
dropdownEl = $('<li class="' + (userObj.unread ? 'unread' : '') + '"/>') function createUserImage(userObj) {
.attr('data-uid', userObj.uid) return '<a data-ajaxify="false">' +
.html('<a data-ajaxify="false">'+
(userObj.picture ? (userObj.picture ?
'<img src="' + userObj.picture + '" title="' + userObj.username +'" />' : '<img src="' + userObj.picture + '" title="' + userObj.username +'" />' :
'<div class="user-icon" style="background-color: ' + userObj['icon:bgColor'] + '">' + userObj['icon:text'] + '</div>') + '<div class="user-icon" style="background-color: ' + userObj['icon:bgColor'] + '">' + userObj['icon:text'] + '</div>') +
'<i class="fa fa-circle status ' + userObj.status + '"></i> ' + '<i class="fa fa-circle status ' + userObj.status + '"></i> ' +
userObj.username + '</a>') roomObj.usernames + '</a>';
}
dropdownEl = $('<li class="' + (roomObj.unread ? 'unread' : '') + '"/>')
.attr('data-roomId', roomObj.roomId)
.appendTo(chatsListEl); .appendTo(chatsListEl);
if (roomObj.lastUser) {
dropdownEl.append(createUserImage(roomObj.lastUser));
}
dropdownEl.click(function() { dropdownEl.click(function() {
if (!ajaxify.currentPage.match(/^chats\//)) { if (!ajaxify.currentPage.match(/^chats\//)) {
app.openChat(userObj.username, userObj.uid); app.openChat(roomObj.roomId);
} else { } else {
ajaxify.go('chats/' + utils.slugify(userObj.username)); ajaxify.go('chats/' + roomObj.roomId);
} }
}); });
}); });
@ -149,12 +153,12 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
chatModal.css('zIndex', topZ + 1); chatModal.css('zIndex', topZ + 1);
}; };
module.getModal = function(touid) { module.getModal = function(roomId) {
return $('#chat-modal-' + touid); return $('#chat-modal-' + roomId);
}; };
module.modalExists = function(touid) { module.modalExists = function(roomId) {
return $('#chat-modal-' + touid).length !== 0; return $('#chat-modal-' + roomId).length !== 0;
}; };
function checkStatus(chatModal) { function checkStatus(chatModal) {
@ -175,8 +179,8 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
uuid = utils.generateUUID(), uuid = utils.generateUUID(),
dragged = false; dragged = false;
chatModal.attr('id', 'chat-modal-' + data.touid); chatModal.attr('id', 'chat-modal-' + data.roomId);
chatModal.attr('touid', data.touid); chatModal.attr('roomId', data.roomId);
chatModal.attr('intervalId', 0); chatModal.attr('intervalId', 0);
chatModal.attr('UUID', uuid); chatModal.attr('UUID', uuid);
chatModal.css('position', 'fixed'); chatModal.css('position', 'fixed');
@ -210,8 +214,6 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
}); });
}); });
chatModal.find('#chat-with-name').html(data.username);
chatModal.find('#chat-close-btn').on('click', function() { chatModal.find('#chat-close-btn').on('click', function() {
module.close(chatModal); module.close(chatModal);
}); });
@ -222,19 +224,17 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
components.get('chat/input').val(text); components.get('chat/input').val(text);
}); });
ajaxify.go('chats/' + utils.slugify(data.username)); ajaxify.go('chats/' + chatModal.attr('roomId'));
module.close(chatModal); module.close(chatModal);
} }
chatModal.find('.modal-header').on('dblclick', gotoChats); chatModal.find('.modal-header').on('dblclick', gotoChats);
chatModal.find('button[data-action="maximize"]').on('click', gotoChats); chatModal.find('button[data-action="maximize"]').on('click', gotoChats);
chatModal.on('click', function(e) { chatModal.on('click', function() {
module.bringModalToTop(chatModal); module.bringModalToTop(chatModal);
if (!dragged) { if (dragged) {
chatModal.find('#chat-message-input').focus();
} else {
dragged = false; dragged = false;
} }
}); });
@ -247,7 +247,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
chatModal.on('mousemove keypress click', function() { chatModal.on('mousemove keypress click', function() {
if (newMessage) { if (newMessage) {
socket.emit('modules.chats.markRead', data.touid); socket.emit('modules.chats.markRead', data.roomId);
newMessage = false; newMessage = false;
} }
}); });
@ -256,22 +256,24 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
.on('click', '[data-action="edit"]', function() { .on('click', '[data-action="edit"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid'); var messageId = $(this).parents('[data-mid]').attr('data-mid');
var inputEl = chatModal.find('[component="chat/input"]'); var inputEl = chatModal.find('[component="chat/input"]');
Chats.prepEdit(inputEl, messageId); Chats.prepEdit(inputEl, messageId, data.roomId);
}) })
.on('click', '[data-action="delete"]', function() { .on('click', '[data-action="delete"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid'); var messageId = $(this).parents('[data-mid]').attr('data-mid');
Chats.delete(messageId); Chats.delete(messageId, data.roomId);
}); });
Chats.addSinceHandler(chatModal.attr('touid'), chatModal.find('.chat-content'), chatModal.find('[data-since]')); Chats.addSinceHandler(chatModal.attr('roomId'), chatModal.find('.chat-content'), chatModal.find('[data-since]'));
Chats.addSendHandlers(chatModal.attr('touid'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn')); Chats.addSendHandlers(chatModal.attr('roomId'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn'));
Chats.loadChatSince(chatModal.attr('touid'), chatModal.find('.chat-content'), 'recent'); Chats.createTagsInput(data.roomId, data.users);
Chats.loadChatSince(chatModal.attr('roomId'), chatModal.find('.chat-content'), 'recent');
checkStatus(chatModal); checkStatus(chatModal);
module.canMessage(data.touid, function(err) { module.canMessage(data.roomId, function(err) {
if (err) { if (err) {
// Disable the text input // Disable the text input
chatModal.find('input[type="text"]').attr('disabled', true); chatModal.find('input[type="text"]').attr('disabled', true);
@ -279,8 +281,8 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
}); });
taskbar.push('chat', chatModal.attr('UUID'), { taskbar.push('chat', chatModal.attr('UUID'), {
title: data.username, title: data.users.length ? data.users[0].username : '',
touid: data.touid, roomId: data.roomId,
icon: 'fa-comment', icon: 'fa-comment',
state: '' state: ''
}); });
@ -304,7 +306,7 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
chatModal.remove(); chatModal.remove();
chatModal.data('modal', null); chatModal.data('modal', null);
taskbar.discard('chat', chatModal.attr('UUID')); taskbar.discard('chat', chatModal.attr('UUID'));
Chats.notifyTyping(chatModal.attr('touid'), false); Chats.notifyTyping(chatModal.attr('roomId'), false);
if (chatModal.attr('data-mobile')) { if (chatModal.attr('data-mobile')) {
module.disableMobileBehaviour(chatModal); module.disableMobileBehaviour(chatModal);
@ -327,14 +329,14 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
}; };
module.load = function(uuid) { module.load = function(uuid) {
var chatModal = $('div[UUID="'+uuid+'"]'); var chatModal = $('div[UUID="' + uuid + '"]');
chatModal.removeClass('hide'); chatModal.removeClass('hide');
checkStatus(chatModal); checkStatus(chatModal);
taskbar.updateActive(uuid); taskbar.updateActive(uuid);
Chats.scrollToBottom(chatModal.find('.chat-content')); Chats.scrollToBottom(chatModal.find('.chat-content'));
module.bringModalToTop(chatModal); module.bringModalToTop(chatModal);
module.focusInput(chatModal); module.focusInput(chatModal);
socket.emit('modules.chats.markRead', chatModal.attr('touid')); socket.emit('modules.chats.markRead', chatModal.attr('roomId'));
var env = utils.findBootstrapEnvironment(); var env = utils.findBootstrapEnvironment();
if (env === 'xs' || env === 'sm') { if (env === 'xs' || env === 'sm') {
@ -373,13 +375,13 @@ define('chat', ['components', 'taskbar', 'string', 'sounds', 'forum/chats', 'tra
taskbar.minimize('chat', uuid); taskbar.minimize('chat', uuid);
clearInterval(chatModal.attr('intervalId')); clearInterval(chatModal.attr('intervalId'));
chatModal.attr('intervalId', 0); chatModal.attr('intervalId', 0);
Chats.notifyTyping(chatModal.attr('touid'), false); Chats.notifyTyping(chatModal.attr('roomId'), false);
}; };
module.toggleNew = taskbar.toggleNew; module.toggleNew = taskbar.toggleNew;
module.canMessage = function(toUid, callback) { module.canMessage = function(roomId, callback) {
socket.emit('modules.chats.canMessage', toUid, callback); socket.emit('modules.chats.canMessage', roomId, callback);
}; };

@ -1,13 +1,11 @@
'use strict'; 'use strict';
var async = require('async'), var async = require('async');
nconf = require('nconf'),
var messaging = require('../../messaging');
var meta = require('../../meta');
var helpers = require('../helpers');
user = require('../../user'),
messaging = require('../../messaging'),
meta = require('../../meta'),
helpers = require('../helpers'),
utils = require('../../../public/src/utils');
var chatsController = {}; var chatsController = {};
@ -16,35 +14,15 @@ chatsController.get = function(req, res, callback) {
return callback(); return callback();
} }
// In case a userNAME is passed in instead of a slug, the route should not 404 messaging.getRecentChats(req.uid, 0, 19, function(err, recentChats) {
var slugified = utils.slugify(req.params.userslug);
if (req.params.userslug && req.params.userslug !== slugified) {
return helpers.redirect(res, '/chats/' + slugified);
}
async.parallel({
contacts: async.apply(user.getFollowing, req.user.uid, 0, 199),
recentChats: async.apply(messaging.getRecentChats, req.user.uid, 0, 19)
}, function(err, results) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
if (results.recentChats.users && results.recentChats.users.length) { if (!req.params.roomid) {
var contactUids = results.recentChats.users.map(function(chatObj) {
return parseInt(chatObj.uid, 10);
});
results.contacts = results.contacts.filter(function(contact) {
return contactUids.indexOf(parseInt(contact.uid, 10)) === -1;
});
}
if (!req.params.userslug) {
return res.render('chats', { return res.render('chats', {
chats: results.recentChats.users, rooms: recentChats.rooms,
nextStart: results.recentChats.nextStart, nextStart: recentChats.nextStart,
contacts: results.contacts,
allowed: true, allowed: true,
title: '[[pages:chats]]', title: '[[pages:chats]]',
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]'}]) breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]'}])
@ -52,21 +30,24 @@ chatsController.get = function(req, res, callback) {
} }
async.waterfall([ async.waterfall([
async.apply(user.getUidByUserslug, req.params.userslug), function (next) {
function(toUid, next) { messaging.isUserInRoom(req.uid, req.params.roomid, next);
if (!toUid || parseInt(toUid, 10) === parseInt(req.user.uid, 10)) { },
function (inRoom, next) {
if (!inRoom) {
return callback(); return callback();
} }
async.parallel({ async.parallel({
toUser: async.apply(user.getUserFields, toUid, ['uid', 'username']), users: async.apply(messaging.getUsersInRoom, req.params.roomid, 0, -1),
messages: async.apply(messaging.getMessages, { messages: async.apply(messaging.getMessages, {
fromuid: req.user.uid, uid: req.uid,
touid: toUid, roomId: req.params.roomid,
since: 'recent', since: 'recent',
isNew: false isNew: false
}), }),
allowed: async.apply(messaging.canMessage, req.user.uid, toUid) allowed: async.apply(messaging.canMessageRoom, req.uid, req.params.roomid),
owner: async.apply(messaging.isRoomOwner, req.uid, req.params.roomid)
}, next); }, next);
} }
], function(err, data) { ], function(err, data) {
@ -74,16 +55,22 @@ chatsController.get = function(req, res, callback) {
return callback(err); return callback(err);
} }
res.render('chats', { data.users = data.users.filter(function(user) {
chats: results.recentChats.users, return user && parseInt(user.uid, 10) !== req.uid;
nextStart: results.recentChats.nextStart,
contacts: results.contacts,
meta: data.toUser,
messages: data.messages,
allowed: data.allowed,
title: '[[pages:chat, ' + data.toUser.username + ']]',
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]', url: '/chats'}, {text: data.toUser.username}])
}); });
data.usernames = data.users.map(function(user) {
return user && user.username;
}).join(', ');
data.roomId = req.params.roomid;
data.rooms = recentChats.rooms;
data.nextStart = recentChats.nextStart;
data.title = '[[pages:chat, ' + data.usernames + ']]';
data.breadcrumbs = helpers.buildBreadcrumbs([{text: '[[pages:chats]]', url: '/chats'}, {text: data.roomId}]);
res.render('chats', data);
}); });
}); });
}; };

@ -1,22 +1,27 @@
'use strict'; 'use strict';
var db = require('./database'),
async = require('async'), var async = require('async'),
nconf = require('nconf'),
winston = require('winston'), winston = require('winston'),
S = require('string'), S = require('string'),
db = require('./database'),
user = require('./user'), user = require('./user'),
plugins = require('./plugins'), plugins = require('./plugins'),
meta = require('./meta'), meta = require('./meta'),
utils = require('../public/src/utils'), utils = require('../public/src/utils'),
notifications = require('./notifications'), notifications = require('./notifications'),
userNotifications = require('./user/notifications'), userNotifications = require('./user/notifications');
emailer = require('./emailer'),
sockets = require('./socket.io');
(function(Messaging) { (function(Messaging) {
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
require('./messaging/create')(Messaging);
require('./messaging/delete')(Messaging);
require('./messaging/edit')(Messaging);
require('./messaging/rooms')(Messaging);
require('./messaging/unread')(Messaging);
require('./messaging/notifications')(Messaging);
var terms = { var terms = {
day: 86400000, day: 86400000,
@ -25,147 +30,9 @@ var db = require('./database'),
threemonths: 7776000000 threemonths: 7776000000
}; };
function sortUids(fromuid, touid) {
return [fromuid, touid].sort();
}
Messaging.addMessage = function(fromuid, touid, content, timestamp, callback) {
if (typeof timestamp === 'function') {
callback = timestamp;
timestamp = Date.now();
} else {
timestamp = timestamp || Date.now();
}
if (!content) {
return callback(new Error('[[error:invalid-chat-message]]'));
}
if (content.length > (meta.config.maximumChatMessageLength || 1000)) {
return callback(new Error('[[error:chat-message-too-long]]'));
}
var uids = sortUids(fromuid, touid);
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) {
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.editMessage = function(mid, content, callback) {
async.series([
function(next) {
// Verify that the message actually changed
Messaging.getMessageField(mid, 'content', function(err, raw) {
if (raw === content) {
// No dice.
return callback();
}
next();
});
},
async.apply(Messaging.setMessageFields, mid, {
content: content,
edited: Date.now()
}),
function(next) {
Messaging.getMessageFields(mid, ['fromuid', 'touid'], function(err, data) {
getMessages([mid], data.fromuid, data.touid, true, function(err, messages) {
sockets.in('uid_' + data.fromuid).emit('event:chats.edit', {
messages: messages
});
sockets.in('uid_' + data.touid).emit('event:chats.edit', {
messages: messages
});
next();
});
});
}
], callback);
};
Messaging.deleteMessage = function(mid, callback) {
var uids = [];
async.series([
function(next) {
db.getObject('message:' + mid, function(err, messageObj) {
messageObj.fromuid = parseInt(messageObj.fromuid, 10);
messageObj.touid = parseInt(messageObj.touid, 10);
uids.push(messageObj.fromuid, messageObj.touid);
uids.sort(function(a, b) {
return a > b ? 1 : -1;
});
next();
});
},
function(next) {
next();
},
function(next) {
db.sortedSetRemove('messages:uid:' + uids[0] + ':to:' + uids[1], mid, next);
},
async.apply(db.delete, 'message:' + mid)
], callback);
};
Messaging.getMessageField = function(mid, field, callback) { Messaging.getMessageField = function(mid, field, callback) {
Messaging.getMessageFields(mid, [field], function(err, fields) { Messaging.getMessageFields(mid, [field], function(err, fields) {
callback(err, fields[field]); callback(err, fields ? fields[field] : null);
}); });
}; };
@ -182,22 +49,21 @@ var db = require('./database'),
}; };
Messaging.getMessages = function(params, callback) { Messaging.getMessages = function(params, callback) {
var fromuid = params.fromuid, var uid = params.uid,
touid = params.touid, roomId = params.roomId,
since = params.since, since = params.since,
isNew = params.isNew, isNew = params.isNew,
count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250, count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250,
markRead = params.markRead || true; markRead = params.markRead || true;
var uids = sortUids(fromuid, touid), var min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
if (since === 'recent') { if (since === 'recent') {
count = 49; count = 49;
min = 0; min = 0;
} }
db.getSortedSetRevRangeByScore('messages:uid:' + uids[0] + ':to:' + uids[1], 0, count, '+inf', min, function(err, mids) { db.getSortedSetRevRangeByScore('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, count, '+inf', min, function(err, mids) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -208,52 +74,60 @@ var db = require('./database'),
mids.reverse(); mids.reverse();
getMessages(mids, fromuid, touid, isNew, callback); Messaging.getMessagesData(mids, uid, roomId, isNew, callback);
}); });
if (markRead) { if (markRead) {
notifications.markRead('chat_' + touid + '_' + fromuid, fromuid, function(err) { notifications.markRead('chat_' + roomId + '_' + uid, uid, function(err) {
if (err) { if (err) {
winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message); winston.error('[messaging] Could not mark notifications related to this chat as read: ' + err.message);
} }
userNotifications.pushCount(fromuid); userNotifications.pushCount(uid);
}); });
} }
}; };
function getMessages(mids, fromuid, touid, isNew, callback) { Messaging.getMessagesData = function(mids, uid, roomId, isNew, callback) {
user.getUsersFields([fromuid, touid], ['uid', 'username', 'userslug', 'picture', 'status'], function(err, userData) {
if(err) {
return callback(err);
}
var keys = mids.map(function(mid) { var keys = mids.map(function(mid) {
return 'message:' + mid; return 'message:' + mid;
}); });
var messages;
async.waterfall([ async.waterfall([
async.apply(db.getObjects, keys), function (next) {
function(messages, next) { db.getObjects(keys, next);
messages = messages.map(function(msg, idx) { },
function (_messages, next) {
messages = _messages.map(function(msg, idx) {
if (msg) { if (msg) {
msg.messageId = parseInt(mids[idx], 10); msg.messageId = parseInt(mids[idx], 10);
} }
return msg; return msg;
}).filter(Boolean); }).filter(Boolean);
async.map(messages, function(message, next) {
var self = parseInt(message.fromuid, 10) === parseInt(fromuid, 10); var uids = messages.map(function(msg) {
message.fromUser = self ? userData[0] : userData[1]; return msg && msg.fromuid;
message.toUser = self ? userData[1] : userData[0]; });
message.timestampISO = utils.toISOString(message.timestamp);
user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], next);
},
function (users, next) {
messages.forEach(function(message, index) {
message.fromUser = users[index];
var self = parseInt(message.fromuid, 10) === parseInt(uid, 10);
message.self = self ? 1 : 0; message.self = self ? 1 : 0;
message.timestampISO = utils.toISOString(message.timestamp);
message.newSet = false; message.newSet = false;
if (message.hasOwnProperty('edited')) { if (message.hasOwnProperty('edited')) {
message.editedISO = new Date(parseInt(message.edited, 10)).toISOString(); message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
} }
});
Messaging.parse(message.content, message.fromuid, fromuid, userData[1], userData[0], isNew, function(result) { async.map(messages, function(message, next) {
Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function(result) {
message.content = result; message.content = result;
message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s; message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s;
next(null, message); next(null, message);
@ -279,8 +153,7 @@ var db = require('./database'),
next(undefined, messages); next(undefined, messages);
} else { } else {
// For single messages, we don't know the context, so look up the previous message and compare // For single messages, we don't know the context, so look up the previous message and compare
var uids = [fromuid, touid].sort(function(a, b) { return a > b ? 1 : -1 }); var key = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
var key = 'messages:uid:' + uids[0] + ':to:' + uids[1];
async.waterfall([ async.waterfall([
async.apply(db.sortedSetRank, key, messages[0].messageId), async.apply(db.sortedSetRank, key, messages[0].messageId),
function(index, next) { function(index, next) {
@ -313,10 +186,10 @@ var db = require('./database'),
} }
} }
], callback); ], callback);
});
}
Messaging.parse = function (message, fromuid, myuid, toUserData, myUserData, isNew, callback) { };
Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) {
plugins.fireHook('filter:parse.raw', message, function(err, parsed) { plugins.fireHook('filter:parse.raw', message, function(err, parsed) {
if (err) { if (err) {
return callback(message); return callback(message);
@ -326,9 +199,8 @@ var db = require('./database'),
message: message, message: message,
parsed: parsed, parsed: parsed,
fromuid: fromuid, fromuid: fromuid,
myuid: myuid, uid: uid,
toUserData: toUserData, roomId: roomId,
myUserData: myUserData,
isNew: isNew, isNew: isNew,
parsedMessage: parsed parsedMessage: parsed
}; };
@ -339,161 +211,107 @@ var db = require('./database'),
}); });
}; };
Messaging.isNewSet = function(fromuid, touid, mid, callback) { Messaging.isNewSet = function(uid, roomId, timestamp, callback) {
var uids = sortUids(fromuid, touid), var setKey = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
setKey = 'messages:uid:' + uids[0] + ':to:' + uids[1];
async.waterfall([ async.waterfall([
async.apply(db.sortedSetRank, setKey, mid), function(next) {
function(index, next) { db.getSortedSetRevRangeWithScores(setKey, 0, 0, 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) { function(messages, next) {
if (typeof messages !== 'boolean' && messages && messages.length) { if (messages && messages.length) {
next(null, parseInt(messages[1].timestamp, 10) > parseInt(messages[0].timestamp, 10) + (1000*60*5)); next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5));
} else { } else {
next(null, messages); next(null, true);
} }
} }
], callback); ], 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) { Messaging.getRecentChats = function(uid, start, stop, callback) {
db.getSortedSetRevRange('uid:' + uid + ':chats', start, stop, function(err, uids) { db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, function(err, roomIds) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
async.parallel({ async.parallel({
unread: function(next) { unread: function(next) {
db.isSortedSetMembers('uid:' + uid + ':chats:unread', uids, next); db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next);
}, },
users: function(next) { users: function(next) {
async.map(roomIds, function(roomId, next) {
db.getSortedSetRevRange('chat:room:' + roomId + ':uids', 0, 3, 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', 'picture', 'status', 'lastonline'] , next); user.getUsersFields(uids, ['uid', 'username', 'picture', 'status', 'lastonline'] , next);
});
}, next);
}, },
teasers: function(next) { teasers: function(next) {
async.map(uids, function(fromuid, next) { async.map(roomIds, function(roomId, next) {
Messaging.getMessages({ Messaging.getTeaser(uid, roomId, next);
fromuid: fromuid,
touid: uid,
isNew: false,
count: 1,
markRead: false
}, function(err, teaser) {
teaser = teaser[0];
teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s;
next(err, teaser);
});
}, next); }, next);
} }
}, function(err, results) { }, function(err, results) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
var rooms = results.users.map(function(users, index) {
results.users.forEach(function(userData, index) { var data = {
users: users,
unread: results.unread[index],
roomId: roomIds[index],
teaser: results.teasers[index]
};
data.users.forEach(function(userData) {
if (userData && parseInt(userData.uid, 10)) { if (userData && parseInt(userData.uid, 10)) {
userData.unread = results.unread[index];
userData.status = user.getStatus(userData); userData.status = user.getStatus(userData);
userData.teaser = results.teasers[index];
} }
}); });
data.users = data.users.filter(function(user) {
results.users = results.users.filter(function(user) {
return user && parseInt(user.uid, 10); return user && parseInt(user.uid, 10);
}); });
data.lastUser = data.users[0];
callback(null, {users: results.users, nextStart: stop + 1}); data.usernames = data.users.map(function(user) {
}); return user.username;
}).join(', ');
return data;
}); });
};
Messaging.getUnreadCount = function(uid, callback) {
db.sortedSetCard('uid:' + uid + ':chats:unread', callback);
};
Messaging.pushUnreadCount = function(uid) { callback(null, {rooms: rooms, nextStart: stop + 1});
Messaging.getUnreadCount(uid, function(err, unreadCount) { });
if (err) {
return;
}
sockets.in('uid_' + uid).emit('event:unread.updateChatCount', unreadCount);
}); });
}; };
Messaging.markRead = function(uid, toUid, callback) { Messaging.getTeaser = function (uid, roomId, callback) {
db.sortedSetRemove('uid:' + uid + ':chats:unread', toUid, callback);
};
Messaging.markUnread = function(uid, toUid, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {
user.exists(toUid, next); db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, 0, next);
}, },
function (exists, next) { function (mids, next) {
if (!exists) { if (!mids || !mids.length) {
return next(new Error('[[error:no-user]]')); return next(null, null);
}
db.sortedSetAdd('uid:' + uid + ':chats:unread', Date.now(), toUid, next);
} }
], callback); Messaging.getMessageFields(mids[0], ['content', 'timestamp'], next);
}; },
function (teaser, next) {
Messaging.notifyUser = function(fromuid, touid, messageObj) { if (teaser && teaser.content) {
// Immediate notifications teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s;
// Recipient teaser.timestampISO = utils.toISOString(teaser.timestamp);
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() { next(null, teaser);
sendNotifications(fromuid, touid, queueObj.message, function(err) {
if (!err) {
delete Messaging.notifyQueue[fromuid + ':' + touid];
} }
}); ], callback);
}, 1000*60); // wait 60s before sending
}; };
Messaging.canMessage = function(fromUid, toUid, callback) { Messaging.canMessageUser = function(uid, toUid, callback) {
if (parseInt(meta.config.disableChat) === 1 || !fromUid || toUid === fromUid) { if (parseInt(meta.config.disableChat) === 1 || !uid || uid === toUid) {
return callback(null, false); return callback(null, false);
} }
@ -505,7 +323,7 @@ var db = require('./database'),
if (!exists) { if (!exists) {
return callback(null, false); return callback(null, false);
} }
user.getUserFields(fromUid, ['banned', 'email:confirmed'], next); user.getUserFields(uid, ['banned', 'email:confirmed'], next);
}, },
function (userData, next) { function (userData, next) {
if (parseInt(userData.banned, 10) === 1) { if (parseInt(userData.banned, 10) === 1) {
@ -523,24 +341,31 @@ var db = require('./database'),
return callback(null, true); return callback(null, true);
} }
user.isAdministrator(fromUid, next); user.isAdministrator(uid, next);
}, },
function(isAdmin, next) { function(isAdmin, next) {
if (isAdmin) { if (isAdmin) {
return callback(null, true); return callback(null, true);
} }
user.isFollowing(toUid, fromUid, next); user.isFollowing(toUid, uid, next);
} }
], callback); ], callback);
}; };
Messaging.canEdit = function(messageId, uid, callback) { Messaging.canMessageRoom = function(uid, roomId, callback) {
if (parseInt(meta.config.disableChat) === 1) { if (parseInt(meta.config.disableChat) === 1 || !uid) {
return callback(null, false); return callback(null, false);
} }
async.waterfall([ async.waterfall([
function (next) { function (next) {
Messaging.isUserInRoom(uid, roomId, next);
},
function (inRoom, next) {
if (!inRoom) {
return callback(null, false);
}
user.getUserFields(uid, ['banned', 'email:confirmed'], next); user.getUserFields(uid, ['banned', 'email:confirmed'], next);
}, },
function (userData, next) { function (userData, next) {
@ -552,54 +377,10 @@ var db = require('./database'),
return callback(null, false); return callback(null, false);
} }
Messaging.getMessageField(messageId, 'fromuid', next); next(null, true);
},
function(fromUid, next) {
if (parseInt(fromUid, 10) === parseInt(uid, 10)) {
return callback(null, true);
}
user.isAdministrator(uid, next);
},
function(isAdmin, next) {
next(null, isAdmin);
} }
], callback); ], callback);
}; };
function sendNotifications(fromuid, touid, messageObj, callback) {
user.isOnline(touid, function(err, isOnline) {
if (err || isOnline) {
return callback(err);
}
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)); }(exports));

@ -0,0 +1,118 @@
'use strict';
var async = require('async');
var meta = require('../meta');
var plugins = require('../plugins');
var db = require('../database');
module.exports = function(Messaging) {
Messaging.sendMessage = function(uid, roomId, content, timestamp, callback) {
async.waterfall([
function (next) {
Messaging.checkContent(content, next);
},
function (next) {
Messaging.isUserInRoom(uid, roomId, next);
},
function (inRoom, next) {
if (!inRoom) {
return next(new Error('[[error:not-allowed]]'));
}
Messaging.addMessage(uid, roomId, content, timestamp, next);
}
], callback);
};
Messaging.checkContent = function(content, callback) {
if (!content) {
return callback(new Error('[[error:invalid-chat-message]]'));
}
if (content.length > (meta.config.maximumChatMessageLength || 1000)) {
return callback(new Error('[[error:chat-message-too-long]]'));
}
callback();
};
Messaging.addMessage = function(fromuid, roomId, content, timestamp, callback) {
var mid;
var message;
var isNewSet;
async.waterfall([
function (next) {
Messaging.checkContent(content, next);
},
function (next) {
db.incrObjectField('global', 'nextMid', next);
},
function (_mid, next) {
mid = _mid;
message = {
content: content,
timestamp: timestamp,
fromuid: fromuid
};
plugins.fireHook('filter:messaging.save', message, next);
},
function (message, next) {
db.setObject('message:' + mid, message, next);
},
function (next) {
Messaging.isNewSet(fromuid, roomId, timestamp, next);
},
function (_isNewSet, next) {
isNewSet = _isNewSet;
db.getSortedSetRange('chat:room:' + roomId + ':uids', 0, -1, next);
},
function (uids, next) {
async.parallel([
async.apply(Messaging.addRoomToUsers, roomId, uids, timestamp),
async.apply(Messaging.addMessageToUsers, roomId, uids, mid, timestamp),
async.apply(Messaging.markUnread, uids, roomId)
], next);
},
function (results, next) {
async.parallel({
markRead: async.apply(Messaging.markRead, fromuid, roomId),
messages: async.apply(Messaging.getMessagesData, [mid], fromuid, 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 = roomId;
next(null, results.messages[0]);
}
], callback);
};
Messaging.addRoomToUsers = function(roomId, uids, timestamp, callback) {
if (!uids.length) {
return callback();
}
var keys = uids.map(function(uid) {
return 'uid:' + uid + ':chat:rooms';
});
db.sortedSetsAdd(keys, timestamp, roomId, callback);
};
Messaging.addMessageToUsers = function(roomId, uids, mid, timestamp, callback) {
if (!uids.length) {
return callback();
}
var keys = uids.map(function(uid) {
return 'uid:' + uid + ':chat:room:' + roomId + ':mids';
});
db.sortedSetsAdd(keys, timestamp, mid, callback);
};
};

@ -0,0 +1,27 @@
'use strict';
var async = require('async');
var db = require('../database');
module.exports = function(Messaging) {
Messaging.deleteMessage = function(mid, roomId, callback) {
async.waterfall([
function (next) {
Messaging.getUidsInRoom(roomId, 0, -1, next);
},
function (uids, next) {
if (!uids.length) {
return next();
}
var keys = uids.map(function(uid) {
return 'uid:' + uid + ':chat:room:' + roomId + 'mids';
});
db.sortedSetsRemove(keys, roomId, next);
},
function(next) {
db.delete('message:' + mid, next);
}
], callback);
};
};

@ -0,0 +1,80 @@
'use strict';
var async = require('async');
var meta = require('../meta');
var user = require('../user');
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();
}
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.canEdit = function(messageId, uid, callback) {
if (parseInt(meta.config.disableChat) === 1) {
return callback(null, false);
}
async.waterfall([
function (next) {
user.getUserFields(uid, ['banned', 'email:confirmed'], next);
},
function (userData, next) {
if (parseInt(userData.banned, 10) === 1) {
return callback(null, false);
}
if (parseInt(meta.config.requireEmailConfirmation, 10) === 1 && parseInt(userData['email:confirmed'], 10) !== 1) {
return callback(null, false);
}
Messaging.getMessageField(messageId, 'fromuid', next);
},
function(fromUid, next) {
if (parseInt(fromUid, 10) === parseInt(uid, 10)) {
return callback(null, true);
}
user.isAdministrator(uid, next);
},
function(isAdmin, next) {
next(null, isAdmin);
}
], callback);
};
};

@ -0,0 +1,105 @@
'use strict';
var async = require('async');
var nconf = require('nconf');
var user = require('../user');
var emailer = require('../emailer');
var notifications = require('../notifications');
var meta = require('../meta');
var utils = require('../../public/src/utils');
var sockets = require('../socket.io');
module.exports = function(Messaging) {
Messaging.notifyQueue = {}; // Only used to notify a user of a new chat message, see Messaging.notifyUser
Messaging.notifyUsersInRoom = function(fromUid, roomId, messageObj) {
Messaging.getUidsInRoom(roomId, 0, -1, function(err, uids) {
if (err) {
return;
}
var data = {
roomId: roomId,
fromUid: fromUid,
message: messageObj
};
uids.forEach(function(uid) {
data.self = parseInt(uid, 10) === parseInt(fromUid) ? 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 = Messaging.notifyQueue[fromUid + ':' + roomId] = {
message: messageObj
};
}
queueObj.timeout = setTimeout(function() {
sendNotifications(fromUid, uids, roomId, queueObj.message, function(err) {
if (!err) {
delete Messaging.notifyQueue[fromUid + ':' + roomId];
}
});
}, 1000*60); // wait 60s before sending
});
};
function sendNotifications(fromuid, uids, roomId, messageObj, callback) {
user.isOnline(uids, function(err, isOnline) {
if (err) {
return callback(err);
}
uids = uids.filter(function(uid, index) {
return isOnline[index] && parseInt(fromuid, 10) !== parseInt(uid, 10);
});
if (!uids.length) {
return callback();
}
notifications.create({
bodyShort: '[[notifications:new_message_from, ' + messageObj.fromUser.username + ']]',
bodyLong: messageObj.content,
nid: 'chat_' + fromuid + '_' + roomId,
from: fromuid,
path: '/chats/' + messageObj.roomId
}, function(err, notification) {
if (!err && notification) {
notifications.push(notification, uids, callback);
}
});
if (parseInt(meta.config.disableEmailSubscriptions, 10) === 1) {
return callback();
}
user.getMultipleUserSettings(uids, function(err, userSettings) {
if (err) {
return callback(err);
}
userSettings = userSettings.filter(function(settings) {
return settings && settings.sendChatNotifications;
});
async.each(userSettings, function(settings, next) {
emailer.send('notif_chat', settings.uid, {
subject: '[[email:notif.chat.subject, ' + messageObj.fromUser.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)
}, next);
}, callback);
});
});
}
};

@ -0,0 +1,106 @@
'use strict';
var async = require('async');
var db = require('../database');
var user = require('../user');
module.exports = function(Messaging) {
Messaging.newRoom = function(uid, toUids, callback) {
var roomId;
var now = Date.now();
async.waterfall([
function (next) {
db.incrObjectField('global', 'nextChatRoomId', next);
},
function (_roomId, next) {
roomId = _roomId;
db.sortedSetAdd('chat:room:' + roomId + ':uids', now, uid, next);
},
function (next) {
Messaging.addUsersToRoom(uid, toUids, roomId, next);
},
function (next) {
Messaging.addRoomToUsers(roomId, [uid].concat(toUids), now, next);
},
function (next) {
next(null, roomId);
}
], callback);
};
Messaging.isUserInRoom = function(uid, roomId, callback) {
db.isSortedSetMember('chat:room:' + roomId + ':uids', uid, callback);
};
Messaging.roomExists = function(roomId, callback) {
db.exists('chat:room:' + roomId + ':uids', callback);
};
Messaging.isRoomOwner = function(uid, roomId, callback) {
db.getSortedSetRange('chat:room:' + roomId + ':uids', 0, 0, function(err, uids) {
if (err) {
return callback(err);
}
if (!Array.isArray(uids) || !uids.length) {
return callback(null, false);
}
callback(null, parseInt(uids[0], 10) === parseInt(uid, 10));
});
};
Messaging.addUsersToRoom = function(uid, uids, roomId, callback) {
async.waterfall([
function (next) {
Messaging.isUserInRoom(uid, roomId, next);
},
function (inRoom, next) {
if (!inRoom) {
return next(new Error('[[error:cant-add-users-to-chat-room]]'));
}
var now = Date.now();
var timestamps = uids.map(function() {
return now;
});
db.sortedSetAdd('chat:room:' + roomId + ':uids', timestamps, uids, next);
}
], callback);
};
Messaging.removeUsersFromRoom = function(uid, uids, roomId, callback) {
async.waterfall([
function (next) {
Messaging.isRoomOwner(uid, roomId, next);
},
function (isOwner, next) {
if (!isOwner) {
return next(new Error('[[error:cant-add-users-to-chat-room]]'));
}
db.sortedSetRemove('chat:room:' + roomId + ':uids', uids, next);
},
function (next) {
var keys = uids.map(function(uid) {
return 'uid:' + uid + ':chat:rooms';
});
db.sortedSetsRemove(keys, roomId, next);
}
], callback);
};
Messaging.getUidsInRoom = function(roomId, start, stop, callback) {
db.getSortedSetRevRange('chat:room:' + roomId + ':uids', start, stop, callback);
};
Messaging.getUsersInRoom = function(roomId, start, stop, callback) {
async.waterfall([
function (next) {
Messaging.getUidsInRoom(roomId, start, stop, next);
},
function (uids, next) {
user.getUsersFields(uids, ['username', 'uid', 'picture', 'status'], next);
}
], callback);
};
};

@ -0,0 +1,45 @@
'use strict';
var async = require('async');
var db = require('../database');
var sockets = require('../socket.io');
module.exports = function(Messaging) {
Messaging.getUnreadCount = function(uid, callback) {
db.sortedSetCard('uid:' + uid + ':chat:rooms:unread', callback);
};
Messaging.pushUnreadCount = function(uid) {
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);
};
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';
});
db.sortedSetsAdd(keys, Date.now(), roomId, next);
}
], callback);
};
};

@ -23,5 +23,5 @@ module.exports = function (app, middleware, controllers) {
setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get); setupPageRoute(app, '/user/:userslug/settings', middleware, accountMiddlewares, controllers.accounts.settings.get);
setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.notifications.get); setupPageRoute(app, '/notifications', middleware, [middleware.authenticate], controllers.accounts.notifications.get);
setupPageRoute(app, '/chats/:userslug?', middleware, [middleware.authenticate], controllers.accounts.chats.get); setupPageRoute(app, '/chats/:roomid?', middleware, [middleware.authenticate], controllers.accounts.chats.get);
}; };

@ -22,6 +22,10 @@ SocketGroups.join = function(socket, data, callback) {
return callback(new Error('[[error:invalid-uid]]')); return callback(new Error('[[error:invalid-uid]]'));
} }
if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) {
return callback(new Error('[[error:not-allowed]]'));
}
groups.exists(data.groupName, function(err, exists) { groups.exists(data.groupName, function(err, exists) {
if (err || !exists) { if (err || !exists) {
return callback(err || new Error('[[error:no-group]]')); return callback(err || new Error('[[error:no-group]]'));

@ -1,14 +1,13 @@
"use strict"; "use strict";
var meta = require('../meta'), var async = require('async');
Messaging = require('../messaging'), var meta = require('../meta');
utils = require('../../public/src/utils'), var Messaging = require('../messaging');
var utils = require('../../public/src/utils');
async = require('async'), var server = require('./');
var user = require('../user');
server = require('./'),
var SocketModules = {
SocketModules = {
chats: {}, chats: {},
sounds: {}, sounds: {},
settings: {} settings: {}
@ -17,36 +16,65 @@ var meta = require('../meta'),
/* Chat */ /* Chat */
SocketModules.chats.get = function(socket, data, callback) { SocketModules.chats.get = function(socket, data, callback) {
if(!data) { if(!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]')); return callback(new Error('[[error:invalid-data]]'));
} }
Messaging.getMessages({ Messaging.getMessages({
fromuid: socket.uid, uid: socket.uid,
touid: data.touid, roomId: data.roomId,
since: data.since, since: data.since,
isNew: false isNew: false
}, callback); }, callback);
// Mark chat as read Messaging.markRead(socket.uid, data.roomId);
Messaging.markRead(socket.uid, data.touid);
}; };
SocketModules.chats.getRaw = function(socket, data, callback) { SocketModules.chats.getRaw = function(socket, data, callback) {
if(!data || !data.hasOwnProperty('mid')) { if (!data || !data.hasOwnProperty('mid')) {
return callback(new Error('[[error:invalid-data]]')); return callback(new Error('[[error:invalid-data]]'));
} }
async.waterfall([
function (next) {
Messaging.isUserInRoom(socket.uid, data.roomId, next);
},
function (inRoom, next) {
if (!inRoom) {
return next(new Error('[[error:not-allowed]]'));
}
Messaging.getMessageField(data.mid, 'content', next);
}
], callback);
};
SocketModules.chats.newRoom = function(socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-data]]'));
}
var now = Date.now();
// Websocket rate limiting
socket.lastChatMessageTime = socket.lastChatMessageTime || 0;
if (now - socket.lastChatMessageTime < 200) {
return callback(new Error('[[error:too-many-messages]]'));
} else {
socket.lastChatMessageTime = now;
}
Messaging.canMessageUser(socket.uid, data.touid, function(err, allowed) {
if (err || !allowed) {
return callback(err || new Error('[[error:chat-restricted]]'));
}
Messaging.getMessageField(data.mid, 'content', callback); Messaging.newRoom(socket.uid, [data.touid], callback);
});
}; };
SocketModules.chats.send = function(socket, data, callback) { SocketModules.chats.send = function(socket, data, callback) {
if (!data) { if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]')); return callback(new Error('[[error:invalid-data]]'));
} }
var now = Date.now(), var now = Date.now();
touid = parseInt(data.touid, 10);
// Websocket rate limiting // Websocket rate limiting
socket.lastChatMessageTime = socket.lastChatMessageTime || 0; socket.lastChatMessageTime = socket.lastChatMessageTime || 0;
@ -56,57 +84,112 @@ SocketModules.chats.send = function(socket, data, callback) {
socket.lastChatMessageTime = now; socket.lastChatMessageTime = now;
} }
Messaging.canMessage(socket.uid, touid, function(err, allowed) { Messaging.canMessageRoom(socket.uid, data.roomId, function(err, allowed) {
if (err || !allowed) { if (err || !allowed) {
return callback(err || new Error('[[error:chat-restricted]]')); return callback(err || new Error('[[error:chat-restricted]]'));
} }
Messaging.addMessage(socket.uid, touid, data.message, function(err, message) { Messaging.sendMessage(socket.uid, data.roomId, data.message, now, function(err, message) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
Messaging.notifyUser(socket.uid, touid, message); Messaging.notifyUsersInRoom(socket.uid, data.roomId, message);
callback(); callback();
}); });
}); });
}; };
SocketModules.chats.getUsersInRoom = function(socket, data, callback) {
if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
Messaging.isUserInRoom(socket.uid, data.roomId, next);
},
function (inRoom, next) {
if (!inRoom) {
return next(new Error('[[error:not-allowerd]]'));
}
Messaging.getUsersInRoom(data.roomId, 0, -1, next);
}
], callback);
};
SocketModules.chats.addUserToRoom = function(socket, data, callback) {
if (!data || !data.roomId || !data.username) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
function (next) {
user.getUidByUsername(data.username, next);
},
function (uid, next) {
if (!uid) {
return next(new Error('[[error:no-user]]'));
}
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.getUidByUsername(data.username, next);
},
function (uid, next) {
if (!uid) {
return next(new Error('[[error:no-user]]'));
}
Messaging.removeUsersFromRoom(socket.uid, [uid], data.roomId, next);
}
], callback);
};
SocketModules.chats.edit = function(socket, data, callback) { SocketModules.chats.edit = function(socket, data, callback) {
if (!data) { if (!data || !data.roomId) {
return callback(new Error('[[error:invalid-data]]')); return callback(new Error('[[error:invalid-data]]'));
} }
Messaging.canEdit(data.mid, socket.uid, function(err, allowed) { Messaging.canEdit(data.mid, socket.uid, function(err, allowed) {
if (allowed) { if (err || !allowed) {
Messaging.editMessage(data.mid, data.message, callback); return callback(err || new Error('[[error:cant-edit-chat-message]]'));
} else {
return callback(new Error('[[error:cant-edit-chat-message]]'));
} }
Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, callback);
}); });
}; };
SocketModules.chats.delete = function(socket, data, callback) { SocketModules.chats.delete = function(socket, data, callback) {
if (!data) { if (!data || !data.roomId || !data.messageId) {
return callback(new Error('[[error:invalid-data]]')); return callback(new Error('[[error:invalid-data]]'));
} }
Messaging.canEdit(data.messageId, socket.uid, function(err, allowed) { Messaging.canEdit(data.messageId, socket.uid, function(err, allowed) {
if (allowed) { if (err || !allowed) {
Messaging.deleteMessage(data.messageId, callback); return callback(err || new Error('[[error:cant-delete-chat-message]]'));
} }
Messaging.deleteMessage(data.messageId, data.roomId, callback);
}); });
} };
SocketModules.chats.canMessage = function(socket, toUid, callback) { SocketModules.chats.canMessage = function(socket, roomId, callback) {
Messaging.canMessage(socket.uid, toUid, function(err, allowed) { Messaging.canMessageRoom(socket.uid, roomId, function(err, allowed) {
callback(!allowed ? new Error('[[error:chat-restricted]]') : undefined); callback(!allowed ? new Error('[[error:chat-restricted]]') : undefined);
}); });
}; };
SocketModules.chats.markRead = function(socket, touid, callback) { SocketModules.chats.markRead = function(socket, roomId, callback) {
Messaging.markRead(socket.uid, touid, function(err) { Messaging.markRead(socket.uid, roomId, function(err) {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -125,10 +208,20 @@ SocketModules.chats.userStopTyping = function(socket, data, callback) {
}; };
function sendTypingNotification(event, socket, data, callback) { function sendTypingNotification(event, socket, data, callback) {
if (!socket.uid || !data) { if (!socket.uid || !data || !data.roomId) {
return; return;
} }
server.in('uid_' + data.touid).emit(event, data.fromUid);
Messaging.getUidsInRoom(data.roomId, 0, -1, function(err, uids) {
if (err) {
return callback(err);
}
uids.forEach(function(uid) {
if (socket.uid !== parseInt(uid, 10)) {
server.in('uid_' + uid).emit(event, data.fromUid);
}
});
});
} }
SocketModules.chats.getRecentChats = function(socket, data, callback) { SocketModules.chats.getRecentChats = function(socket, data, callback) {

@ -10,7 +10,7 @@ var db = require('./database'),
schemaDate, thisSchemaDate, schemaDate, thisSchemaDate,
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
latestSchema = Date.UTC(2015, 10, 6); latestSchema = Date.UTC(2015, 11, 15);
Upgrade.check = function(callback) { Upgrade.check = function(callback) {
db.get('schemaDate', function(err, value) { db.get('schemaDate', function(err, value) {
@ -143,8 +143,96 @@ Upgrade.upgrade = function(callback) {
winston.info('[2015/11/06] Gravatar removal skipped'); winston.info('[2015/11/06] Gravatar removal skipped');
next(); next();
} }
},
function(next) {
thisSchemaDate = Date.UTC(2015, 11, 15);
if (schemaDate < thisSchemaDate) {
updatesMade = true;
winston.info('[2015/12/15] Upgrading chats');
db.getObjectFields('global', ['nextMid', 'nextChatRoomId'], function(err, globalData) {
if (err) {
return next(err);
}
var rooms = {};
var roomId = globalData.nextChatRoomId || 1;
var currentMid = 1;
async.whilst(function() {
return currentMid < globalData.nextMid;
}, function(next) {
db.getObject('message:' + currentMid, function(err, message) {
function addMessageToUids(roomId, callback) {
async.parallel([
function(next) {
db.sortedSetAdd('uid:' + message.fromuid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next);
},
function(next) {
db.sortedSetAdd('uid:' + message.touid + ':chat:room:' + roomId + ':mids', msgTime, currentMid, next);
}
], callback);
}
if (err || !message) {
winston.info('skipping chat message ', currentMid);
currentMid ++;
return next(err);
}
var pairID = [parseInt(message.fromuid, 10), parseInt(message.touid, 10)].sort().join(':');
var msgTime = parseInt(message.timestamp, 10);
if (rooms[pairID]) {
winston.info('adding message ' + currentMid + ' to existing roomID ' + roomId);
addMessageToUids(rooms[pairID], function(err) {
if (err) {
return next(err);
}
currentMid ++;
next();
});
} else {
winston.info('adding message ' + currentMid + ' to new roomID ' + roomId);
async.parallel([
function(next) {
db.sortedSetAdd('uid:' + message.fromuid + ':chat:rooms', msgTime, roomId, next);
},
function(next) {
db.sortedSetAdd('uid:' + message.touid + ':chat:rooms', msgTime, roomId, next);
},
function(next) {
db.sortedSetAdd('chat:room:' + roomId + ':uids', [msgTime, msgTime + 1], [message.fromuid, message.touid], next);
},
function(next) {
addMessageToUids(roomId, next);
}
], function(err) {
if (err) {
return next(err);
}
rooms[pairID] = roomId;
roomId ++;
currentMid ++;
db.setObjectField('global', 'nextChatRoomId', roomId, next);
});
}
});
}, function(err) {
if (err) {
return next(err);
} }
winston.info('[2015/12/15] Chats upgrade done!');
Upgrade.update(thisSchemaDate, next);
});
});
} else {
winston.info('[2015/12/15] Chats upgrade skipped!');
next();
}
}
// Add new schema updates here // Add new schema updates here
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!! // IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
], function(err) { ], function(err) {

@ -132,6 +132,18 @@ var async = require('async'),
}; };
User.isOnline = function(uid, callback) { User.isOnline = function(uid, callback) {
if (Array.isArray(uid)) {
db.sortedSetScores('users:online', uid, function(err, lastonline) {
if (err) {
return callback(err);
}
var now = Date.now();
var isOnline = uid.map(function(uid, index) {
return now - lastonline[index] < 300000;
});
callback(null, isOnline);
});
} else {
db.sortedSetScore('users:online', uid, function(err, lastonline) { db.sortedSetScore('users:online', uid, function(err, lastonline) {
if (err) { if (err) {
return callback(err); return callback(err);
@ -139,6 +151,8 @@ var async = require('async'),
var isOnline = Date.now() - parseInt(lastonline, 10) < 300000; var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
callback(null, isOnline); callback(null, isOnline);
}); });
}
}; };
User.exists = function(uid, callback) { User.exists = function(uid, callback) {

@ -55,6 +55,9 @@ module.exports = function(User) {
function (next) { function (next) {
deleteVotes(uid, next); deleteVotes(uid, next);
}, },
function (next) {
deleteChats(uid, next);
},
function (next) { function (next) {
async.parallel([ async.parallel([
function(next) { function(next) {
@ -97,6 +100,7 @@ module.exports = function(User) {
'uid:' + uid + ':favourites', 'uid:' + uid + ':followed_tids', 'user:' + uid + ':settings', 'uid:' + uid + ':favourites', 'uid:' + uid + ':followed_tids', 'user:' + uid + ':settings',
'uid:' + uid + ':topics', 'uid:' + uid + ':posts', 'uid:' + uid + ':topics', 'uid:' + uid + ':posts',
'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread', 'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread',
'uid:' + uid + ':chat:rooms', 'uid:' + uid + ':chat:rooms:unread',
'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote', 'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote',
'uid:' + uid + ':ignored:cids', 'uid:' + uid + ':flag:pids' 'uid:' + uid + ':ignored:cids', 'uid:' + uid + ':flag:pids'
]; ];
@ -145,6 +149,29 @@ module.exports = function(User) {
}); });
} }
function deleteChats(uid, callback) {
async.waterfall([
function (next) {
db.getSortedSetRange('uid:' + uid + ':chat:rooms', 0, -1, next);
},
function (roomIds, next) {
var userKeys = roomIds.map(function(roomId) {
return 'uid:' + uid + ':chat:room:' + roomId + ':mids';
});
var roomKeys = roomIds.map(function(roomId) {
return 'chat:room:' + roomId + ':uids';
});
async.parallel([
async.apply(db.sortedSetsRemove, roomKeys, uid),
async.apply(db.deleteAll, userKeys)
], next);
}
], function(err) {
callback(err);
});
}
function deleteUserIps(uid, callback) { function deleteUserIps(uid, callback) {
async.waterfall([ async.waterfall([
function (next) { function (next) {

@ -125,9 +125,7 @@ var async = require('async'),
notification.path = pidToPaths[notification.pid] || notification.path || ''; notification.path = pidToPaths[notification.pid] || notification.path || '';
if (notification.nid.startsWith('chat')) { if (notification.nid.startsWith('follow')) {
notification.path = '/chats/' + notification.user.userslug;
} else if (notification.nid.startsWith('follow')) {
notification.path = '/user/' + notification.user.userslug; notification.path = '/user/' + notification.user.userslug;
} }

@ -27,14 +27,14 @@ describe('Messaging Library', function() {
describe('.canMessage()', function() { describe('.canMessage()', function() {
it('should not error out', function(done) { it('should not error out', function(done) {
Messaging.canMessage(testUids[1], testUids[2], function(err, allowed) { Messaging.canMessageUser(testUids[1], testUids[2], function(err, allowed) {
assert.ifError(err); assert.ifError(err);
done(); done();
}); });
}); });
it('should allow messages to be sent to an unrestricted user', function(done) { it('should allow messages to be sent to an unrestricted user', function(done) {
Messaging.canMessage(testUids[1], testUids[2], function(err, allowed) { Messaging.canMessageUser(testUids[1], testUids[2], function(err, allowed) {
assert.strictEqual(allowed, true, 'should be true, received ' + allowed); assert.strictEqual(allowed, true, 'should be true, received ' + allowed);
done(); done();
}); });
@ -42,7 +42,7 @@ describe('Messaging Library', function() {
it('should NOT allow messages to be sent to a restricted user', function(done) { it('should NOT allow messages to be sent to a restricted user', function(done) {
User.setSetting(testUids[1], 'restrictChat', '1', function() { User.setSetting(testUids[1], 'restrictChat', '1', function() {
Messaging.canMessage(testUids[2], testUids[1], function(err, allowed) { Messaging.canMessageUser(testUids[2], testUids[1], function(err, allowed) {
assert.strictEqual(allowed, false, 'should be false, received ' + allowed); assert.strictEqual(allowed, false, 'should be false, received ' + allowed);
done(); done();
}); });
@ -50,7 +50,7 @@ describe('Messaging Library', function() {
}); });
it('should always allow admins through', function(done) { it('should always allow admins through', function(done) {
Messaging.canMessage(testUids[0], testUids[1], function(err, allowed) { Messaging.canMessageUser(testUids[0], testUids[1], function(err, allowed) {
assert.strictEqual(allowed, true, 'should be true, received ' + allowed); assert.strictEqual(allowed, true, 'should be true, received ' + allowed);
done(); done();
}); });
@ -58,7 +58,7 @@ describe('Messaging Library', function() {
it('should allow messages to be sent to a restricted user if restricted user follows sender', function(done) { it('should allow messages to be sent to a restricted user if restricted user follows sender', function(done) {
User.follow(testUids[1], testUids[2], function() { User.follow(testUids[1], testUids[2], function() {
Messaging.canMessage(testUids[2], testUids[1], function(err, allowed) { Messaging.canMessageUser(testUids[2], testUids[1], function(err, allowed) {
assert.strictEqual(allowed, true, 'should be true, received ' + allowed); assert.strictEqual(allowed, true, 'should be true, received ' + allowed);
done(); done();
}); });

Loading…
Cancel
Save