mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-06 15:42:52 +01:00
Merge branch 'master' into develop
This commit is contained in:
70
CHANGELOG.md
70
CHANGELOG.md
@@ -1,3 +1,73 @@
|
||||
#### v3.8.2 (2024-05-29)
|
||||
|
||||
##### Chores
|
||||
|
||||
* up composer (83facb7d)
|
||||
* up harmony (17ea61a0)
|
||||
* incrementing version number - v3.8.1 (527326f7)
|
||||
* update changelog for v3.8.1 (5ef3e0f3)
|
||||
* incrementing version number - v3.8.0 (e228a6eb)
|
||||
* incrementing version number - v3.7.5 (6882894d)
|
||||
* incrementing version number - v3.7.4 (6678744c)
|
||||
* incrementing version number - v3.7.3 (2d62b6f6)
|
||||
* incrementing version number - v3.7.2 (cc257e7e)
|
||||
* 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)
|
||||
|
||||
##### New Features
|
||||
|
||||
* show ignored/watched topics in topic list, closes #10974 (29dbe92d)
|
||||
* convert "All Votes Are Public" toggle to vote visibility (e0515080)
|
||||
|
||||
##### Bug Fixes
|
||||
|
||||
* wrong var for ignored (7969e62d)
|
||||
* reduce docker image size again and speed up build (56ef2bdd)
|
||||
* update thumb count when removing thumbs (6214336c)
|
||||
|
||||
##### Refactors
|
||||
|
||||
* render (2c0f8c91)
|
||||
|
||||
#### v3.8.1 (2024-05-15)
|
||||
|
||||
##### Chores
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "nodebb",
|
||||
"license": "GPL-3.0",
|
||||
"description": "NodeBB Forum",
|
||||
"version": "3.8.1",
|
||||
"version": "3.8.2",
|
||||
"homepage": "https://www.nodebb.org",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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:
|
||||
|
||||
33
public/openapi/write/posts/pid/upvoters.yaml
Normal file
33
public/openapi/write/posts/pid/upvoters.yaml
Normal 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
|
||||
|
||||
37
public/openapi/write/posts/pid/voters.yaml
Normal file
37
public/openapi/write/posts/pid/voters.yaml
Normal 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
|
||||
|
||||
@@ -25,7 +25,7 @@ ajaxify.widgets = { render: render };
|
||||
}
|
||||
ajaxify.go = function (url, callback, quiet) {
|
||||
// Automatically reconnect to socket and re-ajaxify on success
|
||||
if (!socket.connected) {
|
||||
if (!socket.connected && parseInt(app.user.uid, 10) >= 0) {
|
||||
app.reconnect();
|
||||
|
||||
if (ajaxify.reconnectAction) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,6 +16,7 @@ app = window.app || {};
|
||||
reconnectionAttempts: config.maxReconnectionAttempts,
|
||||
reconnectionDelay: config.reconnectionDelay,
|
||||
transports: config.socketioTransports,
|
||||
autoConnect: false,
|
||||
path: config.relative_path + '/socket.io',
|
||||
query: {
|
||||
_csrf: config.csrf_token,
|
||||
@@ -48,11 +49,12 @@ app = window.app || {};
|
||||
hooks = _hooks;
|
||||
if (parseInt(app.user.uid, 10) >= 0) {
|
||||
addHandlers();
|
||||
socket.connect();
|
||||
}
|
||||
});
|
||||
|
||||
window.app.reconnect = () => {
|
||||
if (socket.connected) {
|
||||
if (socket.connected || parseInt(app.user.uid, 10) < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -131,10 +131,10 @@ async function onConnect(socket) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (socket.uid) {
|
||||
if (socket.uid > 0) {
|
||||
socket.join(`uid_${socket.uid}`);
|
||||
socket.join('online_users');
|
||||
} else {
|
||||
} else if (socket.uid === 0) {
|
||||
socket.join('online_guests');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,105 +1,22 @@
|
||||
'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];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,15 +121,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card mb-3 pointer" data-container-html='<div class="card"><h5 class="card-header">\{{title}}</h5><div class="card-body">\{{body}}</div></div>'>
|
||||
<div class="card-header d-flex justify-content-between">
|
||||
<div class="card-header d-flex justify-content-between text-nowrap flex-wrap align-items-center">
|
||||
[[admin/extend/widgets:container.card-header]]
|
||||
<div class="d-flex gap-1 color-selector">
|
||||
<button data-class="text-bg-primary" class="btn btn-sm btn-primary"</button>
|
||||
<button data-class="" class="btn btn-sm btn-secondary"</button>
|
||||
<button data-class="text-bg-success" class="btn btn-sm btn-success"</button>
|
||||
<button data-class="text-bg-info" class="btn btn-sm btn-info"</button>
|
||||
<button data-class="text-bg-warning" class="btn btn-sm btn-warning"</button>
|
||||
<button data-class="text-bg-danger" class="btn btn-sm btn-danger"</button>
|
||||
<div class="d-flex gap-1 color-selector" style="height: 18px;">
|
||||
<button data-class="text-bg-primary" class="btn btn-sm btn-primary"></button>
|
||||
<button data-class="" class="btn btn-sm btn-secondary"></button>
|
||||
<button data-class="text-bg-success" class="btn btn-sm btn-success"></button>
|
||||
<button data-class="text-bg-info" class="btn btn-sm btn-info"></button>
|
||||
<button data-class="text-bg-warning" class="btn btn-sm btn-warning"></button>
|
||||
<button data-class="text-bg-danger" class="btn btn-sm btn-danger"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -138,9 +138,9 @@
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info pointer" data-container-html='<div class="alert alert-info">\{{body}}</div>'>
|
||||
<div class="d-flex justify-content-between">
|
||||
<div class="d-flex justify-content-between text-nowrap flex-wrap align-items-center">
|
||||
[[admin/extend/widgets:container.alert]]
|
||||
<div class="d-flex gap-1 color-selector">
|
||||
<div class="d-flex gap-1 color-selector" style="height: 18px;">
|
||||
<button data-class="alert-success" class="btn btn-sm btn-success"></button>
|
||||
<button data-class="alert-info" class="btn btn-sm btn-info"></button>
|
||||
<button data-class="alert-warning" class="btn btn-sm btn-warning"></button>
|
||||
|
||||
@@ -184,8 +184,8 @@ describe('Post\'s', () => {
|
||||
it('should get upvoters', (done) => {
|
||||
socketPosts.getUpvoters({ uid: globalModUid }, [postData.pid], (err, data) => {
|
||||
assert.ifError(err);
|
||||
assert.equal(data[0].otherCount, 0);
|
||||
assert.equal(data[0].usernames, 'upvoter');
|
||||
assert.equal(data.otherCount, 0);
|
||||
assert.equal(data.usernames, 'upvoter');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user