diff --git a/CHANGELOG.md b/CHANGELOG.md index e41c2eec9e..9dde4f436a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,57 @@ +#### v3.7.3 (2024-04-03) + +##### Chores + +* up ntfy (0058ca68) +* incrementing version number - v3.7.2 (cc257e7e) +* update changelog for v3.7.2 (277e1787) +* incrementing version number - v3.7.1 (712365a5) +* incrementing version number - v3.7.0 (9a6153d7) +* incrementing version number - v3.6.7 (86a17e38) +* incrementing version number - v3.6.6 (6604bf37) +* incrementing version number - v3.6.5 (6c653625) +* incrementing version number - v3.6.4 (83d131b4) +* incrementing version number - v3.6.3 (fc7d2bfd) +* incrementing version number - v3.6.2 (0f577a57) +* incrementing version number - v3.6.1 (f1a69468) +* incrementing version number - v3.6.0 (4cdf85f8) +* incrementing version number - v3.5.3 (ed0e8783) +* incrementing version number - v3.5.2 (52fbb2da) +* incrementing version number - v3.5.1 (4c543488) +* incrementing version number - v3.5.0 (d06fb4f0) +* incrementing version number - v3.4.3 (5c984250) +* incrementing version number - v3.4.2 (3f0dac38) +* incrementing version number - v3.4.1 (01e69574) +* incrementing version number - v3.4.0 (fd9247c5) +* incrementing version number - v3.3.9 (5805e770) +* incrementing version number - v3.3.8 (a5603565) +* incrementing version number - v3.3.7 (b26f1744) +* incrementing version number - v3.3.6 (7fb38792) +* incrementing version number - v3.3.4 (a67f84ea) +* incrementing version number - v3.3.3 (f94d239b) +* incrementing version number - v3.3.2 (ec9dac97) +* incrementing version number - v3.3.1 (151cc68f) +* incrementing version number - v3.3.0 (fc1ad70f) +* incrementing version number - v3.2.3 (b06d3e63) +* incrementing version number - v3.2.2 (758ecfcd) +* incrementing version number - v3.2.1 (20145074) +* incrementing version number - v3.2.0 (9ecac38e) +* incrementing version number - v3.1.7 (0b4e81ab) +* incrementing version number - v3.1.6 (b3a3b130) +* incrementing version number - v3.1.5 (ec19343a) +* incrementing version number - v3.1.4 (2452783c) +* incrementing version number - v3.1.3 (3b4e9d3f) +* incrementing version number - v3.1.2 (40fa3489) +* incrementing version number - v3.1.1 (40250733) +* incrementing version number - v3.1.0 (0cb386bd) +* incrementing version number - v3.0.1 (26f6ea49) +* incrementing version number - v3.0.0 (224e08cd) + +##### Bug Fixes + +* change digest to use posts sorting first (3aae9234) +* #12452, fix admin/mod image change (c206ccdd) + #### v3.7.2 (2024-03-27) ##### Chores diff --git a/install/package.json b/install/package.json index eedae6903e..50dc673850 100644 --- a/install/package.json +++ b/install/package.json @@ -103,10 +103,10 @@ "nodebb-plugin-ntfy": "1.7.4", "nodebb-plugin-spam-be-gone": "2.2.1", "nodebb-rewards-essentials": "1.0.0", - "nodebb-theme-harmony": "1.2.44", + "nodebb-theme-harmony": "1.2.49", "nodebb-theme-lavender": "7.1.8", "nodebb-theme-peace": "2.2.4", - "nodebb-theme-persona": "13.3.11", + "nodebb-theme-persona": "13.3.14", "nodebb-widget-essentials": "7.0.15", "nodemailer": "6.9.11", "nprogress": "0.2.0", diff --git a/public/language/en-GB/admin/settings/navigation.json b/public/language/en-GB/admin/settings/navigation.json index 931ac5f4ba..3a71061ecf 100644 --- a/public/language/en-GB/admin/settings/navigation.json +++ b/public/language/en-GB/admin/settings/navigation.json @@ -10,7 +10,7 @@ "id": "ID: optional", "properties": "Properties:", - "groups": "Groups:", + "show-to-groups": "Show to Groups:", "open-new-window": "Open in a new window", "dropdown": "Dropdown", "dropdown-placeholder": "Place your dropdown menu items below, ie:
<li><a class="dropdown-item" href="https://myforum.com">Link 1</a></li>", diff --git a/public/openapi/components/schemas/UserObject.yaml b/public/openapi/components/schemas/UserObject.yaml index bdd52bc2d1..9b217cee8b 100644 --- a/public/openapi/components/schemas/UserObject.yaml +++ b/public/openapi/components/schemas/UserObject.yaml @@ -743,9 +743,7 @@ BanMuteArray: example: "#f44336" until: type: number - untilReadable: - type: string - timestampReadable: + untilISO: type: string timestampISO: type: string diff --git a/public/openapi/read/categories.yaml b/public/openapi/read/categories.yaml index bc8b254049..f1126e9dfb 100644 --- a/public/openapi/read/categories.yaml +++ b/public/openapi/read/categories.yaml @@ -136,6 +136,8 @@ get: properties: pid: type: number + tid: + type: number timestamp: type: number content: @@ -182,6 +184,8 @@ get: topic: type: object properties: + tid: + type: number slug: type: string title: @@ -196,12 +200,16 @@ get: description: An ISO 8601 formatted date string (complementing `timestamp`) pid: type: number + tid: + type: number index: type: number description: The index of the post topic: type: object properties: + tid: + type: number slug: type: string title: diff --git a/public/openapi/read/index.yaml b/public/openapi/read/index.yaml index f980d2eff4..a984fb35dc 100644 --- a/public/openapi/read/index.yaml +++ b/public/openapi/read/index.yaml @@ -138,6 +138,8 @@ get: properties: pid: type: number + tid: + type: number timestamp: type: number content: @@ -184,6 +186,8 @@ get: topic: type: object properties: + tid: + type: number slug: type: string title: @@ -198,12 +202,16 @@ get: description: An ISO 8601 formatted date string (complementing `timestamp`) pid: type: number + tid: + type: number index: type: number description: The index of the post topic: type: object properties: + tid: + type: number slug: type: string title: diff --git a/public/openapi/read/user/userslug/info.yaml b/public/openapi/read/user/userslug/info.yaml index afec9bc2bd..110e252be5 100644 --- a/public/openapi/read/user/userslug/info.yaml +++ b/public/openapi/read/user/userslug/info.yaml @@ -35,8 +35,6 @@ get: timestampISO: type: string description: An ISO 8601 formatted date string (complementing `timestamp`) - timestampReadable: - type: string additionalProperties: description: Contextual data is added to this object (such as topic data, etc.) bans: diff --git a/public/src/client/account/blocks.js b/public/src/client/account/blocks.js index 921065fd37..91b0745a19 100644 --- a/public/src/client/account/blocks.js +++ b/public/src/client/account/blocks.js @@ -11,12 +11,19 @@ define('forum/account/blocks', [ Blocks.init = function () { header.init(); const blockListEl = $('[component="blocks/search/list"]'); + const startTypingEl = blockListEl.find('[component="blocks/start-typing"]'); + const noUsersEl = blockListEl.find('[component="blocks/no-users"]'); - $('#user-search').on('keyup', function () { + $('#user-search').on('keyup', utils.debounce(function () { const username = this.value; + if (!username) { - return blockListEl.translateHtml('
  • [[admin/menu:search.start-typing]]
  • '); + blockListEl.find('[component="blocks/search/match"]').remove(); + startTypingEl.removeClass('hidden'); + noUsersEl.addClass('hidden'); + return; } + startTypingEl.addClass('hidden'); api.get('/api/users', { query: username, searchBy: 'username', @@ -26,8 +33,10 @@ define('forum/account/blocks', [ return alerts.error(err); } if (!data.users.length) { - return blockListEl.translateHtml('
  • [[users:no-users-found]]
  • '); + noUsersEl.removeClass('hidden'); + return; } + noUsersEl.addClass('hidden'); // Only show first 10 matches if (data.matchCount > 10) { data.users.length = 10; @@ -36,25 +45,36 @@ define('forum/account/blocks', [ app.parseAndTranslate('account/blocks', 'edit', { edit: data.users, }, function (html) { - $('.block-edit').html(html); + blockListEl.find('[component="blocks/search/match"]').remove(); + html.insertAfter(noUsersEl); }); }); + }, 200)); + + $('.block-edit').on('click', '[data-action="block"], [data-action="unblock"]', async function () { + const uid = parseInt(this.getAttribute('data-uid'), 10); + const action = $(this).attr('data-action'); + const currentBtn = $(this); + await performBlock(uid, action); + currentBtn.addClass('hidden').siblings('[data-action]').removeClass('hidden'); + Blocks.refreshList(); }); - $('.block-edit').on('click', '[data-action="toggle"]', function () { - const uid = parseInt(this.getAttribute('data-uid'), 10); - socket.emit('user.toggleBlock', { - blockeeUid: uid, - blockerUid: ajaxify.data.uid, - }, Blocks.refreshList); + $('#users-container').on('click', '[data-action="unblock"]', async function () { + await performBlock($(this).attr('data-uid'), $(this).attr('data-action')); + Blocks.refreshList(); }); }; - Blocks.refreshList = function (err) { - if (err) { - return alerts.error(err); - } + async function performBlock(uid, action) { + return socket.emit('user.toggleBlock', { + blockeeUid: uid, + blockerUid: ajaxify.data.uid, + action: action, + }).catch(alerts.error); + } + Blocks.refreshList = function () { $.get(config.relative_path + '/api/' + ajaxify.currentPage) .done(function (payload) { app.parseAndTranslate('account/blocks', 'users', payload, function (html) { diff --git a/public/src/client/account/header.js b/public/src/client/account/header.js index 9aae8d6a8f..dfa888120b 100644 --- a/public/src/client/account/header.js +++ b/public/src/client/account/header.js @@ -56,8 +56,8 @@ define('forum/account/header', [ components.get('account/delete-content').on('click', () => AccountsDelete.content(ajaxify.data.theirid)); components.get('account/delete-all').on('click', () => AccountsDelete.purge(ajaxify.data.theirid)); components.get('account/flag').on('click', flagAccount); - components.get('account/block').on('click', toggleBlockAccount); - components.get('account/unblock').on('click', toggleBlockAccount); + components.get('account/block').on('click', () => toggleBlockAccount('block')); + components.get('account/unblock').on('click', () => toggleBlockAccount('unblock')); }; function selectActivePill() { @@ -129,10 +129,11 @@ define('forum/account/header', [ }); } - function toggleBlockAccount() { + function toggleBlockAccount(action) { socket.emit('user.toggleBlock', { blockeeUid: ajaxify.data.uid, blockerUid: app.user.uid, + action, }, function (err, blocked) { if (err) { return alerts.error(err); diff --git a/public/src/client/chats.js b/public/src/client/chats.js index a5cb58caf0..abaae9f818 100644 --- a/public/src/client/chats.js +++ b/public/src/client/chats.js @@ -225,18 +225,23 @@ define('forum/chats', [ Chats.addIPHandler = function (container) { container.off('click', '.chat-ip-button') - .on('click', '.chat-ip-button', async function () { + .on('click', '.chat-ip-button', async function (ev) { + ev.stopPropagation(); const ipEl = $(this); + const ipCopyText = ipEl.find('.copy .copy-ip-text'); let ip = ipEl.attr('data-ip'); if (ip) { navigator.clipboard.writeText(ip); - ipEl.translateText('[[global:copied]]'); - setTimeout(() => ipEl.text(ip), 2000); + ipCopyText.translateText('[[global:copied]]'); + setTimeout(() => ipCopyText.text(ip), 2000); return; } const mid = ipEl.parents('[data-mid]').attr('data-mid'); ({ ip } = await api.get(`/chats/${ajaxify.data.roomId}/messages/${mid}/ip`)); - ipEl.text(ip).attr('data-ip', ip); + ipEl.attr('data-ip', ip); + ipEl.find('.show').addClass('hidden'); + ipEl.find('.copy').removeClass('hidden'); + ipCopyText.text(ip); }); }; @@ -248,7 +253,8 @@ define('forum/chats', [ } container.off('click', '[data-action="copy-link"]') - .on('click', '[data-action="copy-link"]', function () { + .on('click', '[data-action="copy-link"]', function (ev) { + ev.stopPropagation(); const copyEl = $(this); const mid = copyEl.attr('data-mid'); if (mid) { @@ -257,7 +263,8 @@ define('forum/chats', [ }); container.off('click', '[data-action="copy-text"]') - .on('click', '[data-action="copy-text"]', function () { + .on('click', '[data-action="copy-text"]', function (ev) { + ev.stopPropagation(); const copyEl = $(this); const messageEl = copyEl.parents('[data-mid]'); if (messageEl.length) { diff --git a/public/src/client/topic/votes.js b/public/src/client/topic/votes.js index 2ead12f804..b4365697ed 100644 --- a/public/src/client/topic/votes.js +++ b/public/src/client/topic/votes.js @@ -37,6 +37,9 @@ define('forum/topic/votes', [ socket.emit('posts.getUpvoters', [pid], function (err, data) { if (err) { + if (err.message === '[[error:no-privileges]]') { + return; + } return alerts.error(err); } if (_showTooltip[pid] && data.length) { @@ -98,7 +101,7 @@ define('forum/topic/votes', [ }; Votes.showVotes = function (pid) { - socket.emit('posts.getVoters', { pid: pid, cid: ajaxify.data.cid }, function (err, data) { + socket.emit('posts.getVoters', { pid: pid }, function (err, data) { if (err) { if (err.message === '[[error:no-privileges]]') { return; diff --git a/src/api/users.js b/src/api/users.js index febcf290e6..2bd4779409 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -41,6 +41,10 @@ usersAPI.create = async function (caller, data) { }; usersAPI.get = async (caller, { uid }) => { + const canView = await privileges.global.can('view:users', caller.uid); + if (!canView) { + throw new Error('[[error:no-privileges]]'); + } const userData = await user.getUserData(uid); return await user.hidePrivateData(userData, caller.uid); }; @@ -601,6 +605,7 @@ usersAPI.search = async function (caller, data) { throw new Error('[[error:no-privileges]]'); } return await user.search({ + uid: caller.uid, query: data.query, searchBy: data.searchBy || 'username', page: data.page || 1, diff --git a/src/categories/recentreplies.js b/src/categories/recentreplies.js index 7b04974659..cb7056bfbb 100644 --- a/src/categories/recentreplies.js +++ b/src/categories/recentreplies.js @@ -116,9 +116,10 @@ module.exports = function (Categories) { if (teaser) { teaser.cid = topicData[index].cid; teaser.parentCids = cidToRoot[teaser.cid]; - teaser.tid = undefined; - teaser.uid = undefined; + teaser.tid = topicData[index].tid; + teaser.uid = topicData[index].uid; teaser.topic = { + tid: topicData[index].tid, slug: topicData[index].slug, title: topicData[index].title, }; diff --git a/src/controllers/helpers.js b/src/controllers/helpers.js index a4a2287160..c17e701b79 100644 --- a/src/controllers/helpers.js +++ b/src/controllers/helpers.js @@ -396,6 +396,7 @@ helpers.setCategoryTeaser = function (category) { url: `${nconf.get('relative_path')}/post/${post.pid}`, timestampISO: post.timestampISO, pid: post.pid, + tid: post.tid, index: post.index, topic: post.topic, user: post.user, diff --git a/src/controllers/users.js b/src/controllers/users.js index f55296bdb9..41194e6c82 100644 --- a/src/controllers/users.js +++ b/src/controllers/users.js @@ -25,7 +25,7 @@ usersController.index = async function (req, res, next) { if (req.query.query) { await usersController.search(req, res, next); - } else if (sectionToController[section]) { + } else if (sectionToController.hasOwnProperty(section) && sectionToController[section]) { await sectionToController[section](req, res, next); } else { await usersController.getUsersSortedByJoinDate(req, res, next); diff --git a/src/flags.js b/src/flags.js index 833bb7edd8..e3932f3bd8 100644 --- a/src/flags.js +++ b/src/flags.js @@ -795,12 +795,10 @@ Flags.resolveUserPostFlags = async function (uid, callerUid) { if (meta.config['flags:autoResolveOnBan']) { await batch.processSortedSet(`uid:${uid}:posts`, async (pids) => { let postData = await posts.getPostsFields(pids, ['pid', 'flagId']); - postData = postData.filter(p => p && p.flagId); + postData = postData.filter(p => p && p.flagId && parseInt(p.flagId, 10)); for (const postObj of postData) { - if (parseInt(postObj.flagId, 10)) { - // eslint-disable-next-line no-await-in-loop - await Flags.update(postObj.flagId, callerUid, { state: 'resolved' }); - } + // eslint-disable-next-line no-await-in-loop + await Flags.update(postObj.flagId, callerUid, { state: 'resolved' }); } }, { batch: 500, diff --git a/src/middleware/render.js b/src/middleware/render.js index 6b1181d3af..21ff25170d 100644 --- a/src/middleware/render.js +++ b/src/middleware/render.js @@ -214,7 +214,7 @@ module.exports = function (middleware) { templateValues.isAdmin = results.user.isAdmin; templateValues.isGlobalMod = results.user.isGlobalMod; templateValues.showModMenu = results.user.isAdmin || results.user.isGlobalMod || results.user.isMod; - templateValues.canChat = results.privileges.chat && meta.config.disableChat !== 1; + templateValues.canChat = (results.privileges.chat || results.privileges['chat:privileged']) && meta.config.disableChat !== 1; templateValues.user = results.user; templateValues.userJSON = jsesc(JSON.stringify(results.user), { isScriptContext: true }); templateValues.useCustomCSS = meta.config.useCustomCSS && meta.config.customCSS; diff --git a/src/middleware/user.js b/src/middleware/user.js index a9573e397c..342730c507 100644 --- a/src/middleware/user.js +++ b/src/middleware/user.js @@ -203,8 +203,12 @@ module.exports = function (middleware) { if (uid <= 0) { return next(); } - const userslug = await user.getUserField(uid, 'userslug'); - if (!userslug) { + const [canView, userslug] = await Promise.all([ + privileges.global.can('view:users', req.uid), + user.getUserField(uid, 'userslug'), + ]); + + if (!userslug || (!canView && req.uid !== uid)) { return next(); } const path = req.url.replace(/^\/api/, '') diff --git a/src/notifications.js b/src/notifications.js index 612c67d95f..fdb9998248 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -197,9 +197,13 @@ async function pushToUids(uids, notification) { await db.sortedSetsRemoveRangeByScore(unreadKeys.concat(readKeys), '-inf', cutoff); const websockets = require('./socket.io'); if (websockets.server) { - uids.forEach((uid) => { + await Promise.all(uids.map(async (uid) => { + await plugins.hooks.fire('filter:sockets.sendNewNoticationToUid', { + uid, + notification, + }); websockets.in(`uid_${uid}`).emit('event:new_notification', notification); - }); + })); } } @@ -223,7 +227,10 @@ async function pushToUids(uids, notification) { // Remove uid from recipients list if they have blocked the user triggering the notification uids = await User.blocks.filterUids(notification.from, uids); - const data = await plugins.hooks.fire('filter:notification.push', { notification: notification, uids: uids }); + const data = await plugins.hooks.fire('filter:notification.push', { + notification, + uids, + }); if (!data || !data.notification || !data.uids || !data.uids.length) { return; } diff --git a/src/posts/delete.js b/src/posts/delete.js index 66c8269334..94f73cf494 100644 --- a/src/posts/delete.js +++ b/src/posts/delete.js @@ -236,7 +236,7 @@ module.exports = function (Posts) { } async function resolveFlags(postData, uid) { - const flaggedPosts = postData.filter(p => parseInt(p.flagId, 10)); + const flaggedPosts = postData.filter(p => p && parseInt(p.flagId, 10)); await Promise.all(flaggedPosts.map(p => flags.update(p.flagId, uid, { state: 'resolved' }))); } }; diff --git a/src/routes/user.js b/src/routes/user.js index 49f551dc59..040f6cb063 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -5,7 +5,11 @@ const helpers = require('./helpers'); const { setupPageRoute } = helpers; module.exports = function (app, name, middleware, controllers) { - const middlewares = [middleware.exposeUid, middleware.canViewUsers, middleware.buildAccountData]; + const middlewares = [ + middleware.exposeUid, + middleware.canViewUsers, + middleware.buildAccountData, + ]; const accountMiddlewares = [ ...middlewares, middleware.ensureLoggedIn, diff --git a/src/socket.io/helpers.js b/src/socket.io/helpers.js index 36b3384d24..7286b79e8e 100644 --- a/src/socket.io/helpers.js +++ b/src/socket.io/helpers.js @@ -28,8 +28,7 @@ SocketHelpers.notifyNew = async function (uid, type, result) { async function notifyUids(uid, uids, type, result) { const post = result.posts[0]; - const { tid } = post.topic; - const { cid } = post.topic; + const { tid, cid } = post.topic; uids = await privileges.topics.filterUids('topics:read', tid, uids); const watchStateUids = uids; @@ -49,14 +48,28 @@ async function notifyUids(uid, uids, type, result) { post.ip = undefined; - data.uidsTo.forEach((toUid) => { - post.categoryWatchState = categoryWatchStates[toUid]; - post.topic.isFollowing = topicFollowState[toUid]; - websockets.in(`uid_${toUid}`).emit('event:new_post', result); - if (result.topic && type === 'newTopic') { - websockets.in(`uid_${toUid}`).emit('event:new_topic', result.topic); + await Promise.all(data.uidsTo.map(async (toUid) => { + const copyResult = _.cloneDeep(result); + const postToUid = copyResult.posts[0]; + postToUid.categoryWatchState = categoryWatchStates[toUid]; + postToUid.topic.isFollowing = topicFollowState[toUid]; + + await plugins.hooks.fire('filter:sockets.sendNewPostToUid', { + uid: toUid, + uidFrom: uid, + post: postToUid, + }); + + websockets.in(`uid_${toUid}`).emit('event:new_post', copyResult); + if (copyResult.topic && type === 'newTopic') { + await plugins.hooks.fire('filter:sockets.sendNewTopicToUid', { + uid: toUid, + uidFrom: uid, + topic: copyResult.topic, + }); + websockets.in(`uid_${toUid}`).emit('event:new_topic', copyResult.topic); } - }); + })); } async function getWatchStates(uids, tid, cid) { diff --git a/src/socket.io/posts/votes.js b/src/socket.io/posts/votes.js index 4c971efd82..05e2ce5198 100644 --- a/src/socket.io/posts/votes.js +++ b/src/socket.io/posts/votes.js @@ -10,14 +10,14 @@ const meta = require('../../meta'); module.exports = function (SocketPosts) { SocketPosts.getVoters = async function (socket, data) { - if (!data || !data.pid || !data.cid) { + if (!data || !data.pid) { throw new Error('[[error:invalid-data]]'); } - const showDownvotes = !meta.config['downvote:disabled']; - const canSeeVotes = meta.config.votesArePublic || await privileges.categories.isAdminOrMod(data.cid, socket.uid); - if (!canSeeVotes) { + const cid = await posts.getCidByPid(data.pid); + if (!await canSeeVotes(socket.uid, cid)) { throw new Error('[[error:no-privileges]]'); } + const showDownvotes = !meta.config['downvote:disabled']; const [upvoteUids, downvoteUids] = await Promise.all([ db.getSetMembers(`pid:${data.pid}:upvote`), showDownvotes ? db.getSetMembers(`pid:${data.pid}:downvote`) : [], @@ -42,21 +42,12 @@ module.exports = function (SocketPosts) { throw new Error('[[error:invalid-data]]'); } - const [cids, data, isAdmin] = await Promise.all([ - posts.getCidsByPids(pids), - posts.getUpvotedUidsByPids(pids), - privileges.users.isAdministrator(socket.uid), - ]); - - if (!isAdmin) { - const isAllowed = await privileges.categories.isUserAllowedTo( - 'topics:read', _.uniq(cids), socket.uid - ); - if (isAllowed.includes(false)) { - throw new Error('[[error:no-privileges]]'); - } + const cids = await posts.getCidsByPids(pids); + if ((await canSeeVotes(socket.uid, cids)).includes(false)) { + throw new Error('[[error:no-privileges]]'); } + const data = await posts.getUpvotedUidsByPids(pids); if (!data.length) { return []; } @@ -84,4 +75,24 @@ module.exports = function (SocketPosts) { ); return result; }; + + async function canSeeVotes(uid, cids) { + const isArray = Array.isArray(cids); + if (!isArray) { + cids = [cids]; + } + const uniqCids = _.uniq(cids); + const [canRead, isAdmin, isMod] = await Promise.all([ + privileges.categories.isUserAllowedTo( + 'topics:read', uniqCids, uid + ), + privileges.users.isAdministrator(uid), + privileges.users.isModerator(uid, cids), + ]); + const cidToAllowed = _.zipObject(uniqCids, canRead); + const checks = cids.map( + (cid, index) => isAdmin || isMod[index] || (cidToAllowed[cid] && !!meta.config.votesArePublic) + ); + return isArray ? checks : checks[0]; + } }; diff --git a/src/socket.io/user/profile.js b/src/socket.io/user/profile.js index 5d4c4624f3..277b75ccc4 100644 --- a/src/socket.io/user/profile.js +++ b/src/socket.io/user/profile.js @@ -41,8 +41,16 @@ module.exports = function (SocketUser) { SocketUser.toggleBlock = async function (socket, data) { const isBlocked = await user.blocks.is(data.blockeeUid, data.blockerUid); - await user.blocks.can(socket.uid, data.blockerUid, data.blockeeUid, isBlocked ? 'unblock' : 'block'); - await user.blocks[isBlocked ? 'remove' : 'add'](data.blockeeUid, data.blockerUid); + const { action, blockerUid, blockeeUid } = data; + if (action !== 'block' && action !== 'unblock') { + throw new Error('[[error:unknow-block-action]]'); + } + await user.blocks.can(socket.uid, blockerUid, blockeeUid, action); + if (data.action === 'block') { + await user.blocks.add(blockeeUid, blockerUid); + } else if (data.action === 'unblock') { + await user.blocks.remove(blockeeUid, blockerUid); + } return !isBlocked; }; }; diff --git a/src/topics/delete.js b/src/topics/delete.js index 5190afd1ff..4e7f5d1400 100644 --- a/src/topics/delete.js +++ b/src/topics/delete.js @@ -5,25 +5,38 @@ const db = require('../database'); const user = require('../user'); const posts = require('../posts'); const categories = require('../categories'); +const flags = require('../flags'); const plugins = require('../plugins'); const batch = require('../batch'); module.exports = function (Topics) { Topics.delete = async function (tid, uid) { - const cid = await Topics.getTopicField(tid, 'cid'); - await removeTopicPidsFromCid(tid, cid); - await Topics.setTopicFields(tid, { - deleted: 1, - deleterUid: uid, - deletedTimestamp: Date.now(), - }); + const [cid, pids] = await Promise.all([ + Topics.getTopicField(tid, 'cid'), + Topics.getPids(tid), + ]); + await Promise.all([ + db.sortedSetRemove(`cid:${cid}:pids`, pids), + resolveTopicPostFlags(pids, uid), + Topics.setTopicFields(tid, { + deleted: 1, + deleterUid: uid, + deletedTimestamp: Date.now(), + }), + ]); + await categories.updateRecentTidForCid(cid); }; - async function removeTopicPidsFromCid(tid, cid) { - const pids = await Topics.getPids(tid); - await db.sortedSetRemove(`cid:${cid}:pids`, pids); + async function resolveTopicPostFlags(pids, uid) { + await batch.processArray(pids, async (pids) => { + const postData = await posts.getPostsFields(pids, ['pid', 'flagId']); + const flaggedPosts = postData.filter(p => p && parseInt(p.flagId, 10)); + await Promise.all(flaggedPosts.map(p => flags.update(p.flagId, uid, { state: 'resolved' }))); + }, { + batch: 500, + }); } async function addTopicPidsToCid(tid, cid) { diff --git a/src/topics/tags.js b/src/topics/tags.js index 6cbe54b646..daab4e5f77 100644 --- a/src/topics/tags.js +++ b/src/topics/tags.js @@ -416,6 +416,7 @@ module.exports = function (Topics) { tags = await Topics.filterTags(tags, cid); await Topics.addTags(tags, [tid]); + plugins.hooks.fire('action:topic.updateTags', { tags, tid }); }; Topics.deleteTopicTags = async function (tid) { diff --git a/src/user/follow.js b/src/user/follow.js index f3b031a582..2fc74f1424 100644 --- a/src/user/follow.js +++ b/src/user/follow.js @@ -21,31 +21,37 @@ module.exports = function (User) { if (parseInt(uid, 10) === parseInt(theiruid, 10)) { throw new Error('[[error:you-cant-follow-yourself]]'); } - const exists = await User.exists(theiruid); + const [exists, isFollowing] = await Promise.all([ + User.exists(theiruid), + User.isFollowing(uid, theiruid), + ]); if (!exists) { throw new Error('[[error:no-user]]'); } - const isFollowing = await User.isFollowing(uid, theiruid); + + await plugins.hooks.fire('filter:user.toggleFollow', { + type, + uid, + theiruid, + isFollowing, + }); + if (type === 'follow') { if (isFollowing) { throw new Error('[[error:already-following]]'); } const now = Date.now(); - await Promise.all([ - db.sortedSetAddBulk([ - [`following:${uid}`, now, theiruid], - [`followers:${theiruid}`, now, uid], - ]), + await db.sortedSetAddBulk([ + [`following:${uid}`, now, theiruid], + [`followers:${theiruid}`, now, uid], ]); } else { if (!isFollowing) { throw new Error('[[error:not-following]]'); } - await Promise.all([ - db.sortedSetRemoveBulk([ - [`following:${uid}`, theiruid], - [`followers:${theiruid}`, uid], - ]), + await db.sortedSetRemoveBulk([ + [`following:${uid}`, theiruid], + [`followers:${theiruid}`, uid], ]); } diff --git a/src/user/info.js b/src/user/info.js index 3abd580d02..d4667bd83f 100644 --- a/src/user/info.js +++ b/src/user/info.js @@ -86,7 +86,6 @@ module.exports = function (User) { flagObj.pid = flagObj.value; flagObj.timestamp = flagObj.score; flagObj.timestampISO = new Date(flagObj.score).toISOString(); - flagObj.timestampReadable = new Date(flagObj.score).toString(); delete flagObj.value; delete flagObj.score; @@ -105,8 +104,7 @@ module.exports = function (User) { return data.map((banObj, index) => { banObj.user = usersData[index]; banObj.until = parseInt(banObj.expire, 10); - banObj.untilReadable = new Date(banObj.until).toString(); - banObj.timestampReadable = new Date(parseInt(banObj.timestamp, 10)).toString(); + banObj.untilISO = utils.toISOString(banObj.until); banObj.timestampISO = utils.toISOString(banObj.timestamp); banObj.reason = validator.escape(String(banObj.reason || '')) || noReasonLangKey; return banObj; diff --git a/src/user/notifications.js b/src/user/notifications.js index d35ba3ed26..1da0bd63cb 100644 --- a/src/user/notifications.js +++ b/src/user/notifications.js @@ -214,7 +214,7 @@ UserNotifications.sendTopicNotificationToFollowers = async function (uid, topicD const notifObj = await notifications.create({ type: 'new-topic', - bodyShort: `[[notifications:user-posted-topic, ${postData.user.displayname}, ${title}]]`, + bodyShort: translator.compile('notifications:user-posted-topic', postData.user.displayname, title), bodyLong: postData.content, pid: postData.pid, path: `/post/${postData.pid}`, diff --git a/src/user/search.js b/src/user/search.js index 2713b3a8dd..ec0b81d025 100644 --- a/src/user/search.js +++ b/src/user/search.js @@ -60,7 +60,19 @@ module.exports = function (User) { uids = uids.slice(start, stop); } - const userData = await User.getUsers(uids, uid); + const [userData, blocks] = await Promise.all([ + User.getUsers(uids, uid), + User.blocks.list(uid), + ]); + + if (blocks.length) { + userData.forEach((user) => { + if (user) { + user.isBlocked = blocks.includes(user.uid); + } + }); + } + searchResult.timing = (process.elapsedTimeSince(startTime) / 1000).toFixed(2); searchResult.users = userData.filter(user => user && user.uid > 0); return searchResult; diff --git a/src/views/admin/settings/navigation.tpl b/src/views/admin/settings/navigation.tpl index 431b052361..6af7e60511 100644 --- a/src/views/admin/settings/navigation.tpl +++ b/src/views/admin/settings/navigation.tpl @@ -75,7 +75,7 @@
    - +