mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-07-06 12:19:10 +02:00
Merge pull request #3968 from NodeBB/group-chat
Group chat closes #1798
This commit is contained in:
@@ -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,17 +251,38 @@ 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);
|
||||
});
|
||||
};
|
||||
|
||||
var titleObj = {
|
||||
active: false,
|
||||
interval: undefined,
|
||||
@@ -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);
|
||||
|
||||
window.history.pushState({
|
||||
url: url
|
||||
}, url, RELATIVE_PATH + '/' + url);
|
||||
}
|
||||
require(['autocomplete'], function(autocomplete) {
|
||||
autocomplete.user(input);
|
||||
});
|
||||
};
|
||||
|
||||
Chats.loadChatSince = function(toUid, chatContentEl, since) {
|
||||
if (!toUid) {
|
||||
Chats.switchChat = function(roomid) {
|
||||
ajaxify.go('chats/' + roomid);
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
589
src/messaging.js
589
src/messaging.js
@@ -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;
|
||||
var messages;
|
||||
|
||||
if (message.hasOwnProperty('edited')) {
|
||||
message.editedISO = new Date(parseInt(message.edited, 10)).toISOString();
|
||||
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);
|
||||
|
||||
var uids = messages.map(function(msg) {
|
||||
return msg && msg.fromuid;
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
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;
|
||||
});
|
||||
|
||||
results.users = results.users.filter(function(user) {
|
||||
return user && parseInt(user.uid, 10);
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
118
src/messaging/create.js
Normal file
118
src/messaging/create.js
Normal file
@@ -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);
|
||||
};
|
||||
};
|
||||
27
src/messaging/delete.js
Normal file
27
src/messaging/delete.js
Normal file
@@ -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);
|
||||
};
|
||||
};
|
||||
80
src/messaging/edit.js
Normal file
80
src/messaging/edit.js
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
};
|
||||
105
src/messaging/notifications.js
Normal file
105
src/messaging/notifications.js
Normal file
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
106
src/messaging/rooms.js
Normal file
106
src/messaging/rooms.js
Normal file
@@ -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);
|
||||
};
|
||||
|
||||
};
|
||||
45
src/messaging/unread.js
Normal file
45
src/messaging/unread.js
Normal file
@@ -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'),
|
||||
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');
|
||||
|
||||
async = require('async'),
|
||||
|
||||
server = require('./'),
|
||||
|
||||
SocketModules = {
|
||||
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]]'));
|
||||
}
|
||||
|
||||
Messaging.getMessageField(data.mid, 'content', callback);
|
||||
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.send = function(socket, data, 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;
|
||||
}
|
||||
|
||||
var now = Date.now(),
|
||||
touid = parseInt(data.touid, 10);
|
||||
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 || !data.roomId) {
|
||||
return callback(new Error('[[error:invalid-data]]'));
|
||||
}
|
||||
|
||||
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]]'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
SocketModules.chats.canMessage = function(socket, toUid, callback) {
|
||||
Messaging.canMessage(socket.uid, toUid, function(err, allowed) {
|
||||
Messaging.deleteMessage(data.messageId, data.roomId, callback);
|
||||
});
|
||||
};
|
||||
|
||||
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) {
|
||||
|
||||
28
src/user.js
28
src/user.js
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user