kick / ban commands + temp bans + kick/ban infraction logging
This commit is contained in:
parent
ce807aa814
commit
60d911e227
8 changed files with 293 additions and 5 deletions
112
src/bot/commands/ban.ts
Normal file
112
src/bot/commands/ban.ts
Normal 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
64
src/bot/commands/kick.ts
Normal 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;
|
|
@ -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 '';
|
||||
}
|
||||
}
|
||||
|
|
35
src/bot/modules/event_handler.ts
Normal file
35
src/bot/modules/event_handler.ts
Normal 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;
|
||||
}
|
||||
});
|
57
src/bot/modules/tempbans.ts
Normal file
57
src/bot/modules/tempbans.ts
Normal 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 };
|
|
@ -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
8
src/struct/TempBan.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
class TempBan {
|
||||
id: string;
|
||||
server: string;
|
||||
bannedUser: string;
|
||||
until: number;
|
||||
}
|
||||
|
||||
export default TempBan;
|
|
@ -3,6 +3,7 @@ import InfractionType from "./InfractionType";
|
|||
class Infraction {
|
||||
_id: string;
|
||||
type: InfractionType;
|
||||
actionType?: 'kick'|'ban';
|
||||
user: string;
|
||||
createdBy: string|null;
|
||||
server: string;
|
||||
|
|
Loading…
Reference in a new issue