diff --git a/public/language/en-GB/user.json b/public/language/en-GB/user.json index 58c3759fe8..14f68b35db 100644 --- a/public/language/en-GB/user.json +++ b/public/language/en-GB/user.json @@ -1,5 +1,6 @@ { "banned": "Banned", + "muted": "Muted", "offline": "Offline", "deleted": "Deleted", "username": "User Name", @@ -173,6 +174,10 @@ "info.banned-permanently": "Banned permanently", "info.banned-reason-label": "Reason", "info.banned-no-reason": "No reason given.", + "info.mute-history": "Recent Mute History", + "info.no-mute-history": "This user has never been muted", + "info.muted-until": "Muted until %1", + "info.muted-expiry": "Expiry", "info.muted-no-reason": "No reason given.", "info.username-history": "Username History", "info.email-history": "Email History", diff --git a/src/api/users.js b/src/api/users.js index 4cf9622d70..aa1ea25687 100644 --- a/src/api/users.js +++ b/src/api/users.js @@ -231,11 +231,24 @@ usersAPI.mute = async function (caller, data) { } else if (await user.isAdministrator(data.uid)) { throw new Error('[[error:cant-mute-other-admins]]'); } + const reason = data.reason || '[[user:info.muted-no-reason]]'; await db.setObject(`user:${data.uid}`, { mutedUntil: data.until, - mutedReason: data.reason || '[[user:info.muted-no-reason]]', + mutedReason: reason, }); - + const now = Date.now(); + const muteKey = `uid:${data.uid}:mute:${now}`; + const muteData = { + fromUid: caller.uid, + uid: data.uid, + timestamp: now, + expire: data.until, + }; + if (data.reason) { + muteData.reason = reason; + } + await db.sortedSetAdd(`uid:${data.uid}:mutes:timestamp`, now, muteKey); + await db.setObject(muteKey, muteData); await events.log({ type: 'user-mute', uid: caller.uid, diff --git a/src/flags.js b/src/flags.js index 932905067d..b5e3c53b64 100644 --- a/src/flags.js +++ b/src/flags.js @@ -757,6 +757,7 @@ Flags.getHistory = async function (flagId) { // Append ban history and username change data history = await mergeBanHistory(history, targetUid, uids); + history = await mergeMuteHistory(history, targetUid, uids); history = await mergeUsernameEmailChanges(history, targetUid, uids); const userData = await user.getUsersFields(uids, ['username', 'userslug', 'picture']); @@ -854,21 +855,39 @@ Flags.notify = async function (flagObj, uid, notifySelf = false) { }; async function mergeBanHistory(history, targetUid, uids) { - let recentBans = await db.getSortedSetRevRange(`uid:${targetUid}:bans:timestamp`, 0, 19); - recentBans = await db.getObjects(recentBans); + return await mergeBanMuteHistory(history, uids, { + set: `uid:${targetUid}:bans:timestamp`, + label: '[[user:banned]]', + reasonDefault: '[[user:info.banned-no-reason]]', + expiryKey: '[[user:info.banned-expiry]]', + }); +} + +async function mergeMuteHistory(history, targetUid, uids) { + return await mergeBanMuteHistory(history, uids, { + set: `uid:${targetUid}:mutes:timestamp`, + label: '[[user:muted]]', + reasonDefault: '[[user:info.muted-no-reason]]', + expiryKey: '[[user:info.muted-expiry]]', + }); +} + +async function mergeBanMuteHistory(history, uids, params) { + let recentObjs = await db.getSortedSetRevRange(params.set, 0, 19); + recentObjs = await db.getObjects(recentObjs); - return history.concat(recentBans.reduce((memo, cur) => { + return history.concat(recentObjs.reduce((memo, cur) => { uids.push(cur.fromUid); memo.push({ uid: cur.fromUid, meta: [ { - key: '[[user:banned]]', - value: validator.escape(String(cur.reason)), + key: params.label, + value: validator.escape(String(cur.reason || params.reasonDefault)), labelClass: 'danger', }, { - key: '[[user:info.banned-expiry]]', + key: params.expiryKey, value: new Date(parseInt(cur.expire, 10)).toISOString(), labelClass: 'default', }, diff --git a/src/user/info.js b/src/user/info.js index a416ea43d5..3abd580d02 100644 --- a/src/user/info.js +++ b/src/user/info.js @@ -30,9 +30,10 @@ module.exports = function (User) { }; User.getModerationHistory = async function (uid) { - let [flags, bans] = await Promise.all([ + 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), ]); // Get pids from flag objects @@ -51,14 +52,16 @@ module.exports = function (User) { return memo; }, []); - [flags, bans] = await Promise.all([ + [flags, bans, mutes] = await Promise.all([ getFlagMetadata(flags), - formatBanData(bans), + formatBanMuteData(bans, '[[user:info.banned-no-reason]]'), + formatBanMuteData(mutes, '[[user:info.muted-no-reason]]'), ]); return { flags: flags, bans: bans, + mutes: mutes, }; }; @@ -95,17 +98,17 @@ module.exports = function (User) { return flags; } - async function formatBanData(bans) { - const banData = await db.getObjects(bans); - const uids = banData.map(banData => banData.fromUid); + async function formatBanMuteData(keys, noReasonLangKey) { + const data = await db.getObjects(keys); + const uids = data.map(d => d.fromUid); const usersData = await User.getUsersFields(uids, ['uid', 'username', 'userslug', 'picture']); - return banData.map((banObj, index) => { + 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.timestampISO = utils.toISOString(banObj.timestamp); - banObj.reason = validator.escape(String(banObj.reason || '')) || '[[user:info.banned-no-reason]]'; + banObj.reason = validator.escape(String(banObj.reason || '')) || noReasonLangKey; return banObj; }); }