Skip to content

Commit

Permalink
feat: closes #12491, add unban & unmute history
Browse files Browse the repository at this point in the history
to account/info page
  • Loading branch information
barisusakli committed Apr 23, 2024
1 parent bde9136 commit 985663f
Show file tree
Hide file tree
Showing 10 changed files with 176 additions and 85 deletions.
4 changes: 2 additions & 2 deletions install/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,10 @@
"nodebb-plugin-ntfy": "1.7.4",
"nodebb-plugin-spam-be-gone": "2.2.2",
"nodebb-rewards-essentials": "1.0.0",
"nodebb-theme-harmony": "1.2.50",
"nodebb-theme-harmony": "1.2.51",
"nodebb-theme-lavender": "7.1.8",
"nodebb-theme-peace": "2.2.4",
"nodebb-theme-persona": "13.3.15",
"nodebb-theme-persona": "13.3.16",
"nodebb-widget-essentials": "7.0.15",
"nodemailer": "6.9.13",
"nprogress": "0.2.0",
Expand Down
3 changes: 3 additions & 0 deletions public/language/en-GB/user.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{
"user-menu": "User menu",
"banned": "Banned",
"unbanned": "Unbanned",
"muted": "Muted",
"unmuted": "Unmuted",
"offline": "Offline",
"deleted": "Deleted",
"username": "User Name",
Expand Down Expand Up @@ -184,6 +186,7 @@
"info.no-ban-history": "This user has never been banned",
"info.banned-until": "Banned until %1",
"info.banned-expiry": "Expiry",
"info.ban-expired": "Ban expired",
"info.banned-permanently": "Banned permanently",
"info.banned-reason-label": "Reason",
"info.banned-no-reason": "No reason given.",
Expand Down
41 changes: 34 additions & 7 deletions public/src/admin/manage/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,11 @@ define('admin/manage/users', [
}

Benchpress.render('modals/temporary-ban', {}).then(function (html) {
bootbox.dialog({
className: 'ban-modal',
const modal = bootbox.dialog({
title: '[[user:ban-account]]',
message: html,
show: true,
onEscape: true,
buttons: {
close: {
label: '[[global:close]]',
Expand All @@ -272,7 +272,7 @@ define('admin/manage/users', [
submit: {
label: '[[admin/manage/users:alerts.button-ban-x, ' + uids.length + ']]',
callback: function () {
const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) {
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
data[cur.name] = cur.value;
return data;
}, {});
Expand Down Expand Up @@ -302,10 +302,37 @@ define('admin/manage/users', [
return false; // specifically to keep the menu open
}

Promise.all(uids.map(function (uid) {
return api.del('/users/' + uid + '/ban');
})).then(() => {
onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false);
Benchpress.render('modals/unban', {}).then(function (html) {
const modal = bootbox.dialog({
title: '[[user:unban-account]]',
message: html,
show: true,
onEscape: true,
buttons: {
close: {
label: '[[global:close]]',
className: 'btn-link',
},
submit: {
label: '[[user:unban-account]]',
callback: function () {
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
data[cur.name] = cur.value;
return data;
}, {});


Promise.all(uids.map(function (uid) {
return api.del('/users/' + uid + '/ban', {
reason: formData.reason || '',
});
})).then(() => {
onSuccess('[[admin/manage/users:alerts.unban-success]]', '.ban', false);
}).catch(alerts.error);
},
},
},
});
});
});

Expand Down
137 changes: 72 additions & 65 deletions public/src/modules/accounts/moderate.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,98 +11,105 @@ define('forum/account/moderate', [
AccountModerate.banAccount = function (theirid, onSuccess) {
theirid = theirid || ajaxify.data.theirid;

Benchpress.render('modals/temporary-ban', {}).then(function (html) {
bootbox.dialog({
className: 'ban-modal',
title: '[[user:ban-account]]',
message: html,
show: true,
buttons: {
close: {
label: '[[global:close]]',
className: 'btn-link',
},
submit: {
label: '[[user:ban-account]]',
callback: function () {
const formData = $('.ban-modal form').serializeArray().reduce(function (data, cur) {
data[cur.name] = cur.value;
return data;
}, {});
throwModal({
tpl: 'modals/temporary-ban',
title: '[[user:ban-account]]',
onSubmit: function (formData) {
const until = formData.length > 0 ? (
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
) : 0;
api.put('/users/' + theirid + '/ban', {
until: until,
reason: formData.reason || '',
}).then(() => {
if (typeof onSuccess === 'function') {
return onSuccess();
}

const until = formData.length > 0 ? (
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
) : 0;

api.put('/users/' + theirid + '/ban', {
until: until,
reason: formData.reason || '',
}).then(() => {
if (typeof onSuccess === 'function') {
return onSuccess();
}

ajaxify.refresh();
}).catch(alerts.error);
},
},
},
});
ajaxify.refresh();
}).catch(alerts.error);
},
});
};

AccountModerate.unbanAccount = function (theirid) {
api.del('/users/' + theirid + '/ban').then(() => {
ajaxify.refresh();
}).catch(alerts.error);
throwModal({
tpl: 'modals/unban',
title: '[[user:unban-account]]',
onSubmit: function (formData) {
api.del('/users/' + theirid + '/ban', {
reason: formData.reason || '',
}).then(() => {
ajaxify.refresh();
}).catch(alerts.error);
},
});
};

AccountModerate.muteAccount = function (theirid, onSuccess) {
theirid = theirid || ajaxify.data.theirid;
Benchpress.render('modals/temporary-mute', {}).then(function (html) {
bootbox.dialog({
className: 'mute-modal',
title: '[[user:mute-account]]',
throwModal({
tpl: 'modals/temporary-mute',
title: '[[user:mute-account]]',
onSubmit: function (formData) {
const until = formData.length > 0 ? (
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
) : 0;

api.put('/users/' + theirid + '/mute', {
until: until,
reason: formData.reason || '',
}).then(() => {
if (typeof onSuccess === 'function') {
return onSuccess();
}
ajaxify.refresh();
}).catch(alerts.error);
},
});
};

AccountModerate.unmuteAccount = function (theirid) {
throwModal({
tpl: 'modals/unmute',
title: '[[user:unmute-account]]',
onSubmit: function (formData) {
api.del('/users/' + theirid + '/mute', {
reason: formData.reason || '',
}).then(() => {
ajaxify.refresh();
}).catch(alerts.error);
},
});
};

function throwModal(options) {
Benchpress.render(options.tpl, {}).then(function (html) {
const modal = bootbox.dialog({
title: options.title,
message: html,
show: true,
onEscape: true,
buttons: {
close: {
label: '[[global:close]]',
className: 'btn-link',
},
submit: {
label: '[[user:mute-account]]',
label: options.title,
callback: function () {
const formData = $('.mute-modal form').serializeArray().reduce(function (data, cur) {
const formData = modal.find('form').serializeArray().reduce(function (data, cur) {
data[cur.name] = cur.value;
return data;
}, {});

const until = formData.length > 0 ? (
Date.now() + (formData.length * 1000 * 60 * 60 * (parseInt(formData.unit, 10) ? 24 : 1))
) : 0;

api.put('/users/' + theirid + '/mute', {
until: until,
reason: formData.reason || '',
}).then(() => {
if (typeof onSuccess === 'function') {
return onSuccess();
}
ajaxify.refresh();
}).catch(alerts.error);
options.onSubmit(formData);
},
},
},
});
});
};

AccountModerate.unmuteAccount = function (theirid) {
api.del('/users/' + theirid + '/mute').then(() => {
ajaxify.refresh();
}).catch(alerts.error);
};
}

return AccountModerate;
});
18 changes: 16 additions & 2 deletions src/api/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,8 @@ usersAPI.unban = async function (caller, data) {
throw new Error('[[error:no-privileges]]');
}

await user.bans.unban(data.uid);
const unbanData = await user.bans.unban(data.uid, data.reason);
await db.setObjectField(`uid:${data.uid}:unban:${unbanData.timestamp}`, 'fromUid', caller.uid);

sockets.in(`uid_${data.uid}`).emit('event:unbanned');

Expand Down Expand Up @@ -283,6 +284,7 @@ usersAPI.mute = async function (caller, data) {
const now = Date.now();
const muteKey = `uid:${data.uid}:mute:${now}`;
const muteData = {
type: 'mute',
fromUid: caller.uid,
uid: data.uid,
timestamp: now,
Expand Down Expand Up @@ -315,7 +317,19 @@ usersAPI.unmute = async function (caller, data) {
}

await db.deleteObjectFields(`user:${data.uid}`, ['mutedUntil', 'mutedReason']);

const now = Date.now();
const unmuteKey = `uid:${data.uid}:unmute:${now}`;
const unmuteData = {
type: 'unmute',
fromUid: caller.uid,
uid: data.uid,
timestamp: now,
};
if (data.reason) {
unmuteData.reason = data.reason;
}
await db.sortedSetAdd(`uid:${data.uid}:unmutes:timestamp`, now, unmuteKey);
await db.setObject(unmuteKey, unmuteData);
await events.log({
type: 'user-unmute',
uid: caller.uid,
Expand Down
28 changes: 22 additions & 6 deletions src/user/bans.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ module.exports = function (User) {

const banKey = `uid:${uid}:ban:${now}`;
const banData = {
type: 'ban',
uid: uid,
timestamp: now,
expire: until > now ? until : 0,
Expand Down Expand Up @@ -63,24 +64,39 @@ module.exports = function (User) {
return banData;
};

User.bans.unban = async function (uids) {
uids = Array.isArray(uids) ? uids : [uids];
User.bans.unban = async function (uids, reason = '') {
const isArray = Array.isArray(uids);
uids = isArray ? uids : [uids];
const userData = await User.getUsersFields(uids, ['email:confirmed']);

await db.setObject(uids.map(uid => `user:${uid}`), { 'banned:expire': 0 });

const now = Date.now();
const unbanDataArray = [];
/* eslint-disable no-await-in-loop */
for (const user of userData) {
const systemGroupsToJoin = [
'registered-users',
(parseInt(user['email:confirmed'], 10) === 1 ? 'verified-users' : 'unverified-users'),
];
await groups.leave(groups.BANNED_USERS, user.uid);
// An unbanned user would lost its previous "Global Moderator" status
await groups.join(systemGroupsToJoin, user.uid);
const unbanKey = `uid:${user.uid}:unban:${now}`;
const unbanData = {
type: 'unban',
uid: user.uid,
reason,
timestamp: now,
};
await Promise.all([
db.sortedSetAdd(`uid:${user.uid}:unbans:timestamp`, now, unbanKey),
db.setObject(unbanKey, unbanData),
groups.leave(groups.BANNED_USERS, user.uid),
// An unbanned user would lost its previous "Global Moderator" status
groups.join(systemGroupsToJoin, user.uid),
]);
unbanDataArray.push(unbanData);
}

await db.sortedSetRemove(['users:banned', 'users:banned:expire'], uids);
return isArray ? unbanDataArray : unbanDataArray[0];
};

User.bans.isBanned = async function (uids) {
Expand Down
2 changes: 1 addition & 1 deletion src/user/data.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ module.exports = function (User) {
user.banned_until = unban ? 0 : user['banned:expire'];
user.banned_until_readable = user.banned_until && !unban ? utils.toISOString(user.banned_until) : 'Not Banned';
if (unban) {
await User.bans.unban(user.uid);
await User.bans.unban(user.uid, '[[user:info.ban-expired]]');
user.banned = false;
}
}
Expand Down
8 changes: 6 additions & 2 deletions src/user/info.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ module.exports = function (User) {
User.getModerationHistory = async function (uid) {
let [flags, bans, mutes] = await Promise.all([
db.getSortedSetRevRangeWithScores(`flags:byTargetUid:${uid}`, 0, 19),
db.getSortedSetRevRange(`uid:${uid}:bans:timestamp`, 0, 19),
db.getSortedSetRevRange(`uid:${uid}:mutes:timestamp`, 0, 19),
db.getSortedSetRevRange([
`uid:${uid}:bans:timestamp`, `uid:${uid}:unbans:timestamp`,
], 0, 19),
db.getSortedSetRevRange([
`uid:${uid}:mutes:timestamp`, `uid:${uid}:unmutes:timestamp`,
], 0, 19),
]);

// Get pids from flag objects
Expand Down
10 changes: 10 additions & 0 deletions src/views/modals/unban.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<form class="form">
<div class="row">
<div class="col-12">
<div class="mb-3">
<label class="form-label" for="reason">[[admin/manage/users:temp-ban.reason]]</label>
<input type="text" class="form-control" id="reason" name="reason" />
</div>
</div>
</div>
</form>

0 comments on commit 985663f

Please sign in to comment.