mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-05-07 19:15:28 +02:00
Merge commit '110ce66ac7dcc99c44c1b9417ed0b78015ce85f6' into v3.x
This commit is contained in:
54
CHANGELOG.md
54
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"id": "ID: <small>optional</small>",
|
||||
|
||||
"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: <br/><li><a class="dropdown-item" href="https://myforum.com">Link 1</a></li>",
|
||||
|
||||
@@ -743,9 +743,7 @@ BanMuteArray:
|
||||
example: "#f44336"
|
||||
until:
|
||||
type: number
|
||||
untilReadable:
|
||||
type: string
|
||||
timestampReadable:
|
||||
untilISO:
|
||||
type: string
|
||||
timestampISO:
|
||||
type: string
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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('<li><a href="#" class="dropdown-item" role="menuitem">[[admin/menu:search.start-typing]]</a></li>');
|
||||
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('<li><a href="#" class="dropdown-item" role="menuitem">[[users:no-users-found]]</a></li>');
|
||||
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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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/, '')
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' })));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-12">
|
||||
<label class="form-label">[[admin/settings/navigation:groups]]</label>
|
||||
<label class="form-label">[[admin/settings/navigation:show-to-groups]]</label>
|
||||
|
||||
<select name="groups" class="form-select" size="10" multiple>
|
||||
{{{ each enabled.groups }}}
|
||||
|
||||
@@ -54,7 +54,10 @@
|
||||
|
||||
{{{ if isAdminOrGlobalMod }}}
|
||||
<li>
|
||||
<a href="#" class="dropdown-item rounded-1 chat-ip-button" role="menuitem"><span class="d-inline-flex align-items-center gap-2"><i class="fa fa-fw fa-info-circle text-muted"></i> [[modules:chat.show-ip]]</span></a>
|
||||
<a href="#" class="dropdown-item rounded-1 chat-ip-button" role="menuitem">
|
||||
<span class="d-inline-flex align-items-center gap-2 show"><i class="fa fa-fw fa-info-circle text-muted"></i> [[modules:chat.show-ip]]</span>
|
||||
<span class="d-inline-flex align-items-center gap-2 copy hidden"><i class="fa fa-fw fa-copy text-muted"></i> <span class="copy-ip-text"></span></span>
|
||||
</a>
|
||||
</li>
|
||||
{{{ end }}}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
{{{ if ./teaser }}}
|
||||
<div class="teaser-content text-sm line-clamp-3 text-break">
|
||||
<span href="#" class="text-decoration-none">{buildAvatar(./teaser.user, "14px", true)}</span>
|
||||
{buildAvatar(./teaser.user, "14px", true, "align-middle")}
|
||||
<strong class="text-xs fw-semibold teaser-username">{./teaser.user.username}:</strong>
|
||||
{./teaser.content}
|
||||
</div>
|
||||
|
||||
@@ -2457,7 +2457,7 @@ describe('User', () => {
|
||||
|
||||
describe('.toggle()', () => {
|
||||
it('should toggle block', (done) => {
|
||||
socketUser.toggleBlock({ uid: 1 }, { blockerUid: 1, blockeeUid: blockeeUid }, (err) => {
|
||||
socketUser.toggleBlock({ uid: 1 }, { blockerUid: 1, blockeeUid: blockeeUid, action: 'block' }, (err) => {
|
||||
assert.ifError(err);
|
||||
User.blocks.is(blockeeUid, 1, (err, blocked) => {
|
||||
assert.ifError(err);
|
||||
@@ -2468,7 +2468,7 @@ describe('User', () => {
|
||||
});
|
||||
|
||||
it('should toggle block', (done) => {
|
||||
socketUser.toggleBlock({ uid: 1 }, { blockerUid: 1, blockeeUid: blockeeUid }, (err) => {
|
||||
socketUser.toggleBlock({ uid: 1 }, { blockerUid: 1, blockeeUid: blockeeUid, action: 'unblock' }, (err) => {
|
||||
assert.ifError(err);
|
||||
User.blocks.is(blockeeUid, 1, (err, blocked) => {
|
||||
assert.ifError(err);
|
||||
|
||||
Reference in New Issue
Block a user