feat: ability to mute users

new mute privilege
isekai-main
Barış Soner Uşaklı 3 years ago
parent feaf3068f8
commit be6bbabd0e

@ -10,6 +10,7 @@
"upload-files": "Upload Files",
"signature": "Signature",
"ban": "Ban",
"mute": "Mute",
"invite": "Invite",
"search-content": "Search Content",
"search-users": "Search Users",

@ -62,7 +62,7 @@
"create.password": "Password",
"create.password-confirm": "Confirm Password",
"temp-ban.length": "Ban Length",
"temp-ban.length": "Length",
"temp-ban.reason": "Reason <span class=\"text-muted\">(Optional)</span>",
"temp-ban.hours": "Hours",
"temp-ban.days": "Days",

@ -124,6 +124,9 @@
"already-unbookmarked": "You have already unbookmarked this post",
"cant-ban-other-admins": "You can't ban other admins!",
"cant-mute-other-admins": "You can't mute other admins!",
"user-muted-for-hours": "You have been muted, you will be able to post in %1 hour(s)",
"user-muted-for-minutes": "You have been muted, you will be able to post in %1 minute(s)",
"cant-make-banned-users-admin": "You can't make banned users admin.",
"cant-remove-last-admin": "You are the only administrator. Add another user as an administrator before removing yourself as admin",
"account-deletion-disabled": "Account deletion is disabled",

@ -13,6 +13,8 @@
"ban_account": "Ban Account",
"ban_account_confirm": "Do you really want to ban this user?",
"unban_account": "Unban Account",
"mute_account": "Mute Account",
"unmute_account": "Unmute Account",
"delete_account": "Delete Account",
"delete_account_as_admin": "Delete <strong>Account</strong>",
"delete_content": "Delete Account <strong>Content</strong>",
@ -171,6 +173,7 @@
"info.banned-permanently": "Banned permanently",
"info.banned-reason-label": "Reason",
"info.banned-no-reason": "No reason given.",
"info.muted-no-reason": "No reason given.",
"info.username-history": "Username History",
"info.email-history": "Email History",
"info.moderation-note": "Moderation Note",

@ -68,6 +68,8 @@ paths:
$ref: 'write/users/uid/follow.yaml'
/users/{uid}/ban:
$ref: 'write/users/uid/ban.yaml'
/users/{uid}/mute:
$ref: 'write/users/uid/mute.yaml'
/users/{uid}/tokens:
$ref: 'write/users/uid/tokens.yaml'
/users/{uid}/tokens/{token}:

@ -0,0 +1,61 @@
put:
tags:
- users
summary: mute a user
parameters:
- in: path
name: uid
schema:
type: integer
required: true
description: uid of the user to mute
example: 2
requestBody:
content:
application/json:
schema:
type: object
properties:
until:
type: number
description: UNIX timestamp of the mute expiry
example: 1585775608076
reason:
type: string
example: the reason for the mute
responses:
'200':
description: successfully muted user
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object
delete:
tags:
- users
summary: unmute a user
parameters:
- in: path
name: uid
schema:
type: integer
required: true
description: uid of the user to unmute
example: 2
responses:
'200':
description: successfully unmuted user
content:
application/json:
schema:
type: object
properties:
status:
$ref: ../../../components/schemas/Status.yaml#/Status
response:
type: object

@ -54,7 +54,9 @@ define('forum/account/header', [
components.get('account/ban').on('click', function () {
banAccount(ajaxify.data.theirid);
});
components.get('account/mute').on('click', muteAccount);
components.get('account/unban').on('click', unbanAccount);
components.get('account/unmute').on('click', unmuteAccount);
components.get('account/delete-account').on('click', handleDeleteEvent.bind(null, 'account'));
components.get('account/delete-content').on('click', handleDeleteEvent.bind(null, 'content'));
components.get('account/delete-all').on('click', handleDeleteEvent.bind(null, 'purge'));
@ -177,6 +179,49 @@ define('forum/account/header', [
}).catch(alerts.error);
}
function muteAccount() {
Benchpress.render('admin/partials/temporary-mute', {}).then(function (html) {
bootbox.dialog({
className: 'mute-modal',
title: '[[user:mute_account]]',
message: html,
show: true,
buttons: {
close: {
label: '[[global:close]]',
className: 'btn-link',
},
submit: {
label: '[[user:mute_account]]',
callback: function () {
const formData = $('.mute-modal 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/' + ajaxify.data.theirid + '/mute', {
until: until,
reason: formData.reason || '',
}).then(() => {
ajaxify.refresh();
}).catch(alerts.error);
},
},
},
});
});
}
function unmuteAccount() {
api.del('/users/' + ajaxify.data.theirid + '/mute').then(() => {
ajaxify.refresh();
}).catch(alerts.error);
}
function flagAccount() {
require(['flags'], function (flags) {
flags.showFlagModal({

@ -225,6 +225,53 @@ usersAPI.unban = async function (caller, data) {
});
};
usersAPI.mute = async function (caller, data) {
if (!await privileges.users.hasMutePrivilege(caller.uid)) {
throw new Error('[[error:no-privileges]]');
} else if (await user.isAdministrator(data.uid)) {
throw new Error('[[error:cant-mute-other-admins]]');
}
await db.setObject(`user:${data.uid}`, {
mutedUntil: data.until,
mutedReason: data.reason || '[[user:info.muted-no-reason]]',
});
await events.log({
type: 'user-mute',
uid: caller.uid,
targetUid: data.uid,
ip: caller.ip,
reason: data.reason || undefined,
});
plugins.hooks.fire('action:user.muted', {
callerUid: caller.uid,
ip: caller.ip,
uid: data.uid,
until: data.until > 0 ? data.until : undefined,
reason: data.reason || undefined,
});
};
usersAPI.unmute = async function (caller, data) {
if (!await privileges.users.hasMutePrivilege(caller.uid)) {
throw new Error('[[error:no-privileges]]');
}
await db.deleteObjectFields(`user:${data.uid}`, ['mutedUntil', 'mutedReason']);
await events.log({
type: 'user-unmute',
uid: caller.uid,
targetUid: data.uid,
ip: caller.ip,
});
plugins.hooks.fire('action:user.unmuted', {
callerUid: caller.uid,
ip: caller.ip,
uid: data.uid,
});
};
async function isPrivilegedOrSelfAndPasswordMatch(caller, data) {
const { uid } = caller;
const isSelf = parseInt(uid, 10) === parseInt(data.uid, 10);

@ -73,6 +73,7 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
userData.isSelfOrAdminOrGlobalModerator = isSelf || isAdmin || isGlobalModerator;
userData.canEdit = results.canEdit;
userData.canBan = results.canBanUser;
userData.canMute = results.canMuteUser;
userData.canFlag = (await privileges.users.canFlag(callerUID, userData.uid)).flag;
userData.canChangePassword = isAdmin || (isSelf && !meta.config['password:disableEdit']);
userData.isSelf = isSelf;
@ -95,6 +96,7 @@ helpers.getUserDataByUserSlug = async function (userslug, callerUID, query = {})
userData.sso = results.sso.associations;
userData.banned = Boolean(userData.banned);
userData.muted = parseInt(userData.mutedUntil, 10) > Date.now();
userData.website = escape(userData.website);
userData.websiteLink = !userData.website.startsWith('http') ? `http://${userData.website}` : userData.website;
userData.websiteName = userData.website.replace(validator.escape('http://'), '').replace(validator.escape('https://'), '');
@ -144,6 +146,7 @@ async function getAllData(uid, callerUID) {
sso: plugins.hooks.fire('filter:auth.list', { uid: uid, associations: [] }),
canEdit: privileges.users.canEdit(callerUID, uid),
canBanUser: privileges.users.canBanUser(callerUID, uid),
canMuteUser: privileges.users.canMuteUser(callerUID, uid),
isBlocked: user.blocks.is(uid, callerUID),
canViewInfo: privileges.global.can('view:users:info', callerUID),
hasPrivateChat: messaging.hasPrivateChat(callerUID, uid),

@ -111,6 +111,16 @@ Users.unban = async (req, res) => {
helpers.formatApiResponse(200, res);
};
Users.mute = async (req, res) => {
await api.users.mute(req, { ...req.body, uid: req.params.uid });
helpers.formatApiResponse(200, res);
};
Users.unmute = async (req, res) => {
await api.users.unmute(req, { ...req.body, uid: req.params.uid });
helpers.formatApiResponse(200, res);
};
Users.generateToken = async (req, res) => {
await hasAdminPrivilege(req.uid, 'settings');
if (parseInt(req.params.uid, 10) !== parseInt(req.user.uid, 10)) {

@ -36,6 +36,8 @@ events.types = [
'user-removeAdmin',
'user-ban',
'user-unban',
'user-mute',
'user-unmute',
'user-delete',
'user-deleteAccount',
'user-deleteContent',

@ -26,6 +26,7 @@ privsGlobal.privilegeLabels = [
{ name: '[[admin/manage/privileges:view-groups]]' },
{ name: '[[admin/manage/privileges:allow-local-login]]' },
{ name: '[[admin/manage/privileges:ban]]' },
{ name: '[[admin/manage/privileges:mute]]' },
{ name: '[[admin/manage/privileges:view-users-info]]' },
];
@ -44,6 +45,7 @@ privsGlobal.userPrivilegeList = [
'view:groups',
'local:login',
'ban',
'mute',
'view:users:info',
];

@ -109,6 +109,21 @@ privsUsers.canBanUser = async function (callerUid, uid) {
return data.canBan;
};
privsUsers.canMuteUser = async function (callerUid, uid) {
const privsGlobal = require('./global');
const [canMute, isTargetAdmin] = await Promise.all([
privsGlobal.can('mute', callerUid),
privsUsers.isAdministrator(uid),
]);
const data = await plugins.hooks.fire('filter:user.canMuteUser', {
canMute: canMute && !isTargetAdmin,
callerUid: callerUid,
uid: uid,
});
return data.canMute;
};
privsUsers.canFlag = async function (callerUid, uid) {
const [userReputation, targetPrivileged, reporterPrivileged] = await Promise.all([
user.getUserField(callerUid, 'reputation'),
@ -126,6 +141,7 @@ privsUsers.canFlag = async function (callerUid, uid) {
};
privsUsers.hasBanPrivilege = async uid => await hasGlobalPrivilege('ban', uid);
privsUsers.hasMutePrivilege = async uid => await hasGlobalPrivilege('mute', uid);
privsUsers.hasInvitePrivilege = async uid => await hasGlobalPrivilege('invite', uid);
async function hasGlobalPrivilege(privilege, uid) {

@ -36,6 +36,9 @@ function authenticatedRoutes() {
setupApiRoute(router, 'put', '/:uid/ban', [...middlewares, middleware.assert.user], controllers.write.users.ban);
setupApiRoute(router, 'delete', '/:uid/ban', [...middlewares, middleware.assert.user], controllers.write.users.unban);
setupApiRoute(router, 'put', '/:uid/mute', [...middlewares, middleware.assert.user], controllers.write.users.mute);
setupApiRoute(router, 'delete', '/:uid/mute', [...middlewares, middleware.assert.user], controllers.write.users.unmute);
setupApiRoute(router, 'post', '/:uid/tokens', [...middlewares, middleware.assert.user], controllers.write.users.generateToken);
setupApiRoute(router, 'delete', '/:uid/tokens/:token', [...middlewares, middleware.assert.user], controllers.write.users.deleteToken);

@ -15,7 +15,7 @@ const intFields = [
'uid', 'postcount', 'topiccount', 'reputation', 'profileviews',
'banned', 'banned:expire', 'email:confirmed', 'joindate', 'lastonline',
'lastqueuetime', 'lastposttime', 'followingCount', 'followerCount',
'blocksCount', 'passwordExpiry',
'blocksCount', 'passwordExpiry', 'mutedUntil',
];
module.exports = function (User) {
@ -25,7 +25,7 @@ module.exports = function (User) {
'aboutme', 'signature', 'uploadedpicture', 'profileviews', 'reputation',
'postcount', 'topiccount', 'lastposttime', 'banned', 'banned:expire',
'status', 'flags', 'followerCount', 'followingCount', 'cover:url',
'cover:position', 'groupTitle',
'cover:position', 'groupTitle', 'mutedUntil', 'mutedReason',
];
User.guestData = {

@ -18,7 +18,7 @@ module.exports = function (User) {
return;
}
const [userData, isAdminOrMod] = await Promise.all([
User.getUserFields(uid, ['uid', 'banned', 'joindate', 'email', 'reputation'].concat([field])),
User.getUserFields(uid, ['uid', 'banned', 'mutedUntil', 'joindate', 'email', 'reputation'].concat([field])),
privileges.categories.isAdminOrMod(cid, uid),
]);
@ -35,6 +35,16 @@ module.exports = function (User) {
}
const now = Date.now();
if (userData.mutedUntil > now) {
let muteLeft = ((userData.mutedUntil - now) / (1000 * 60));
if (muteLeft > 60) {
muteLeft = (muteLeft / 60).toFixed(0);
throw new Error(`[[error:user-muted-for-hours, ${muteLeft}]]`);
} else {
throw new Error(`[[error:user-muted-for-minutes, ${muteLeft.toFixed(0)}]]`);
}
}
if (now - userData.joindate < meta.config.initialPostDelay * 1000) {
throw new Error(`[[error:user-too-new, ${meta.config.initialPostDelay}]]`);
}

@ -3,15 +3,13 @@
<thead>
{{{ if !isAdminPriv }}}
<tr class="privilege-table-header">
<th colspan="3"></th>
<th class="arrowed" colspan="6">
[[admin/manage/categories:privileges.section-posting]]
</th>
<th class="arrowed" colspan="7">
[[admin/manage/categories:privileges.section-viewing]]
</th>
<th class="arrowed" colspan="2">
[[admin/manage/categories:privileges.section-moderation]]
<th class="privilege-filters btn-toolbar" colspan="100">
<!-- IF privileges.columnCountGroupOther -->
<button type="button" data-filter="19,99" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-other]]</button>
<!-- END -->
<button type="button" data-filter="16,18" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-moderation]]</button>
<button type="button" data-filter="3,8" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-posting]]</button>
<button type="button" data-filter="9,15" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-viewing]]</button>
</th>
</tr><tr><!-- zebrastripe reset --></tr>
{{{ end }}}
@ -65,9 +63,18 @@
<label>[[admin/manage/privileges:user-privileges]]</label>
<table class="table table-striped privilege-table">
<thead>
{{{ if !isAdminPriv }}}
<tr class="privilege-table-header">
<th colspan="15"></th>
<th class="privilege-filters btn-toolbar" colspan="100">
<!-- IF privileges.columnCountGroupOther -->
<button type="button" data-filter="21,99" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-other]]</button>
<!-- END -->
<button type="button" data-filter="18,20" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-moderation]]</button>
<button type="button" data-filter="10,17" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-posting]]</button>
<button type="button" data-filter="3,9" class="btn btn-default pull-right">[[admin/manage/categories:privileges.section-viewing]]</button>
</th>
</tr><tr><!-- zebrastripe reset --></tr>
{{{ end }}}
<tr>
<th colspan="2">[[admin/manage/categories:privileges.section-user]]</th>
<th class="text-center">[[admin/manage/privileges:select-clear-all]]</th>

@ -0,0 +1,27 @@
<form class="form">
<div class="row">
<div class="col-xs-4">
<div class="form-group">
<label for="length">[[admin/manage/users:temp-ban.length]]</label>
<input class="form-control" id="length" name="length" type="number" min="0" value="1" />
</div>
</div>
<div class="col-xs-8">
<div class="form-group">
<label for="reason">[[admin/manage/users:temp-ban.reason]]</label>
<input type="text" class="form-control" id="reason" name="reason" />
</div>
</div>
</div>
<div class="row">
<div class="col-sm-4 text-center">
<div class="form-group units">
<label>[[admin/manage/users:temp-ban.hours]]</label>
<input type="radio" name="unit" value="0" checked />
&nbsp;&nbsp;
<label>[[admin/manage/users:temp-ban.days]]</label>
<input type="radio" name="unit" value="1" />
</div>
</div>
</div>
</form>
Loading…
Cancel
Save