diff --git a/bot/src/bot/commands/moderation/ban.ts b/bot/src/bot/commands/moderation/ban.ts index 6bede96..9f54936 100644 --- a/bot/src/bot/commands/moderation/ban.ts +++ b/bot/src/bot/commands/moderation/ban.ts @@ -3,15 +3,15 @@ import { client } from "../../../index"; import Infraction from "../../../struct/antispam/Infraction"; import InfractionType from "../../../struct/antispam/InfractionType"; import SimpleCommand from "../../../struct/commands/SimpleCommand"; -import MessageCommandContext from "../../../struct/MessageCommandContext"; import { fetchUsername, logModAction } from "../../modules/mod_logs"; import { storeTempBan } from "../../modules/tempbans"; -import { dedupeArray, embed, EmbedColor, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; +import { dedupeArray, embed, EmbedColor, generateInfractionDMEmbed, getDmChannel, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; import Day from 'dayjs'; import RelativeTime from 'dayjs/plugin/relativeTime'; import CommandCategory from "../../../struct/commands/CommandCategory"; import { SendableEmbed } from "@janderedev/revolt.js/node_modules/revolt-api"; import { User } from "@janderedev/revolt.js"; +import logger from "../../logger"; Day.extend(RelativeTime); @@ -22,7 +22,7 @@ export default { syntax: '/ban @username [10m|1h|...?] [reason?]', removeEmptyArgs: true, category: CommandCategory.Moderation, - run: async (message: MessageCommandContext, args: string[]) => { + run: async (message, args, serverConfig) => { if (!await isModerator(message)) return message.reply(NO_MANAGER_MSG); if (!message.serverContext.havePermission('BanMembers')) { @@ -134,6 +134,7 @@ export default { type: InfractionType.Manual, user: user._id, actionType: 'ban', + expires: Infinity, } const { userWarnCount } = await storeInfraction(infraction); @@ -157,6 +158,20 @@ export default { continue; } + if (serverConfig?.dmOnKick) { + try { + const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message); + const dmChannel = await getDmChannel(user); + + if (dmChannel.havePermission('SendMessage') || dmChannel.havePermission('SendEmbeds')) { + await dmChannel.sendMessage({ embeds: [ embed ] }); + } + else logger.warn('Missing permission to DM user.'); + } catch(e) { + console.error(e); + } + } + await message.serverContext.banUser(user._id, { reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})` }); @@ -186,9 +201,24 @@ export default { type: InfractionType.Manual, user: user._id, actionType: 'ban', + expires: banUntil, } const { userWarnCount } = await storeInfraction(infraction); + if (serverConfig?.dmOnKick) { + try { + const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message); + const dmChannel = await getDmChannel(user); + + if (dmChannel.havePermission('SendMessage') || dmChannel.havePermission('SendEmbeds')) { + await dmChannel.sendMessage({ embeds: [ embed ] }); + } + else logger.warn('Missing permission to DM user.'); + } catch(e) { + console.error(e); + } + } + await message.serverContext.banUser(user._id, { reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id}) (${durationStr})` }); diff --git a/bot/src/bot/commands/moderation/kick.ts b/bot/src/bot/commands/moderation/kick.ts index 1c1929a..8dc9b62 100644 --- a/bot/src/bot/commands/moderation/kick.ts +++ b/bot/src/bot/commands/moderation/kick.ts @@ -1,5 +1,4 @@ import { User } from "@janderedev/revolt.js"; -import { Member } from "@janderedev/revolt.js/dist/maps/Members"; import { SendableEmbed } from "revolt-api"; import { ulid } from "ulid"; import { client } from "../../../"; @@ -7,9 +6,9 @@ import Infraction from "../../../struct/antispam/Infraction"; import InfractionType from "../../../struct/antispam/InfractionType"; import CommandCategory from "../../../struct/commands/CommandCategory"; import SimpleCommand from "../../../struct/commands/SimpleCommand"; -import MessageCommandContext from "../../../struct/MessageCommandContext"; +import logger from "../../logger"; import { fetchUsername, logModAction } from "../../modules/mod_logs"; -import { dedupeArray, embed, EmbedColor, isModerator, NO_MANAGER_MSG, parseUser, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; +import { dedupeArray, embed, EmbedColor, generateInfractionDMEmbed, getDmChannel, isModerator, NO_MANAGER_MSG, parseUser, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; export default { name: 'kick', @@ -18,7 +17,7 @@ export default { syntax: '/kick @username [reason?]', removeEmptyArgs: true, category: CommandCategory.Moderation, - run: async (message: MessageCommandContext, args: string[]) => { + run: async (message, args, serverConfig) => { if (!await isModerator(message)) return message.reply(NO_MANAGER_MSG); if (!message.serverContext.havePermission('KickMembers')) { @@ -104,13 +103,27 @@ export default { _id: infId, createdBy: message.author_id, date: Date.now(), - reason: reason, + reason: reason || 'No reason provided', server: message.serverContext._id, type: InfractionType.Manual, user: user._id, actionType: 'kick', } + if (serverConfig?.dmOnKick) { + try { + const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message); + const dmChannel = await getDmChannel(user); + + if (dmChannel.havePermission('SendMessage') || dmChannel.havePermission('SendEmbeds')) { + await dmChannel.sendMessage({ embeds: [ embed ] }); + } + else logger.warn('Missing permission to DM user.'); + } catch(e) { + console.error(e); + } + } + let [ { userWarnCount } ] = await Promise.all([ storeInfraction(infraction), logModAction('kick', message.serverContext, message.member!, user._id, reason, infraction._id), diff --git a/bot/src/bot/commands/moderation/warn.ts b/bot/src/bot/commands/moderation/warn.ts index 1cc80ab..2eeccf3 100644 --- a/bot/src/bot/commands/moderation/warn.ts +++ b/bot/src/bot/commands/moderation/warn.ts @@ -1,13 +1,13 @@ import SimpleCommand from "../../../struct/commands/SimpleCommand"; -import { dedupeArray, embed, EmbedColor, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; +import { dedupeArray, embed, EmbedColor, generateInfractionDMEmbed, getDmChannel, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; import Infraction from "../../../struct/antispam/Infraction"; import { ulid } from "ulid"; import InfractionType from "../../../struct/antispam/InfractionType"; import { fetchUsername, logModAction } from "../../modules/mod_logs"; -import MessageCommandContext from "../../../struct/MessageCommandContext"; import CommandCategory from "../../../struct/commands/CommandCategory"; import { SendableEmbed } from "revolt-api"; import { User } from "@janderedev/revolt.js"; +import logger from "../../logger"; export default { name: 'warn', @@ -15,7 +15,7 @@ export default { removeEmptyArgs: false, description: 'add an infraction to an user\'s record', category: CommandCategory.Moderation, - run: async (message: MessageCommandContext, args: string[]) => { + run: async (message, args, serverConfig) => { if (!await isModerator(message)) return message.reply(NO_MANAGER_MSG); const userInput = args.shift() || ''; @@ -94,15 +94,32 @@ export default { } as Infraction; let { userWarnCount } = await storeInfraction(infraction); - await logModAction( - 'warn', - message.serverContext, - message.member!, - user._id, - reason || 'No reason provided', - infraction._id, - `This is warn number ${userWarnCount} for this user.` - ); + await Promise.all([ + logModAction( + 'warn', + message.serverContext, + message.member!, + user._id, + reason || 'No reason provided', + infraction._id, + `This is warn number ${userWarnCount} for this user.` + ), + (async () => { + if (serverConfig?.dmOnWarn) { + try { + const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message); + const dmChannel = await getDmChannel(user); + + if (dmChannel.havePermission('SendMessage') || dmChannel.havePermission('SendEmbeds')) { + await dmChannel.sendMessage({ embeds: [ embed ] }); + } + else logger.warn('Missing permission to DM user.'); + } catch(e) { + console.error(e); + } + } + })(), + ]); embeds.push({ title: `User warned`, diff --git a/bot/src/bot/modules/command_handler.ts b/bot/src/bot/modules/command_handler.ts index 29f2de3..56c48ca 100644 --- a/bot/src/bot/modules/command_handler.ts +++ b/bot/src/bot/modules/command_handler.ts @@ -136,7 +136,7 @@ let commands: SimpleCommand[]; } try { - await cmd.run(message, args); + await cmd.run(message, args, config); } catch(e) { console.error(e); message.reply(`### An error has occurred:\n\`\`\`js\n${e}\n\`\`\``); diff --git a/bot/src/bot/util.ts b/bot/src/bot/util.ts index b5be848..01421d3 100644 --- a/bot/src/bot/util.ts +++ b/bot/src/bot/util.ts @@ -15,11 +15,16 @@ import { Permission } from "@janderedev/revolt.js/dist/permissions/definitions"; import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { isSudo } from "./commands/admin/botadm"; import { SendableEmbed } from "revolt-api"; +import MessageCommandContext from "../struct/MessageCommandContext"; +import ServerConfig from "../struct/ServerConfig"; const NO_MANAGER_MSG = '🔒 Missing permission'; const ULID_REGEX = /^[0-9A-HJ-KM-NP-TV-Z]{26}$/i; const USER_MENTION_REGEX = /^<@[0-9A-HJ-KM-NP-TV-Z]{26}>$/i; const CHANNEL_MENTION_REGEX = /^<#[0-9A-HJ-KM-NP-TV-Z]{26}>$/i; +const RE_HTTP_URI = /^http(s?):\/\//g; +const RE_MAILTO_URI = /^mailto:/g; + let autumn_url: string|null = null; let apiConfig: any = axios.get(client.apiURL).then(res => { autumn_url = (res.data as any).features.autumn.url; @@ -347,6 +352,52 @@ const awaitClient = () => new Promise(async resolve => { else resolve(); }); +const getDmChannel = async (user: string|{_id: string}|User) => { + if (typeof user == 'string') user = client.users.get(user) || await client.users.fetch(user); + if (!(user instanceof User)) user = client.users.get(user._id) || await client.users.fetch(user._id); + + return Array.from(client.channels).find( + c => c[1].channel_type == 'DirectMessage' && c[1].recipient?._id == (user as User)._id + )?.[1] || await (user as User).openDM(); +} + +const generateInfractionDMEmbed = (server: Server, serverConfig: ServerConfig, infraction: Infraction, message: MessageCommandContext) => { + const embed: SendableEmbed = { + title: message.serverContext.name, + icon_url: message.serverContext.generateIconURL({ max_side: 128 }), + colour: '#ff9e2f', + url: message.url, + description: 'You have been ' + + (infraction.actionType + ? `**${infraction.actionType == 'ban' ? 'banned' : 'kicked'}** from ` + : `**warned** in `) + + `'${sanitizeMessageContent(message.serverContext.name).trim()}' .\n` + + `**Reason:** ${infraction.reason}\n` + + `**Moderator:** [@${sanitizeMessageContent(message.author?.username || 'Unknown')}](/@${message.author_id})\n` + + `**Infraction ID:** \`${infraction._id}\`` + + (infraction.actionType == 'ban' && infraction.expires + ? (infraction.expires == Infinity + ? '\n**Ban duration:** Permanent' + : `\n**Ban expires** `) + : '') + } + + if (serverConfig.contact) { + if (RE_MAILTO_URI.test(serverConfig.contact)) { + embed.description += `\n\nIf you wish to appeal this decision, you may contact the server's moderation team at ` + + `[${serverConfig.contact.replace(RE_MAILTO_URI, '')}](${serverConfig.contact}).` + } + else if (RE_HTTP_URI.test(serverConfig.contact)) { + embed.description += `\n\nIf you wish to appeal this decision, you may do so [here](${serverConfig.contact}).` + } + else { + embed.description += `\n\n${serverConfig.contact}`; + } + } + + return embed; +} + export { getAutumnURL, hasPerm, @@ -366,6 +417,8 @@ export { dedupeArray, awaitClient, getMutualServers, + getDmChannel, + generateInfractionDMEmbed, EmbedColor, NO_MANAGER_MSG, ULID_REGEX, diff --git a/bot/src/struct/ServerConfig.ts b/bot/src/struct/ServerConfig.ts index 01b890c..8d17f37 100644 --- a/bot/src/struct/ServerConfig.ts +++ b/bot/src/struct/ServerConfig.ts @@ -25,6 +25,9 @@ class ServerConfig { modAction?: LogConfig, // User warned, kicked or banned }; allowBlacklistedUsers?: boolean; // Whether the server explicitly allows users that are globally blacklisted + dmOnKick?: boolean; // Whether users should receive a DM when kicked/banned. Default false + dmOnWarn?: boolean; // Whether users should receive a DM when warned. Default false + contact?: string; // How to contact the server staff. Sent on kick/ban/warn DMs. http(s)/mailto link or normal text. } export default ServerConfig; diff --git a/bot/src/struct/antispam/Infraction.ts b/bot/src/struct/antispam/Infraction.ts index 612c51a..fbb7227 100644 --- a/bot/src/struct/antispam/Infraction.ts +++ b/bot/src/struct/antispam/Infraction.ts @@ -12,6 +12,7 @@ class Infraction { targetMessages?: string[]; reason: string; date: number; + expires?: number; // Only applies to bans } export default Infraction; diff --git a/bot/src/struct/commands/SimpleCommand.ts b/bot/src/struct/commands/SimpleCommand.ts index bbfd229..f042bd7 100644 --- a/bot/src/struct/commands/SimpleCommand.ts +++ b/bot/src/struct/commands/SimpleCommand.ts @@ -1,5 +1,6 @@ import CommandCategory from "./CommandCategory"; import MessageCommandContext from "../MessageCommandContext"; +import ServerConfig from "../ServerConfig"; /** * A basic command, consisting of basic attributes @@ -26,7 +27,7 @@ class SimpleCommand { removeEmptyArgs?: boolean | null; // This is executed whenever the command is ran. - run: (message: MessageCommandContext, args: string[]) => Promise; + run: (message: MessageCommandContext, args: string[], serverConfig?: ServerConfig|null) => Promise; // The category the command belongs to, used for /help. category: CommandCategory;