option to DM users on kick/ban/warn

This commit is contained in:
JandereDev 2022-05-28 14:00:22 +02:00
parent 7cee4638a9
commit 5546ec3c59
No known key found for this signature in database
GPG key ID: 5D5E18ACB990F57A
8 changed files with 140 additions and 22 deletions

View file

@ -3,15 +3,15 @@ import { client } from "../../../index";
import Infraction from "../../../struct/antispam/Infraction"; import Infraction from "../../../struct/antispam/Infraction";
import InfractionType from "../../../struct/antispam/InfractionType"; import InfractionType from "../../../struct/antispam/InfractionType";
import SimpleCommand from "../../../struct/commands/SimpleCommand"; import SimpleCommand from "../../../struct/commands/SimpleCommand";
import MessageCommandContext from "../../../struct/MessageCommandContext";
import { fetchUsername, logModAction } from "../../modules/mod_logs"; import { fetchUsername, logModAction } from "../../modules/mod_logs";
import { storeTempBan } from "../../modules/tempbans"; 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 Day from 'dayjs';
import RelativeTime from 'dayjs/plugin/relativeTime'; import RelativeTime from 'dayjs/plugin/relativeTime';
import CommandCategory from "../../../struct/commands/CommandCategory"; import CommandCategory from "../../../struct/commands/CommandCategory";
import { SendableEmbed } from "@janderedev/revolt.js/node_modules/revolt-api"; import { SendableEmbed } from "@janderedev/revolt.js/node_modules/revolt-api";
import { User } from "@janderedev/revolt.js"; import { User } from "@janderedev/revolt.js";
import logger from "../../logger";
Day.extend(RelativeTime); Day.extend(RelativeTime);
@ -22,7 +22,7 @@ export default {
syntax: '/ban @username [10m|1h|...?] [reason?]', syntax: '/ban @username [10m|1h|...?] [reason?]',
removeEmptyArgs: true, removeEmptyArgs: true,
category: CommandCategory.Moderation, category: CommandCategory.Moderation,
run: async (message: MessageCommandContext, args: string[]) => { run: async (message, args, serverConfig) => {
if (!await isModerator(message)) if (!await isModerator(message))
return message.reply(NO_MANAGER_MSG); return message.reply(NO_MANAGER_MSG);
if (!message.serverContext.havePermission('BanMembers')) { if (!message.serverContext.havePermission('BanMembers')) {
@ -134,6 +134,7 @@ export default {
type: InfractionType.Manual, type: InfractionType.Manual,
user: user._id, user: user._id,
actionType: 'ban', actionType: 'ban',
expires: Infinity,
} }
const { userWarnCount } = await storeInfraction(infraction); const { userWarnCount } = await storeInfraction(infraction);
@ -157,6 +158,20 @@ export default {
continue; 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, { await message.serverContext.banUser(user._id, {
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})` reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})`
}); });
@ -186,9 +201,24 @@ export default {
type: InfractionType.Manual, type: InfractionType.Manual,
user: user._id, user: user._id,
actionType: 'ban', actionType: 'ban',
expires: banUntil,
} }
const { userWarnCount } = await storeInfraction(infraction); 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, { await message.serverContext.banUser(user._id, {
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id}) (${durationStr})` reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id}) (${durationStr})`
}); });

View file

@ -1,5 +1,4 @@
import { User } from "@janderedev/revolt.js"; import { User } from "@janderedev/revolt.js";
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
import { SendableEmbed } from "revolt-api"; import { SendableEmbed } from "revolt-api";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { client } from "../../../"; import { client } from "../../../";
@ -7,9 +6,9 @@ import Infraction from "../../../struct/antispam/Infraction";
import InfractionType from "../../../struct/antispam/InfractionType"; import InfractionType from "../../../struct/antispam/InfractionType";
import CommandCategory from "../../../struct/commands/CommandCategory"; import CommandCategory from "../../../struct/commands/CommandCategory";
import SimpleCommand from "../../../struct/commands/SimpleCommand"; import SimpleCommand from "../../../struct/commands/SimpleCommand";
import MessageCommandContext from "../../../struct/MessageCommandContext"; import logger from "../../logger";
import { fetchUsername, logModAction } from "../../modules/mod_logs"; 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 { export default {
name: 'kick', name: 'kick',
@ -18,7 +17,7 @@ export default {
syntax: '/kick @username [reason?]', syntax: '/kick @username [reason?]',
removeEmptyArgs: true, removeEmptyArgs: true,
category: CommandCategory.Moderation, category: CommandCategory.Moderation,
run: async (message: MessageCommandContext, args: string[]) => { run: async (message, args, serverConfig) => {
if (!await isModerator(message)) if (!await isModerator(message))
return message.reply(NO_MANAGER_MSG); return message.reply(NO_MANAGER_MSG);
if (!message.serverContext.havePermission('KickMembers')) { if (!message.serverContext.havePermission('KickMembers')) {
@ -104,13 +103,27 @@ export default {
_id: infId, _id: infId,
createdBy: message.author_id, createdBy: message.author_id,
date: Date.now(), date: Date.now(),
reason: reason, reason: reason || 'No reason provided',
server: message.serverContext._id, server: message.serverContext._id,
type: InfractionType.Manual, type: InfractionType.Manual,
user: user._id, user: user._id,
actionType: 'kick', 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([ let [ { userWarnCount } ] = await Promise.all([
storeInfraction(infraction), storeInfraction(infraction),
logModAction('kick', message.serverContext, message.member!, user._id, reason, infraction._id), logModAction('kick', message.serverContext, message.member!, user._id, reason, infraction._id),

View file

@ -1,13 +1,13 @@
import SimpleCommand from "../../../struct/commands/SimpleCommand"; 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 Infraction from "../../../struct/antispam/Infraction";
import { ulid } from "ulid"; import { ulid } from "ulid";
import InfractionType from "../../../struct/antispam/InfractionType"; import InfractionType from "../../../struct/antispam/InfractionType";
import { fetchUsername, logModAction } from "../../modules/mod_logs"; import { fetchUsername, logModAction } from "../../modules/mod_logs";
import MessageCommandContext from "../../../struct/MessageCommandContext";
import CommandCategory from "../../../struct/commands/CommandCategory"; import CommandCategory from "../../../struct/commands/CommandCategory";
import { SendableEmbed } from "revolt-api"; import { SendableEmbed } from "revolt-api";
import { User } from "@janderedev/revolt.js"; import { User } from "@janderedev/revolt.js";
import logger from "../../logger";
export default { export default {
name: 'warn', name: 'warn',
@ -15,7 +15,7 @@ export default {
removeEmptyArgs: false, removeEmptyArgs: false,
description: 'add an infraction to an user\'s record', description: 'add an infraction to an user\'s record',
category: CommandCategory.Moderation, 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 (!await isModerator(message)) return message.reply(NO_MANAGER_MSG);
const userInput = args.shift() || ''; const userInput = args.shift() || '';
@ -94,15 +94,32 @@ export default {
} as Infraction; } as Infraction;
let { userWarnCount } = await storeInfraction(infraction); let { userWarnCount } = await storeInfraction(infraction);
await logModAction( await Promise.all([
'warn', logModAction(
message.serverContext, 'warn',
message.member!, message.serverContext,
user._id, message.member!,
reason || 'No reason provided', user._id,
infraction._id, reason || 'No reason provided',
`This is warn number ${userWarnCount} for this user.` 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({ embeds.push({
title: `User warned`, title: `User warned`,

View file

@ -136,7 +136,7 @@ let commands: SimpleCommand[];
} }
try { try {
await cmd.run(message, args); await cmd.run(message, args, config);
} catch(e) { } catch(e) {
console.error(e); console.error(e);
message.reply(`### An error has occurred:\n\`\`\`js\n${e}\n\`\`\``); message.reply(`### An error has occurred:\n\`\`\`js\n${e}\n\`\`\``);

View file

@ -15,11 +15,16 @@ import { Permission } from "@janderedev/revolt.js/dist/permissions/definitions";
import { Message } from "@janderedev/revolt.js/dist/maps/Messages"; import { Message } from "@janderedev/revolt.js/dist/maps/Messages";
import { isSudo } from "./commands/admin/botadm"; import { isSudo } from "./commands/admin/botadm";
import { SendableEmbed } from "revolt-api"; import { SendableEmbed } from "revolt-api";
import MessageCommandContext from "../struct/MessageCommandContext";
import ServerConfig from "../struct/ServerConfig";
const NO_MANAGER_MSG = '🔒 Missing permission'; const NO_MANAGER_MSG = '🔒 Missing permission';
const ULID_REGEX = /^[0-9A-HJ-KM-NP-TV-Z]{26}$/i; 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 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 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 autumn_url: string|null = null;
let apiConfig: any = axios.get(client.apiURL).then(res => { let apiConfig: any = axios.get(client.apiURL).then(res => {
autumn_url = (res.data as any).features.autumn.url; autumn_url = (res.data as any).features.autumn.url;
@ -347,6 +352,52 @@ const awaitClient = () => new Promise<void>(async resolve => {
else 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()}' <t:${Math.round(infraction.date / 1000)}:R>.\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** <t:${Math.round(infraction.expires / 1000)}:R>`)
: '')
}
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 { export {
getAutumnURL, getAutumnURL,
hasPerm, hasPerm,
@ -366,6 +417,8 @@ export {
dedupeArray, dedupeArray,
awaitClient, awaitClient,
getMutualServers, getMutualServers,
getDmChannel,
generateInfractionDMEmbed,
EmbedColor, EmbedColor,
NO_MANAGER_MSG, NO_MANAGER_MSG,
ULID_REGEX, ULID_REGEX,

View file

@ -25,6 +25,9 @@ class ServerConfig {
modAction?: LogConfig, // User warned, kicked or banned modAction?: LogConfig, // User warned, kicked or banned
}; };
allowBlacklistedUsers?: boolean; // Whether the server explicitly allows users that are globally blacklisted 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; export default ServerConfig;

View file

@ -12,6 +12,7 @@ class Infraction {
targetMessages?: string[]; targetMessages?: string[];
reason: string; reason: string;
date: number; date: number;
expires?: number; // Only applies to bans
} }
export default Infraction; export default Infraction;

View file

@ -1,5 +1,6 @@
import CommandCategory from "./CommandCategory"; import CommandCategory from "./CommandCategory";
import MessageCommandContext from "../MessageCommandContext"; import MessageCommandContext from "../MessageCommandContext";
import ServerConfig from "../ServerConfig";
/** /**
* A basic command, consisting of basic attributes * A basic command, consisting of basic attributes
@ -26,7 +27,7 @@ class SimpleCommand {
removeEmptyArgs?: boolean | null; removeEmptyArgs?: boolean | null;
// This is executed whenever the command is ran. // This is executed whenever the command is ran.
run: (message: MessageCommandContext, args: string[]) => Promise<any>; run: (message: MessageCommandContext, args: string[], serverConfig?: ServerConfig|null) => Promise<any>;
// The category the command belongs to, used for /help. // The category the command belongs to, used for /help.
category: CommandCategory; category: CommandCategory;