kick / ban commands + temp bans + kick/ban infraction logging

This commit is contained in:
JandereDev 2021-12-06 20:03:17 +01:00
parent ce807aa814
commit 60d911e227
Signed by: Lea
GPG key ID: 5D5E18ACB990F57A
8 changed files with 293 additions and 5 deletions

112
src/bot/commands/ban.ts Normal file
View file

@ -0,0 +1,112 @@
import { ulid } from "ulid";
import { client } from "../..";
import Infraction from "../../struct/antispam/Infraction";
import InfractionType from "../../struct/antispam/InfractionType";
import Command from "../../struct/Command";
import MessageCommandContext from "../../struct/MessageCommandContext";
import TempBan from "../../struct/TempBan";
import { fetchUsername } from "../modules/mod_logs";
import { storeTempBan } from "../modules/tempbans";
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
export default {
name: 'ban',
aliases: null,
description: 'Ban a member from the server',
syntax: '/ban @username [10m?] [reason?]',
removeEmptyArgs: true,
run: async (message: MessageCommandContext, args: string[]) => {
if (!await isModerator(message.member!, message.serverContext))
return message.reply(NO_MANAGER_MSG);
if (args.length == 0)
return message.reply(`You need to provide a target user!`);
let targetUser = await parseUser(args.shift()!);
if (!targetUser) return message.reply('Sorry, I can\'t find that user.');
if (targetUser._id == message.author_id) {
return message.reply('nah');
}
if (targetUser._id == client.user!._id) {
return message.reply('lol no');
}
let banDuration = 0;
let durationStr = args.shift();
if (durationStr && /([0-9]{1,3}[smhdwy])+/g.test(durationStr)) {
let pieces = durationStr.match(/([0-9]{1,3}[smhdwy])/g) ?? [];
// Being able to specify the same letter multiple times
// (e.g. 1s1s) and having their values stack is a feature
for (const piece of pieces) {
let [ num, letter ] = [ Number(piece.slice(0, piece.length - 1)), piece.slice(piece.length - 1) ];
let multiplier = 0;
switch(letter) {
case 's': multiplier = 1000; break;
case 'm': multiplier = 1000 * 60; break;
case 'h': multiplier = 1000 * 60 * 60; break;
case 'd': multiplier = 1000 * 60 * 60 * 24; break;
case 'w': multiplier = 1000 * 60 * 60 * 24 * 7; break;
case 'y': multiplier = 1000 * 60 * 60 * 24 * 365; break;
}
banDuration += num * multiplier;
}
} else if (durationStr) args.splice(0, 0, durationStr);
let reason = args.join(' ') || 'No reason provided';
if (banDuration == 0) {
message.serverContext.banUser(targetUser._id, {
reason: reason + ` (by @${await fetchUsername(message.author_id)} ${message.author_id})`
})
.catch(e => message.reply(`Failed to ban user: \`${e}\``));
let infId = ulid();
let { userWarnCount } = await storeInfraction({
_id: infId,
createdBy: message.author_id,
date: Date.now(),
reason: reason,
server: message.serverContext._id,
type: InfractionType.Manual,
user: targetUser._id,
actionType: 'ban',
} as Infraction);
message.reply(`### @${targetUser.username} has bee banned.\n`
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
} else {
message.serverContext.banUser(targetUser._id, {
reason: reason + ` (by @${await fetchUsername(message.author_id)} ${message.author_id}) (${durationStr})`
})
.catch(e => message.reply(`Failed to ban user: \`${e}\``));
let banUntil = Date.now() + banDuration;
let infId = ulid();
let { userWarnCount } = await storeInfraction({
_id: infId,
createdBy: message.author_id,
date: Date.now(),
reason: reason + ` (${durationStr})`,
server: message.serverContext._id,
type: InfractionType.Manual,
user: targetUser._id,
actionType: 'ban',
} as Infraction);
await storeTempBan({
id: infId,
bannedUser: targetUser._id,
server: message.serverContext._id,
until: banUntil,
} as TempBan);
message.reply(`### ${targetUser.username} has been temporarily banned.\n`
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
}
}
} as Command;

64
src/bot/commands/kick.ts Normal file
View file

@ -0,0 +1,64 @@
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
import { ulid } from "ulid";
import { client } from "../..";
import Infraction from "../../struct/antispam/Infraction";
import InfractionType from "../../struct/antispam/InfractionType";
import Command from "../../struct/Command";
import MessageCommandContext from "../../struct/MessageCommandContext";
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
export default {
name: 'kick',
aliases: [ 'yeet', 'eject' ],
description: 'Eject a member from the server',
syntax: '/kick @username [reason?]',
removeEmptyArgs: true,
run: async (message: MessageCommandContext, args: string[]) => {
if (!await isModerator(message.member!, message.serverContext))
return message.reply(NO_MANAGER_MSG);
if (args.length == 0)
return message.reply(`You need to provide a target user!`);
let targetUser = await parseUser(args.shift()!);
if (!targetUser) return message.reply('Sorry, I can\'t find that user.');
if (targetUser._id == message.author_id) {
return message.reply('nah');
}
if (targetUser._id == client.user!._id) {
return message.reply('lol no');
}
let reason = args.join(' ') || 'No reason provided';
let targetMember: Member;
try {
targetMember = await message.serverContext.fetchMember(targetUser._id);
} catch(e) {
return message.reply(`Failed to fetch member: \`${e}\``);
}
try {
await targetMember.kick();
} catch(e) {
return message.reply(`Failed to kick user: \`${e}\``);
}
let infId = ulid();
let { userWarnCount } = await storeInfraction({
_id: infId,
createdBy: message.author_id,
date: Date.now(),
reason: reason,
server: message.serverContext._id,
type: InfractionType.Manual,
user: targetUser._id,
actionType: 'kick',
} as Infraction);
message.reply(`### @${targetUser.username} has been ${Math.random() > 0.8 ? 'ejected' : 'kicked'}.\n`
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
}
} as Command;

View file

@ -38,7 +38,7 @@ export default {
for (let inf of Array.from(userInfractions.values()).sort((a, b) => b.length - a.length).slice(0, 9)) {
inf = inf.sort((a, b) => b.date - a.date);
msg += `**${await fetchUsername(inf[0].user)}** (${inf[0].user}): **${inf.length}** infractions\n`;
msg += `\u200b \u200b \u200b \u200b \u200b ↳ Most recent warning: \`${inf[0].reason}\` `
msg += `\u200b \u200b \u200b \u200b \u200b ↳ Most recent infraction: ${getInfEmoji(inf[0])}\`${inf[0].reason}\` `
+ `${inf[0].type == InfractionType.Manual ? `(${await fetchUsername(inf[0].createdBy ?? '')})` : ''}\n`;
};
@ -60,7 +60,7 @@ export default {
message.reply(`## Infraction deleted\n\u200b\n`
+ `ID: \`${inf._id}\`\n`
+ `Reason: \`${inf.reason}\` `
+ `Reason: ${getInfEmoji(inf)}\`${inf.reason}\` `
+ `(${inf.type == InfractionType.Manual ? await fetchUsername(inf.createdBy ?? '') : 'System'})\n`
+ `Created ${Day(inf.date).fromNow()}`);
break;
@ -73,10 +73,10 @@ export default {
else {
let msg = `## ${infs.length} infractions stored for @${user.username}\n\u200b\n`;
let attachSpreadsheet = false;
for (const i in infs) { console.log(i)
for (const i in infs) {
let inf = infs[i];
let toAdd = '';
toAdd += `#${Number(i)+1}: \`${inf.reason}\` (${inf.type == InfractionType.Manual ? await fetchUsername(inf.createdBy!) : 'System'})\n`;
toAdd += `#${Number(i)+1}: ${getInfEmoji(inf)} \`${inf.reason}\` (${inf.type == InfractionType.Manual ? await fetchUsername(inf.createdBy!) : 'System'})\n`;
toAdd += `\u200b \u200b \u200b \u200b \u200b ↳ ${Day(inf.date).fromNow()} (Infraction ID: \`${inf._id}\`)\n`;
if ((msg + toAdd).length > 1900 || Number(i) > 5) {
@ -96,7 +96,7 @@ export default {
let csv_data = [
[`Warns for @${user.username} (${user._id}) - ${Day().toString()}`],
[],
['Date', 'Reason', 'Created By', 'Type', 'ID'],
['Date', 'Reason', 'Created By', 'Type', 'Action Type', 'ID'],
];
for (const inf of infs) {
@ -105,6 +105,7 @@ export default {
inf.reason,
inf.type == InfractionType.Manual ? `${await fetchUsername(inf.createdBy!)} (${inf.createdBy})` : 'SYSTEM',
inf.type == InfractionType.Automatic ? 'Automatic' : 'Manual',
inf.actionType || 'warn',
inf._id,
]);
}
@ -124,3 +125,11 @@ export default {
}
}
} as Command;
function getInfEmoji(inf: Infraction) {
switch(inf.actionType) {
case 'kick': return ':mans_shoe: ';
case 'ban': return ':hammer: ';
default: return '';
}
}

View file

@ -0,0 +1,35 @@
import { ulid } from "ulid";
import { client } from "../..";
import Infraction from "../../struct/antispam/Infraction";
import InfractionType from "../../struct/antispam/InfractionType";
import { storeInfraction } from "../util";
// Listen to system messages
client.on('message', message => {
if (typeof message.content != 'object') return;
let sysMsg = message.asSystemMessage;
switch(sysMsg.type) {
case 'user_kicked':
case 'user_banned':
if (message.channel &&
sysMsg.user &&
sysMsg.by &&
sysMsg.by._id != client.user?._id) return;
storeInfraction({
_id: ulid(),
createdBy: sysMsg.by?._id,
reason: 'Unknown reason (caught system message)',
date: message.createdAt,
server: message.channel!.server_id,
type: InfractionType.Manual,
user: sysMsg.user!._id,
actionType: sysMsg.type == 'user_kicked' ? 'kick' : 'ban',
} as Infraction).catch(console.warn);
break;
case 'user_joined': break;
case 'user_left' : break;
}
});

View file

@ -0,0 +1,57 @@
import { FindResult } from "monk";
import { client } from "../..";
import TempBan from "../../struct/TempBan";
import logger from "../logger";
// Array of ban IDs which should not get processed in this session
let dontProcess: string[] = [];
async function tick() {
let found: FindResult<TempBan> = await client.db.get('tempbans').find({ until: { $lt: Date.now() + 60000 } });
for (const ban of found) {
if (!dontProcess.includes(ban.id))
setTimeout(() => processUnban(ban), ban.until - Date.now());
}
}
new Promise((r: (value: void) => void) => {
if (client.user) r();
else client.once('ready', r);
}).then(() => {
tick();
setInterval(tick, 60000);
});
async function processUnban(ban: TempBan) {
try {
let server = client.servers.get(ban.server) || await client.servers.fetch(ban.server);
let serverBans = await server.fetchBans();
if (serverBans.bans.find(b => b._id.user == ban.bannedUser)) {
logger.debug(`Unbanning user ${ban.bannedUser} from ${server._id}`);
let promises = [
server.unbanUser(ban.bannedUser),
client.db.get('tempbans').remove({ id: ban.id }),
];
await Promise.allSettled(promises);
}
else client.db.get('tempbans').remove({ id: ban.id });
} catch(e) { console.error(e) }
}
async function storeTempBan(ban: TempBan): Promise<void> {
if (Date.now() >= ban.until - 60000) {
dontProcess.push(ban.id);
setTimeout(() => {
processUnban(ban);
dontProcess = dontProcess.filter(id => id != ban.id);
}, ban.until - Date.now());
}
client.db.get('tempbans').insert(ban);
}
export { storeTempBan };

View file

@ -21,4 +21,6 @@ export { client }
// Load modules
import('./bot/modules/command_handler');
import('./bot/modules/mod_logs');
import('./bot/modules/event_handler');
import('./bot/modules/tempbans');
})();

8
src/struct/TempBan.ts Normal file
View file

@ -0,0 +1,8 @@
class TempBan {
id: string;
server: string;
bannedUser: string;
until: number;
}
export default TempBan;

View file

@ -3,6 +3,7 @@ import InfractionType from "./InfractionType";
class Infraction {
_id: string;
type: InfractionType;
actionType?: 'kick'|'ban';
user: string;
createdBy: string|null;
server: string;