feat: add voters/upvoters v3 routes

closes #12423
This commit is contained in:
Barış Soner Uşaklı
2024-05-31 11:45:41 -04:00
parent 3854a43427
commit 1aaa6cbbc5
8 changed files with 186 additions and 99 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
};

View File

@@ -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);

View File

@@ -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);

View File

@@ -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];
}
};