Add support for timed blocks. Add !is_blocked.

master
Dragory 2019-06-09 20:04:40 +03:00
parent 4bfd247be3
commit 1259669c9d
5 changed files with 151 additions and 37 deletions

View File

@ -35,6 +35,7 @@ See [CHANGELOG.md](CHANGELOG.md)
`!logs <user>` Lists previous modmail logs with the specified user `!logs <user>` Lists previous modmail logs with the specified user
`!block <user>` Blocks the specified user from using modmail `!block <user>` Blocks the specified user from using modmail
`!unblock <user>` Unblocks the specified user from using modmail `!unblock <user>` Unblocks the specified user from using modmail
`!is_blocked <user>` Checks whether the user is blocked and for how long
`!s <shortcut> <text>` Adds a snippet (a canned response). See below for how to use it. `!s <shortcut> <text>` Adds a snippet (a canned response). See below for how to use it.
`!edit_snippet <shortcut> <text>` Edits an existing snippet (alias `!es`) `!edit_snippet <shortcut> <text>` Edits an existing snippet (alias `!es`)
`!delete_snippet <shortcut>` Deletes the specified snippet (alias `!ds`) `!delete_snippet <shortcut>` Deletes the specified snippet (alias `!ds`)
@ -47,8 +48,8 @@ See [CHANGELOG.md](CHANGELOG.md)
`!anonreply <text>` Sends an anonymous reply to the user in the format "Role: text" (alias `!ar`) `!anonreply <text>` Sends an anonymous reply to the user in the format "Role: text" (alias `!ar`)
`!close <time>` Closes the modmail thread. If a time is specified, the thread is scheduled to be closed later. Scheduled closing is cancelled if a message is sent to or received from the user. `!close <time>` Closes the modmail thread. If a time is specified, the thread is scheduled to be closed later. Scheduled closing is cancelled if a message is sent to or received from the user.
`!logs` Lists previous modmail logs with this user `!logs` Lists previous modmail logs with this user
`!block` Blocks the user from using modmail `!block <time>` Blocks the user from using modmail. If a time is specified, the block is temporary.
`!unblock` Unblocks the user from using modmail `!unblock <time>` Unblocks the user from using modmail. If a time is specified, the user will be scheduled to be unblocked after that time.
`!!shortcut` Reply with a snippet. Replace `shortcut` with the snippet's actual shortcut. `!!shortcut` Reply with a snippet. Replace `shortcut` with the snippet's actual shortcut.
`!!!shortcut` Reply with a snippet anonymously. Replace `shortcut` with the snippet's actual shortcut. `!!!shortcut` Reply with a snippet anonymously. Replace `shortcut` with the snippet's actual shortcut.
`!move <category>` If `allowMove` is enabled, moves the thread channel to the specified category `!move <category>` If `allowMove` is enabled, moves the thread channel to the specified category

View File

@ -0,0 +1,11 @@
exports.up = async function(knex, Promise) {
await knex.schema.table('blocked_users', table => {
table.dateTime('expires_at').nullable();
});
};
exports.down = async function(knex, Promise) {
await knex.schema.table('blocked_users', table => {
table.dropColumn('expires_at');
});
};

View File

@ -1,17 +1,28 @@
const moment = require('moment'); const moment = require('moment');
const knex = require('../knex'); const knex = require('../knex');
/**
* @param {String} userId
* @returns {Promise<{ isBlocked: boolean, expiresAt: string }>}
*/
async function getBlockStatus(userId) {
const row = await knex('blocked_users')
.where('user_id', userId)
.first();
return {
isBlocked: !! row,
expiresAt: row && row.expires_at
};
}
/** /**
* Checks whether userId is blocked * Checks whether userId is blocked
* @param {String} userId * @param {String} userId
* @returns {Promise<Boolean>} * @returns {Promise<Boolean>}
*/ */
async function isBlocked(userId) { async function isBlocked(userId) {
const row = await knex('blocked_users') return (await getBlockStatus(userId)).isBlocked;
.where('user_id', userId)
.first();
return !! row;
} }
/** /**
@ -21,7 +32,7 @@ async function isBlocked(userId) {
* @param {String} blockedBy * @param {String} blockedBy
* @returns {Promise} * @returns {Promise}
*/ */
async function block(userId, userName = '', blockedBy = null) { async function block(userId, userName = '', blockedBy = null, expiresAt = null) {
if (await isBlocked(userId)) return; if (await isBlocked(userId)) return;
return knex('blocked_users') return knex('blocked_users')
@ -29,7 +40,8 @@ async function block(userId, userName = '', blockedBy = null) {
user_id: userId, user_id: userId,
user_name: userName, user_name: userName,
blocked_by: blockedBy, blocked_by: blockedBy,
blocked_at: moment.utc().format('YYYY-MM-DD HH:mm:ss') blocked_at: moment.utc().format('YYYY-MM-DD HH:mm:ss'),
expires_at: expiresAt
}); });
} }
@ -44,8 +56,39 @@ async function unblock(userId) {
.delete(); .delete();
} }
/**
* Updates the expiry time of the block for the given userId
* @param {String} userId
* @param {String} expiresAt
* @returns {Promise<void>}
*/
async function updateExpiryTime(userId, expiresAt) {
return knex('blocked_users')
.where('user_id', userId)
.update({
expires_at: expiresAt
});
}
/**
* @returns {String[]}
*/
async function getExpiredBlocks() {
const now = moment.utc().format('YYYY-MM-DD HH:mm:ss');
const blocks = await knex('blocked_users')
.whereNotNull('expires_at')
.where('expires_at', '<=', now)
.select();
return blocks.map(block => block.user_id);
}
module.exports = { module.exports = {
getBlockStatus,
isBlocked, isBlocked,
block, block,
unblock, unblock,
updateExpiryTime,
getExpiredBlocks,
}; };

View File

@ -1,3 +1,5 @@
const humanizeDuration = require('humanize-duration');
const moment = require('moment');
const threadUtils = require('../threadUtils'); const threadUtils = require('../threadUtils');
const blocked = require("../data/blocked"); const blocked = require("../data/blocked");
const utils = require("../utils"); const utils = require("../utils");
@ -5,38 +7,95 @@ const utils = require("../utils");
module.exports = bot => { module.exports = bot => {
const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args); const addInboxServerCommand = (...args) => threadUtils.addInboxServerCommand(bot, ...args);
addInboxServerCommand('block', (msg, args, thread) => { async function removeExpiredBlocks() {
async function block(userId) { const expiredBlocks = await blocked.getExpiredBlocks();
const user = bot.users.get(userId); const logChannel = utils.getLogChannel();
await blocked.block(userId, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id); for (const userId of expiredBlocks) {
msg.channel.createMessage(`Blocked <@${userId}> (id ${userId}) from modmail`); await blocked.unblock(userId);
logChannel.createMessage(`Block of <@!${userId}> (id \`${userId}\`) expired`);
}
}
async function expiredBlockLoop() {
try {
removeExpiredBlocks();
} catch (e) {
console.error(e);
} }
if (args.length > 0) { setTimeout(expiredBlockLoop, 2000);
// User mention/id as argument }
const userId = utils.getUserMention(args.join(' '));
if (! userId) return; bot.on('ready', expiredBlockLoop);
block(userId);
} else if (thread) { addInboxServerCommand('block', async (msg, args, thread) => {
// Calling !block without args in a modmail thread blocks the user of that thread const userIdToBlock = args[0]
block(thread.user_id); ? utils.getUserMention(args[0])
: thread && thread.user_id;
if (! userIdToBlock) return;
const isBlocked = await blocked.isBlocked(userIdToBlock);
if (isBlocked) {
msg.channel.createMessage('User is already blocked');
return;
}
const expiryTime = args[1] ? utils.convertDelayStringToMS(args[1]) : null;
const expiresAt = expiryTime
? moment.utc().add(expiryTime, 'ms').format('YYYY-MM-DD HH:mm:ss')
: null;
const user = bot.users.get(userIdToBlock);
await blocked.block(userIdToBlock, (user ? `${user.username}#${user.discriminator}` : ''), msg.author.id, expiresAt);
if (expiresAt) {
const humanized = humanizeDuration(expiryTime, { largest: 2, round: true });
msg.channel.createMessage(`Blocked <@${userIdToBlock}> (id \`${userIdToBlock}\`) from modmail for ${humanized}`);
} else {
msg.channel.createMessage(`Blocked <@${userIdToBlock}> (id \`${userIdToBlock}\`) from modmail indefinitely`);
} }
}); });
addInboxServerCommand('unblock', (msg, args, thread) => { addInboxServerCommand('unblock', async (msg, args, thread) => {
async function unblock(userId) { const userIdToUnblock = args[0]
await blocked.unblock(userId); ? utils.getUserMention(args[0])
msg.channel.createMessage(`Unblocked <@${userId}> (id ${userId}) from modmail`); : thread && thread.user_id;
}
if (args.length > 0) { if (! userIdToUnblock) return;
// User mention/id as argument
const userId = utils.getUserMention(args.join(' ')); const unblockDelay = args[1] ? utils.convertDelayStringToMS(args[1]) : null;
if (! userId) return; const unblockAt = unblockDelay
unblock(userId); ? moment.utc().add(unblockDelay, 'ms').format('YYYY-MM-DD HH:mm:ss')
} else if (thread) { : null;
// Calling !unblock without args in a modmail thread unblocks the user of that thread
unblock(thread.user_id); const user = bot.users.get(userIdToUnblock);
if (unblockAt) {
const humanized = humanizeDuration(unblockDelay, { largest: 2, round: true });
await blocked.updateExpiryTime(userIdToUnblock, unblockAt);
msg.channel.createMessage(`Scheduled <@${userIdToUnblock}> (id \`${userIdToUnblock}\`) to be unblocked in ${humanized}`);
} else {
await blocked.unblock(userIdToUnblock);
msg.channel.createMessage(`Unblocked <@${userIdToUnblock}> (id ${userIdToUnblock}) from modmail`);
}
});
addInboxServerCommand('is_blocked', async (msg, args, thread) => {
const userIdToCheck = args[0]
? utils.getUserMention(args[0])
: thread && thread.user_id;
if (! userIdToCheck) return;
const blockStatus = await blocked.getBlockStatus(userIdToCheck);
if (blockStatus.isBlocked) {
if (blockStatus.expiresAt) {
msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked until ${blockStatus.expiresAt} (UTC)`);
} else {
msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is blocked indefinitely`);
}
} else {
msg.channel.createMessage(`<@!${userIdToCheck}> (id \`${userIdToCheck}\`) is NOT blocked`);
} }
}); });
}; };

View File

@ -224,7 +224,7 @@ const delayStringRegex = /^([0-9]+)(?:([dhms])[a-z]*)?/i;
/** /**
* Turns a "delay string" such as "1h30m" to milliseconds * Turns a "delay string" such as "1h30m" to milliseconds
* @param {String} str * @param {String} str
* @returns {Number} * @returns {Number|null}
*/ */
function convertDelayStringToMS(str) { function convertDelayStringToMS(str) {
let match; let match;