diff --git a/public/openapi/write.yaml b/public/openapi/write.yaml index 79657b519e..c59b9bce29 100644 --- a/public/openapi/write.yaml +++ b/public/openapi/write.yaml @@ -180,6 +180,10 @@ paths: $ref: 'write/posts/pid/move.yaml' /posts/{pid}/vote: $ref: 'write/posts/pid/vote.yaml' + /posts/{pid}/voters: + $ref: 'write/posts/pid/voters.yaml' + /posts/{pid}/upvoters: + $ref: 'write/posts/pid/upvoters.yaml' /posts/{pid}/bookmark: $ref: 'write/posts/pid/bookmark.yaml' /posts/{pid}/diffs: diff --git a/public/openapi/write/posts/pid/upvoters.yaml b/public/openapi/write/posts/pid/upvoters.yaml new file mode 100644 index 0000000000..d005e33529 --- /dev/null +++ b/public/openapi/write/posts/pid/upvoters.yaml @@ -0,0 +1,33 @@ +get: + tags: + - posts + summary: get upvoter usernames of a post + description: This is used for getting a list of upvoter usernames for the vote tooltip + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + responses: + '200': + description: Usernames of upvoters of post + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + otherCount: + type: number + usernames: + type: array + cutoff: + type: number + diff --git a/public/openapi/write/posts/pid/voters.yaml b/public/openapi/write/posts/pid/voters.yaml new file mode 100644 index 0000000000..868b587c36 --- /dev/null +++ b/public/openapi/write/posts/pid/voters.yaml @@ -0,0 +1,37 @@ +get: + tags: + - posts + summary: get voters of a post + description: This returns the upvoters and downvoters of a post if the user has permission to view them + parameters: + - in: path + name: pid + schema: + type: string + required: true + description: a valid post id + example: 2 + responses: + '200': + description: Data about upvoters and downvoters of the post + content: + application/json: + schema: + type: object + properties: + status: + $ref: ../../../components/schemas/Status.yaml#/Status + response: + type: object + properties: + upvoteCount: + type: number + downvoteCount: + type: number + showDownvotes: + type: boolean + upvoters: + type: array + downvoters: + type: array + diff --git a/public/src/client/topic/votes.js b/public/src/client/topic/votes.js index b4365697ed..29876ac148 100644 --- a/public/src/client/topic/votes.js +++ b/public/src/client/topic/votes.js @@ -35,15 +35,15 @@ define('forum/topic/votes', [ $this.attr('title', ''); } - socket.emit('posts.getUpvoters', [pid], function (err, data) { + api.get(`/posts/${pid}/upvoters`, {}, function (err, data) { if (err) { if (err.message === '[[error:no-privileges]]') { return; } return alerts.error(err); } - if (_showTooltip[pid] && data.length) { - createTooltip($this, data[0]); + if (_showTooltip[pid] && data) { + createTooltip($this, data); } }); } @@ -101,7 +101,7 @@ define('forum/topic/votes', [ }; Votes.showVotes = function (pid) { - socket.emit('posts.getVoters', { pid: pid }, function (err, data) { + api.get(`/posts/${pid}/voters`, {}, function (err, data) { if (err) { if (err.message === '[[error:no-privileges]]') { return; diff --git a/src/api/posts.js b/src/api/posts.js index 603e3bf2aa..791fcdb598 100644 --- a/src/api/posts.js +++ b/src/api/posts.js @@ -3,6 +3,7 @@ const validator = require('validator'); const _ = require('lodash'); +const db = require('../database'); const utils = require('../utils'); const user = require('../user'); const posts = require('../posts'); @@ -306,6 +307,95 @@ postsAPI.unvote = async function (caller, data) { return await apiHelpers.postCommand(caller, 'unvote', 'voted', '', data); }; +postsAPI.getVoters = async function (caller, data) { + if (!data || !data.pid) { + throw new Error('[[error:invalid-data]]'); + } + const { pid } = data; + const cid = await posts.getCidByPid(pid); + if (!await canSeeVotes(caller.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`) : [], + ]); + + const [upvoters, downvoters] = await Promise.all([ + user.getUsersFields(upvoteUids, ['username', 'userslug', 'picture']), + user.getUsersFields(downvoteUids, ['username', 'userslug', 'picture']), + ]); + + return { + upvoteCount: upvoters.length, + downvoteCount: downvoters.length, + showDownvotes: showDownvotes, + upvoters: upvoters, + downvoters: downvoters, + }; +}; + +postsAPI.getUpvoters = async function (caller, data) { + if (!data.pid) { + throw new Error('[[error:invalid-data]]'); + } + const { pid } = data; + const cid = await posts.getCidByPid(pid); + if (!await canSeeVotes(caller.uid, cid)) { + throw new Error('[[error:no-privileges]]'); + } + + let upvotedUids = (await posts.getUpvotedUidsByPids([pid]))[0]; + const cutoff = 6; + if (!upvotedUids.length) { + return { + otherCount: 0, + usernames: [], + cutoff, + }; + } + let otherCount = 0; + if (upvotedUids.length > cutoff) { + otherCount = upvotedUids.length - (cutoff - 1); + upvotedUids = upvotedUids.slice(0, cutoff - 1); + } + + const usernames = await user.getUsernamesByUids(upvotedUids); + return { + otherCount, + usernames, + cutoff, + }; +}; + +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.voteVisibility === 'all' || + (meta.config.voteVisibility === 'loggedin' && parseInt(uid, 10) > 0) + ) + ) + ); + return isArray ? checks : checks[0]; +} + postsAPI.bookmark = async function (caller, data) { return await apiHelpers.postCommand(caller, 'bookmark', 'bookmarked', '', data); }; diff --git a/src/controllers/write/posts.js b/src/controllers/write/posts.js index 529eabfe44..1dc8cf6800 100644 --- a/src/controllers/write/posts.js +++ b/src/controllers/write/posts.js @@ -131,6 +131,16 @@ Posts.unvote = async (req, res) => { helpers.formatApiResponse(200, res); }; +Posts.getVoters = async (req, res) => { + const data = await api.posts.getVoters(req, { pid: req.params.pid }); + helpers.formatApiResponse(200, res, data); +}; + +Posts.getUpvoters = async (req, res) => { + const data = await api.posts.getUpvoters(req, { pid: req.params.pid }); + helpers.formatApiResponse(200, res, data); +}; + Posts.bookmark = async (req, res) => { const data = await mock(req); await api.posts.bookmark(req, data); diff --git a/src/routes/write/posts.js b/src/routes/write/posts.js index a834d26088..e573bbb9b0 100644 --- a/src/routes/write/posts.js +++ b/src/routes/write/posts.js @@ -26,6 +26,8 @@ module.exports = function () { setupApiRoute(router, 'put', '/:pid/vote', [...middlewares, middleware.checkRequired.bind(null, ['delta'])], controllers.write.posts.vote); setupApiRoute(router, 'delete', '/:pid/vote', middlewares, controllers.write.posts.unvote); + setupApiRoute(router, 'get', '/:pid/voters', [middleware.assert.post], controllers.write.posts.getVoters); + setupApiRoute(router, 'get', '/:pid/upvoters', [middleware.assert.post], controllers.write.posts.getUpvoters); setupApiRoute(router, 'put', '/:pid/bookmark', middlewares, controllers.write.posts.bookmark); setupApiRoute(router, 'delete', '/:pid/bookmark', middlewares, controllers.write.posts.unbookmark); diff --git a/src/socket.io/posts/votes.js b/src/socket.io/posts/votes.js index cd0b6f3b9b..a7426e49ba 100644 --- a/src/socket.io/posts/votes.js +++ b/src/socket.io/posts/votes.js @@ -1,105 +1,16 @@ 'use strict'; -const _ = require('lodash'); - -const db = require('../../database'); -const user = require('../../user'); -const posts = require('../../posts'); -const privileges = require('../../privileges'); -const meta = require('../../meta'); +const api = require('../../api'); +const sockets = require('../index'); module.exports = function (SocketPosts) { SocketPosts.getVoters = async function (socket, data) { - if (!data || !data.pid) { - throw new Error('[[error:invalid-data]]'); - } - 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`) : [], - ]); - - const [upvoters, downvoters] = await Promise.all([ - user.getUsersFields(upvoteUids, ['username', 'userslug', 'picture']), - user.getUsersFields(downvoteUids, ['username', 'userslug', 'picture']), - ]); - - return { - upvoteCount: upvoters.length, - downvoteCount: downvoters.length, - showDownvotes: showDownvotes, - upvoters: upvoters, - downvoters: downvoters, - }; + sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/voters'); + return await api.posts.getVoters(socket, { pid: data.pid }); }; SocketPosts.getUpvoters = async function (socket, pids) { - if (!Array.isArray(pids)) { - throw new Error('[[error:invalid-data]]'); - } - - 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 []; - } - const cutoff = 6; - const sliced = data.map((uids) => { - let otherCount = 0; - if (uids.length > cutoff) { - otherCount = uids.length - (cutoff - 1); - uids = uids.slice(0, cutoff - 1); - } - return { - otherCount, - uids, - }; - }); - - const uniqUids = _.uniq(_.flatten(sliced.map(d => d.uids))); - const usernameMap = _.zipObject(uniqUids, await user.getUsernamesByUids(uniqUids)); - const result = sliced.map( - data => ({ - otherCount: data.otherCount, - cutoff: cutoff, - usernames: data.uids.map(uid => usernameMap[uid]), - }) - ); - return result; + sockets.warnDeprecated(socket, 'GET /api/v3/posts/:pid/upvoters'); + return await api.posts.getUpvoters(socket, { pid: pids[0] }); }; - - 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.voteVisibility === 'all' || - (meta.config.voteVisibility === 'loggedin' && parseInt(uid, 10) > 0) - ) - ) - ); - return isArray ? checks : checks[0]; - } };