diff --git a/bot/src/bot/commands/ban.ts b/bot/src/bot/commands/ban.ts index 2a9476d..0cc4914 100644 --- a/bot/src/bot/commands/ban.ts +++ b/bot/src/bot/commands/ban.ts @@ -87,7 +87,7 @@ export default { await Promise.all([ message.reply(`### ${targetName} has been ${Math.random() > 0.8 ? 'ejected' : 'banned'}.\n` + `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`), - logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction, `Ban duration: **Permanent**`), + logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction._id, `Ban duration: **Permanent**`), ]); } else { let banUntil = Date.now() + banDuration; @@ -119,7 +119,7 @@ export default { await Promise.all([ message.reply(`### ${targetName} has been temporarily banned.\n` + `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`), - logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction, `Ban duration: **${Day(banUntil).fromNow(true)}**`), + logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction._id, `Ban duration: **${Day(banUntil).fromNow(true)}**`), ]); } } diff --git a/bot/src/bot/commands/kick.ts b/bot/src/bot/commands/kick.ts index 8346255..fd6f42d 100644 --- a/bot/src/bot/commands/kick.ts +++ b/bot/src/bot/commands/kick.ts @@ -64,7 +64,7 @@ export default { await Promise.all([ message.reply(`### @${targetUser.username} has been ${Math.random() > 0.8 ? 'yeeted' : 'kicked'}.\n` + `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`), - logModAction('kick', message.serverContext, message.member!, targetUser._id, reason, infraction), + logModAction('kick', message.serverContext, message.member!, targetUser._id, reason, infraction._id), ]); } } as Command; diff --git a/bot/src/bot/commands/votekick.ts b/bot/src/bot/commands/votekick.ts new file mode 100644 index 0000000..cc7c0a5 --- /dev/null +++ b/bot/src/bot/commands/votekick.ts @@ -0,0 +1,116 @@ +import { FindResult } from "monk"; +import { ulid } from "ulid"; +import { client } from "../.."; +import Command from "../../struct/Command"; +import MessageCommandContext from "../../struct/MessageCommandContext"; +import ServerConfig from "../../struct/ServerConfig"; +import { logModAction } from "../modules/mod_logs"; +import { storeTempBan } from "../modules/tempbans"; +import { getPermissionLevel, isModerator, parseUser } from "../util"; + +type VoteEntry = { + id: string; + target: string; + user: string; // Whoever issued the vote kick + server: string; + time: number; + ignore: boolean; +} + +export default { + name: 'votekick', + aliases: [ 'voteban' ], + description: 'Allow trusted users to vote kick users', + category: 'moderation', + run: async (message: MessageCommandContext, args: string[]) => { + try { + const serverConfig: ServerConfig = await client.db.get('servers').findOne({ id: message.serverContext._id }); + if (!serverConfig?.votekick?.enabled) return message.reply('Vote kick is not enabled for this server.'); + if (!message.member!.roles?.filter(r => serverConfig.votekick?.trustedRoles.includes(r)).length + && !(await isModerator(message))) { + return message.reply('🔒 Access denied'); + } + if (args.length == 0) return message.reply(`**Votekick configuration:**\n` + + `Votes required: **${serverConfig.votekick.votesRequired}**\n` + + `Ban duration: **${serverConfig.votekick.banDuration}** (In minutes, -1 = Kick, 0: Permanent)\n` + + `Trusted role IDs: \`${serverConfig.votekick.trustedRoles.join('\`, \`')}\`\n\n` + + `Run \`/votekick [Username, ID or @mention]\` to votekick someone.`); + + const target = await parseUser(args[0]); + if (!target) return message.reply('Sorry, I can\'t find this user.'); + const targetMember = await message.serverContext.fetchMember(target); + + if (await getPermissionLevel(target, message.serverContext) > 0 + || targetMember.roles?.filter(r => serverConfig.votekick?.trustedRoles.includes(r)).length) { + return message.reply('This target can not be votekicked.'); + } + + const vote: VoteEntry = { + id: ulid(), + target: target._id, + user: message.author_id, + server: message.serverContext._id, + time: Date.now(), + ignore: false, + } + + const votes: FindResult = await client.db.get('votekicks').find({ + server: message.serverContext._id, + target: target._id, + time: { + $gt: Date.now() - 1000 * 60 * 30, // Last 30 minutes + }, + ignore: false, + }); + + if (votes.find(v => v.user == message.author_id)) return message.reply('You can\'t vote twice for this user.'); + + await client.db.get('votekicks').insert(vote); + votes.push({ _id: '' as any, ...vote }); + + await logModAction( + "votekick", + message.serverContext, + message.member!, + target._id, + `n/a`, + vote.id, + `This is vote ${votes.length}/${serverConfig.votekick.votesRequired} for this user.`, + ); + + if (votes.length >= serverConfig.votekick.votesRequired) { + if (serverConfig.votekick.banDuration == -1) { + targetMember.kick(); + } else if (serverConfig.votekick.banDuration == 0) { + message.serverContext.banUser(target._id, { reason: 'Automatic permanent ban triggered by /votekick' }); + } else { + message.serverContext.banUser(target._id, { reason: `Automatic temporary ban triggered by /votekick ` + + `(${serverConfig.votekick.banDuration} minutes)` }); + + await storeTempBan({ + id: ulid(), + bannedUser: target._id, + server: message.serverContext._id, + until: Date.now() + (1000 * 60 * serverConfig.votekick.banDuration), + }); + } + + message.reply(`**${votes.length}/${serverConfig.votekick.votesRequired}** votes - ` + + `Banned @${target.username} for ${serverConfig.votekick.banDuration} minutes.`); // Todo: display ban duration properly (Permban, kick, etc) + + await client.db.get('votekicks').update({ + server: message.serverContext._id, + target: target._id, + time: { $gt: Date.now() - 1000 * 60 * 30 }, + ignore: false, + }, { $set: { ignore: true } }); + } else { + message.reply(`Voted to temporarily remove **@${target.username}**. ` + + `**${votes.length}/${serverConfig.votekick.votesRequired}** votes.`); + } + } catch(e) { + console.error(e); + message.reply('Oops, something happened: ' + e); + } + } +} as Command; diff --git a/bot/src/bot/commands/warn.ts b/bot/src/bot/commands/warn.ts index d914d8b..7c5eaad 100644 --- a/bot/src/bot/commands/warn.ts +++ b/bot/src/bot/commands/warn.ts @@ -43,7 +43,7 @@ export default { + ` for ${await fetchUsername(user._id)}.\n` + `**Infraction ID:** \`${infraction._id}\`\n` + `**Reason:** \`${infraction.reason}\``), - logModAction('warn', message.serverContext, message.member!, user._id, reason, infraction, `This is warn number ${userWarnCount} for this user.`), + logModAction('warn', message.serverContext, message.member!, user._id, reason, infraction._id, `This is warn number ${userWarnCount} for this user.`), ]); } } as Command; diff --git a/bot/src/bot/modules/mod_logs.ts b/bot/src/bot/modules/mod_logs.ts index 6d36bef..178ec1f 100644 --- a/bot/src/bot/modules/mod_logs.ts +++ b/bot/src/bot/modules/mod_logs.ts @@ -114,7 +114,7 @@ client.on('packet', async (packet) => { } }); -async function logModAction(type: 'warn'|'kick'|'ban', server: Server, mod: Member, target: string, reason: string|null, infraction: Infraction, extraText?: string): Promise { +async function logModAction(type: 'warn'|'kick'|'ban'|'votekick', server: Server, mod: Member, target: string, reason: string|null, infractionID: string, extraText?: string): Promise { try { let config: ServerConfig = await client.db.get('servers').findOne({ id: server._id }) ?? {}; @@ -130,7 +130,7 @@ async function logModAction(type: 'warn'|'kick'|'ban', server: Server, mod: Memb description: `\`@${mod.user?.username}\` **${aType}** \`` + `${await fetchUsername(target)}\`${type == 'warn' ? '.' : ` from ${server.name}.`}\n` + `**Reason**: \`${reason ? reason : 'No reason provided.'}\`\n` - + `**Warn ID**: \`${infraction._id}\`\n` + + `**Warn ID**: \`${infractionID}\`\n` + (extraText ?? ''), color: embedColor, overrides: { @@ -138,7 +138,7 @@ async function logModAction(type: 'warn'|'kick'|'ban', server: Server, mod: Memb description: `@${mod.user?.username} ${aType} ` + `${await fetchUsername(target)}${type == 'warn' ? '.' : ` from ${server.name}.`}\n` + `Reason: ${reason ? reason : 'No reason provided.'}\n` - + `Warn ID: ${infraction._id}\n` + + `Warn ID: ${infractionID}\n` + (extraText ?? ''), } } diff --git a/bot/src/struct/ServerConfig.ts b/bot/src/struct/ServerConfig.ts index 2740637..f1f3260 100644 --- a/bot/src/struct/ServerConfig.ts +++ b/bot/src/struct/ServerConfig.ts @@ -8,6 +8,12 @@ class ServerConfig { automodSettings: AutomodSettings | undefined; botManagers: string[] | undefined; moderators: string[] | undefined; + votekick: { + enabled: boolean; + votesRequired: number; + banDuration: number; // -1: Only kick, 0: Permanent, >0: Ban duration in minutes + trustedRoles: string[]; + } | undefined; linkedServer: string | undefined; whitelist: { users: string[] | undefined,