From 41b026b4d6925beea7c01a076ad109b19bb91679 Mon Sep 17 00:00:00 2001 From: Peter Jaszkowiak <p.jaszkow@gmail.com> Date: Mon, 20 Nov 2017 15:16:19 -0700 Subject: [PATCH 01/41] Test Node 9 --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9a8a0fd66a..653ddd14f1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,7 @@ addons: packages: - g++-4.8 node_js: + - "9" - "8" - "7" - "6" From 541aa7fbc60892f3e484ea86ca9ba2604bfcb5cc Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 15 Dec 2017 16:02:20 -0500 Subject: [PATCH 02/41] closes #6177 --- public/language/en-GB/user.json | 1 + src/messaging/data.js | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 466f6c27e4..d1bf34e94a 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -1,6 +1,7 @@ { "banned": "Banned", "offline": "Offline", + "deleted": "Deleted", "username": "User Name", "joindate": "Join Date", "postcount": "Post Count", diff --git a/src/messaging/data.js b/src/messaging/data.js index 88b6683a11..e18d71113f 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -51,11 +51,14 @@ module.exports = function (Messaging) { return msg && msg.fromuid; }); - user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status'], next); + user.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture', 'status', 'banned'], next); }, function (users, next) { messages.forEach(function (message, index) { message.fromUser = users[index]; + message.fromUser.banned = !!parseInt(message.fromUser.banned, 10); + message.fromUser.deleted = parseInt(message.fromuid, 10) !== message.fromUser.uid && message.fromUser.uid === 0; + var self = parseInt(message.fromuid, 10) === parseInt(uid, 10); message.self = self ? 1 : 0; message.timestampISO = utils.toISOString(message.timestamp); From 82a95a03beda6a9126d9f382e2f2394695bf15f7 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Mon, 18 Dec 2017 14:45:06 -0500 Subject: [PATCH 03/41] Chat message soft deletion -- closes #6181 Squashed commit of the following: commit f84c06bdcc45f24ef7ffde6a8f33b48d8f97fc36 Author: Julian Lam <julian@nodebb.org> Date: Mon Dec 18 14:42:47 2017 -0500 added restore handler for chat messages commit 725cd370c6ea1e8f4a28298350f3dc024d4e668e Author: Julian Lam <julian@nodebb.org> Date: Mon Dec 18 14:23:52 2017 -0500 backend logic and testing complete for deletion and restoration of chat messages commit 072da758319cc93fa4c6f8bc0d672a1b716dc06e Author: Julian Lam <julian@nodebb.org> Date: Mon Dec 18 13:52:35 2017 -0500 changing message delete logic to not remove mids, but to filter when retrieving commit 68bf373305ab82737658a7c31dc5549af4d6d69f Author: Julian Lam <julian@nodebb.org> Date: Mon Dec 18 12:37:58 2017 -0500 logic to handle deletion of a deleted chat message -- added some failing tests commit 6899d0d234fa752e227188aa69cfcabd0d0500cc Author: Julian Lam <julian@nodebb.org> Date: Mon Dec 18 11:35:36 2017 -0500 chat message deletion logic --- public/language/en-GB/error.json | 2 + public/src/client/chats.js | 27 +++++++---- public/src/client/chats/messages.js | 17 +++++-- src/messaging.js | 10 +++++ src/messaging/create.js | 1 + src/messaging/data.js | 2 + src/messaging/delete.js | 31 +++++++------ src/socket.io/modules.js | 15 +++++++ test/messaging.js | 70 ++++++++++++++++++++++++----- 9 files changed, 141 insertions(+), 34 deletions(-) diff --git a/public/language/en-GB/error.json b/public/language/en-GB/error.json index 7700006520..0cb282a8d4 100644 --- a/public/language/en-GB/error.json +++ b/public/language/en-GB/error.json @@ -140,6 +140,8 @@ "cant-delete-chat-message": "You are not allowed to delete this message", "chat-edit-duration-expired": "You are only allowed to edit chat messages for %1 second(s) after posting", "chat-delete-duration-expired": "You are only allowed to delete chat messages for %1 second(s) after posting", + "chat-deleted-already": "This chat message has already been deleted.", + "chat-restored'already": "This chat message has already been restored.", "already-voting-for-this-post": "You have already voted for this post.", "reputation-system-disabled": "Reputation system is disabled.", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index f82623fd5d..59e923fb8a 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -71,7 +71,7 @@ define('forum/chats', [ }); }); - Chats.addEditDeleteHandler(components.get('chat/messages'), ajaxify.data.roomId); + Chats.addActionHandlers(components.get('chat/messages'), ajaxify.data.roomId); Chats.addRenameHandler(ajaxify.data.roomId, $('[component="chat/room/name"]')); Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); @@ -123,14 +123,25 @@ define('forum/chats', [ }); }; - Chats.addEditDeleteHandler = function (element, roomId) { - element.on('click', '[data-action="edit"]', function () { + Chats.addActionHandlers = function (element, roomId) { + element.on('click', '[data-action]', function () { var messageId = $(this).parents('[data-mid]').attr('data-mid'); - var inputEl = $('[data-roomid="' + roomId + '"] [component="chat/input"]'); - messages.prepEdit(inputEl, messageId, roomId); - }).on('click', '[data-action="delete"]', function () { - var messageId = $(this).parents('[data-mid]').attr('data-mid'); - messages.delete(messageId, roomId); + var action = this.getAttribute('data-action'); + + switch (action) { + case 'edit': + var inputEl = $('[data-roomid="' + roomId + '"] [component="chat/input"]'); + messages.prepEdit(inputEl, messageId, roomId); + break; + + case 'delete': + messages.delete(messageId, roomId); + break; + + case 'restore': + messages.restore(messageId, roomId); + break; + } }); }; diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index fc4a327021..cfbcacf8f8 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -146,13 +146,24 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres return app.alertError(err.message); } - components.get('chat/message', messageId).slideUp('slow', function () { - $(this).remove(); - }); + components.get('chat/message', messageId).toggleClass('deleted', true); }); }); }); }; + messages.restore = function (messageId, roomId) { + socket.emit('modules.chats.restore', { + messageId: messageId, + roomId: roomId, + }, function (err) { + if (err) { + return app.alertError(err.message); + } + + components.get('chat/message', messageId).toggleClass('deleted', false); + }); + }; + return messages; }); diff --git a/src/messaging.js b/src/messaging.js index 9ceda65b7d..ab22b3c773 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -56,6 +56,16 @@ Messaging.getMessages = function (params, callback) { messageData.forEach(function (messageData) { messageData.index = indices[messageData.messageId.toString()]; }); + + // Filter out deleted messages unless you're the sender of said message + messageData = messageData.filter(function (messageData) { + if (messageData.deleted && parseInt(messageData.fromuid, 10) !== parseInt(params.uid, 10)) { + return false; + } + + return true; + }); + next(null, messageData); }, ], callback); diff --git a/src/messaging/create.js b/src/messaging/create.js index f4fe421eac..6f355a9ff8 100644 --- a/src/messaging/create.js +++ b/src/messaging/create.js @@ -58,6 +58,7 @@ module.exports = function (Messaging) { timestamp: timestamp, fromuid: fromuid, roomId: roomId, + deleted: 0, }; plugins.fireHook('filter:messaging.save', message, next); diff --git a/src/messaging/data.js b/src/messaging/data.js index e18d71113f..bae9c6deac 100644 --- a/src/messaging/data.js +++ b/src/messaging/data.js @@ -67,6 +67,8 @@ module.exports = function (Messaging) { if (message.hasOwnProperty('edited')) { message.editedISO = new Date(parseInt(message.edited, 10)).toISOString(); } + + message.deleted = !!parseInt(message.deleted, 10); }); async.map(messages, function (message, next) { diff --git a/src/messaging/delete.js b/src/messaging/delete.js index e3058c0ba9..55907491f2 100644 --- a/src/messaging/delete.js +++ b/src/messaging/delete.js @@ -1,25 +1,30 @@ '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(); + async.apply(Messaging.getMessageField, mid, 'deleted'), + function (deleted, next) { + if (parseInt(deleted, 10)) { + return next(new Error('[[error:chat-deleted-already]]')); } - var keys = uids.map(function (uid) { - return 'uid:' + uid + ':chat:room:' + roomId + ':mids'; - }); - db.sortedSetsRemove(keys, mid, next); + + Messaging.setMessageField(mid, 'deleted', 1, next); }, - function (next) { - db.delete('message:' + mid, next); + ], callback); + }; + + Messaging.restoreMessage = function (mid, roomId, callback) { + async.waterfall([ + async.apply(Messaging.getMessageField, mid, 'deleted'), + function (deleted, next) { + if (!parseInt(deleted, 10)) { + return next(new Error('[[error:chat-restored-already]]')); + } + + Messaging.setMessageField(mid, 'deleted', 0, next); }, ], callback); }; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index d58fb7fa59..7ff5accf40 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -267,6 +267,21 @@ SocketModules.chats.delete = function (socket, data, callback) { ], callback); }; +SocketModules.chats.restore = function (socket, data, callback) { + if (!data || !data.roomId || !data.messageId) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.waterfall([ + function (next) { + Messaging.canDelete(data.messageId, socket.uid, next); + }, + function (next) { + Messaging.restoreMessage(data.messageId, data.roomId, next); + }, + ], callback); +}; + SocketModules.chats.canMessage = function (socket, roomId, callback) { Messaging.canMessageRoom(socket.uid, roomId, callback); }; diff --git a/test/messaging.js b/test/messaging.js index 0253fbf6b0..024c204ff3 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -14,8 +14,8 @@ var helpers = require('./helpers'); var socketModules = require('../src/socket.io/modules'); describe('Messaging Library', function () { - var fooUid; - var bazUid; + var fooUid; // the admin + var bazUid; // the user with chat restriction enabled var herpUid; var roomId; @@ -552,21 +552,71 @@ describe('Messaging Library', function () { }); }); + it('should mark the message as deleted', function (done) { + socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) { + assert.ifError(err); + db.getObjectField('message:' + mid, 'deleted', function (err, value) { + assert.ifError(err); + assert.strictEqual(1, parseInt(value, 10)); + done(); + }); + }); + }); + + it('should show deleted message to original users', function (done) { + socketModules.chats.getMessages({ uid: fooUid }, { uid: fooUid, roomId: roomId, start: 0 }, function (err, messages) { + assert.ifError(err); + + // Reduce messages to their mids + var mids = messages.reduce(function (mids, cur) { + mids.push(cur.messageId); + return mids; + }, []); + + assert(mids.includes(mid)); + done(); + }); + }); + + it('should not show deleted message to other users', function (done) { + socketModules.chats.getMessages({ uid: herpUid }, { uid: herpUid, roomId: roomId, start: 0 }, function (err, messages) { + assert.ifError(err); - it('should delete message', function (done) { + // Reduce messages to their mids + var mids = messages.reduce(function (mids, cur) { + mids.push(cur.messageId); + return mids; + }, []); + + assert(!mids.includes(mid)); + done(); + }); + }); + + it('should error out if a message is deleted again', function (done) { socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) { + assert.strictEqual('[[error:chat-deleted-already]]', err.message); + done(); + }); + }); + + it('should restore the message', function (done) { + socketModules.chats.restore({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) { assert.ifError(err); - db.exists('message:' + mid, function (err, exists) { + db.getObjectField('message:' + mid, 'deleted', function (err, value) { assert.ifError(err); - assert(!exists); - db.isSortedSetMember('uid:' + fooUid + ':chat:room:' + roomId + ':mids', mid, function (err, isMember) { - assert.ifError(err); - assert(!isMember); - done(); - }); + assert.strictEqual(0, parseInt(value, 10)); + done(); }); }); }); + + it('should error out if a message is restored again', function (done) { + socketModules.chats.restore({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) { + assert.strictEqual('[[error:chat-restored-already]]', err.message); + done(); + }); + }); }); describe('controller', function () { From 290aa312b4756af6b4c8b6aa654e87f96bb44b47 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Mon, 18 Dec 2017 15:01:05 -0500 Subject: [PATCH 04/41] fixed call to missing method, #6181 --- public/src/modules/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 9a2ca3e7c4..c0249724d7 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -249,7 +249,7 @@ define('chat', [ } }); - Chats.addEditDeleteHandler(chatModal.find('[component="chat/messages"]'), data.roomId); + Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), data.roomId); chatModal.find('[component="chat/controlsToggle"]').on('click', function () { var messagesEl = chatModal.find('[component="chat/messages"]'); From d967201b2aa722b53ee247b14093dfbbcbbc1ad6 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Tue, 19 Dec 2017 16:27:41 -0500 Subject: [PATCH 05/41] drastically simplified main window resizing logic, some language changes --- public/language/en-GB/modules.json | 2 +- public/src/client/chats.js | 18 +++--------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 8f8e4ad1ed..3d46004ad1 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -1,5 +1,5 @@ { - "chat.chatting_with": "Chat with <span id=\"chat-with-name\"></span>", + "chat.chatting_with": "Chat with", "chat.placeholder": "Type chat message here, press enter to send", "chat.send": "Send", "chat.no_active": "You have no active chats.", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 59e923fb8a..44e2a2afd1 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -407,22 +407,10 @@ define('forum/chats', [ }; Chats.resizeMainWindow = function () { - var messagesList = $('.expanded-chat .chat-content'); - var searchHeight = $('.chat-search').height(); - var searchListHeight = $('[component="chat/search/list"]').outerHeight(true) - $('[component="chat/search/list"]').height(); - var fromTop = components.get('chat/recent').offset().top; - - if (messagesList.length) { - var margin = $('.expanded-chat ul').outerHeight(true) - $('.expanded-chat ul').height(); - var inputHeight = $('.chat-input').outerHeight(true); - - messagesList.height($(window).height() - (fromTop + inputHeight + (margin * 4))); - components.get('chat/recent').height($('.expanded-chat').height() - (searchHeight + searchListHeight)); - $('[component="chat/search/list"]').css('max-height', (components.get('chat/recent').height() / 2) + 'px'); - } else { - components.get('chat/recent').height($(window).height() - (fromTop + searchHeight + searchListHeight)); - } + var viewportHeight = $(window).height(); + var fromTop = components.get('chat/nav-wrapper').offset().top; + $('.chats-full').height(viewportHeight - fromTop); Chats.setActive(); }; From 3d74fc8220f00129bef78b808d57e1ae0c02a0cd Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 20 Dec 2017 12:58:59 -0500 Subject: [PATCH 06/41] some changes to fix class names used, character countdown support, etc. --- public/src/client/chats.js | 9 +++++---- public/src/client/chats/search.js | 2 ++ src/controllers/api.js | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 44e2a2afd1..7b9131cf05 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -120,6 +120,7 @@ define('forum/chats', [ var element = parent.find('[component="chat/input"]'); element.on('keyup', function () { parent.find('[component="chat/message/length"]').text(element.val().length); + parent.find('[component="chat/message/remaining"]').text(config.maximumChatMessageLength - element.val().length); }); }; @@ -147,7 +148,7 @@ define('forum/chats', [ Chats.addHotkeys = function () { mousetrap.bind('ctrl+up', function () { - var activeContact = $('.chats-list .bg-primary'); + var activeContact = $('.chats-list .bg-info'); var prev = activeContact.prev(); if (prev.length) { @@ -155,7 +156,7 @@ define('forum/chats', [ } }); mousetrap.bind('ctrl+down', function () { - var activeContact = $('.chats-list .bg-primary'); + var activeContact = $('.chats-list .bg-info'); var next = activeContact.next(); if (next.length) { @@ -419,8 +420,8 @@ define('forum/chats', [ socket.emit('modules.chats.markRead', ajaxify.data.roomId); $('.expanded-chat input').focus(); } - $('.chats-list li').removeClass('bg-primary'); - $('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-primary'); + $('.chats-list li').removeClass('bg-info'); + $('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-info'); }; diff --git a/public/src/client/chats/search.js b/public/src/client/chats/search.js index 1b76d665d8..e40551ed05 100644 --- a/public/src/client/chats/search.js +++ b/public/src/client/chats/search.js @@ -52,6 +52,8 @@ define('forum/chats/search', ['components'], function (components) { var chatEl = displayUser(chatsListEl, userObj); onUserClick(chatEl, userObj); }); + + chatsListEl.parent().toggleClass('open', true); } function displayUser(chatsListEl, userObj) { diff --git a/src/controllers/api.js b/src/controllers/api.js index 4f9430826a..a9e3dacf79 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -39,6 +39,7 @@ apiController.loadConfig = function (req, callback) { config.usePagination = parseInt(meta.config.usePagination, 10) === 1; config.disableChat = parseInt(meta.config.disableChat, 10) === 1; config.disableChatMessageEditing = parseInt(meta.config.disableChatMessageEditing, 10) === 1; + config.maximumChatMessageLength = parseInt(meta.config.maximumChatMessageLength, 10) || 1000; config.socketioTransports = nconf.get('socket.io:transports') || ['polling', 'websocket']; config.websocketAddress = nconf.get('socket.io:address') || ''; config.maxReconnectionAttempts = meta.config.maxReconnectionAttempts || 5; From 5c3c6081097f4c3e52fc6b61b23dbaa598d4bad5 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 20 Dec 2017 13:47:45 -0500 Subject: [PATCH 07/41] fix chat resizing of modal to work with flexbox implementation --- public/src/modules/chat.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index c0249724d7..094cfeb2b5 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -190,7 +190,7 @@ define('chat', [ return; } - chatModal.find('.chat-content').css('height', module.calculateChatListHeight(chatModal)); + chatModal.find('.modal-body').css('height', module.calculateChatListHeight(chatModal)); }); chatModal.draggable({ @@ -346,12 +346,8 @@ define('chat', [ }; module.calculateChatListHeight = function (modalEl) { - var totalHeight = modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight(); - var padding = parseInt(modalEl.find('.modal-body').css('padding-top'), 10) + parseInt(modalEl.find('.modal-body').css('padding-bottom'), 10); - var contentMargin = parseInt(modalEl.find('.chat-content').css('margin-top'), 10) + parseInt(modalEl.find('.chat-content').css('margin-bottom'), 10); - var inputGroupHeight = modalEl.find('.input-group').outerHeight(); - - return totalHeight - padding - contentMargin - inputGroupHeight; + // Formula: modal height minus header height. Simple(tm). + return modalEl.find('.modal-content').outerHeight() - modalEl.find('.modal-header').outerHeight(); }; module.minimize = function (uuid) { From 8cb914c15e220e51057ec3ca5b73faceea90f279 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 20 Dec 2017 15:34:26 -0500 Subject: [PATCH 08/41] more fixes to better handle transitioning from chat to chat and closing chats, etc. also modals no longer used on mobile view at all --- public/src/client/chats.js | 6 ++++-- public/src/client/chats/recent.js | 7 +------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 7b9131cf05..d4f9afcfd1 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -29,13 +29,13 @@ define('forum/chats', [ Chats.addEventListeners(); Chats.createTagsInput($('[component="chat/messages"] .users-tag-input'), ajaxify.data); Chats.createAutoComplete($('[component="chat/input"]')); + Chats.resizeMainWindow(); components.get('expanded-chat/controlsToggle').on('click', function () { components.get('expanded-chat/controls').toggleClass('hide'); }); if (env === 'md' || env === 'lg') { - Chats.resizeMainWindow(); Chats.addHotkeys(); } @@ -409,7 +409,7 @@ define('forum/chats', [ Chats.resizeMainWindow = function () { var viewportHeight = $(window).height(); - var fromTop = components.get('chat/nav-wrapper').offset().top; + var fromTop = components.get('chat/main-wrapper').offset().top || components.get('chat/nav-wrapper').offset().top; $('.chats-full').height(viewportHeight - fromTop); Chats.setActive(); @@ -422,6 +422,8 @@ define('forum/chats', [ } $('.chats-list li').removeClass('bg-info'); $('.chats-list li[data-roomid="' + ajaxify.data.roomId + '"]').addClass('bg-info'); + + components.get('chat/nav-wrapper').attr('data-loaded', ajaxify.data.roomId ? '1' : '0'); }; diff --git a/public/src/client/chats/recent.js b/public/src/client/chats/recent.js index 51650c8795..7d6f16211a 100644 --- a/public/src/client/chats/recent.js +++ b/public/src/client/chats/recent.js @@ -12,12 +12,7 @@ define('forum/chats/recent', function () { }); $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function () { - var env = utils.findBootstrapEnvironment(); - if (env === 'xs' || env === 'sm') { - app.openChat($(this).attr('data-roomid')); - } else { - Chats.switchChat($(this).attr('data-roomid')); - } + Chats.switchChat($(this).attr('data-roomid')); }); $('[component="chat/recent"]').on('scroll', function () { From e091568d1788d743f4b6908e78dcf95a909c78a6 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Thu, 21 Dec 2017 11:19:16 -0500 Subject: [PATCH 09/41] closes #6191 --- public/src/client/chats.js | 1 + 1 file changed, 1 insertion(+) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index d4f9afcfd1..d6594fc1f8 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -332,6 +332,7 @@ define('forum/chats', [ response.json().then(function (payload) { app.parseAndTranslate('partials/chats/message-window', payload, function (html) { components.get('chat/main-wrapper').html(html); + html.find('.timeago').timeago(); Chats.resizeMainWindow(); ajaxify.data = payload; Chats.setActive(); From eea124b312ff7001d0bf41a220f589c91cc5e6dd Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Thu, 21 Dec 2017 16:51:16 -0500 Subject: [PATCH 10/41] room rename logic --- public/language/en-GB/modules.json | 5 ++++ public/src/client/chats.js | 46 +++++++++++++++++------------- public/src/modules/chat.js | 9 +----- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 3d46004ad1..529e015c0f 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -12,6 +12,7 @@ "chat.recent-chats": "Recent Chats", "chat.contacts": "Contacts", "chat.message-history": "Message History", + "chat.options": "Chat options", "chat.pop-out": "Pop out chat", "chat.minimize": "Minimize", "chat.maximize": "Maximize", @@ -21,6 +22,10 @@ "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "chat.add-users-to-room": "Add users to room", "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", + "chat.rename-room": "Rename room", + "chat.rename-placeholder": "Enter your room name here", + "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.in-room": "In this room", "composer.compose": "Compose", "composer.show_preview": "Show Preview", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index d6594fc1f8..2816f28c0f 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -31,10 +31,6 @@ define('forum/chats', [ Chats.createAutoComplete($('[component="chat/input"]')); Chats.resizeMainWindow(); - components.get('expanded-chat/controlsToggle').on('click', function () { - components.get('expanded-chat/controls').toggleClass('hide'); - }); - if (env === 'md' || env === 'lg') { Chats.addHotkeys(); } @@ -72,8 +68,7 @@ define('forum/chats', [ }); Chats.addActionHandlers(components.get('chat/messages'), ajaxify.data.roomId); - - Chats.addRenameHandler(ajaxify.data.roomId, $('[component="chat/room/name"]')); + Chats.addRenameHandler(ajaxify.data.roomId, components.get('expanded-chat/controls').find('[data-action="rename"]')); Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]')); }; @@ -175,28 +170,39 @@ define('forum/chats', [ }); }; - Chats.addRenameHandler = function (roomId, inputEl) { - var oldName = inputEl.val(); - inputEl.on('blur keypress', function (ev) { - if (ev.type === 'keypress' && ev.keyCode !== 13) { - return; - } - var newName = inputEl.val(); + Chats.addRenameHandler = function (roomId, buttonEl, roomName) { + var modal; + + buttonEl.on('click', function () { + Benchpress.parse('partials/modals/rename_room', { + name: roomName || ajaxify.data.roomName, + }, function (html) { + translator.translate(html, function (html) { + modal = bootbox.dialog({ + title: '[[modules:chat.rename-room]]', + message: html, + buttons: { + save: { + label: '[[global:save]]', + className: 'btn-primary', + callback: submit, + }, + }, + }); + }); + }); + }); - if (oldName === newName) { - return; - } + function submit() { socket.emit('modules.chats.renameRoom', { roomId: roomId, - newName: newName, + newName: modal.find('#roomName').val(), }, function (err) { if (err) { return app.alertError(err.message); } - oldName = newName; - inputEl.blur(); }); - }); + } }; Chats.addSendHandlers = function (roomId, inputEl, sendEl) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 094cfeb2b5..d215f9281c 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -251,14 +251,7 @@ define('chat', [ Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), data.roomId); - chatModal.find('[component="chat/controlsToggle"]').on('click', function () { - var messagesEl = chatModal.find('[component="chat/messages"]'); - - chatModal.find('[component="chat/controls"]').toggle(); - messagesEl.css('height', module.calculateChatListHeight(chatModal)); - }); - - Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[component="chat/room/name"]')); + Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="rename"]'), chatModal.attr('data-name')); Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn')); From 82bfd6edfdeb43673549cf1f4fb72a208e694c8d Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Thu, 4 Jan 2018 16:04:02 -0500 Subject: [PATCH 11/41] added a new component shortcut and updated title setting logic --- public/src/client/chats.js | 5 ++++- public/src/modules/components.js | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 2816f28c0f..39167db694 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -410,7 +410,10 @@ define('forum/chats', [ messages.onChatMessageEdit(); socket.on('event:chats.roomRename', function (data) { - $('[component="chat/room/name"]').val($('<div/>').html(data.newName).text()); + var roomEl = components.get('chat/recent/room', data.roomId); + var titleEl = roomEl.find('[component="chat/title"]'); + + titleEl.text(data.newName); }); }; diff --git a/public/src/modules/components.js b/public/src/modules/components.js index 29d9e883a1..08454f21bc 100644 --- a/public/src/modules/components.js +++ b/public/src/modules/components.js @@ -50,9 +50,14 @@ define('components', function () { 'chat/message': function (messageId) { return $('[component="chat/message"][data-mid="' + messageId + '"]'); }, + 'chat/message/body': function (messageId) { return $('[component="chat/message"][data-mid="' + messageId + '"] [component="chat/message/body"]'); }, + + 'chat/recent/room': function (roomid) { + return $('[component="chat/recent/room"][data-roomid="' + roomid + '"]'); + }, }; components.get = function () { From cd4d0049e6fe4534909083ea9fd6a7d189188f7f Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 5 Jan 2018 14:40:38 -0500 Subject: [PATCH 12/41] fixed broken UI in chat modal, send btn --- public/src/client/chats.js | 2 +- public/src/modules/chat.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 39167db694..3988843c08 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -35,7 +35,7 @@ define('forum/chats', [ Chats.addHotkeys(); } - messages.scrollToBottom($('.expanded-chat ul')); + messages.scrollToBottom($('.expanded-chat ul.chat-content')); Chats.initialised = true; diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index d215f9281c..160abc074e 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -253,7 +253,7 @@ define('chat', [ Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="rename"]'), chatModal.attr('data-name')); - Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('#chat-message-input'), chatModal.find('#chat-message-send-btn')); + Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('.chat-input'), chatModal.find('[data-action="send"]')); Chats.createTagsInput(chatModal.find('.users-tag-input'), data); Chats.createAutoComplete(chatModal.find('[component="chat/input"]')); From b2b57d49bf5b0d0d2906c3f0054499d5818872bf Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Sat, 6 Jan 2018 12:03:52 -0500 Subject: [PATCH 13/41] WIP add user modal --- public/language/en-GB/global.json | 4 ++- public/language/en-GB/modules.json | 2 ++ public/src/client/chats.js | 46 ++++++++++++++++++++++++++---- src/socket.io/modules.js | 8 ++++++ 4 files changed, 54 insertions(+), 6 deletions(-) diff --git a/public/language/en-GB/global.json b/public/language/en-GB/global.json index b61d8f27b9..1195d792a0 100644 --- a/public/language/en-GB/global.json +++ b/public/language/en-GB/global.json @@ -134,5 +134,7 @@ "edited": "Edited", "disabled": "Disabled", - "select": "Select" + "select": "Select", + + "user-search-prompt": "Type something here to find users..." } diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 529e015c0f..93433d1008 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -21,6 +21,8 @@ "chat.three_months": "3 Months", "chat.delete_message_confirm": "Are you sure you wish to delete this message?", "chat.add-users-to-room": "Add users to room", + "chat.retrieving-users": "Retrieving users...", + "chat.manage-room": "Manage Chat Room", "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", "chat.rename-room": "Rename room", "chat.rename-placeholder": "Enter your room name here", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 3988843c08..94ecdbe2e2 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -48,7 +48,15 @@ define('forum/chats', [ Chats.addEventListeners = function () { Chats.addSendHandlers(ajaxify.data.roomId, $('.chat-input'), $('.expanded-chat button[data-action="send"]')); + Chats.addPopoutHandler(); + Chats.addActionHandlers(components.get('chat/messages'), ajaxify.data.roomId); + Chats.addMemberHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="members"]')); + Chats.addRenameHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="rename"]')); + Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); + Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]')); + }; + Chats.addPopoutHandler = function () { $('[data-action="pop-out"]').on('click', function () { var text = components.get('chat/input').val(); var roomId = ajaxify.data.roomId; @@ -66,11 +74,6 @@ define('forum/chats', [ components.get('chat/input').val(text); }); }); - - Chats.addActionHandlers(components.get('chat/messages'), ajaxify.data.roomId); - Chats.addRenameHandler(ajaxify.data.roomId, components.get('expanded-chat/controls').find('[data-action="rename"]')); - Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); - Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]')); }; Chats.addScrollHandler = function (roomId, uid, el) { @@ -170,6 +173,39 @@ define('forum/chats', [ }); }; + Chats.addMemberHandler = function (roomId, buttonEl) { + var modal; + + buttonEl.on('click', function () { + Benchpress.parse('partials/modals/manage_room', {}, function (html) { + translator.translate(html, function (html) { + modal = bootbox.dialog({ + title: '[[modules:chat.manage-room]]', + message: html, + }); + + modal.attr('component', 'chat/manage-modal'); + + socket.emit('modules.chats.getUsersInRoom', { roomId: roomId }, function (err, users) { + var listEl = modal.find('.list-group'); + + if (err) { + return translator.translate('[[error:invalid-data]]', function (translated) { + listEl.find('li').text(translated); + }); + } + + Benchpress.parse('partials/modals/manage_room_users', { + users: users, + }, function (html) { + listEl.html(html); + }); + }); + }); + }); + }); + }; + Chats.addRenameHandler = function (roomId, buttonEl, roomName) { var modal; diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index 7ff5accf40..cb8c43cbfa 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -167,6 +167,14 @@ SocketModules.chats.loadRoom = function (socket, data, callback) { ], callback); }; +SocketModules.chats.getUsersInRoom = function (socket, data, callback) { + if (!data || !data.roomId) { + return callback(new Error('[[error:invalid-data]]')); + } + + Messaging.getUsersInRoom(data.roomId, 0, -1, callback); +}; + SocketModules.chats.addUserToRoom = function (socket, data, callback) { if (!data || !data.roomId || !data.username) { return callback(new Error('[[error:invalid-data]]')); From 19e53ab61b8da836b901a0a337b26d2eb1ebdd8d Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Tue, 9 Jan 2018 16:30:16 -0500 Subject: [PATCH 14/41] Lots o' chat changes - Added chat room management (instead of tagsinput) - Chat leave button in chat options dropdown - Refreshing participants when user is added - Updated autocomplete module so that it can be shown above a modal (z-index 100005 > 20000) - Using cant-chat-with-yourself source string, which wasn't defined before, but was still used --- public/language/en-GB/modules.json | 4 ++ public/src/client/chats.js | 77 +++++++++++++++++++++++++----- public/src/modules/autocomplete.js | 2 +- public/src/modules/chat.js | 3 +- src/socket.io/modules.js | 2 +- 5 files changed, 73 insertions(+), 15 deletions(-) diff --git a/public/language/en-GB/modules.json b/public/language/en-GB/modules.json index 93433d1008..5104549457 100644 --- a/public/language/en-GB/modules.json +++ b/public/language/en-GB/modules.json @@ -23,10 +23,14 @@ "chat.add-users-to-room": "Add users to room", "chat.retrieving-users": "Retrieving users...", "chat.manage-room": "Manage Chat Room", + "chat.add-user-help": "Search for users here. When selected, the user will be added to the chat. The new user will not be able to see chat messages written before they were added to the conversation.", "chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?", "chat.rename-room": "Rename room", "chat.rename-placeholder": "Enter your room name here", "chat.rename-help": "The room name set here will be viewable by all participants in the room.", + "chat.leave": "Leave Chat", + "chat.leave-prompt": "Are you sure you wish to leave this chat?", + "chat.leave-help": "Leaving this chat will remove you from future correspondence in this chat. If you are re-added in the future, you will not see any chat history from prior to your re-joining.", "chat.in-room": "In this room", "composer.compose": "Compose", diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 8b1f472737..911c12f7cb 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -52,6 +52,7 @@ define('forum/chats', [ Chats.addActionHandlers(components.get('chat/messages'), ajaxify.data.roomId); Chats.addMemberHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="members"]')); Chats.addRenameHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="rename"]')); + Chats.addLeaveHandler(ajaxify.data.roomId, components.get('chat/controls').find('[data-action="leave"]')); Chats.addScrollHandler(ajaxify.data.roomId, ajaxify.data.uid, $('.chat-content')); Chats.addCharactersLeftHandler($('[component="chat/main-wrapper"]')); }; @@ -186,19 +187,26 @@ define('forum/chats', [ modal.attr('component', 'chat/manage-modal'); - socket.emit('modules.chats.getUsersInRoom', { roomId: roomId }, function (err, users) { - var listEl = modal.find('.list-group'); + Chats.refreshParticipantsList(roomId, modal); + + var searchInput = modal.find('input'); + var errorEl = modal.find('.text-danger'); + require(['autocomplete', 'translator'], function (autocomplete, translator) { + autocomplete.user(searchInput, function (event, selected) { + errorEl.text(''); + socket.emit('modules.chats.addUserToRoom', { + roomId: roomId, + username: selected.item.user.name, + }, function (err) { + if (err) { + translator.translate(err.message, function (translated) { + errorEl.text(translated); + }); + } - if (err) { - return translator.translate('[[error:invalid-data]]', function (translated) { - listEl.find('li').text(translated); + Chats.refreshParticipantsList(roomId, modal); + searchInput.val(''); }); - } - - Benchpress.parse('partials/modals/manage_room_users', { - users: users, - }, function (html) { - listEl.html(html); }); }); }); @@ -206,6 +214,53 @@ define('forum/chats', [ }); }; + Chats.addLeaveHandler = function (roomId, buttonEl) { + buttonEl.on('click', function () { + bootbox.confirm({ + size: 'small', + title: '[[modules:chat.leave]]', + message: '<p>[[modules:chat.leave-prompt]]</p><p class="help-block">[[modules:chat.leave-help]]</p>', + callback: function (ok) { + if (ok) { + socket.emit('modules.chats.leave', roomId, function (err) { + if (err) { + app.alertError(err.message); + } + + // Return user to chats page. If modal, close modal. + var modal = buttonEl.parents('.chat-modal'); + if (modal.length) { + require(['chat'], function (chatLib) { + chatLib.close(modal); + }); + } else { + ajaxify.go('chats'); + } + }); + } + }, + }); + }); + }; + + Chats.refreshParticipantsList = function (roomId, modal) { + socket.emit('modules.chats.getUsersInRoom', { roomId: roomId }, function (err, users) { + var listEl = modal.find('.list-group'); + + if (err) { + return translator.translate('[[error:invalid-data]]', function (translated) { + listEl.find('li').text(translated); + }); + } + + Benchpress.parse('partials/modals/manage_room_users', { + users: users, + }, function (html) { + listEl.html(html); + }); + }); + }; + Chats.addRenameHandler = function (roomId, buttonEl, roomName) { var modal; diff --git a/public/src/modules/autocomplete.js b/public/src/modules/autocomplete.js index 6c32cb36f2..738252b038 100644 --- a/public/src/modules/autocomplete.js +++ b/public/src/modules/autocomplete.js @@ -10,7 +10,7 @@ define('autocomplete', function () { input.autocomplete({ delay: 200, open: function () { - $(this).autocomplete('widget').css('z-index', 20000); + $(this).autocomplete('widget').css('z-index', 100005); }, select: onselect, source: function (request, response) { diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 160abc074e..d493e3aa2d 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -250,9 +250,8 @@ define('chat', [ }); Chats.addActionHandlers(chatModal.find('[component="chat/messages"]'), data.roomId); - Chats.addRenameHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="rename"]'), chatModal.attr('data-name')); - + Chats.addLeaveHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="leave"]')); Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('.chat-input'), chatModal.find('[data-action="send"]')); Chats.createTagsInput(chatModal.find('.users-tag-input'), data); diff --git a/src/socket.io/modules.js b/src/socket.io/modules.js index c04b9bae0b..e3e24654a4 100644 --- a/src/socket.io/modules.js +++ b/src/socket.io/modules.js @@ -228,7 +228,7 @@ SocketModules.chats.addUserToRoom = function (socket, data, callback) { return next(new Error('[[error:no-user]]')); } if (socket.uid === parseInt(uid, 10)) { - return next(new Error('[[error:cant-add-self-to-chat-room]]')); + return next(new Error('[[error:cant-chat-with-yourself]]')); } async.parallel({ settings: async.apply(user.getSettings, uid), From 8f5dccdfb5214895b131e044e414357c991a1865 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 10 Jan 2018 10:53:09 -0500 Subject: [PATCH 15/41] Chat fixes - Fixed issue where clicking into a chat didn't update unread state - Fixed issue where receiving new messages in chat page didn't mark chat unread - Fixed issue where chat title sets were broken in modal and page - Removed chat leave toggle in chat list (is now in chat dropdown) --- public/src/client/chats.js | 4 +++- public/src/client/chats/recent.js | 5 ----- public/src/modules/chat.js | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 911c12f7cb..cee14a516e 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -471,7 +471,7 @@ define('forum/chats', [ data.message.self = data.self; messages.appendChatMessage($('.expanded-chat .chat-content'), data.message); - } else if (ajaxify.currentPage.startsWith('chats')) { + } else if (ajaxify.data.template.chats) { var roomEl = $('[data-roomid=' + data.roomId + ']'); if (roomEl.length > 0) { @@ -503,6 +503,7 @@ define('forum/chats', [ socket.on('event:chats.roomRename', function (data) { var roomEl = components.get('chat/recent/room', data.roomId); var titleEl = roomEl.find('[component="chat/title"]'); + ajaxify.data.roomName = data.newName; titleEl.text(data.newName); }); @@ -519,6 +520,7 @@ define('forum/chats', [ Chats.setActive = function () { if (ajaxify.data.roomId) { socket.emit('modules.chats.markRead', ajaxify.data.roomId); + $('[data-roomid="' + ajaxify.data.roomId + '"]').toggleClass('unread', false); $('.expanded-chat input').focus(); } $('.chats-list li').removeClass('bg-info'); diff --git a/public/src/client/chats/recent.js b/public/src/client/chats/recent.js index 7d6f16211a..32156cfc8f 100644 --- a/public/src/client/chats/recent.js +++ b/public/src/client/chats/recent.js @@ -6,11 +6,6 @@ define('forum/chats/recent', function () { recent.init = function () { require(['forum/chats'], function (Chats) { - $('[component="chat/recent"]').on('click', '[component="chat/leave"]', function () { - Chats.leave($(this).parents('[data-roomid]')); - return false; - }); - $('[component="chat/recent"]').on('click', '[component="chat/recent/room"]', function () { Chats.switchChat($(this).attr('data-roomid')); }); diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index d493e3aa2d..fadd1d6f82 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -106,7 +106,7 @@ define('chat', [ socket.on('event:chats.roomRename', function (data) { var newTitle = $('<div/>').html(data.newName).text(); var modal = module.getModal(data.roomId); - modal.find('[component="chat/room/name"]').val(newTitle); + modal.find('[component="chat/room/name"]').text(newTitle); taskbar.updateTitle('chat', modal.attr('UUID'), newTitle); }); From 89383c261f87cad2f226477f20e28282be4f4fda Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 10 Jan 2018 15:12:25 -0500 Subject: [PATCH 16/41] fixed scrollToBottom detection in chats page --- public/src/client/chats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index cee14a516e..402124da16 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -434,7 +434,7 @@ define('forum/chats', [ ajaxify.data = payload; Chats.setActive(); Chats.addEventListeners(); - messages.scrollToBottom($('.expanded-chat ul')); + messages.scrollToBottom($('.expanded-chat ul.chat-content')); if (history.pushState) { history.pushState({ url: 'user/' + payload.userslug + '/chats/' + payload.roomId, From 2ad2ef96ba6345d660041d5e55736fb4cd51260b Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Thu, 11 Jan 2018 15:26:46 -0500 Subject: [PATCH 17/41] no taskbar invocation on chats page, fixes #6227 --- public/src/modules/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index fadd1d6f82..9c25ad07a0 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -61,7 +61,7 @@ define('chat', [ if (modal.is(':visible')) { taskbar.updateActive(modal.attr('UUID')); ChatsMessages.scrollToBottom(modal.find('.chat-content')); - } else { + } else if (!ajaxify.data.template.chats) { module.toggleNew(modal.attr('UUID'), true, true); } @@ -75,7 +75,7 @@ define('chat', [ roomId: data.roomId, }); } - } else { + } else if (!ajaxify.data.template.chats) { socket.emit('modules.chats.loadRoom', { roomId: data.roomId, }, function (err, roomData) { From c47987b3054d42ffd45fbe11d5049cccae839898 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 12 Jan 2018 13:19:14 -0500 Subject: [PATCH 18/41] fixed test for error when adding self to chat --- test/messaging.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/messaging.js b/test/messaging.js index 11b1295d38..d343ac825a 100644 --- a/test/messaging.js +++ b/test/messaging.js @@ -156,7 +156,7 @@ describe('Messaging Library', function () { it('should fail to add self to room', function (done) { socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'foo' }, function (err) { - assert.equal(err.message, '[[error:cant-add-self-to-chat-room]]'); + assert.equal(err.message, '[[error:cant-chat-with-yourself]]'); done(); }); }); From 5b1ed216348f6b9474bddf91208556bbcd34c8a8 Mon Sep 17 00:00:00 2001 From: Ben Lubar <ben.lubar+github@gmail.com> Date: Fri, 12 Jan 2018 12:32:07 -0600 Subject: [PATCH 19/41] [database/mongo] Improve speed of sortedSetRank (#6229) * [database/mongo] Improve speed of sortedSetRank * [database/mongo] Fix sortedSetRank to filter by _key --- src/database/mongo/sorted.js | 29 ++++++++++++++++++++--------- test/database/sorted.js | 30 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/src/database/mongo/sorted.js b/src/database/mongo/sorted.js index 22c7e44196..ca1d53f63b 100644 --- a/src/database/mongo/sorted.js +++ b/src/database/mongo/sorted.js @@ -203,25 +203,36 @@ module.exports = function (db, module) { }; module.sortedSetRank = function (key, value, callback) { - getSortedSetRank(module.getSortedSetRange, key, value, callback); + getSortedSetRank(false, key, value, callback); }; module.sortedSetRevRank = function (key, value, callback) { - getSortedSetRank(module.getSortedSetRevRange, key, value, callback); + getSortedSetRank(true, key, value, callback); }; - function getSortedSetRank(method, key, value, callback) { + function getSortedSetRank(reverse, key, value, callback) { if (!key) { return callback(); } value = helpers.valueToString(value); - method(key, 0, -1, function (err, result) { - if (err) { - return callback(err); + module.sortedSetScore(key, value, function (err, score) { + if (err || score === null) { + return callback(err, null); } - var rank = result.indexOf(value); - callback(null, rank !== -1 ? rank : null); + db.collection('objects').count({ + $or: [ + { + _key: key, + score: reverse ? { $gt: score } : { $lt: score }, + }, + { + _key: key, + score: score, + value: reverse ? { $gt: value } : { $lt: value }, + }, + ], + }, function (err, rank) { callback(err, rank); }); }); } @@ -235,7 +246,7 @@ module.exports = function (db, module) { } async.map(data, function (item, next) { - getSortedSetRank(module.getSortedSetRange, item.key, item.value, next); + getSortedSetRank(false, item.key, item.value, next); }, callback); }; diff --git a/test/database/sorted.js b/test/database/sorted.js index 703d3e93b4..08799513b2 100644 --- a/test/database/sorted.js +++ b/test/database/sorted.js @@ -17,6 +17,9 @@ describe('Sorted Set methods', function () { function (next) { db.sortedSetAdd('sortedSetTest3', [2, 4], ['value2', 'value4'], next); }, + function (next) { + db.sortedSetAdd('sortedSetTest4', [1, 1, 2, 3, 5], ['b', 'a', 'd', 'e', 'c'], next); + }, function (next) { db.sortedSetAdd('sortedSetLex', [0, 0, 0, 0], ['a', 'b', 'c', 'd'], next); }, @@ -305,6 +308,33 @@ describe('Sorted Set methods', function () { done(); }); }); + + it('should return the rank sorted by the score and then the value (a)', function (done) { + db.sortedSetRank('sortedSetTest4', 'a', function (err, rank) { + assert.equal(err, null); + assert.equal(arguments.length, 2); + assert.equal(rank, 0); + done(); + }); + }); + + it('should return the rank sorted by the score and then the value (b)', function (done) { + db.sortedSetRank('sortedSetTest4', 'b', function (err, rank) { + assert.equal(err, null); + assert.equal(arguments.length, 2); + assert.equal(rank, 1); + done(); + }); + }); + + it('should return the rank sorted by the score and then the value (c)', function (done) { + db.sortedSetRank('sortedSetTest4', 'c', function (err, rank) { + assert.equal(err, null); + assert.equal(arguments.length, 2); + assert.equal(rank, 4); + done(); + }); + }); }); describe('sortedSetRevRank()', function () { From d0a1eb61bc4ff1a9f05bde8097e6bd1655729176 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 12 Jan 2018 16:06:31 -0500 Subject: [PATCH 20/41] Updates to allow better chats integration - Removed unused .createTagsInput method - Firing action:chat.loaded client-side hook in all cases of chat now - New chat window (modal) - New chat page load - Chat switch event --- public/src/client/chats.js | 76 +++----------------------------------- public/src/modules/chat.js | 1 - 2 files changed, 5 insertions(+), 72 deletions(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 402124da16..4061362432 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -27,7 +27,6 @@ define('forum/chats', [ recentChats.init(); Chats.addEventListeners(); - Chats.createTagsInput($('[component="chat/messages"] .users-tag-input'), ajaxify.data); Chats.createAutoComplete($('[component="chat/input"]')); Chats.resizeMainWindow(); @@ -35,9 +34,12 @@ define('forum/chats', [ Chats.addHotkeys(); } - messages.scrollToBottom($('.expanded-chat ul.chat-content')); + $(document).ready(function () { + $(window).trigger('action:chat.loaded', $('.chats-full')); + }); Chats.initialised = true; + messages.scrollToBottom($('.expanded-chat ul.chat-content')); search.init(); @@ -331,75 +333,6 @@ define('forum/chats', [ } }; - Chats.createTagsInput = function (tagEl, data) { - tagEl.tagsinput({ - confirmKeys: [13, 44], - trimValue: true, - }); - - if (data.users && data.users.length) { - data.users.forEach(function (user) { - tagEl.tagsinput('add', $('<div/>').html(user.username).text()); - }); - } - - tagEl.on('beforeItemAdd', function (event) { - event.cancel = event.item === app.user.username; - }); - - tagEl.on('itemAdded', function (event) { - if (event.item === app.user.username) { - return; - } - socket.emit('modules.chats.addUserToRoom', { - roomId: data.roomId, - username: event.item, - }, function (err) { - if (err) { - app.alertError(err.message); - tagEl.tagsinput('remove', event.item, { - nouser: true, - }); - } - }); - }); - - tagEl.on('beforeItemRemove', function (event) { - if (event.options && event.options.nouser) { - return; - } - - event.cancel = !data.isOwner || tagEl.tagsinput('items').length < 2; - if (!data.owner) { - return app.alertError('[[error:not-allowed]]'); - } - - if (tagEl.tagsinput('items').length < 2) { - return app.alertError('[[error:cant-remove-last-user]]'); - } - }); - - tagEl.on('itemRemoved', function (event) { - if (event.options && event.options.nouser) { - return; - } - socket.emit('modules.chats.removeUserFromRoom', { - roomId: data.roomId, - username: event.item, - }, function (err) { - if (err) { - return app.alertError(err.message); - } - }); - }); - - var input = $('.users-tag-container').find('.bootstrap-tagsinput input'); - - require(['autocomplete'], function (autocomplete) { - autocomplete.user(input); - }); - }; - Chats.leave = function (el) { var roomId = el.attr('data-roomid'); socket.emit('modules.chats.leave', roomId, function (err) { @@ -434,6 +367,7 @@ define('forum/chats', [ ajaxify.data = payload; Chats.setActive(); Chats.addEventListeners(); + $(window).trigger('action:chat.loaded', $('.chats-full')); messages.scrollToBottom($('.expanded-chat ul.chat-content')); if (history.pushState) { history.pushState({ diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index 9c25ad07a0..dfd0acf20f 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -254,7 +254,6 @@ define('chat', [ Chats.addLeaveHandler(chatModal.attr('data-roomid'), chatModal.find('[data-action="leave"]')); Chats.addSendHandlers(chatModal.attr('data-roomid'), chatModal.find('.chat-input'), chatModal.find('[data-action="send"]')); - Chats.createTagsInput(chatModal.find('.users-tag-input'), data); Chats.createAutoComplete(chatModal.find('[component="chat/input"]')); Chats.addScrollHandler(chatModal.attr('data-roomid'), data.uid, chatModal.find('.chat-content')); From d8b67606bca91ff604cef4e7f111720405db56df Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Tue, 16 Jan 2018 14:48:16 -0500 Subject: [PATCH 21/41] fixed height detection in chat modal --- public/src/modules/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/modules/chat.js b/public/src/modules/chat.js index dfd0acf20f..5791a7828f 100644 --- a/public/src/modules/chat.js +++ b/public/src/modules/chat.js @@ -324,7 +324,7 @@ define('chat', [ module.enableMobileBehaviour = function (modalEl) { app.toggleNavbar(false); modalEl.attr('data-mobile', '1'); - var messagesEl = modalEl.find('.chat-content'); + var messagesEl = modalEl.find('.modal-body'); messagesEl.css('height', module.calculateChatListHeight(modalEl)); $(window).on('resize', function () { From c20aca8933191c6c9aef2a2d4db2db4c997ffb8e Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Thu, 18 Jan 2018 11:41:28 -0500 Subject: [PATCH 22/41] decreased full chat height by 1px --- public/src/client/chats.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/src/client/chats.js b/public/src/client/chats.js index 4061362432..76ca7e2e34 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -446,8 +446,8 @@ define('forum/chats', [ Chats.resizeMainWindow = function () { var viewportHeight = $(window).height(); var fromTop = components.get('chat/main-wrapper').offset().top || components.get('chat/nav-wrapper').offset().top; + $('.chats-full').height(viewportHeight - fromTop - 1); - $('.chats-full').height(viewportHeight - fromTop); Chats.setActive(); }; From e85aabbe74d7838185d0b102bf37a20c1a1b62a1 Mon Sep 17 00:00:00 2001 From: Ben Lubar <ben.lubar+github@gmail.com> Date: Thu, 18 Jan 2018 12:02:56 -0600 Subject: [PATCH 23/41] Allow running as a cluster without Redis (#6233) * [database/*] Allow databases other than Redis to provide pubsub for clustering if Redis is not present * [pubsub] Delay messages sent before the database is ready until the database is ready. * [pubsub] Restore old behavior of not using the database in non-clustered NodeBB instances. See comment: https://github.com/NodeBB/NodeBB/pull/6233#issuecomment-357814968 --- install/package.json | 2 + src/database/mongo.js | 22 +++++--- src/database/mongo/hash.js | 19 ------- src/database/mongo/pubsub.js | 34 +++++++++++++ src/database/redis.js | 12 +++++ src/database/redis/pubsub.js | 40 +++++++++++++++ src/pubsub.js | 98 ++++++++++++++++++++++-------------- src/socket.io/index.js | 18 +------ 8 files changed, 165 insertions(+), 80 deletions(-) create mode 100644 src/database/mongo/pubsub.js create mode 100644 src/database/redis/pubsub.js diff --git a/install/package.json b/install/package.json index e06f51139e..baa45377fa 100644 --- a/install/package.json +++ b/install/package.json @@ -58,6 +58,7 @@ "mongodb": "2.2.33", "morgan": "^1.9.0", "mousetrap": "^1.6.1", + "mubsub": "^1.4.0", "nconf": "^0.9.1", "nodebb-plugin-composer-default": "6.0.8", "nodebb-plugin-dbsearch": "2.0.9", @@ -89,6 +90,7 @@ "serve-favicon": "^2.4.5", "sitemap": "^1.13.0", "socket.io": "2.0.4", + "socket.io-adapter-mongo": "^2.0.1", "socket.io-client": "2.0.4", "socket.io-redis": "5.2.0", "socketio-wildcard": "2.0.0", diff --git a/src/database/mongo.js b/src/database/mongo.js index 5bcab76a6f..4042cf6025 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -61,11 +61,7 @@ mongoModule.questions = [ mongoModule.helpers = mongoModule.helpers || {}; mongoModule.helpers.mongo = require('./mongo/helpers'); -mongoModule.init = function (callback) { - callback = callback || function () { }; - - var mongoClient = require('mongodb').MongoClient; - +function getConnectionString() { var usernamePassword = ''; if (nconf.get('mongo:username') && nconf.get('mongo:password')) { usernamePassword = nconf.get('mongo:username') + ':' + encodeURIComponent(nconf.get('mongo:password')) + '@'; @@ -92,7 +88,15 @@ mongoModule.init = function (callback) { servers.push(hosts[i] + ':' + ports[i]); } - var connString = nconf.get('mongo:uri') || 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database'); + return nconf.get('mongo:uri') || 'mongodb://' + usernamePassword + servers.join() + '/' + nconf.get('mongo:database'); +} + +mongoModule.init = function (callback) { + callback = callback || function () { }; + + var mongoClient = require('mongodb').MongoClient; + + var connString = getConnectionString(); var connOptions = { poolSize: 10, @@ -118,6 +122,7 @@ mongoModule.init = function (callback) { require('./mongo/sets')(db, mongoModule); require('./mongo/sorted')(db, mongoModule); require('./mongo/list')(db, mongoModule); + require('./mongo/pubsub')(db, mongoModule); callback(); }); }; @@ -262,3 +267,8 @@ mongoModule.close = function (callback) { callback = callback || function () {}; db.close(callback); }; + +mongoModule.socketAdapter = function () { + var mongoAdapter = require('socket.io-adapter-mongo'); + return mongoAdapter(getConnectionString()); +}; diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index aef54bf124..84d66660c0 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -5,7 +5,6 @@ module.exports = function (db, module) { var LRU = require('lru-cache'); var _ = require('lodash'); - var pubsub = require('../../pubsub'); var cache = LRU({ max: 10000, @@ -17,24 +16,6 @@ module.exports = function (db, module) { cache.hits = 0; module.objectCache = cache; - pubsub.on('mongo:hash:cache:del', function (key) { - cache.del(key); - }); - - pubsub.on('mongo:hash:cache:reset', function () { - cache.reset(); - }); - - module.delObjectCache = function (key) { - pubsub.publish('mongo:hash:cache:del', key); - cache.del(key); - }; - - module.resetObjectCache = function () { - pubsub.publish('mongo:hash:cache:reset'); - cache.reset(); - }; - module.setObject = function (key, data, callback) { callback = callback || helpers.noop; if (!key || !data) { diff --git a/src/database/mongo/pubsub.js b/src/database/mongo/pubsub.js new file mode 100644 index 0000000000..157d4652bd --- /dev/null +++ b/src/database/mongo/pubsub.js @@ -0,0 +1,34 @@ +'use strict'; + +var nconf = require('nconf'); + +module.exports = function (db, mongoModule) { + var pubsub; + + if (!nconf.get('redis')) { + var mubsub = require('mubsub'); + var client = mubsub(db); + pubsub = client.channel('pubsub'); + mongoModule.pubsub = pubsub; + } else { + pubsub = require('../../pubsub'); + } + + pubsub.on('mongo:hash:cache:del', function (key) { + mongoModule.objectCache.del(key); + }); + + pubsub.on('mongo:hash:cache:reset', function () { + mongoModule.objectCache.reset(); + }); + + mongoModule.delObjectCache = function (key) { + pubsub.publish('mongo:hash:cache:del', key); + mongoModule.objectCache.del(key); + }; + + mongoModule.resetObjectCache = function () { + pubsub.publish('mongo:hash:cache:reset'); + mongoModule.objectCache.reset(); + }; +}; diff --git a/src/database/redis.js b/src/database/redis.js index 70a8ffbd53..55037393cb 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -50,6 +50,7 @@ redisModule.init = function (callback) { require('./redis/sets')(redisClient, redisModule); require('./redis/sorted')(redisClient, redisModule); require('./redis/list')(redisClient, redisModule); + require('./redis/pubsub')(redisClient, redisModule); callback(); }); @@ -169,5 +170,16 @@ redisModule.info = function (cxn, callback) { ], callback); }; +redisModule.socketAdapter = function () { + var redisAdapter = require('socket.io-redis'); + var pub = redisModule.connect(); + var sub = redisModule.connect(); + return redisAdapter({ + key: 'db:' + nconf.get('redis:database') + ':adapter_key', + pubClient: pub, + subClient: sub, + }); +}; + redisModule.helpers = redisModule.helpers || {}; redisModule.helpers.redis = require('./redis/helpers'); diff --git a/src/database/redis/pubsub.js b/src/database/redis/pubsub.js new file mode 100644 index 0000000000..3daf0f40cc --- /dev/null +++ b/src/database/redis/pubsub.js @@ -0,0 +1,40 @@ +'use strict'; + +var nconf = require('nconf'); +var util = require('util'); +var winston = require('winston'); +var EventEmitter = require('events').EventEmitter; + +var channelName; + +var PubSub = function (redisModule) { + var self = this; + var subClient = redisModule.connect(); + this.pubClient = redisModule.connect(); + + channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel'; + subClient.subscribe(channelName); + + subClient.on('message', function (channel, message) { + if (channel !== channelName) { + return; + } + + try { + var msg = JSON.parse(message); + self.emit(msg.event, msg.data); + } catch (err) { + winston.error(err.stack); + } + }); +}; + +util.inherits(PubSub, EventEmitter); + +PubSub.prototype.publish = function (event, data) { + this.pubClient.publish(channelName, JSON.stringify({ event: event, data: data })); +}; + +module.exports = function (redisClient, redisModule) { + redisModule.pubsub = new PubSub(redisModule); +}; diff --git a/src/pubsub.js b/src/pubsub.js index b33a03b6bf..2364612b88 100644 --- a/src/pubsub.js +++ b/src/pubsub.js @@ -1,48 +1,70 @@ - 'use strict'; var nconf = require('nconf'); -var util = require('util'); -var winston = require('winston'); -var EventEmitter = require('events').EventEmitter; - -var channelName; - -var PubSub = function () { - var self = this; - if (nconf.get('redis')) { - var redis = require('./database/redis'); - var subClient = redis.connect(); - this.pubClient = redis.connect(); - - channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel'; - subClient.subscribe(channelName); - - subClient.on('message', function (channel, message) { - if (channel !== channelName) { - return; - } - - try { - var msg = JSON.parse(message); - self.emit(msg.event, msg.data); - } catch (err) { - winston.error(err.stack); - } - }); - } + +var real; +var fake = { + publishQueue: [], + publish: function (event, data) { + fake.publishQueue.push({ event: event, data: data }); + }, + listenQueue: {}, + on: function (event, callback) { + if (!Object.prototype.hasOwnProperty.call(fake.listenQueue, event)) { + fake.listenQueue[event] = []; + } + fake.listenQueue[event].push(callback); + }, + removeAllListeners: function (event) { + delete fake.listenQueue[event]; + }, }; -util.inherits(PubSub, EventEmitter); +function get() { + if (real) { + return real; + } + + var pubsub; -PubSub.prototype.publish = function (event, data) { - if (this.pubClient) { - this.pubClient.publish(channelName, JSON.stringify({ event: event, data: data })); + if (nconf.get('isCluster') === 'false') { + var EventEmitter = require('events'); + pubsub = new EventEmitter(); + pubsub.publish = pubsub.emit.bind(pubsub); + } else if (nconf.get('redis')) { + pubsub = require('./database/redis').pubsub; } else { - this.emit(event, data); + pubsub = require('./database').pubsub; } -}; -var pubsub = new PubSub(); + if (!pubsub) { + return fake; + } + + Object.keys(fake.listenQueue).forEach(function (event) { + fake.listenQueue[event].forEach(function (callback) { + pubsub.on(event, callback); + }); + }); + + fake.publishQueue.forEach(function (msg) { + pubsub.publish(msg.event, msg.data); + }); -module.exports = pubsub; + real = pubsub; + fake = null; + + return pubsub; +} + +module.exports = { + publish: function (event, data) { + get().publish(event, data); + }, + on: function (event, callback) { + get().on(event, callback); + }, + removeAllListeners: function (event) { + get().removeAllListeners(event); + }, +}; diff --git a/src/socket.io/index.js b/src/socket.io/index.js index 2e897d0c4c..ead298b492 100644 --- a/src/socket.io/index.js +++ b/src/socket.io/index.js @@ -26,7 +26,7 @@ Sockets.init = function (server) { path: nconf.get('relative_path') + '/socket.io', }); - addRedisAdapter(io); + io.adapter(nconf.get('redis') ? require('../database/redis').socketAdapter() : db.socketAdapter()); io.use(socketioWildcard); io.use(authorize); @@ -212,22 +212,6 @@ function authorize(socket, callback) { ], callback); } -function addRedisAdapter(io) { - if (nconf.get('redis')) { - var redisAdapter = require('socket.io-redis'); - var redis = require('../database/redis'); - var pub = redis.connect(); - var sub = redis.connect(); - io.adapter(redisAdapter({ - key: 'db:' + nconf.get('redis:database') + ':adapter_key', - pubClient: pub, - subClient: sub, - })); - } else if (nconf.get('isCluster') === 'true') { - winston.warn('[socket.io] Clustering detected, you are advised to configure Redis as a websocket store.'); - } -} - Sockets.in = function (room) { return io.in(room); }; From 6855a87a9761bd721cbc42f653bc06ffa750eb9e Mon Sep 17 00:00:00 2001 From: Ben Lubar <ben.lubar+github@gmail.com> Date: Thu, 18 Jan 2018 12:04:51 -0600 Subject: [PATCH 24/41] Run travis builds in containers (#6232) * [travis] Cache node_modules between test runs * [travis] Enable container-based builds for less waiting time --- .travis.yml | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13e6afdd3e..99f8a599e7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,29 +1,31 @@ services: + - mongodb - redis-server before_install: - - "sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6" - - "echo 'deb [ arch=amd64 ] http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.4 multiverse' | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list" - - "sudo apt-get update" - - "sudo apt-get install -y mongodb-org" - - "sudo service mongod start" + - cp install/package.json package.json +cache: + directories: + - node_modules before_script: - sleep 15 # wait for mongodb to be ready - - cp install/package.json package.json - - npm install - sh -c "if [ '$DB' = 'mongodb' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"mongo\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":27017,\\\"database\\\":0}\"; fi" - sh -c "if [ '$DB' = 'redis' ]; then node app --setup=\"{\\\"url\\\":\\\"http://127.0.0.1:4567\\\",\\\"secret\\\":\\\"abcdef\\\",\\\"database\\\":\\\"redis\\\",\\\"mongo:host\\\":\\\"127.0.0.1\\\",\\\"mongo:port\\\":27017,\\\"mongo:username\\\":\\\"\\\",\\\"mongo:password\\\":\\\"\\\",\\\"mongo:database\\\":0,\\\"redis:host\\\":\\\"127.0.0.1\\\",\\\"redis:port\\\":6379,\\\"redis:password\\\":\\\"\\\",\\\"redis:database\\\":0,\\\"admin:username\\\":\\\"admin\\\",\\\"admin:email\\\":\\\"test@example.org\\\",\\\"admin:password\\\":\\\"abcdef\\\",\\\"admin:password:confirm\\\":\\\"abcdef\\\"}\" --ci=\"{\\\"host\\\":\\\"127.0.0.1\\\",\\\"port\\\":6379,\\\"database\\\":0}\"; fi" after_success: - "npm run coveralls" language: node_js +sudo: false +dist: trusty env: - CXX=g++-4.8 DB=mongodb - CXX=g++-4.8 DB=redis addons: apt: sources: - - ubuntu-toolchain-r-test + - ubuntu-toolchain-r-test + - mongodb-3.4-trusty packages: - - g++-4.8 + - g++-4.8 + - mongodb-org-server node_js: - "9" - "8" From 8a6345ded7df6ad116dd3b33be24a125c4af7c29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Mon, 22 Jan 2018 16:41:22 -0500 Subject: [PATCH 25/41] possible fix for pubsub --- src/database/mongo.js | 1 - src/database/mongo/hash.js | 21 +++++++++++++++++++++ src/database/mongo/pubsub.js | 34 ++++------------------------------ src/database/redis.js | 1 - src/database/redis/pubsub.js | 11 ++++++----- src/pubsub.js | 6 +++--- 6 files changed, 34 insertions(+), 40 deletions(-) diff --git a/src/database/mongo.js b/src/database/mongo.js index 4042cf6025..be6789389b 100644 --- a/src/database/mongo.js +++ b/src/database/mongo.js @@ -122,7 +122,6 @@ mongoModule.init = function (callback) { require('./mongo/sets')(db, mongoModule); require('./mongo/sorted')(db, mongoModule); require('./mongo/list')(db, mongoModule); - require('./mongo/pubsub')(db, mongoModule); callback(); }); }; diff --git a/src/database/mongo/hash.js b/src/database/mongo/hash.js index 84d66660c0..d11cb0aacd 100644 --- a/src/database/mongo/hash.js +++ b/src/database/mongo/hash.js @@ -1,5 +1,7 @@ 'use strict'; +var pubsub = require('../../pubsub'); + module.exports = function (db, module) { var helpers = module.helpers.mongo; @@ -16,6 +18,25 @@ module.exports = function (db, module) { cache.hits = 0; module.objectCache = cache; + pubsub.on('mongo:hash:cache:del', function (key) { + cache.del(key); + }); + + pubsub.on('mongo:hash:cache:reset', function () { + cache.reset(); + }); + + module.delObjectCache = function (key) { + pubsub.publish('mongo:hash:cache:del', key); + cache.del(key); + }; + + module.resetObjectCache = function () { + pubsub.publish('mongo:hash:cache:reset'); + cache.reset(); + }; + + module.setObject = function (key, data, callback) { callback = callback || helpers.noop; if (!key || !data) { diff --git a/src/database/mongo/pubsub.js b/src/database/mongo/pubsub.js index 157d4652bd..83ff676294 100644 --- a/src/database/mongo/pubsub.js +++ b/src/database/mongo/pubsub.js @@ -1,34 +1,8 @@ 'use strict'; -var nconf = require('nconf'); +var mubsub = require('mubsub'); -module.exports = function (db, mongoModule) { - var pubsub; +var db = require('../mongo'); +var client = mubsub(db.client); - if (!nconf.get('redis')) { - var mubsub = require('mubsub'); - var client = mubsub(db); - pubsub = client.channel('pubsub'); - mongoModule.pubsub = pubsub; - } else { - pubsub = require('../../pubsub'); - } - - pubsub.on('mongo:hash:cache:del', function (key) { - mongoModule.objectCache.del(key); - }); - - pubsub.on('mongo:hash:cache:reset', function () { - mongoModule.objectCache.reset(); - }); - - mongoModule.delObjectCache = function (key) { - pubsub.publish('mongo:hash:cache:del', key); - mongoModule.objectCache.del(key); - }; - - mongoModule.resetObjectCache = function () { - pubsub.publish('mongo:hash:cache:reset'); - mongoModule.objectCache.reset(); - }; -}; +module.exports = client.channel('pubsub'); diff --git a/src/database/redis.js b/src/database/redis.js index 55037393cb..e88663cf70 100644 --- a/src/database/redis.js +++ b/src/database/redis.js @@ -50,7 +50,6 @@ redisModule.init = function (callback) { require('./redis/sets')(redisClient, redisModule); require('./redis/sorted')(redisClient, redisModule); require('./redis/list')(redisClient, redisModule); - require('./redis/pubsub')(redisClient, redisModule); callback(); }); diff --git a/src/database/redis/pubsub.js b/src/database/redis/pubsub.js index 3daf0f40cc..f7724bb016 100644 --- a/src/database/redis/pubsub.js +++ b/src/database/redis/pubsub.js @@ -7,10 +7,10 @@ var EventEmitter = require('events').EventEmitter; var channelName; -var PubSub = function (redisModule) { +var PubSub = function (db) { var self = this; - var subClient = redisModule.connect(); - this.pubClient = redisModule.connect(); + var subClient = db.connect(); + this.pubClient = db.connect(); channelName = 'db:' + nconf.get('redis:database') + 'pubsub_channel'; subClient.subscribe(channelName); @@ -35,6 +35,7 @@ PubSub.prototype.publish = function (event, data) { this.pubClient.publish(channelName, JSON.stringify({ event: event, data: data })); }; -module.exports = function (redisClient, redisModule) { - redisModule.pubsub = new PubSub(redisModule); +module.exports = function () { + var db = require('../redis'); + return new PubSub(db); }; diff --git a/src/pubsub.js b/src/pubsub.js index 2364612b88..270ae37bf8 100644 --- a/src/pubsub.js +++ b/src/pubsub.js @@ -32,9 +32,9 @@ function get() { pubsub = new EventEmitter(); pubsub.publish = pubsub.emit.bind(pubsub); } else if (nconf.get('redis')) { - pubsub = require('./database/redis').pubsub; - } else { - pubsub = require('./database').pubsub; + pubsub = require('./database/redis/pubsub'); + } else if (nconf.get('mongo')) { + pubsub = require('./database/mongo/pubsub'); } if (!pubsub) { From c0b7d4f5b5feca2b70e31b049cf6a60ff1cac6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Mon, 22 Jan 2018 16:51:12 -0500 Subject: [PATCH 26/41] fix redis export --- src/database/redis/pubsub.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/database/redis/pubsub.js b/src/database/redis/pubsub.js index f7724bb016..cb989207a6 100644 --- a/src/database/redis/pubsub.js +++ b/src/database/redis/pubsub.js @@ -7,8 +7,9 @@ var EventEmitter = require('events').EventEmitter; var channelName; -var PubSub = function (db) { +var PubSub = function () { var self = this; + var db = require('../redis'); var subClient = db.connect(); this.pubClient = db.connect(); @@ -35,7 +36,4 @@ PubSub.prototype.publish = function (event, data) { this.pubClient.publish(channelName, JSON.stringify({ event: event, data: data })); }; -module.exports = function () { - var db = require('../redis'); - return new PubSub(db); -}; +module.exports = new PubSub(); From 57f0368c69619e645a8f58b1f0da154676bd3bee Mon Sep 17 00:00:00 2001 From: Andrew Rodrigues <rodrigues.andrew@gmail.com> Date: Tue, 23 Jan 2018 15:26:01 -0500 Subject: [PATCH 27/41] bump emoji --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index c676ff0381..1b262bef06 100644 --- a/install/package.json +++ b/install/package.json @@ -62,7 +62,7 @@ "nconf": "^0.9.1", "nodebb-plugin-composer-default": "6.0.8", "nodebb-plugin-dbsearch": "2.0.9", - "nodebb-plugin-emoji": "2.0.9", + "nodebb-plugin-emoji": "2.1.0", "nodebb-plugin-emoji-android": "2.0.0", "nodebb-plugin-markdown": "8.2.2", "nodebb-plugin-mentions": "2.2.2", From 49c4a23e044e4b8161ca4ec81b5232d7299a7dbc Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 24 Jan 2018 13:44:48 -0500 Subject: [PATCH 28/41] closes #6277 --- src/messaging.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/messaging.js b/src/messaging.js index ab22b3c773..02ca02683e 100644 --- a/src/messaging.js +++ b/src/messaging.js @@ -82,9 +82,6 @@ function canGet(hook, callerUid, uid, callback) { } Messaging.parse = function (message, fromuid, uid, roomId, isNew, callback) { - message = utils.decodeHTMLEntities(utils.stripHTMLTags(message)); - message = validator.escape(String(message)); - plugins.fireHook('filter:parse.raw', message, function (err, parsed) { if (err) { return callback(err); From 5c3155f145f28a1b4d6bbe9f4412516c8dca1970 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 26 Jan 2018 13:22:11 -0500 Subject: [PATCH 29/41] firing hook on chat message load and exposing upload_url to client-side --- public/src/client/chats/messages.js | 4 ++++ src/controllers/api.js | 1 + 2 files changed, 5 insertions(+) diff --git a/public/src/client/chats/messages.js b/public/src/client/chats/messages.js index cfbcacf8f8..6a772dd6e5 100644 --- a/public/src/client/chats/messages.js +++ b/public/src/client/chats/messages.js @@ -82,6 +82,10 @@ define('forum/chats/messages', ['components', 'sounds', 'translator', 'benchpres newMessage.find('.timeago').timeago(); newMessage.find('img:not(.not-responsive)').addClass('img-responsive'); messages.scrollToBottom(chatContentEl); + + $(window).trigger('action:chat.received', { + messageEl: newMessage, + }); } diff --git a/src/controllers/api.js b/src/controllers/api.js index a9e3dacf79..790029694f 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -18,6 +18,7 @@ var apiController = module.exports; apiController.loadConfig = function (req, callback) { var config = {}; config.relative_path = nconf.get('relative_path'); + config.upload_url = nconf.get('upload_url'); config.siteTitle = validator.escape(String(meta.config.title || meta.config.browserTitle || 'NodeBB')); config.browserTitle = validator.escape(String(meta.config.browserTitle || meta.config.title || 'NodeBB')); config.titleLayout = (meta.config.titleLayout || '{pageTitle} | {browserTitle}').replace(/{/g, '{').replace(/}/g, '}'); From 08fa0683eb7724f4109fdc5f15e487d065f86ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Fri, 26 Jan 2018 19:04:34 -0500 Subject: [PATCH 30/41] check is file --- src/views/admin/manage/uploads.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/views/admin/manage/uploads.tpl b/src/views/admin/manage/uploads.tpl index 0af5bd43c8..a18b233aa6 100644 --- a/src/views/admin/manage/uploads.tpl +++ b/src/views/admin/manage/uploads.tpl @@ -27,7 +27,7 @@ </td> <!-- ENDIF files.isFile --> - <td class="col-md-2 text-right"><!-- IF files.size -->{files.sizeHumanReadable}<!-- ELSE -->[[admin/manage/uploads:filecount, {files.fileCount}]]<!-- ENDIF files.size --></td> + <td class="col-md-2 text-right"><!-- IF files.isFile -->{files.sizeHumanReadable}<!-- ELSE -->[[admin/manage/uploads:filecount, {files.fileCount}]]<!-- ENDIF files.isFile --></td> <td role="button" class="col-md-1 text-right"><i class="delete fa fa-fw fa-trash-o <!-- IF !files.isFile --> hidden<!-- ENDIF !files.isFile -->"></i></td> </tr> From 212486d230f2e9114bb2624df0fa4bea6f77cfbb Mon Sep 17 00:00:00 2001 From: psychobunny <rodrigues.andrew@gmail.com> Date: Mon, 29 Jan 2018 16:41:34 -0500 Subject: [PATCH 31/41] hide topic upvotes on category page when rep system is disabled --- src/controllers/category.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/controllers/category.js b/src/controllers/category.js index 89f924e479..f3e90897cf 100644 --- a/src/controllers/category.js +++ b/src/controllers/category.js @@ -135,6 +135,7 @@ categoryController.get = function (req, res, callback) { addTags(categoryData, res); categoryData['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; + categoryData['reputation:disabled'] = parseInt(meta.config['reputation:disabled'], 10) === 1; categoryData.title = translator.escape(categoryData.name); pageCount = Math.max(1, Math.ceil(categoryData.topic_count / settings.topicsPerPage)); categoryData.pagination = pagination.create(currentPage, pageCount, req.query); From 6af7d0d07d315de7bfe1268a30472f162137236f Mon Sep 17 00:00:00 2001 From: psychobunny <rodrigues.andrew@gmail.com> Date: Mon, 29 Jan 2018 16:46:08 -0500 Subject: [PATCH 32/41] up persona --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index ee77fc6e78..8a514d6e1b 100644 --- a/install/package.json +++ b/install/package.json @@ -70,7 +70,7 @@ "nodebb-plugin-spam-be-gone": "0.5.1", "nodebb-rewards-essentials": "0.0.11", "nodebb-theme-lavender": "5.0.1", - "nodebb-theme-persona": "7.2.19", + "nodebb-theme-persona": "7.2.21", "nodebb-theme-slick": "1.1.4", "nodebb-theme-vanilla": "8.1.7", "nodebb-widget-essentials": "4.0.1", From 7581dd11746b6fef0821795aa1fd3f99a5aaeb64 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Mon, 29 Jan 2018 17:33:45 -0500 Subject: [PATCH 33/41] Closes #6245 - Moved account info to within the profile menu hook, so it can be better filtered and shown to mods - Also serves as a nice template for plugins that wish to listen to the profileMenu hook! --- src/controllers/accounts/helpers.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/controllers/accounts/helpers.js b/src/controllers/accounts/helpers.js index 64170ce39d..8951976a78 100644 --- a/src/controllers/accounts/helpers.js +++ b/src/controllers/accounts/helpers.js @@ -50,11 +50,26 @@ helpers.getUserDataByUserSlug = function (userslug, callerUID, callback) { ips: function (next) { user.getIPs(uid, 4, next); }, - profile_links: function (next) { + profile_links: function (next) { // DEPRECATED, do not use plugins.fireHook('filter:user.profileLinks', [], next); }, profile_menu: function (next) { - plugins.fireHook('filter:user.profileMenu', { uid: uid, callerUID: callerUID, links: [] }, next); + plugins.fireHook('filter:user.profileMenu', { + uid: uid, + callerUID: callerUID, + links: [{ + id: 'info', + route: 'info', + name: '[[user:account_info]]', + visibility: { + self: false, + other: false, + moderator: true, + globalMod: true, + admin: true, + }, + }], + }, next); }, groups: function (next) { groups.getUserGroups([uid], next); From ac1f7eefe5ef98d58ea8edb34183a273ec3d244a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Wed, 31 Jan 2018 15:20:17 -0500 Subject: [PATCH 34/41] closes #2304 --- install/package.json | 1 + src/controllers/accounts/chats.js | 2 +- src/controllers/admin/categories.js | 2 +- src/controllers/admin/uploads.js | 2 +- src/controllers/admin/users.js | 2 +- src/controllers/api.js | 2 +- src/controllers/authentication.js | 2 +- src/controllers/helpers.js | 8 +-- src/controllers/index.js | 2 +- src/controllers/popular.js | 4 +- src/controllers/recent.js | 2 +- src/controllers/search.js | 7 +-- src/controllers/top.js | 2 +- src/controllers/topics.js | 4 +- src/controllers/user.js | 2 +- src/groups.js | 2 +- src/middleware/header.js | 2 +- src/middleware/index.js | 12 ++--- src/middleware/user.js | 8 +-- src/privileges/helpers.js | 21 +++++--- src/routes/authentication.js | 10 +++- src/upgrades/1.8.0/give_spiders_privileges.js | 49 +++++++++++++++++++ src/webserver.js | 2 + 23 files changed, 106 insertions(+), 44 deletions(-) create mode 100644 src/upgrades/1.8.0/give_spiders_privileges.js diff --git a/install/package.json b/install/package.json index 97dbfaf68f..6b28d05bb5 100644 --- a/install/package.json +++ b/install/package.json @@ -95,6 +95,7 @@ "socket.io-redis": "5.2.0", "socketio-wildcard": "2.0.0", "spdx-license-list": "^3.0.1", + "spider-detector": "1.0.18", "toobusy-js": "^0.5.1", "uglify-js": "^3.3.4", "validator": "9.2.0", diff --git a/src/controllers/accounts/chats.js b/src/controllers/accounts/chats.js index ff5a07d157..103af601ce 100644 --- a/src/controllers/accounts/chats.js +++ b/src/controllers/accounts/chats.js @@ -97,7 +97,7 @@ chatsController.get = function (req, res, callback) { chatsController.redirectToChat = function (req, res, next) { var roomid = parseInt(req.params.roomid, 10); - if (!req.uid) { + if (!req.loggedIn) { return next(); } async.waterfall([ diff --git a/src/controllers/admin/categories.js b/src/controllers/admin/categories.js index e03b51745c..78108caca1 100644 --- a/src/controllers/admin/categories.js +++ b/src/controllers/admin/categories.js @@ -13,7 +13,7 @@ categoriesController.get = function (req, res, callback) { async.waterfall([ function (next) { async.parallel({ - category: async.apply(categories.getCategories, [req.params.category_id], req.user.uid), + category: async.apply(categories.getCategories, [req.params.category_id], req.uid), allCategories: async.apply(categories.buildForSelect, req.uid, 'read'), }, next); }, diff --git a/src/controllers/admin/uploads.js b/src/controllers/admin/uploads.js index db26284205..7256e35b7b 100644 --- a/src/controllers/admin/uploads.js +++ b/src/controllers/admin/uploads.js @@ -253,7 +253,7 @@ function uploadImage(filename, folder, uploadedFile, req, res, next) { async.waterfall([ function (next) { if (plugins.hasListeners('filter:uploadImage')) { - plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.user.uid }, next); + plugins.fireHook('filter:uploadImage', { image: uploadedFile, uid: req.uid }, next); } else { file.saveFileToLocal(filename, folder, uploadedFile.path, next); } diff --git a/src/controllers/admin/users.js b/src/controllers/admin/users.js index 5c71c19da7..f33435c325 100644 --- a/src/controllers/admin/users.js +++ b/src/controllers/admin/users.js @@ -191,7 +191,7 @@ usersController.getCSV = function (req, res, next) { } events.log({ type: 'getUsersCSV', - uid: req.user.uid, + uid: req.uid, ip: req.ip, }); async.waterfall([ diff --git a/src/controllers/api.js b/src/controllers/api.js index 790029694f..44ce2a582b 100644 --- a/src/controllers/api.js +++ b/src/controllers/api.js @@ -78,7 +78,7 @@ apiController.loadConfig = function (req, callback) { async.waterfall([ function (next) { - if (!req.uid) { + if (!req.loggedIn) { return next(null, config); } user.getSettings(req.uid, next); diff --git a/src/controllers/authentication.js b/src/controllers/authentication.js index b2d46dcd70..88d0ec5b49 100644 --- a/src/controllers/authentication.js +++ b/src/controllers/authentication.js @@ -417,7 +417,7 @@ authenticationController.localLogin = function (req, username, password, next) { }; authenticationController.logout = function (req, res, next) { - if (!req.uid || !req.sessionID) { + if (!req.loggedIn || !req.sessionID) { return res.status(200).send('not-logged-in'); } diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index 319f10e841..21dccd0044 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -24,7 +24,7 @@ helpers.noScriptErrors = function (req, res, error, httpStatus) { middleware.buildHeader(req, res, function () { res.status(httpStatus).render(httpStatusString, { path: req.path, - loggedIn: true, + loggedIn: req.loggedIn, error: error, returnLink: true, title: '[[global:' + httpStatusString + '.title]]', @@ -67,11 +67,11 @@ helpers.notAllowed = function (req, res, error) { if (err) { return winston.error(err); } - if (req.uid) { + if (req.loggedIn) { if (res.locals.isAPI) { res.status(403).json({ path: req.path.replace(/^\/api/, ''), - loggedIn: !!req.uid, + loggedIn: req.loggedIn, error: error, title: '[[global:403.title]]', }); @@ -79,7 +79,7 @@ helpers.notAllowed = function (req, res, error) { middleware.buildHeader(req, res, function () { res.status(403).render('403', { path: req.path, - loggedIn: !!req.uid, + loggedIn: req.loggedIn, error: error, title: '[[global:403.title]]', }); diff --git a/src/controllers/index.js b/src/controllers/index.js index 6e82dbf4a2..4c4fa0ade5 100644 --- a/src/controllers/index.js +++ b/src/controllers/index.js @@ -113,7 +113,7 @@ Controllers.login = function (req, res, next) { } return res.redirect(nconf.get('relative_path') + data.authentication[0].url); } - if (req.uid) { + if (req.loggedIn) { user.getUserFields(req.uid, ['username', 'email'], function (err, user) { if (err) { return next(err); diff --git a/src/controllers/popular.js b/src/controllers/popular.js index 6c7c2e6011..fae1bbaa16 100644 --- a/src/controllers/popular.js +++ b/src/controllers/popular.js @@ -37,7 +37,7 @@ popularController.get = function (req, res, next) { alltime: '[[global:header.popular]]', }; - if (!req.uid) { + if (!req.loggedIn) { if (anonCache[term] && (Date.now() - lastUpdateTime) < 60 * 60 * 1000) { return res.render('popular', anonCache[term]); } @@ -73,7 +73,7 @@ popularController.get = function (req, res, next) { data.breadcrumbs = helpers.buildBreadcrumbs(breadcrumbs); } - if (!req.uid) { + if (!req.loggedIn) { anonCache[term] = data; lastUpdateTime = Date.now(); } diff --git a/src/controllers/recent.js b/src/controllers/recent.js index 7b997894c1..1ef041604f 100644 --- a/src/controllers/recent.js +++ b/src/controllers/recent.js @@ -58,7 +58,7 @@ recentController.get = function (req, res, next) { data.set = 'topics:recent'; data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; data.rssFeedUrl = nconf.get('relative_path') + '/recent.rss'; - if (req.uid) { + if (req.loggedIn) { data.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; } data.title = meta.config.homePageTitle || '[[pages:home]]'; diff --git a/src/controllers/search.js b/src/controllers/search.js index 49e762407a..4c37b6258d 100644 --- a/src/controllers/search.js +++ b/src/controllers/search.js @@ -11,15 +11,14 @@ var categories = require('../categories'); var pagination = require('../pagination'); var helpers = require('./helpers'); - -var searchController = {}; +var searchController = module.exports; searchController.search = function (req, res, next) { if (!plugins.hasListeners('filter:search.query')) { return next(); } - if (!req.user && parseInt(meta.config.allowGuestSearching, 10) !== 1) { + if (!req.loggedIn && parseInt(meta.config.allowGuestSearching, 10) !== 1) { return helpers.notAllowed(req, res); } @@ -78,5 +77,3 @@ searchController.search = function (req, res, next) { res.render('search', searchData); }); }; - -module.exports = searchController; diff --git a/src/controllers/top.js b/src/controllers/top.js index 7b500533d5..3be4fb261d 100644 --- a/src/controllers/top.js +++ b/src/controllers/top.js @@ -58,7 +58,7 @@ topController.get = function (req, res, next) { data.set = 'topics:votes'; data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1; data.rssFeedUrl = nconf.get('relative_path') + '/top.rss'; - if (req.uid) { + if (req.loggedIn) { data.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; } data.title = meta.config.homePageTitle || '[[pages:home]]'; diff --git a/src/controllers/topics.js b/src/controllers/topics.js index 1936c99481..9e89a1e929 100644 --- a/src/controllers/topics.js +++ b/src/controllers/topics.js @@ -146,7 +146,7 @@ topicsController.get = function (req, res, callback) { topicData.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0; topicData.scrollToMyPost = settings.scrollToMyPost; topicData.rssFeedUrl = nconf.get('relative_path') + '/topic/' + topicData.tid + '.rss'; - if (req.uid) { + if (req.loggedIn) { topicData.rssFeedUrl += '?uid=' + req.uid + '&token=' + rssToken; } @@ -165,7 +165,7 @@ topicsController.get = function (req, res, callback) { req.session.tids_viewed[tid] = Date.now(); } - if (req.uid) { + if (req.loggedIn) { topics.markAsRead([tid], req.uid, function (err, markedRead) { if (err) { return callback(err); diff --git a/src/controllers/user.js b/src/controllers/user.js index ea027b9406..3405c1ea1d 100644 --- a/src/controllers/user.js +++ b/src/controllers/user.js @@ -9,7 +9,7 @@ var accountHelpers = require('./accounts/helpers'); var userController = module.exports; userController.getCurrentUser = function (req, res, next) { - if (!req.uid) { + if (!req.loggedIn) { return res.status(401).json('not-authorized'); } async.waterfall([ diff --git a/src/groups.js b/src/groups.js index 3ac8f36914..fffa3d8ebd 100644 --- a/src/groups.js +++ b/src/groups.js @@ -22,7 +22,7 @@ require('./groups/posts')(Groups); require('./groups/user')(Groups); -Groups.ephemeralGroups = ['guests']; +Groups.ephemeralGroups = ['guests', 'spiders']; Groups.getEphemeralGroup = function (groupName) { return { diff --git a/src/middleware/header.js b/src/middleware/header.js index a0cf65d396..386304806e 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -93,7 +93,7 @@ module.exports = function (middleware) { reputation: 0, 'email:confirmed': 0, }; - if (req.uid) { + if (req.loggedIn) { user.getUserFields(req.uid, Object.keys(userData), next); } else { next(null, userData); diff --git a/src/middleware/index.js b/src/middleware/index.js index c0cce7623a..da4f4198a7 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -59,12 +59,12 @@ middleware.pageView = function (req, res, next) { plugins.fireHook('action:middleware.pageView', { req: req }); - if (req.user) { - user.updateLastOnlineTime(req.user.uid); + if (req.loggedIn) { + user.updateLastOnlineTime(req.uid); if (req.path.startsWith('/api/users') || req.path.startsWith('/users')) { - user.updateOnlineUsers(req.user.uid, next); + user.updateOnlineUsers(req.uid, next); } else { - user.updateOnlineUsers(req.user.uid); + user.updateOnlineUsers(req.uid); next(); } } else { @@ -112,7 +112,7 @@ middleware.routeTouchIcon = function (req, res) { }; middleware.privateTagListing = function (req, res, next) { - if (!req.user && parseInt(meta.config.privateTagListing, 10) === 1) { + if (!req.loggedIn && parseInt(meta.config.privateTagListing, 10) === 1) { controllers.helpers.notAllowed(req, res); } else { next(); @@ -143,7 +143,7 @@ function expose(exposedField, method, field, req, res, next) { } middleware.privateUploads = function (req, res, next) { - if (req.user || parseInt(meta.config.privateUploads, 10) !== 1) { + if (req.loggedIn || parseInt(meta.config.privateUploads, 10) !== 1) { return next(); } if (req.path.startsWith(nconf.get('relative_path') + '/assets/uploads/files')) { diff --git a/src/middleware/user.js b/src/middleware/user.js index 2b29aa5ca1..9ddbbf086d 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -14,7 +14,7 @@ var controllers = { module.exports = function (middleware) { middleware.authenticate = function (req, res, next) { - if (req.uid) { + if (req.loggedIn) { return next(); } @@ -44,7 +44,7 @@ module.exports = function (middleware) { */ async.waterfall([ function (next) { - if (!req.uid) { + if (!req.loggedIn) { return setImmediate(next, null, false); } @@ -64,7 +64,7 @@ module.exports = function (middleware) { } middleware.checkGlobalPrivacySettings = function (req, res, next) { - if (!req.uid && !!parseInt(meta.config.privateUserInfo, 10)) { + if (!req.loggedIn && !!parseInt(meta.config.privateUserInfo, 10)) { return middleware.authenticate(req, res, next); } @@ -202,7 +202,7 @@ module.exports = function (middleware) { }; middleware.requireUser = function (req, res, next) { - if (req.uid) { + if (req.loggedIn) { return next(); } diff --git a/src/privileges/helpers.js b/src/privileges/helpers.js index c3452c495e..3c0d5834ae 100644 --- a/src/privileges/helpers.js +++ b/src/privileges/helpers.js @@ -10,6 +10,11 @@ var plugins = require('../plugins'); var helpers = module.exports; +var uidToSystemGroup = { + 0: 'guests', + '-1': 'spiders', +}; + helpers.some = function (tasks, callback) { async.some(tasks, function (task, next) { task(next); @@ -27,8 +32,8 @@ helpers.isUserAllowedTo = function (privilege, uid, cid, callback) { }; function isUserAllowedToCids(privilege, uid, cids, callback) { - if (parseInt(uid, 10) === 0) { - return isGuestAllowedToCids(privilege, cids, callback); + if (parseInt(uid, 10) <= 0) { + return isSystemGroupAllowedToCids(privilege, uid, cids, callback); } var userKeys = []; @@ -42,8 +47,8 @@ function isUserAllowedToCids(privilege, uid, cids, callback) { } function isUserAllowedToPrivileges(privileges, uid, cid, callback) { - if (parseInt(uid, 10) === 0) { - return isGuestAllowedToPrivileges(privileges, cid, callback); + if (parseInt(uid, 10) <= 0) { + return isSystemGroupAllowedToPrivileges(privileges, uid, cid, callback); } var userKeys = []; @@ -100,20 +105,20 @@ helpers.isUsersAllowedTo = function (privilege, uids, cid, callback) { ], callback); }; -function isGuestAllowedToCids(privilege, cids, callback) { +function isSystemGroupAllowedToCids(privilege, uid, cids, callback) { var groupKeys = cids.map(function (cid) { return 'cid:' + cid + ':privileges:groups:' + privilege; }); - groups.isMemberOfGroups('guests', groupKeys, callback); + groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys, callback); } -function isGuestAllowedToPrivileges(privileges, cid, callback) { +function isSystemGroupAllowedToPrivileges(privileges, uid, cid, callback) { var groupKeys = privileges.map(function (privilege) { return 'cid:' + cid + ':privileges:groups:' + privilege; }); - groups.isMemberOfGroups('guests', groupKeys, callback); + groups.isMemberOfGroups(uidToSystemGroup[uid], groupKeys, callback); } helpers.getUserPrivileges = function (cid, hookName, userPrivilegeList, callback) { diff --git a/src/routes/authentication.js b/src/routes/authentication.js index c85e03fe27..212c2b1302 100644 --- a/src/routes/authentication.js +++ b/src/routes/authentication.js @@ -20,7 +20,15 @@ Auth.initialize = function (app, middleware) { app.use(passport.session()); app.use(function (req, res, next) { - req.uid = req.user ? parseInt(req.user.uid, 10) : 0; + var isSpider = req.isSpider(); + req.loggedIn = !isSpider && !!req.user; + if (isSpider) { + req.uid = -1; + } else if (req.user) { + req.uid = parseInt(req.user.uid, 10); + } else { + req.uid = 0; + } next(); }); diff --git a/src/upgrades/1.8.0/give_spiders_privileges.js b/src/upgrades/1.8.0/give_spiders_privileges.js new file mode 100644 index 0000000000..da3a3b2792 --- /dev/null +++ b/src/upgrades/1.8.0/give_spiders_privileges.js @@ -0,0 +1,49 @@ +'use strict'; + + +var async = require('async'); +var groups = require('../../groups'); +var privileges = require('../../privileges'); +var db = require('../../database'); + +module.exports = { + name: 'Give category access privileges to spiders system group', + timestamp: Date.UTC(2018, 0, 31), + method: function (callback) { + db.getSortedSetRange('categories:cid', 0, -1, function (err, cids) { + if (err) { + return callback(err); + } + async.eachSeries(cids, function (cid, next) { + getGroupPrivileges(cid, function (err, groupPrivileges) { + if (err) { + return next(err); + } + + var privs = []; + if (groupPrivileges['groups:find']) { + privs.push('find'); + } + if (groupPrivileges['groups:read']) { + privs.push('read'); + } + if (groupPrivileges['groups:topics:read']) { + privs.push('topics:read'); + } + + privileges.categories.give(privs, cid, 'spiders', next); + }); + }, callback); + }); + }, +}; + +function getGroupPrivileges(cid, callback) { + var tasks = {}; + + ['groups:find', 'groups:read', 'groups:topics:read'].forEach(function (privilege) { + tasks[privilege] = async.apply(groups.isMember, 'guests', 'cid:' + cid + ':privileges:' + privilege); + }); + + async.parallel(tasks, callback); +} diff --git a/src/webserver.js b/src/webserver.js index 57904f8dd0..fdc67ad8ff 100644 --- a/src/webserver.js +++ b/src/webserver.js @@ -16,6 +16,7 @@ var cookieParser = require('cookie-parser'); var session = require('express-session'); var useragent = require('express-useragent'); var favicon = require('serve-favicon'); +var detector = require('spider-detector'); var db = require('./database'); var file = require('./file'); @@ -159,6 +160,7 @@ function setupExpressApp(app, callback) { app.use(bodyParser.json()); app.use(cookieParser()); app.use(useragent.express()); + app.use(detector.middleware()); app.use(session({ store: db.sessionStore, From 089769c107805b5e24179f1d814e821dcf0bfc64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Fri, 2 Feb 2018 09:41:58 -0500 Subject: [PATCH 35/41] closes #6302 --- public/src/modules/settings.js | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/public/src/modules/settings.js b/public/src/modules/settings.js index 5dfa5def2e..f891ea2ae4 100644 --- a/public/src/modules/settings.js +++ b/public/src/modules/settings.js @@ -457,19 +457,6 @@ define('settings', function () { return callback(err); } - // Parse all values. If they are json, return json - for (var key in values) { - if (values.hasOwnProperty(key)) { - try { - if (!utils.isNumber(values[key])) { - values[key] = JSON.parse(values[key]); - } - } catch (e) { - // Leave the value as is - } - } - } - // Save loaded settings into ajaxify.data for use client-side ajaxify.data.settings = values; @@ -500,12 +487,6 @@ define('settings', function () { } }); - // Normalizing value of multiple selects - formEl.find('select[multiple]').each(function (idx, selectEl) { - selectEl = $(selectEl); - values[selectEl.attr('name')] = JSON.stringify(selectEl.val()); - }); - socket.emit('admin.settings.set', { hash: hash, values: values, From cb326958cb78c6965ffb46f724eff860f72b8b59 Mon Sep 17 00:00:00 2001 From: Andrew Rodrigues <rodrigues.andrew@gmail.com> Date: Thu, 8 Feb 2018 20:04:48 -0500 Subject: [PATCH 36/41] bump emoji to ^2.1.0 --- install/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install/package.json b/install/package.json index 107a92b2b8..2aa1c73db3 100644 --- a/install/package.json +++ b/install/package.json @@ -62,7 +62,7 @@ "nconf": "^0.9.1", "nodebb-plugin-composer-default": "6.0.12", "nodebb-plugin-dbsearch": "2.0.9", - "nodebb-plugin-emoji": "2.1.0", + "nodebb-plugin-emoji": "^2.1.0", "nodebb-plugin-emoji-android": "2.0.0", "nodebb-plugin-markdown": "8.3.0", "nodebb-plugin-mentions": "2.2.3", From 501fc3cee976946552ebddbd5836bad4492229eb Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 16 Feb 2018 16:41:06 -0500 Subject: [PATCH 37/41] WIP post diffs --- install/package.json | 1 + src/posts.js | 1 + src/posts/diffs.js | 51 ++++++++++++++++++++++++++++++++++++ src/posts/edit.js | 5 ++++ src/socket.io/posts.js | 1 + src/socket.io/posts/diffs.js | 13 +++++++++ 6 files changed, 72 insertions(+) create mode 100644 src/posts/diffs.js create mode 100644 src/socket.io/posts/diffs.js diff --git a/install/package.json b/install/package.json index 99411a5c3d..2dffc94228 100644 --- a/install/package.json +++ b/install/package.json @@ -39,6 +39,7 @@ "cropperjs": "^1.2.2", "csurf": "^1.9.0", "daemon": "^1.1.0", + "diff": "^3.4.0", "express": "^4.16.2", "express-session": "^1.15.6", "express-useragent": "1.0.8", diff --git a/src/posts.js b/src/posts.js index cf30bd4c64..6061017b89 100644 --- a/src/posts.js +++ b/src/posts.js @@ -25,6 +25,7 @@ require('./posts/tools')(Posts); require('./posts/votes')(Posts); require('./posts/bookmarks')(Posts); require('./posts/queue')(Posts); +require('./posts/diffs')(Posts); Posts.exists = function (pid, callback) { db.isSortedSetMember('posts:pid', pid, callback); diff --git a/src/posts/diffs.js b/src/posts/diffs.js new file mode 100644 index 0000000000..60e9510f92 --- /dev/null +++ b/src/posts/diffs.js @@ -0,0 +1,51 @@ +'use strict'; + +var async = require('async'); + +var db = require('../database'); +var diff = require('diff'); + +module.exports = function (Posts) { + Posts.diffs = {}; + + Posts.diffs.list = function (pid, callback) { + db.getSortedSetRangeWithScores('post:' + pid + ':diffs', 0, -1, function (err, diffs) { + callback(err, diffs ? diffs.map(function (diffObj) { + return diffObj.score; + }) : null); + }); + }; + + Posts.diffs.save = function (pid, oldContent, newContent, callback) { + db.sortedSetAdd('post:' + pid + ':diffs', Date.now(), diff.createPatch('', newContent, oldContent), callback); + }; + + Posts.diffs.load = function (pid, since, callback) { + // Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` + since = parseInt(since, 10); + + if (isNaN(since) || since > Date.now()) { + return callback(new Error('[[error:invalid-data]]')); + } + + async.parallel({ + post: async.apply(Posts.getPostData, pid), + diffs: async.apply(db.getSortedSetRangeByScore.bind(db), 'post:' + pid + ':diffs', 0, -1, since, Date.now()), + }, function (err, data) { + if (err) { + return callback(err); + } + + // Replace content with re-constructed content from that point in time + data.post.content = data.diffs.reverse().reduce(function (content, diffString) { + return diff.applyPatch(content, diffString); + }, data.post.content); + + // Clear editor data (as it is outdated for this content) + delete data.post.edited; + data.post.editor = null; + + return callback(null, data.post); + }); + }; +}; diff --git a/src/posts/edit.js b/src/posts/edit.js index 8780fa6016..2ca8e3b534 100644 --- a/src/posts/edit.js +++ b/src/posts/edit.js @@ -20,6 +20,7 @@ module.exports = function (Posts) { }); Posts.edit = function (data, callback) { + var oldContent; // for diffing purposes var postData; var results; @@ -39,6 +40,7 @@ module.exports = function (Posts) { } postData = _postData; + oldContent = postData.content; postData.content = data.content; postData.edited = Date.now(); postData.editor = data.uid; @@ -63,6 +65,9 @@ module.exports = function (Posts) { results = _results; Posts.setPostFields(data.pid, postData, next); }, + function (next) { + Posts.diffs.save(data.pid, oldContent, data.content, next); + }, function (next) { postData.cid = results.topic.cid; postData.topic = results.topic; diff --git a/src/socket.io/posts.js b/src/socket.io/posts.js index 39dd1fe64e..e0b7884ece 100644 --- a/src/socket.io/posts.js +++ b/src/socket.io/posts.js @@ -20,6 +20,7 @@ require('./posts/move')(SocketPosts); require('./posts/votes')(SocketPosts); require('./posts/bookmarks')(SocketPosts); require('./posts/tools')(SocketPosts); +require('./posts/diffs')(SocketPosts); SocketPosts.reply = function (socket, data, callback) { if (!data || !data.tid || (parseInt(meta.config.minimumPostLength, 10) !== 0 && !data.content)) { diff --git a/src/socket.io/posts/diffs.js b/src/socket.io/posts/diffs.js new file mode 100644 index 0000000000..3ce7542399 --- /dev/null +++ b/src/socket.io/posts/diffs.js @@ -0,0 +1,13 @@ +'use strict'; + +var posts = require('../../posts'); + +module.exports = function (SocketPosts) { + SocketPosts.getDiffs = function (socket, data, callback) { + posts.diffs.list(data.pid, callback); + }; + + SocketPosts.showPostAt = function (socket, data, callback) { + posts.diffs.load(data.pid, data.since, callback); + }; +}; From c1fef4561bd2fb566662132db2264a62f96a6d1d Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 16 Feb 2018 18:20:27 -0500 Subject: [PATCH 38/41] showing history state in post tools load --- src/posts/diffs.js | 6 ++++++ src/socket.io/posts/tools.js | 2 ++ 2 files changed, 8 insertions(+) diff --git a/src/posts/diffs.js b/src/posts/diffs.js index 60e9510f92..869cd25298 100644 --- a/src/posts/diffs.js +++ b/src/posts/diffs.js @@ -8,6 +8,12 @@ var diff = require('diff'); module.exports = function (Posts) { Posts.diffs = {}; + Posts.diffs.exists = function (pid, callback) { + db.sortedSetCard('post:' + pid + ':diffs', function (err, numDiffs) { + return callback(err, numDiffs > 0); + }); + }; + Posts.diffs.list = function (pid, callback) { db.getSortedSetRangeWithScores('post:' + pid + ':diffs', 0, -1, function (err, diffs) { callback(err, diffs ? diffs.map(function (diffObj) { diff --git a/src/socket.io/posts/tools.js b/src/socket.io/posts/tools.js index a61e50ec6c..b8533937ce 100644 --- a/src/socket.io/posts/tools.js +++ b/src/socket.io/posts/tools.js @@ -51,6 +51,7 @@ module.exports = function (SocketPosts) { postSharing: function (next) { social.getActivePostSharing(next); }, + history: async.apply(posts.diffs.exists, data.pid), }, next); }, function (results, next) { @@ -64,6 +65,7 @@ module.exports = function (SocketPosts) { results.posts.display_moderator_tools = results.posts.display_edit_tools || results.posts.display_delete_tools; results.posts.display_move_tools = results.isAdmin || results.isModerator; results.posts.display_ip_ban = (results.isAdmin || results.isGlobalMod) && !results.posts.selfPost; + results.posts.display_history = results.history; if (!results.isAdmin && !results.isGlobalMod && !results.isModerator) { results.posts.ip = undefined; From a86d91a5520f8dcdc0f15a4bf506431e806292a8 Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Fri, 16 Feb 2018 21:22:26 -0500 Subject: [PATCH 39/41] wrapping up post history work --- public/language/en-GB/topic.json | 7 ++- public/src/client/topic/diffs.js | 65 ++++++++++++++++++++++++++++ public/src/client/topic/postTools.js | 8 +++- src/posts/diffs.js | 16 +++++-- src/socket.io/posts/diffs.js | 2 +- 5 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 public/src/client/topic/diffs.js diff --git a/public/language/en-GB/topic.json b/public/language/en-GB/topic.json index e084a9f24f..4b30d84356 100644 --- a/public/language/en-GB/topic.json +++ b/public/language/en-GB/topic.json @@ -35,6 +35,7 @@ "moved": "Moved", "copy-ip": "Copy IP", "ban-ip": "Ban IP", + "view-history": "Edit History", "bookmark_instructions" : "Click here to return to the last read post in this thread.", @@ -143,5 +144,9 @@ "stale.create": "Create a new topic", "stale.reply_anyway": "Reply to this topic anyway", - "link_back": "Re: [%1](%2)\n\n" + "link_back": "Re: [%1](%2)\n\n", + + "diffs.title": "Post Edit History", + "diffs.description": "This post has <strong>%1</strong> revisions. Click one of the revisions below to see the post content at that point in time.", + "diffs.no-revisions-description": "This post has <strong>%1</strong> revisions." } diff --git a/public/src/client/topic/diffs.js b/public/src/client/topic/diffs.js new file mode 100644 index 0000000000..63dd2b7b28 --- /dev/null +++ b/public/src/client/topic/diffs.js @@ -0,0 +1,65 @@ +'use strict'; + +define('forum/topic/diffs', ['benchpress', 'translator'], function (Benchpress, translator) { + var Diffs = {}; + + Diffs.open = function (pid) { + var localeStringOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric' }; + + socket.emit('posts.getDiffs', { pid: pid }, function (err, timestamps) { + if (err) { + return app.alertError(err.message); + } + + Benchpress.parse('partials/modals/post_history', { + diffs: timestamps.map(function (timestamp) { + return { + timestamp: timestamp, + pretty: new Date(timestamp).toLocaleString(config.userLang.replace('_', '-'), localeStringOpts), + }; + }), + numDiffs: timestamps.length, + }, function (html) { + translator.translate(html, function (html) { + var modal = bootbox.dialog({ + title: '[[topic:diffs.title]]', + message: html, + }); + + if (!timestamps.length) { + return; + } + + var selectEl = modal.find('select'); + var postContainer = modal.find('ul.posts-list'); + + selectEl.on('change', function () { + Diffs.load(pid, this.value, postContainer); + }); + + modal.on('shown.bs.modal', function () { + Diffs.load(pid, selectEl.val(), postContainer); + }); + }); + }); + }); + }; + + Diffs.load = function (pid, since, postContainer) { + socket.emit('posts.showPostAt', { pid: pid, since: since }, function (err, data) { + if (err) { + return app.alertError(err.message); + } + + data.deleted = !!parseInt(data.deleted, 10); + + app.parseAndTranslate('partials/posts_list', 'posts', { + posts: [data], + }, function (html) { + postContainer.empty().append(html); + }); + }); + }; + + return Diffs; +}); diff --git a/public/src/client/topic/postTools.js b/public/src/client/topic/postTools.js index 9cbfdbb366..a164f97ee2 100644 --- a/public/src/client/topic/postTools.js +++ b/public/src/client/topic/postTools.js @@ -8,7 +8,8 @@ define('forum/topic/postTools', [ 'translator', 'forum/topic/votes', 'forum/topic/move-post', -], function (share, navigator, components, translator, votes, movePost) { + 'forum/topic/diffs', +], function (share, navigator, components, translator, votes, movePost, diffs) { var PostTools = {}; var staleReplyAnyway = false; @@ -139,6 +140,11 @@ define('forum/topic/postTools', [ } }); + postContainer.on('click', '[component="post/view-history"], [component="post/edit-indicator"]', function () { + var btn = $(this); + diffs.open(getData(btn, 'data-pid')); + }); + postContainer.on('click', '[component="post/delete"]', function () { var btn = $(this); var timestamp = parseInt(getData(btn, 'data-timestamp'), 10); diff --git a/src/posts/diffs.js b/src/posts/diffs.js index 869cd25298..1ce67f8e0f 100644 --- a/src/posts/diffs.js +++ b/src/posts/diffs.js @@ -1,6 +1,7 @@ 'use strict'; var async = require('async'); +var validator = require('validator'); var db = require('../database'); var diff = require('diff'); @@ -18,7 +19,7 @@ module.exports = function (Posts) { db.getSortedSetRangeWithScores('post:' + pid + ':diffs', 0, -1, function (err, diffs) { callback(err, diffs ? diffs.map(function (diffObj) { return diffObj.score; - }) : null); + }).reverse() : null); }); }; @@ -26,7 +27,7 @@ module.exports = function (Posts) { db.sortedSetAdd('post:' + pid + ':diffs', Date.now(), diff.createPatch('', newContent, oldContent), callback); }; - Posts.diffs.load = function (pid, since, callback) { + Posts.diffs.load = function (pid, since, uid, callback) { // Retrieves all diffs made since `since` and replays them to reconstruct what the post looked like at `since` since = parseInt(since, 10); @@ -35,13 +36,18 @@ module.exports = function (Posts) { } async.parallel({ - post: async.apply(Posts.getPostData, pid), + post: async.apply(Posts.getPostSummaryByPids, [pid], uid, { + parse: false, + }), diffs: async.apply(db.getSortedSetRangeByScore.bind(db), 'post:' + pid + ':diffs', 0, -1, since, Date.now()), }, function (err, data) { if (err) { return callback(err); } + data.post = data.post[0]; + data.post.content = validator.unescape(data.post.content); + // Replace content with re-constructed content from that point in time data.post.content = data.diffs.reverse().reduce(function (content, diffString) { return diff.applyPatch(content, diffString); @@ -51,7 +57,9 @@ module.exports = function (Posts) { delete data.post.edited; data.post.editor = null; - return callback(null, data.post); + Posts.parsePost(data.post, function (err, post) { + callback(err, post); + }); }); }; }; diff --git a/src/socket.io/posts/diffs.js b/src/socket.io/posts/diffs.js index 3ce7542399..7f208dc71f 100644 --- a/src/socket.io/posts/diffs.js +++ b/src/socket.io/posts/diffs.js @@ -8,6 +8,6 @@ module.exports = function (SocketPosts) { }; SocketPosts.showPostAt = function (socket, data, callback) { - posts.diffs.load(data.pid, data.since, callback); + posts.diffs.load(data.pid, data.since, socket.uid, callback); }; }; From 5069892b97cfb5d098c39cc3dea9496b51883a03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bar=C4=B1=C5=9F=20Soner=20U=C5=9Fakl=C4=B1?= <baris@nodebb.org> Date: Fri, 23 Feb 2018 15:34:31 -0500 Subject: [PATCH 40/41] pass correct uid to client side -1 spider,0 guest, >0 regular user --- src/middleware/header.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/header.js b/src/middleware/header.js index 7e192e449b..c769991753 100644 --- a/src/middleware/header.js +++ b/src/middleware/header.js @@ -83,7 +83,7 @@ module.exports = function (middleware) { }, user: function (next) { var userData = { - uid: 0, + uid: req.uid, username: '[[global:guest]]', userslug: '', fullname: '[[global:guest]]', From c938711f9c4b15100b4035546cc4581817800e1d Mon Sep 17 00:00:00 2001 From: Julian Lam <julian@nodebb.org> Date: Wed, 28 Feb 2018 15:56:55 -0500 Subject: [PATCH 41/41] not using Posts.parsePost for diffs parsing --- src/posts/diffs.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/posts/diffs.js b/src/posts/diffs.js index 1ce67f8e0f..1472e913e4 100644 --- a/src/posts/diffs.js +++ b/src/posts/diffs.js @@ -2,9 +2,11 @@ var async = require('async'); var validator = require('validator'); +var diff = require('diff'); var db = require('../database'); -var diff = require('diff'); +var plugins = require('../plugins'); +var translator = require('../translator'); module.exports = function (Posts) { Posts.diffs = {}; @@ -50,16 +52,26 @@ module.exports = function (Posts) { // Replace content with re-constructed content from that point in time data.post.content = data.diffs.reverse().reduce(function (content, diffString) { - return diff.applyPatch(content, diffString); + return diff.applyPatch(content, diffString, { + fuzzFactor: 1, + }); }, data.post.content); // Clear editor data (as it is outdated for this content) delete data.post.edited; data.post.editor = null; - Posts.parsePost(data.post, function (err, post) { - callback(err, post); - }); + data.post.content = String(data.post.content || ''); + + async.waterfall([ + function (next) { + plugins.fireHook('filter:parse.post', { postData: data.post }, next); + }, + function (data, next) { + data.postData.content = translator.escape(data.postData.content); + next(null, data.postData); + }, + ], callback); }); }; };