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

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

@ -1,6 +1,6 @@
'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) {
var Chats = {
@ -18,6 +18,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}
Chats.addEventListeners();
Chats.createTagsInput(ajaxify.data.roomId, ajaxify.data.users);
if (env === 'md' || env === 'lg') {
Chats.resizeMainWindow();
@ -28,39 +29,30 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.initialised = true;
if (ajaxify.data.hasOwnProperty('meta') && ajaxify.data.meta.hasOwnProperty('uid')) {
// This is an active chat, focus on the input box
if (ajaxify.data.hasOwnProperty('roomId')) {
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() {
components.get('chat/recent').on('click', 'li', function(e) {
Chats.switchChat(parseInt($(this).attr('data-uid'), 10), $(this).attr('data-username'));
$('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function() {
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() {
var username = $('.expanded-chat').attr('data-username'),
uid = Chats.getRecipientUid(),
text = components.get('chat/input').val();
var text = components.get('chat/input').val();
var roomId = ajaxify.data.roomId;
if (app.previousUrl && app.previousUrl.match(/chats/)) {
ajaxify.go('chats', function() {
app.openChat(username, uid);
app.openChat(roomId);
}, true);
} else {
window.history.go(-1);
app.openChat(username, uid);
app.openChat(roomId);
}
$(window).one('action:chat.loaded', function() {
@ -72,14 +64,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
.on('click', '[data-action="edit"]', function() {
var messageId = $(this).parents('[data-mid]').attr('data-mid');
var inputEl = components.get('chat/input');
Chats.prepEdit(inputEl, messageId);
Chats.prepEdit(inputEl, messageId, ajaxify.data.roomId);
})
.on('click', '[data-action="delete"]', function() {
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 bottom = ($this[0].scrollHeight - $this.height()) * 0.9;
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() {
@ -96,7 +88,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
prev = activeContact.prev();
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() {
@ -104,7 +96,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
next = activeContact.next();
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) {
@ -114,13 +106,16 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
var lastMid = message.attr('data-mid');
var inputEl = components.get('chat/input');
Chats.prepEdit(inputEl, lastMid);
Chats.prepEdit(inputEl, lastMid, ajaxify.data.roomId);
}
});
};
Chats.prepEdit = function(inputEl, messageId) {
socket.emit('modules.chats.getRaw', { mid: messageId }, function(err, raw) {
Chats.prepEdit = function(inputEl, messageId, roomId) {
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
if (inputEl.val().length === 0) {
// 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) {
bootbox.confirm(translated, function(ok) {
if (ok) {
socket.emit('modules.chats.delete', {
messageId: messageId
}, function(err) {
if (err) {
return app.alertError(err.message);
}
// Remove message from list
components.get('chat/message', messageId).slideUp('slow', function() {
$(this).remove();
});
});
if (!ok) {
return;
}
socket.emit('modules.chats.delete', {
messageId: messageId,
roomId: roomId
}, function(err) {
if (err) {
return app.alertError(err.message);
}
components.get('chat/message', messageId).slideUp('slow', function() {
$(this).remove();
});
});
});
});
};
Chats.addSinceHandler = function(toUid, chatContentEl, sinceEl) {
Chats.addSinceHandler = function(roomId, chatContentEl, sinceEl) {
sinceEl.on('click', function() {
var since = $(this).attr('data-since');
sinceEl.removeClass('selected');
$(this).addClass('selected');
Chats.loadChatSince(toUid, chatContentEl, since);
Chats.loadChatSince(roomId, chatContentEl, since);
return false;
});
};
Chats.addSendHandlers = function(toUid, inputEl, sendEl) {
Chats.addSendHandlers = function(roomId, inputEl, sendEl) {
inputEl.off('keypress').on('keypress', function(e) {
if (e.which === 13 && !e.shiftKey) {
Chats.sendMessage(toUid, inputEl);
Chats.sendMessage(roomId, inputEl);
return false;
}
});
@ -177,51 +174,66 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return;
}
Chats.notifyTyping(toUid, val);
Chats.notifyTyping(roomId, val);
$(this).attr('data-typing', val);
});
sendEl.off('click').on('click', function(e) {
Chats.sendMessage(toUid, inputEl);
sendEl.off('click').on('click', function() {
Chats.sendMessage(roomId, inputEl);
inputEl.focus();
return false;
});
};
Chats.switchChat = function(uid, username) {
if (!$('#content [component="chat/messages"]').length) {
return ajaxify.go('chats/' + utils.slugify(username));
Chats.createTagsInput = function(roomId, users) {
var tagEl = $('.users-tag-input');
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');
Chats.addSendHandlers(uid, components.get('chat/input'), $('[data-action="send"]'));
tagEl.on('beforeItemRemove', function(event) {
event.cancel = !ajaxify.data.owner;
});
contactEl
.removeClass('unread')
.addClass('bg-primary')
.siblings().removeClass('bg-primary');
tagEl.on('itemRemoved', function(event) {
socket.emit('modules.chats.removeUserFromRoom', {roomId: roomId, username: event.item});
});
components.get('chat/title').text(username);
components.get('chat/messages').attr('data-uid', uid).attr('data-username', username);
components.get('breadcrumb/current').text(username);
components.get('chat/input').focus();
var input = $('.users-tag-container').find('.bootstrap-tagsinput input');
if (window.history && window.history.pushState) {
var url = 'chats/' + utils.slugify(username);
require(['autocomplete'], function(autocomplete) {
autocomplete.user(input);
});
};
window.history.pushState({
url: url
}, url, RELATIVE_PATH + '/' + url);
}
Chats.switchChat = function(roomid) {
ajaxify.go('chats/' + roomid);
};
Chats.loadChatSince = function(toUid, chatContentEl, since) {
if (!toUid) {
Chats.loadChatSince = function(roomId, chatContentEl, since) {
if (!roomId) {
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) {
return app.alertError(err.message);
}
@ -235,12 +247,9 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.addGlobalEventListeners = function() {
$(window).on('resize', Chats.resizeMainWindow);
$(window).on('mousemove keypress click', function() {
if (newMessage) {
var recipientUid = Chats.getRecipientUid();
if (recipientUid) {
socket.emit('modules.chats.markRead', recipientUid);
newMessage = false;
}
if (newMessage && ajaxify.data.roomId) {
socket.emit('modules.chats.markRead', ajaxify.data.roomId);
newMessage = false;
}
});
};
@ -268,42 +277,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.addSocketListeners = function() {
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;
data.message.self = data.self;
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) {
var message;
data.messages.forEach(function(message) {
templates.parse('partials/chat_message', {
messages: message
}, function(html) {
body = components.get('chat/message', message.messageId);
var body = components.get('chat/message', message.messageId);
if (body.length) {
body.replaceWith(html);
components.get('chat/message', message.messageId).find('.timeago').timeago();
@ -351,14 +328,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
Chats.setActive();
};
Chats.notifyTyping = function(toUid, typing) {
Chats.notifyTyping = function(roomId, typing) {
socket.emit('modules.chats.user' + (typing ? 'Start' : 'Stop') + 'Typing', {
touid: toUid,
roomId: roomId,
fromUid: app.user.uid
});
};
Chats.sendMessage = function(toUid, inputEl) {
Chats.sendMessage = function(roomId, inputEl) {
var msg = inputEl.val(),
mid = inputEl.attr('data-mid');
@ -375,7 +352,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
if (!mid) {
socket.emit('modules.chats.send', {
touid: toUid,
roomId: roomId,
message: msg
}, function(err) {
if (err) {
@ -386,10 +363,11 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
}
sounds.play('chat-outgoing');
Chats.notifyTyping(toUid, false);
Chats.notifyTyping(roomId, false);
});
} else {
socket.emit('modules.chats.edit', {
roomId: roomId,
mid: mid,
message: msg
}, function(err) {
@ -397,7 +375,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
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() {
var recipientUid = Chats.getRecipientUid();
if (recipientUid) {
socket.emit('modules.chats.markRead', recipientUid);
if (ajaxify.data.roomId) {
socket.emit('modules.chats.markRead', ajaxify.data.roomId);
$('.expanded-chat input').focus();
}
$('.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) {
@ -429,7 +406,7 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
};
function loadMoreRecentChats() {
var recentChats = $('.recent-chats');
var recentChats = $('[component="chat/recent"]');
if (recentChats.attr('loading')) {
return;
}
@ -441,8 +418,8 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
return app.alertError(err.message);
}
if (data && data.users.length) {
onRecentChatsLoaded(data.users, function() {
if (data && data.rooms.length) {
onRecentChatsLoaded(data, function() {
recentChats.removeAttr('loading');
recentChats.attr('data-nextstart', data.nextStart);
});
@ -452,17 +429,14 @@ define('forum/chats', ['components', 'string', 'sounds', 'forum/infinitescroll',
});
}
function onRecentChatsLoaded(users, callback) {
users = users.filter(function(user) {
return !$('.recent-chats li[data-uid=' + user.uid + ']').length;
});
if (!users.length) {
function onRecentChatsLoaded(data, callback) {
if (!data.rooms.length) {
return callback();
}
app.parseAndTranslate('chats', 'chats', {chats: users}, function(html) {
$('.recent-chats').append(html);
app.parseAndTranslate('chats', 'rooms', data, function(html) {
$('[component="chat/recent"]').append(html);
html.find('.timeago').timeago();
callback();
});
}

@ -405,12 +405,10 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
});
}
function openChat(button) {
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();
return false;
}

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

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

@ -1,13 +1,11 @@
'use strict';
var async = require('async'),
nconf = require('nconf'),
var async = require('async');
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 = {};
@ -16,35 +14,15 @@ chatsController.get = function(req, res, callback) {
return callback();
}
// In case a userNAME is passed in instead of a slug, the route should not 404
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) {
messaging.getRecentChats(req.uid, 0, 19, function(err, recentChats) {
if (err) {
return callback(err);
}
if (results.recentChats.users && results.recentChats.users.length) {
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) {
if (!req.params.roomid) {
return res.render('chats', {
chats: results.recentChats.users,
nextStart: results.recentChats.nextStart,
contacts: results.contacts,
rooms: recentChats.rooms,
nextStart: recentChats.nextStart,
allowed: true,
title: '[[pages:chats]]',
breadcrumbs: helpers.buildBreadcrumbs([{text: '[[pages:chats]]'}])
@ -52,21 +30,24 @@ chatsController.get = function(req, res, callback) {
}
async.waterfall([
async.apply(user.getUidByUserslug, req.params.userslug),
function(toUid, next) {
if (!toUid || parseInt(toUid, 10) === parseInt(req.user.uid, 10)) {
function (next) {
messaging.isUserInRoom(req.uid, req.params.roomid, next);
},
function (inRoom, next) {
if (!inRoom) {
return callback();
}
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, {
fromuid: req.user.uid,
touid: toUid,
uid: req.uid,
roomId: req.params.roomid,
since: 'recent',
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);
}
], function(err, data) {
@ -74,16 +55,22 @@ chatsController.get = function(req, res, callback) {
return callback(err);
}
res.render('chats', {
chats: results.recentChats.users,
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.users = data.users.filter(function(user) {
return user && parseInt(user.uid, 10) !== req.uid;
});
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';
var db = require('./database'),
async = require('async'),
nconf = require('nconf'),
var async = require('async'),
winston = require('winston'),
S = require('string'),
db = require('./database'),
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');
userNotifications = require('./user/notifications');
(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 = {
day: 86400000,
@ -25,147 +30,9 @@ var db = require('./database'),
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.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) {
var fromuid = params.fromuid,
touid = params.touid,
var uid = params.uid,
roomId = params.roomId,
since = params.since,
isNew = params.isNew,
count = params.count || parseInt(meta.config.chatMessageInboxSize, 10) || 250,
markRead = params.markRead || true;
var uids = sortUids(fromuid, touid),
min = params.count ? 0 : Date.now() - (terms[since] || terms.day);
var min = params.count ? 0 : 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) {
db.getSortedSetRevRangeByScore('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, count, '+inf', min, function(err, mids) {
if (err) {
return callback(err);
}
@ -208,115 +74,122 @@ var db = require('./database'),
mids.reverse();
getMessages(mids, fromuid, touid, isNew, callback);
Messaging.getMessagesData(mids, uid, roomId, isNew, callback);
});
if (markRead) {
notifications.markRead('chat_' + touid + '_' + fromuid, fromuid, function(err) {
notifications.markRead('chat_' + roomId + '_' + uid, uid, function(err) {
if (err) {
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) {
user.getUsersFields([fromuid, touid], ['uid', 'username', 'userslug', 'picture', 'status'], function(err, userData) {
if(err) {
return callback(err);
}
Messaging.getMessagesData = function(mids, uid, roomId, isNew, callback) {
var keys = mids.map(function(mid) {
return 'message:' + mid;
});
var keys = mids.map(function(mid) {
return 'message:' + mid;
});
async.waterfall([
async.apply(db.getObjects, keys),
function(messages, next) {
messages = messages.map(function(msg, idx) {
if (msg) {
msg.messageId = parseInt(mids[idx], 10);
}
return msg;
}).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;
if (message.hasOwnProperty('edited')) {
message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
}
var messages;
Messaging.parse(message.content, message.fromuid, fromuid, userData[1], userData[0], isNew, function(result) {
message.content = result;
message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s;
next(null, message);
});
}, next);
},
function(messages, next) {
if (messages.length > 1) {
// Add a spacer in between messages with time gaps between them
messages = messages.map(function(message, index) {
// Compare timestamps with the previous message, and check if a spacer needs to be added
if (index > 0 && 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;
}
async.waterfall([
function (next) {
db.getObjects(keys, next);
},
function (_messages, next) {
messages = _messages.map(function(msg, idx) {
if (msg) {
msg.messageId = parseInt(mids[idx], 10);
}
return msg;
}).filter(Boolean);
return message;
});
var uids = messages.map(function(msg) {
return msg && msg.fromuid;
});
next(undefined, messages);
} else {
// 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 = 'messages:uid:' + uids[0] + ':to:' + uids[1];
async.waterfall([
async.apply(db.sortedSetRank, key, messages[0].messageId),
function(index, next) {
// Continue only if this isn't the first message in sorted set
if (index > 0) {
db.getSortedSetRange(key, index-1, index-1, next);
} else {
messages[0].newSet = true;
return next(undefined, messages);
}
},
function(mid, next) {
Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next);
}
], function(err, fields) {
if (err) {
return next(err);
}
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.timestampISO = utils.toISOString(message.timestamp);
message.newSet = false;
if (message.hasOwnProperty('edited')) {
message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
}
});
async.map(messages, function(message, next) {
Messaging.parse(message.content, message.fromuid, uid, roomId, isNew, function(result) {
message.content = result;
message.cleanedContent = S(result).stripTags().decodeHTMLEntities().s;
next(null, message);
});
}, next);
},
function(messages, next) {
if (messages.length > 1) {
// Add a spacer in between messages with time gaps between them
messages = messages.map(function(message, index) {
// Compare timestamps with the previous message, and check if a spacer needs to be added
if (index > 0 && 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;
}
if (
(parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000*60*5)) ||
(parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10))
) {
// If it's been 5 minutes, this is a new set of messages
return message;
});
next(undefined, messages);
} else {
// For single messages, we don't know the context, so look up the previous message and compare
var key = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
async.waterfall([
async.apply(db.sortedSetRank, key, messages[0].messageId),
function(index, next) {
// Continue only if this isn't the first message in sorted set
if (index > 0) {
db.getSortedSetRange(key, index-1, index-1, next);
} else {
messages[0].newSet = true;
return next(undefined, messages);
}
},
function(mid, next) {
Messaging.getMessageFields(mid, ['fromuid', 'timestamp'], next);
}
], function(err, fields) {
if (err) {
return next(err);
}
next(undefined, messages);
});
}
if (
(parseInt(messages[0].timestamp, 10) > parseInt(fields.timestamp, 10) + (1000*60*5)) ||
(parseInt(messages[0].fromuid, 10) !== parseInt(fields.fromuid, 10))
) {
// If it's been 5 minutes, this is a new set of messages
messages[0].newSet = true;
}
next(undefined, messages);
});
}
], 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) {
if (err) {
return callback(message);
@ -326,9 +199,8 @@ var db = require('./database'),
message: message,
parsed: parsed,
fromuid: fromuid,
myuid: myuid,
toUserData: toUserData,
myUserData: myUserData,
uid: uid,
roomId: roomId,
isNew: isNew,
parsedMessage: parsed
};
@ -339,161 +211,107 @@ var db = require('./database'),
});
};
Messaging.isNewSet = function(fromuid, touid, mid, callback) {
var uids = sortUids(fromuid, touid),
setKey = 'messages:uid:' + uids[0] + ':to:' + uids[1];
Messaging.isNewSet = function(uid, roomId, timestamp, callback) {
var setKey = 'uid:' + uid + ':chat:room:' + roomId + ':mids';
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(next) {
db.getSortedSetRevRangeWithScores(setKey, 0, 0, next);
},
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));
if (messages && messages.length) {
next(null, parseInt(timestamp, 10) > parseInt(messages[0].score, 10) + (1000 * 60 * 5));
} else {
next(null, messages);
next(null, true);
}
}
], 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) {
db.getSortedSetRevRange('uid:' + uid + ':chat:rooms', start, stop, function(err, roomIds) {
if (err) {
return callback(err);
}
async.parallel({
unread: function(next) {
db.isSortedSetMembers('uid:' + uid + ':chats:unread', uids, next);
db.isSortedSetMembers('uid:' + uid + ':chat:rooms:unread', roomIds, next);
},
users: function(next) {
user.getUsersFields(uids, ['uid', 'username', 'picture', 'status', 'lastonline'] , 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);
});
}, next);
},
teasers: function(next) {
async.map(uids, function(fromuid, next) {
Messaging.getMessages({
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);
});
async.map(roomIds, function(roomId, next) {
Messaging.getTeaser(uid, roomId, next);
}, next);
}
}, function(err, results) {
if (err) {
return callback(err);
}
results.users.forEach(function(userData, index) {
if (userData && parseInt(userData.uid, 10)) {
userData.unread = results.unread[index];
userData.status = user.getStatus(userData);
userData.teaser = results.teasers[index];
}
});
results.users = results.users.filter(function(user) {
return user && parseInt(user.uid, 10);
var rooms = results.users.map(function(users, 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)) {
userData.status = user.getStatus(userData);
}
});
data.users = data.users.filter(function(user) {
return user && parseInt(user.uid, 10);
});
data.lastUser = data.users[0];
data.usernames = data.users.map(function(user) {
return user.username;
}).join(', ');
return data;
});
callback(null, {users: results.users, nextStart: stop + 1});
callback(null, {rooms: rooms, 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', unreadCount);
});
};
Messaging.markRead = function(uid, toUid, callback) {
db.sortedSetRemove('uid:' + uid + ':chats:unread', toUid, callback);
};
Messaging.markUnread = function(uid, toUid, callback) {
Messaging.getTeaser = function (uid, roomId, callback) {
async.waterfall([
function (next) {
user.exists(toUid, next);
db.getSortedSetRevRange('uid:' + uid + ':chat:room:' + roomId + ':mids', 0, 0, next);
},
function (exists, next) {
if (!exists) {
return next(new Error('[[error:no-user]]'));
function (mids, next) {
if (!mids || !mids.length) {
return next(null, null);
}
db.sortedSetAdd('uid:' + uid + ':chats:unread', Date.now(), toUid, next);
Messaging.getMessageFields(mids[0], ['content', 'timestamp'], next);
},
function (teaser, next) {
if (teaser && teaser.content) {
teaser.content = S(teaser.content).stripTags().decodeHTMLEntities().s;
teaser.timestampISO = utils.toISOString(teaser.timestamp);
}
next(null, teaser);
}
], 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 || !fromUid || toUid === fromUid) {
Messaging.canMessageUser = function(uid, toUid, callback) {
if (parseInt(meta.config.disableChat) === 1 || !uid || uid === toUid) {
return callback(null, false);
}
@ -505,7 +323,7 @@ var db = require('./database'),
if (!exists) {
return callback(null, false);
}
user.getUserFields(fromUid, ['banned', 'email:confirmed'], next);
user.getUserFields(uid, ['banned', 'email:confirmed'], next);
},
function (userData, next) {
if (parseInt(userData.banned, 10) === 1) {
@ -523,24 +341,31 @@ var db = require('./database'),
return callback(null, true);
}
user.isAdministrator(fromUid, next);
user.isAdministrator(uid, next);
},
function(isAdmin, next) {
if (isAdmin) {
return callback(null, true);
}
user.isFollowing(toUid, fromUid, next);
user.isFollowing(toUid, uid, next);
}
], callback);
};
Messaging.canEdit = function(messageId, uid, callback) {
if (parseInt(meta.config.disableChat) === 1) {
Messaging.canMessageRoom = function(uid, roomId, callback) {
if (parseInt(meta.config.disableChat) === 1 || !uid) {
return callback(null, false);
}
async.waterfall([
function (next) {
Messaging.isUserInRoom(uid, roomId, next);
},
function (inRoom, next) {
if (!inRoom) {
return callback(null, false);
}
user.getUserFields(uid, ['banned', 'email:confirmed'], next);
},
function (userData, next) {
@ -552,54 +377,10 @@ var db = require('./database'),
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);
next(null, true);
}
], 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));

@ -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, '/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]]'));
}
if (data.groupName === 'administrators' || groups.isPrivilegeGroup(data.groupName)) {
return callback(new Error('[[error:not-allowed]]'));
}
groups.exists(data.groupName, function(err, exists) {
if (err || !exists) {
return callback(err || new Error('[[error:no-group]]'));

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

@ -10,7 +10,7 @@ var db = require('./database'),
schemaDate, thisSchemaDate,
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema
latestSchema = Date.UTC(2015, 10, 6);
latestSchema = Date.UTC(2015, 11, 15);
Upgrade.check = function(callback) {
db.get('schemaDate', function(err, value) {
@ -122,7 +122,7 @@ Upgrade.upgrade = function(callback) {
db.setObjectField('user:' + uid, 'picture', '', next);
},
function (next) {
db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
db.deleteObjectField('user:' + uid, 'gravatarpicture', next);
}
], next);
} else {
@ -143,8 +143,96 @@ Upgrade.upgrade = function(callback) {
winston.info('[2015/11/06] Gravatar removal skipped');
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
// IMPORTANT: REMEMBER TO UPDATE VALUE OF latestSchema IN LINE 24!!!
], function(err) {

@ -132,13 +132,27 @@ var async = require('async'),
};
User.isOnline = function(uid, callback) {
db.sortedSetScore('users:online', uid, function(err, lastonline) {
if (err) {
return callback(err);
}
var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
callback(null, isOnline);
});
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) {
if (err) {
return callback(err);
}
var isOnline = Date.now() - parseInt(lastonline, 10) < 300000;
callback(null, isOnline);
});
}
};
User.exists = function(uid, callback) {

@ -55,6 +55,9 @@ module.exports = function(User) {
function (next) {
deleteVotes(uid, next);
},
function (next) {
deleteChats(uid, next);
},
function (next) {
async.parallel([
function(next) {
@ -97,6 +100,7 @@ module.exports = function(User) {
'uid:' + uid + ':favourites', 'uid:' + uid + ':followed_tids', 'user:' + uid + ':settings',
'uid:' + uid + ':topics', 'uid:' + uid + ':posts',
'uid:' + uid + ':chats', 'uid:' + uid + ':chats:unread',
'uid:' + uid + ':chat:rooms', 'uid:' + uid + ':chat:rooms:unread',
'uid:' + uid + ':upvote', 'uid:' + uid + ':downvote',
'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) {
async.waterfall([
function (next) {

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

@ -27,14 +27,14 @@ describe('Messaging Library', function() {
describe('.canMessage()', function() {
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);
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);
done();
});
@ -42,7 +42,7 @@ describe('Messaging Library', function() {
it('should NOT allow messages to be sent to a restricted user', function(done) {
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);
done();
});
@ -50,7 +50,7 @@ describe('Messaging Library', function() {
});
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);
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) {
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);
done();
});

Loading…
Cancel
Save