Confirmation prompt when kicking/banning via reply

This commit is contained in:
Lea 2023-03-15 20:21:28 +01:00
parent 2f9792c616
commit 5c3479268d
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
4 changed files with 783 additions and 291 deletions

View file

@ -1,5 +1,5 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": false,
"editor.formatOnSaveMode": "modifications", "editor.formatOnSaveMode": "modifications",
"prettier.tabWidth": 4 "prettier.tabWidth": 4
} }

View file

@ -5,9 +5,22 @@ import InfractionType from "automod/dist/types/antispam/InfractionType";
import SimpleCommand from "../../../struct/commands/SimpleCommand"; import SimpleCommand from "../../../struct/commands/SimpleCommand";
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, generateInfractionDMEmbed, getDmChannel, isModerator, NO_MANAGER_MSG, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; import {
import Day from 'dayjs'; dedupeArray,
import RelativeTime from 'dayjs/plugin/relativeTime'; embed,
EmbedColor,
generateInfractionDMEmbed,
getDmChannel,
getMembers,
isModerator,
NO_MANAGER_MSG,
parseUserOrId,
sanitizeMessageContent,
storeInfraction,
yesNoMessage,
} from "../../util";
import Day from "dayjs";
import RelativeTime from "dayjs/plugin/relativeTime";
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";
@ -16,30 +29,40 @@ import logger from "../../logger";
Day.extend(RelativeTime); Day.extend(RelativeTime);
export default { export default {
name: 'ban', name: "ban",
aliases: [ 'eject' ], aliases: ["eject"],
description: 'Ban a member from the server', description: "Ban a member from the server",
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, args, serverConfig) => { 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')) { return await message.reply({
return await message.reply({ embeds: [ embeds: [
embed(`Sorry, I do not have \`BanMembers\` permission.`, '', EmbedColor.SoftError) embed(
] }); `Sorry, I do not have \`BanMembers\` permission.`,
"",
EmbedColor.SoftError
),
],
});
} }
const userInput = !message.reply_ids?.length ? args.shift() || '' : undefined; const userInput = !message.reply_ids?.length
if (!userInput && !message.reply_ids?.length) return message.reply({ embeds: [ ? args.shift() || ""
: undefined;
if (!userInput && !message.reply_ids?.length)
return message.reply({
embeds: [
embed( embed(
`Please specify one or more users by replying to their message while running this command or ` + `Please specify one or more users by replying to their message while running this command or ` +
`by specifying a comma-separated list of usernames.`, `by specifying a comma-separated list of usernames.`,
'No target user specified', "No target user specified",
EmbedColor.SoftError, EmbedColor.SoftError
), ),
] }); ],
});
let banDuration = 0; let banDuration = 0;
let durationStr = args.shift(); let durationStr = args.shift();
@ -49,48 +72,84 @@ export default {
// Being able to specify the same letter multiple times // Being able to specify the same letter multiple times
// (e.g. 1s1s) and having their values stack is a feature // (e.g. 1s1s) and having their values stack is a feature
for (const piece of pieces) { for (const piece of pieces) {
let [ num, letter ] = [ Number(piece.slice(0, piece.length - 1)), piece.slice(piece.length - 1) ]; let [num, letter] = [
Number(piece.slice(0, piece.length - 1)),
piece.slice(piece.length - 1),
];
let multiplier = 0; let multiplier = 0;
switch(letter) { switch (letter) {
case 's': multiplier = 1000; break; case "s":
case 'm': multiplier = 1000 * 60; break; multiplier = 1000;
case 'h': multiplier = 1000 * 60 * 60; break; break;
case 'd': multiplier = 1000 * 60 * 60 * 24; break; case "m":
case 'w': multiplier = 1000 * 60 * 60 * 24 * 7; break; multiplier = 1000 * 60;
case 'y': multiplier = 1000 * 60 * 60 * 24 * 365; break; 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; banDuration += num * multiplier;
} }
} else if (durationStr) args.unshift(durationStr); } else if (durationStr) args.unshift(durationStr);
let reason = args.join(' ') let reason = args
?.replace(new RegExp('`', 'g'), '\'') .join(" ")
?.replace(new RegExp('\n', 'g'), ' '); ?.replace(new RegExp("`", "g"), "'")
?.replace(new RegExp("\n", "g"), " ");
if (reason.length > 500) return message.reply({ if (reason.length > 500)
embeds: [ embed('Ban reason may not be longer than 500 characters.', null, EmbedColor.SoftError) ] return message.reply({
embeds: [
embed(
"Ban reason may not be longer than 500 characters.",
null,
EmbedColor.SoftError
),
],
}); });
const embeds: SendableEmbed[] = []; const embeds: SendableEmbed[] = [];
const handledUsers: string[] = []; const handledUsers: string[] = [];
const targetUsers: User|{ _id: string }[] = []; const targetUsers: User | { _id: string }[] = [];
const targetInput = dedupeArray( const targetInput = dedupeArray(
message.reply_ids?.length message.reply_ids?.length
? (await Promise.allSettled( ? (
message.reply_ids.map(msg => message.channel?.fetchMessage(msg)) await Promise.allSettled(
)) message.reply_ids.map((msg) =>
.filter(m => m.status == 'fulfilled').map(m => (m as any).value.author_id) message.channel?.fetchMessage(msg)
: userInput!.split(','), )
)
)
.filter((m) => m.status == "fulfilled")
.map((m) => (m as any).value.author_id)
: userInput!.split(",")
); );
for (const userStr of targetInput) { for (const userStr of targetInput) {
try { try {
let user = await parseUserOrId(userStr); let user = await parseUserOrId(userStr);
if (!user) { if (!user) {
embeds.push(embed(`I can't resolve \`${sanitizeMessageContent(userStr).trim()}\` to a user.`, null, EmbedColor.SoftError)); embeds.push(
embed(
`I can't resolve \`${sanitizeMessageContent(
userStr
).trim()}\` to a user.`,
null,
EmbedColor.SoftError
)
);
continue; continue;
} }
@ -99,27 +158,57 @@ export default {
handledUsers.push(user._id); handledUsers.push(user._id);
if (user._id == message.author_id) { if (user._id == message.author_id) {
embeds.push(embed('I recommend against banning yourself :yeahokayyy:', null, EmbedColor.Warning)); embeds.push(
embed(
"I recommend against banning yourself :yeahokayyy:",
null,
EmbedColor.Warning
)
);
continue; continue;
} }
if (user._id == client.user!._id) { if (user._id == client.user!._id) {
embeds.push(embed('I\'m not going to ban myself :flushee:', null, EmbedColor.Warning)); embeds.push(
embed(
"I'm not going to ban myself :flushee:",
null,
EmbedColor.Warning
)
);
continue; continue;
} }
targetUsers.push(user); targetUsers.push(user);
} catch(e) { } catch (e) {
console.error(e); console.error(e);
embeds.push(embed( embeds.push(
`Failed to ban target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`, embed(
`Failed to ban target \`${sanitizeMessageContent(
userStr
).trim()}\`: ${e}`,
`Failed to ban: An error has occurred`, `Failed to ban: An error has occurred`,
EmbedColor.Error, EmbedColor.Error
)); )
);
} }
} }
const members = await message.serverContext.fetchMembers(); if (message.reply_ids?.length && targetUsers.length) {
let res = await yesNoMessage(
message.channel!,
message.author_id,
`This will ban the author${targetUsers.length > 1 ? 's' : ''} `
+ `of the message${message.reply_ids.length > 1 ? 's' : ''} you replied to.\n`
+ `The following user${targetUsers.length > 1 ? 's' : ''} will be affected: `
+ `${targetUsers.map(u => `<@${u._id}>`).join(', ')}.\n`
+ `Are you sure?`,
'Confirm action'
);
if (!res) return;
}
const members = getMembers(message.serverContext._id);
for (const user of targetUsers) { for (const user of targetUsers) {
try { try {
@ -129,64 +218,106 @@ export default {
_id: infId, _id: infId,
createdBy: message.author_id, createdBy: message.author_id,
date: Date.now(), date: Date.now(),
reason: reason || 'No reason provided', 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: 'ban', actionType: "ban",
expires: Infinity, expires: Infinity,
} };
const { userWarnCount } = await storeInfraction(infraction); const { userWarnCount } = await storeInfraction(infraction);
const member = members.members.find(m => m._id.user == user._id); const member = members.find((m) => m._id.user == user._id);
if (member && message.member && !member.inferiorTo(message.member)) { if (
embeds.push(embed( member &&
message.member &&
!member.inferiorTo(message.member)
) {
embeds.push(
embed(
`\`${member.user?.username}\` has an equally or higher ranked role than you; refusing to ban.`, `\`${member.user?.username}\` has an equally or higher ranked role than you; refusing to ban.`,
'Missing permission', "Missing permission",
EmbedColor.SoftError EmbedColor.SoftError
)); )
);
continue; continue;
} }
if (member && !member.bannable) { if (member && !member.bannable) {
embeds.push(embed( embeds.push(
`I don't have permission to ban \`${member?.user?.username || user._id}\`.`, embed(
`I don't have permission to ban \`${
member?.user?.username || user._id
}\`.`,
null, null,
EmbedColor.SoftError EmbedColor.SoftError
)); )
);
continue; continue;
} }
if (serverConfig?.dmOnKick) { if (serverConfig?.dmOnKick) {
try { try {
const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message); const embed = generateInfractionDMEmbed(
message.serverContext,
serverConfig,
infraction,
message
);
const dmChannel = await getDmChannel(user); const dmChannel = await getDmChannel(user);
if (dmChannel.havePermission('SendMessage') || dmChannel.havePermission('SendEmbeds')) { if (
await dmChannel.sendMessage({ embeds: [ embed ] }); dmChannel.havePermission("SendMessage") ||
} dmChannel.havePermission("SendEmbeds")
else logger.warn('Missing permission to DM user.'); ) {
} catch(e) { await dmChannel.sendMessage({
embeds: [embed],
});
} else
logger.warn("Missing permission to DM user.");
} catch (e) {
console.error(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
})`,
}); });
await logModAction('ban', message.serverContext, message.member!, user._id, reason, infraction._id, `Ban duration: **Permanent**`); await logModAction(
"ban",
message.serverContext,
message.member!,
user._id,
reason,
infraction._id,
`Ban duration: **Permanent**`
);
embeds.push({ embeds.push({
title: `User ${Math.random() > 0.8 ? 'ejected' : 'banned'}`, title: `User ${
icon_url: user instanceof User ? user.generateAvatarURL() : undefined, Math.random() > 0.8 ? "ejected" : "banned"
}`,
icon_url:
user instanceof User
? user.generateAvatarURL()
: undefined,
colour: EmbedColor.Success, colour: EmbedColor.Success,
description: `This is ${userWarnCount == 1 ? '**the first infraction**' : `infraction number **${userWarnCount}**`}` + description:
`This is ${
userWarnCount == 1
? "**the first infraction**"
: `infraction number **${userWarnCount}**`
}` +
` for ${await fetchUsername(user._id)}.\n` + ` for ${await fetchUsername(user._id)}.\n` +
`**User ID:** \`${user._id}\`\n` + `**User ID:** \`${user._id}\`\n` +
`**Infraction ID:** \`${infraction._id}\`\n` + `**Infraction ID:** \`${infraction._id}\`\n` +
`**Reason:** \`${infraction.reason}\`` `**Reason:** \`${infraction.reason}\``,
}); });
} else { } else {
const banUntil = Date.now() + banDuration; const banUntil = Date.now() + banDuration;
@ -196,31 +327,47 @@ export default {
_id: infId, _id: infId,
createdBy: message.author_id, createdBy: message.author_id,
date: Date.now(), date: Date.now(),
reason: (reason || 'No reason provided') + ` (${durationStr})`, reason:
(reason || "No reason provided") +
` (${durationStr})`,
server: message.serverContext._id, server: message.serverContext._id,
type: InfractionType.Manual, type: InfractionType.Manual,
user: user._id, user: user._id,
actionType: 'ban', actionType: "ban",
expires: banUntil, expires: banUntil,
} };
const { userWarnCount } = await storeInfraction(infraction); const { userWarnCount } = await storeInfraction(infraction);
if (serverConfig?.dmOnKick) { if (serverConfig?.dmOnKick) {
try { try {
const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message); const embed = generateInfractionDMEmbed(
message.serverContext,
serverConfig,
infraction,
message
);
const dmChannel = await getDmChannel(user); const dmChannel = await getDmChannel(user);
if (dmChannel.havePermission('SendMessage') || dmChannel.havePermission('SendEmbeds')) { if (
await dmChannel.sendMessage({ embeds: [ embed ] }); dmChannel.havePermission("SendMessage") ||
} dmChannel.havePermission("SendEmbeds")
else logger.warn('Missing permission to DM user.'); ) {
} catch(e) { await dmChannel.sendMessage({
embeds: [embed],
});
} else
logger.warn("Missing permission to DM user.");
} catch (e) {
console.error(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})`,
}); });
await Promise.all([ await Promise.all([
@ -231,7 +378,7 @@ export default {
until: banUntil, until: banUntil,
}), }),
logModAction( logModAction(
'ban', "ban",
message.serverContext, message.serverContext,
message.member!, message.member!,
user._id, user._id,
@ -243,23 +390,36 @@ export default {
embeds.push({ embeds.push({
title: `User temporarily banned`, title: `User temporarily banned`,
icon_url: user instanceof User ? user.generateAvatarURL() : undefined, icon_url:
user instanceof User
? user.generateAvatarURL()
: undefined,
colour: EmbedColor.Success, colour: EmbedColor.Success,
description: `This is ${userWarnCount == 1 ? '**the first infraction**' : `infraction number **${userWarnCount}**`}` + description:
`This is ${
userWarnCount == 1
? "**the first infraction**"
: `infraction number **${userWarnCount}**`
}` +
` for ${await fetchUsername(user._id)}.\n` + ` for ${await fetchUsername(user._id)}.\n` +
`**Ban duration:** ${banDurationFancy}\n` + `**Ban duration:** ${banDurationFancy}\n` +
`**User ID:** \`${user._id}\`\n` + `**User ID:** \`${user._id}\`\n` +
`**Infraction ID:** \`${infraction._id}\`\n` + `**Infraction ID:** \`${infraction._id}\`\n` +
`**Reason:** \`${infraction.reason}\`` `**Reason:** \`${infraction.reason}\``,
}); });
} }
} catch(e) { } catch (e) {
console.error(e); console.error(e);
embeds.push(embed( embeds.push(
`Failed to ban target \`${await fetchUsername(user._id, user._id)}\`: ${e}`, embed(
'Failed to ban: An error has occurred', `Failed to ban target \`${await fetchUsername(
EmbedColor.Error, user._id,
)); user._id
)}\`: ${e}`,
"Failed to ban: An error has occurred",
EmbedColor.Error
)
);
} }
} }
@ -268,11 +428,14 @@ export default {
const targetEmbeds = embeds.splice(0, 10); const targetEmbeds = embeds.splice(0, 10);
if (firstMsg) { if (firstMsg) {
await message.reply({ embeds: targetEmbeds, content: 'Operation completed.' }, false); await message.reply(
{ embeds: targetEmbeds, content: "Operation completed." },
false
);
} else { } else {
await message.channel?.sendMessage({ embeds: targetEmbeds }); await message.channel?.sendMessage({ embeds: targetEmbeds });
} }
firstMsg = false; firstMsg = false;
} }
} },
} as SimpleCommand; } as SimpleCommand;

View file

@ -8,58 +8,99 @@ import CommandCategory from "../../../struct/commands/CommandCategory";
import SimpleCommand from "../../../struct/commands/SimpleCommand"; import SimpleCommand from "../../../struct/commands/SimpleCommand";
import logger from "../../logger"; import logger from "../../logger";
import { fetchUsername, logModAction } from "../../modules/mod_logs"; import { fetchUsername, logModAction } from "../../modules/mod_logs";
import { dedupeArray, embed, EmbedColor, generateInfractionDMEmbed, getDmChannel, isModerator, NO_MANAGER_MSG, parseUser, parseUserOrId, sanitizeMessageContent, storeInfraction } from "../../util"; import {
dedupeArray,
embed,
EmbedColor,
generateInfractionDMEmbed,
getDmChannel,
getMembers,
isModerator,
NO_MANAGER_MSG,
parseUser,
parseUserOrId,
sanitizeMessageContent,
storeInfraction,
yesNoMessage,
} from "../../util";
export default { export default {
name: 'kick', name: "kick",
aliases: [ 'yeet', 'vent' ], aliases: ["yeet", "vent"],
description: 'Kick a member from the server', description: "Kick a member from the server",
syntax: '/kick @username [reason?]', syntax: "/kick @username [reason?]",
removeEmptyArgs: true, removeEmptyArgs: true,
category: CommandCategory.Moderation, category: CommandCategory.Moderation,
run: async (message, args, serverConfig) => { 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')) { return await message.reply(
return await message.reply(`Sorry, I do not have \`KickMembers\` permission.`); `Sorry, I do not have \`KickMembers\` permission.`
);
} }
const userInput = !message.reply_ids?.length ? args.shift() || '' : undefined; const userInput = !message.reply_ids?.length
if (!userInput && !message.reply_ids?.length) return message.reply({ embeds: [ ? args.shift() || ""
: undefined;
if (!userInput && !message.reply_ids?.length)
return message.reply({
embeds: [
embed( embed(
`Please specify one or more users by replying to their message while running this command or ` + `Please specify one or more users by replying to their message while running this command or ` +
`by specifying a comma-separated list of usernames.`, `by specifying a comma-separated list of usernames.`,
'No target user specified', "No target user specified",
EmbedColor.SoftError, EmbedColor.SoftError
), ),
] }); ],
});
let reason = args.join(' ') let reason = args
?.replace(new RegExp('`', 'g'), '\'') .join(" ")
?.replace(new RegExp('\n', 'g'), ' '); ?.replace(new RegExp("`", "g"), "'")
?.replace(new RegExp("\n", "g"), " ");
if (reason.length > 500) return message.reply({ if (reason.length > 500)
embeds: [ embed('Kick reason may not be longer than 500 characters.', null, EmbedColor.SoftError) ] return message.reply({
embeds: [
embed(
"Kick reason may not be longer than 500 characters.",
null,
EmbedColor.SoftError
),
],
}); });
const embeds: SendableEmbed[] = []; const embeds: SendableEmbed[] = [];
const handledUsers: string[] = []; const handledUsers: string[] = [];
const targetUsers: User|{ _id: string }[] = []; const targetUsers: User | { _id: string }[] = [];
const targetInput = dedupeArray( const targetInput = dedupeArray(
message.reply_ids?.length message.reply_ids?.length
? (await Promise.allSettled( ? (
message.reply_ids.map(msg => message.channel?.fetchMessage(msg)) await Promise.allSettled(
)) message.reply_ids.map((msg) =>
.filter(m => m.status == 'fulfilled').map(m => (m as any).value.author_id) message.channel?.fetchMessage(msg)
: userInput!.split(','), )
)
)
.filter((m) => m.status == "fulfilled")
.map((m) => (m as any).value.author_id)
: userInput!.split(",")
); );
for (const userStr of targetInput) { for (const userStr of targetInput) {
try { try {
let user = await parseUserOrId(userStr); let user = await parseUserOrId(userStr);
if (!user) { if (!user) {
embeds.push(embed(`I can't resolve \`${sanitizeMessageContent(userStr).trim()}\` to a user.`, null, EmbedColor.SoftError)); embeds.push(
embed(
`I can't resolve \`${sanitizeMessageContent(
userStr
).trim()}\` to a user.`,
null,
EmbedColor.SoftError
)
);
continue; continue;
} }
@ -68,33 +109,69 @@ export default {
handledUsers.push(user._id); handledUsers.push(user._id);
if (user._id == message.author_id) { if (user._id == message.author_id) {
embeds.push(embed('You might want to avoid kicking yourself...', null, EmbedColor.Warning)); embeds.push(
embed(
"You might want to avoid kicking yourself...",
null,
EmbedColor.Warning
)
);
continue; continue;
} }
if (user._id == client.user!._id) { if (user._id == client.user!._id) {
embeds.push(embed('I won\'t allow you to get rid of me this easily :trol:', null, EmbedColor.Warning)); embeds.push(
embed(
"I won't allow you to get rid of me this easily :trol:",
null,
EmbedColor.Warning
)
);
continue; continue;
} }
targetUsers.push(user); targetUsers.push(user);
} catch(e) { } catch (e) {
console.error(e); console.error(e);
embeds.push(embed( embeds.push(
`Failed to kick target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`, embed(
`Failed to kick target \`${sanitizeMessageContent(
userStr
).trim()}\`: ${e}`,
`Failed to kick: An error has occurred`, `Failed to kick: An error has occurred`,
EmbedColor.Error, EmbedColor.Error
)); )
);
} }
} }
const members = await message.serverContext.fetchMembers(); if (message.reply_ids?.length && targetUsers.length) {
let res = await yesNoMessage(
message.channel!,
message.author_id,
`This will kick the author${targetUsers.length > 1 ? 's' : ''} `
+ `of the message${message.reply_ids.length > 1 ? 's' : ''} you replied to.\n`
+ `The following user${targetUsers.length > 1 ? 's' : ''} will be affected: `
+ `${targetUsers.map(u => `<@${u._id}>`).join(', ')}.\n`
+ `Are you sure?`,
'Confirm action'
);
if (!res) return;
}
const members = getMembers(message.serverContext._id);
for (const user of targetUsers) { for (const user of targetUsers) {
try { try {
const member = members.members.find(m => m._id.user == user._id); const member = members.find((m) => m._id.user == user._id);
if (!member) { if (!member) {
embeds.push(embed('')); embeds.push(
embed(
`\`${await fetchUsername(
user._id
)}\` is not a member of this server.`
)
);
continue; continue;
} }
@ -103,45 +180,75 @@ export default {
_id: infId, _id: infId,
createdBy: message.author_id, createdBy: message.author_id,
date: Date.now(), date: Date.now(),
reason: reason || 'No reason provided', 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) { if (serverConfig?.dmOnKick) {
try { try {
const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message); const embed = generateInfractionDMEmbed(
message.serverContext,
serverConfig,
infraction,
message
);
const dmChannel = await getDmChannel(user); const dmChannel = await getDmChannel(user);
if (dmChannel.havePermission('SendMessage') || dmChannel.havePermission('SendEmbeds')) { if (
await dmChannel.sendMessage({ embeds: [ embed ] }); dmChannel.havePermission("SendMessage") ||
} dmChannel.havePermission("SendEmbeds")
else logger.warn('Missing permission to DM user.'); ) {
} catch(e) { await dmChannel.sendMessage({ embeds: [embed] });
} else logger.warn("Missing permission to DM user.");
} catch (e) {
console.error(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
),
member.kick(), member.kick(),
]); ]);
embeds.push({ embeds.push({
title: `User kicked`, title: `User kicked`,
icon_url: user instanceof User ? user.generateAvatarURL() : undefined, icon_url:
user instanceof User
? user.generateAvatarURL()
: undefined,
colour: EmbedColor.Success, colour: EmbedColor.Success,
description: `This is ${userWarnCount == 1 ? '**the first infraction**' : `infraction number **${userWarnCount}**`}` + description:
`This is ${
userWarnCount == 1
? "**the first infraction**"
: `infraction number **${userWarnCount}**`
}` +
` for ${await fetchUsername(user._id)}.\n` + ` for ${await fetchUsername(user._id)}.\n` +
`**User ID:** \`${user._id}\`\n` + `**User ID:** \`${user._id}\`\n` +
`**Infraction ID:** \`${infraction._id}\`\n` + `**Infraction ID:** \`${infraction._id}\`\n` +
`**Reason:** \`${infraction.reason}\`` `**Reason:** \`${infraction.reason}\``,
}); });
} catch(e) { } catch (e) {
embeds.push(embed(`Failed to kick user ${await fetchUsername(user._id)}: ${e}`, 'Failed to kick user', EmbedColor.Error)); embeds.push(
embed(
`Failed to kick user ${await fetchUsername(
user._id
)}: ${e}`,
"Failed to kick user",
EmbedColor.Error
)
);
} }
} }
@ -150,11 +257,14 @@ export default {
const targetEmbeds = embeds.splice(0, 10); const targetEmbeds = embeds.splice(0, 10);
if (firstMsg) { if (firstMsg) {
await message.reply({ embeds: targetEmbeds, content: 'Operation completed.' }, false); await message.reply(
{ embeds: targetEmbeds, content: "Operation completed." },
false
);
} else { } else {
await message.channel?.sendMessage({ embeds: targetEmbeds }); await message.channel?.sendMessage({ embeds: targetEmbeds });
} }
firstMsg = false; firstMsg = false;
} }
} },
} as SimpleCommand; } as SimpleCommand;

View file

@ -17,21 +17,25 @@ import { isSudo } from "./commands/admin/botadm";
import { SendableEmbed } from "revolt-api"; import { SendableEmbed } from "revolt-api";
import MessageCommandContext from "../struct/MessageCommandContext"; import MessageCommandContext from "../struct/MessageCommandContext";
import ServerConfig from "automod/dist/types/ServerConfig"; import ServerConfig from "automod/dist/types/ServerConfig";
import { ClientboundNotification } from "@janderedev/revolt.js";
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_HTTP_URI = /^http(s?):\/\//g;
const RE_MAILTO_URI = /^mailto:/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;
}); });
async function getAutumnURL() { async function getAutumnURL() {
return autumn_url || ((await axios.get(client.apiURL)).data as any).features.autumn.url; return (
autumn_url ||
((await axios.get(client.apiURL)).data as any).features.autumn.url
);
} }
/** /**
@ -40,20 +44,20 @@ async function getAutumnURL() {
* @param text * @param text
* @returns null if not found, otherwise user object * @returns null if not found, otherwise user object
*/ */
async function parseUser(text: string): Promise<User|null> { async function parseUser(text: string): Promise<User | null> {
if (!text) return null; if (!text) return null;
let uid: string|null = null; let uid: string | null = null;
if (USER_MENTION_REGEX.test(text)) { if (USER_MENTION_REGEX.test(text)) {
uid = text.replace(/<@|>/g, '').toUpperCase(); uid = text.replace(/<@|>/g, "").toUpperCase();
} else if (/^[0-9A-HJ-KM-NP-TV-Z]{26}$/gi.test(text)) { } else if (/^[0-9A-HJ-KM-NP-TV-Z]{26}$/gi.test(text)) {
uid = text.toUpperCase(); uid = text.toUpperCase();
} else { } else {
if (text.startsWith('@')) text = text.substr(1); if (text.startsWith("@")) text = text.substr(1);
// Why is there no .find() or .filter() // Why is there no .find() or .filter()
let user: User|null = null; let user: User | null = null;
client.users.forEach(u => { client.users.forEach((u) => {
if (u.username?.toLowerCase() == text.toLowerCase()) { if (u.username?.toLowerCase() == text.toLowerCase()) {
user = u; user = u;
} }
@ -63,16 +67,20 @@ async function parseUser(text: string): Promise<User|null> {
} }
try { try {
if (uid) return await client.users.fetch(uid) || null; if (uid) return (await client.users.fetch(uid)) || null;
else return null; else return null;
} catch(e) { return null; } } catch (e) {
return null;
}
} }
/** /**
* Does the exact same as `parseUser`, but returns only `_id` instead * Does the exact same as `parseUser`, but returns only `_id` instead
* of null if the user was not found and the input is also an ID * of null if the user was not found and the input is also an ID
*/ */
async function parseUserOrId(text: string): Promise<User|{_id: string}|null> { async function parseUserOrId(
text: string
): Promise<User | { _id: string } | null> {
let parsed = await parseUser(text); let parsed = await parseUser(text);
if (parsed) return parsed; if (parsed) return parsed;
if (ULID_REGEX.test(text)) return { _id: text.toUpperCase() }; if (ULID_REGEX.test(text)) return { _id: text.toUpperCase() };
@ -80,63 +88,87 @@ async function parseUserOrId(text: string): Promise<User|{_id: string}|null> {
} }
async function isModerator(message: Message, announceSudo?: boolean) { async function isModerator(message: Message, announceSudo?: boolean) {
let member = message.member!, server = message.channel!.server!; let member = message.member!,
server = message.channel!.server!;
if (hasPerm(member, 'KickMembers')) return true; if (hasPerm(member, "KickMembers")) return true;
const [ isManager, mods, isSudo ] = await Promise.all([ const [isManager, mods, isSudo] = await Promise.all([
isBotManager(message), isBotManager(message),
dbs.SERVERS.findOne({ id: server._id }), dbs.SERVERS.findOne({ id: server._id }),
checkSudoPermission(message, announceSudo), checkSudoPermission(message, announceSudo),
]); ]);
return isManager return (
|| (mods?.moderators?.indexOf(member.user?._id!) ?? -1) > -1 isManager ||
|| isSudo; (mods?.moderators?.indexOf(member.user?._id!) ?? -1) > -1 ||
isSudo
);
} }
async function isBotManager(message: Message, announceSudo?: boolean) { async function isBotManager(message: Message, announceSudo?: boolean) {
let member = message.member!, server = message.channel!.server!; let member = message.member!,
server = message.channel!.server!;
if (hasPerm(member, 'ManageServer')) return true; if (hasPerm(member, "ManageServer")) return true;
const [ managers, isSudo ] = await Promise.all([ const [managers, isSudo] = await Promise.all([
dbs.SERVERS.findOne({ id: server._id }), dbs.SERVERS.findOne({ id: server._id }),
checkSudoPermission(message, announceSudo), checkSudoPermission(message, announceSudo),
]); ]);
return (managers?.botManagers?.indexOf(member.user?._id!) ?? -1) > -1 return (
|| isSudo; (managers?.botManagers?.indexOf(member.user?._id!) ?? -1) > -1 || isSudo
);
} }
async function checkSudoPermission(message: Message, announce?: boolean): Promise<boolean> { async function checkSudoPermission(
message: Message,
announce?: boolean
): Promise<boolean> {
const hasPerm = isSudo(message.author!); const hasPerm = isSudo(message.author!);
if (!hasPerm) return false; if (!hasPerm) return false;
else { else {
if (announce !== false) { if (announce !== false) {
await message.reply(`# :unlock: Bypassed permission check\n` await message.reply(
+ `Sudo mode is enabled for @${message.author!.username}.\n`); `# :unlock: Bypassed permission check\n` +
`Sudo mode is enabled for @${message.author!.username}.\n`
);
} }
return true; return true;
} }
} }
async function getPermissionLevel(user: User|Member, server: Server): Promise<0|1|2|3> { async function getPermissionLevel(
if (isSudo(user instanceof User ? user : (user.user || await client.users.fetch(user._id.user)))) return 3; user: User | Member,
server: Server
): Promise<0 | 1 | 2 | 3> {
if (
isSudo(
user instanceof User
? user
: user.user || (await client.users.fetch(user._id.user))
)
)
return 3;
const member = user instanceof User ? await server.fetchMember(user) : user; const member = user instanceof User ? await server.fetchMember(user) : user;
if (user instanceof Member) user = user.user!; if (user instanceof Member) user = user.user!;
if (hasPerm(member, 'ManageServer')) return 3; if (hasPerm(member, "ManageServer")) return 3;
const config = await dbs.SERVERS.findOne({ id: server._id }); const config = await dbs.SERVERS.findOne({ id: server._id });
if (config?.botManagers?.includes(user._id)) return 2; if (config?.botManagers?.includes(user._id)) return 2;
if (config?.moderators?.includes(user._id) || hasPerm(member, 'KickMembers')) return 1; if (
config?.moderators?.includes(user._id) ||
hasPerm(member, "KickMembers")
)
return 1;
return 0; return 0;
} }
function getPermissionBasedOnRole(member: Member): 0|1|2|3 { function getPermissionBasedOnRole(member: Member): 0 | 1 | 2 | 3 {
if (hasPerm(member, 'ManageServer')) return 3; if (hasPerm(member, "ManageServer")) return 3;
if (hasPerm(member, 'KickMembers')) return 1; if (hasPerm(member, "KickMembers")) return 1;
return 0; return 0;
} }
@ -153,85 +185,117 @@ function hasPerm(member: Member, perm: keyof typeof Permission): boolean {
/** /**
* @deprecated Unnecessary * @deprecated Unnecessary
*/ */
function hasPermForChannel(member: Member, channel: Channel, perm: keyof typeof Permission): boolean { function hasPermForChannel(
if (!member.server) throw 'hasPermForChannel(): Server is undefined'; member: Member,
channel: Channel,
perm: keyof typeof Permission
): boolean {
if (!member.server) throw "hasPermForChannel(): Server is undefined";
return member.hasPermission(channel, perm); return member.hasPermission(channel, perm);
} }
async function getOwnMemberInServer(server: Server): Promise<Member> { async function getOwnMemberInServer(server: Server): Promise<Member> {
return client.members.getKey({ server: server._id, user: client.user!._id }) return (
|| await server.fetchMember(client.user!._id); client.members.getKey({ server: server._id, user: client.user!._id }) ||
(await server.fetchMember(client.user!._id))
);
} }
async function storeInfraction(infraction: Infraction): Promise<{ userWarnCount: number }> { async function storeInfraction(
infraction: Infraction
): Promise<{ userWarnCount: number }> {
let r = await Promise.all([ let r = await Promise.all([
dbs.INFRACTIONS.insert(infraction, { castIds: false }), dbs.INFRACTIONS.insert(infraction, { castIds: false }),
dbs.INFRACTIONS.find({ dbs.INFRACTIONS.find({
server: infraction.server, server: infraction.server,
user: infraction.user, user: infraction.user,
_id: { $not: { $eq: infraction._id } } }, _id: { $not: { $eq: infraction._id } },
), }),
]); ]);
return { userWarnCount: (r[1].length ?? 0) + 1 } return { userWarnCount: (r[1].length ?? 0) + 1 };
} }
async function uploadFile(file: any, filename: string): Promise<string> { async function uploadFile(file: any, filename: string): Promise<string> {
let data = new FormData(); let data = new FormData();
data.append("file", file, { filename: filename }); data.append("file", file, { filename: filename });
let req = await axios.post(await getAutumnURL() + '/attachments', data, { headers: data.getHeaders() }); let req = await axios.post((await getAutumnURL()) + "/attachments", data, {
return (req.data as any)['id'] as string; headers: data.getHeaders(),
});
return (req.data as any)["id"] as string;
} }
async function sendLogMessage(config: LogConfig, content: LogMessage) { async function sendLogMessage(config: LogConfig, content: LogMessage) {
if (config.discord?.webhookUrl) { if (config.discord?.webhookUrl) {
let c = { ...content, ...content.overrides?.discord } let c = { ...content, ...content.overrides?.discord };
const embed = new MessageEmbed(); const embed = new MessageEmbed();
if (c.title) embed.setTitle(content.title); if (c.title) embed.setTitle(content.title);
if (c.description) embed.setDescription(c.description); if (c.description) embed.setDescription(c.description);
if (c.color?.match(/^#[0-9a-fA-F]+$/)) embed.setColor(c.color as ColorResolvable); if (c.color?.match(/^#[0-9a-fA-F]+$/))
embed.setColor(c.color as ColorResolvable);
if (c.fields?.length) { if (c.fields?.length) {
for (const field of c.fields) { for (const field of c.fields) {
embed.addField(field.title, field.content.trim() || "\u200b", field.inline); embed.addField(
field.title,
field.content.trim() || "\u200b",
field.inline
);
} }
} }
if (content.image) { if (content.image) {
if (content.image.type == 'THUMBNAIL') embed.setThumbnail(content.image.url); if (content.image.type == "THUMBNAIL")
else if (content.image.type == 'BIG') embed.setImage(content.image.url); embed.setThumbnail(content.image.url);
else if (content.image.type == "BIG")
embed.setImage(content.image.url);
} }
if (content.attachments?.length) { if (content.attachments?.length) {
embed.setFooter(`Attachments: ${content.attachments.map(a => a.name).join(', ')}`); embed.setFooter(
`Attachments: ${content.attachments
.map((a) => a.name)
.join(", ")}`
);
} }
let data = new FormData(); let data = new FormData();
content.attachments?.forEach(a => { content.attachments?.forEach((a) => {
data.append(`files[${ulid()}]`, a.content, { filename: a.name }); data.append(`files[${ulid()}]`, a.content, { filename: a.name });
}); });
data.append("payload_json", JSON.stringify({ embeds: [ embed.toJSON() ] }), { contentType: 'application/json' }); data.append(
"payload_json",
JSON.stringify({ embeds: [embed.toJSON()] }),
{ contentType: "application/json" }
);
axios.post(config.discord.webhookUrl, data, {headers: data.getHeaders() }) axios
.catch(e => logger.error(`Failed to send log message (discord): ${e}`)); .post(config.discord.webhookUrl, data, {
headers: data.getHeaders(),
})
.catch((e) =>
logger.error(`Failed to send log message (discord): ${e}`)
);
} }
if (config.revolt?.channel) { if (config.revolt?.channel) {
let c = { ...content, ...content.overrides?.revolt }; let c = { ...content, ...content.overrides?.revolt };
try { try {
const channel = client.channels.get(config.revolt.channel) || await client.channels.fetch(config.revolt.channel); const channel =
client.channels.get(config.revolt.channel) ||
(await client.channels.fetch(config.revolt.channel));
let message = ''; let message = "";
let embed: SendableEmbed|undefined = undefined; let embed: SendableEmbed | undefined = undefined;
switch(config.revolt.type) { switch (config.revolt.type) {
case 'EMBED': case "EMBED":
c = { ...c, ...content.overrides?.revoltEmbed }; c = { ...c, ...content.overrides?.revoltEmbed };
embed = { embed = {
title: c.title, title: c.title,
description: c.description, description: c.description,
colour: c.color, colour: c.color,
} };
if (c.fields?.length) { if (c.fields?.length) {
for (const field of c.fields) { for (const field of c.fields) {
@ -241,35 +305,55 @@ async function sendLogMessage(config: LogConfig, content: LogMessage) {
break; break;
default: // QUOTEBLOCK, PLAIN or unspecified default: // QUOTEBLOCK, PLAIN or unspecified
// Wrap entire message in quotes
// please disregard this mess // please disregard this mess
c = { ...c, ...content.overrides?.revoltQuoteblock }; c = { ...c, ...content.overrides?.revoltQuoteblock };
const quote = config.revolt.type == 'PLAIN' ? '' : '>'; const quote = config.revolt.type == "PLAIN" ? "" : ">";
if (c.title) message += `## ${c.title}\n`; if (c.title) message += `## ${c.title}\n`;
if (c.description) message += `${c.description}\n`; if (c.description) message += `${c.description}\n`;
if (c.fields?.length) { if (c.fields?.length) {
for (const field of c.fields) { for (const field of c.fields) {
message += `${quote ? '\u200b\n' : ''}${quote}### ${field.title}\n` + message +=
`${quote}${field.content.trim().split('\n').join('\n' + quote)}\n${quote ? '\n' : ''}`; `${quote ? "\u200b\n" : ""}${quote}### ${
field.title
}\n` +
`${quote}${field.content
.trim()
.split("\n")
.join("\n" + quote)}\n${quote ? "\n" : ""}`;
} }
} }
message = message.trim().split('\n').join('\n' + quote); // Wrap entire message in quotes message = message
if (c.image?.url) message += `\n[Attachment](${c.image.url})`; .trim()
.split("\n")
.join("\n" + quote);
if (c.image?.url)
message += `\n[Attachment](${c.image.url})`;
break; break;
} }
channel.sendMessage({ channel
.sendMessage({
content: message, content: message,
embeds: embed ? [ embed ] : undefined, embeds: embed ? [embed] : undefined,
attachments: content.attachments ? attachments: content.attachments
await Promise.all(content.attachments?.map(a => uploadFile(a.content, a.name))) : ? await Promise.all(
undefined content.attachments?.map((a) =>
}).catch(e => logger.error(`Failed to send log message (revolt): ${e}`)); uploadFile(a.content, a.name)
} catch(e) { )
logger.error(`Failed to send log message in ${config.revolt.channel}: ${e}`); )
: undefined,
})
.catch((e) =>
logger.error(`Failed to send log message (revolt): ${e}`)
);
} catch (e) {
logger.error(
`Failed to send log message in ${config.revolt.channel}: ${e}`
);
} }
} }
} }
@ -278,17 +362,17 @@ async function sendLogMessage(config: LogConfig, content: LogMessage) {
* Attempts to escape a message's markdown content (qoutes, headers, **bold** / *italic*, etc) * Attempts to escape a message's markdown content (qoutes, headers, **bold** / *italic*, etc)
*/ */
function sanitizeMessageContent(msg: string): string { function sanitizeMessageContent(msg: string): string {
let str = ''; let str = "";
for (let line of msg.split('\n')) { for (let line of msg.split("\n")) {
line = line.trim(); line = line.trim();
if (line.startsWith('#') || // headers if (
line.startsWith('>') || // quotes line.startsWith("#") || // headers
line.startsWith('|') || // tables line.startsWith(">") || // quotes
line.startsWith('*') || // unordered lists line.startsWith("|") || // tables
line.startsWith('-') || // ^ line.startsWith("*") || // unordered lists
line.startsWith('+') // ^ line.startsWith("-") || // ^
line.startsWith("+") // ^
) { ) {
line = `\\${line}`; line = `\\${line}`;
} }
@ -299,14 +383,17 @@ function sanitizeMessageContent(msg: string): string {
line = `\u200b${line}`; line = `\u200b${line}`;
} }
for (const char of ['_', '!!', '~', '`', '*', '^', '$']) { for (const char of ["_", "!!", "~", "`", "*", "^", "$"]) {
line = line.replace(new RegExp(`(?<!\\\\)\\${char}`, 'g'), `\\${char}`); line = line.replace(
new RegExp(`(?<!\\\\)\\${char}`, "g"),
`\\${char}`
);
} }
// Mentions // Mentions
line = line.replace(/<@/g, `<\\@`); line = line.replace(/<@/g, `<\\@`);
str += line + '\n'; str += line + "\n";
} }
return str; return str;
@ -319,12 +406,16 @@ enum EmbedColor {
Success = "var(--success)", Success = "var(--success)",
} }
function embed(content: string, title?: string|null, color?: string|EmbedColor): SendableEmbed { function embed(
content: string,
title?: string | null,
color?: string | EmbedColor
): SendableEmbed {
return { return {
description: content, description: content,
title: title, title: title,
colour: color, colour: color,
} };
} }
function dedupeArray<T>(...arrays: T[][]): T[] { function dedupeArray<T>(...arrays: T[][]): T[] {
@ -342,65 +433,191 @@ function dedupeArray<T>(...arrays: T[][]): T[] {
function getMutualServers(user: User) { function getMutualServers(user: User) {
const servers: Server[] = []; const servers: Server[] = [];
for (const member of client.members) { for (const member of client.members) {
if (member[1]._id.user == user._id && member[1].server) servers.push(member[1].server); if (member[1]._id.user == user._id && member[1].server)
servers.push(member[1].server);
} }
return servers; return servers;
} }
const awaitClient = () => new Promise<void>(async resolve => { const awaitClient = () =>
if (!client.user) client.once('ready', () => resolve()); new Promise<void>(async (resolve) => {
if (!client.user) client.once("ready", () => resolve());
else resolve(); else resolve();
}); });
const getDmChannel = async (user: string|{_id: string}|User) => { const getDmChannel = async (user: string | { _id: string } | User) => {
if (typeof user == 'string') user = client.users.get(user) || await client.users.fetch(user); if (typeof user == "string")
if (!(user instanceof User)) user = client.users.get(user._id) || await client.users.fetch(user._id); 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( return (
c => c[1].channel_type == 'DirectMessage' && c[1].recipient?._id == (user as User)._id Array.from(client.channels).find(
)?.[1] || await (user as User).openDM(); (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 generateInfractionDMEmbed = (
server: Server,
serverConfig: ServerConfig,
infraction: Infraction,
message: MessageCommandContext
) => {
const embed: SendableEmbed = { const embed: SendableEmbed = {
title: message.serverContext.name, title: message.serverContext.name,
icon_url: message.serverContext.generateIconURL({ max_side: 128 }), icon_url: message.serverContext.generateIconURL({ max_side: 128 }),
colour: '#ff9e2f', colour: "#ff9e2f",
url: message.url, url: message.url,
description: 'You have been ' + description:
"You have been " +
(infraction.actionType (infraction.actionType
? `**${infraction.actionType == 'ban' ? 'banned' : 'kicked'}** from ` ? `**${
infraction.actionType == "ban" ? "banned" : "kicked"
}** from `
: `**warned** in `) + : `**warned** in `) +
`'${sanitizeMessageContent(message.serverContext.name).trim()}' <t:${Math.round(infraction.date / 1000)}:R>.\n` + `'${sanitizeMessageContent(
message.serverContext.name
).trim()}' <t:${Math.round(infraction.date / 1000)}:R>.\n` +
`**Reason:** ${infraction.reason}\n` + `**Reason:** ${infraction.reason}\n` +
`**Moderator:** [@${sanitizeMessageContent(message.author?.username || 'Unknown')}](/@${message.author_id})\n` + `**Moderator:** [@${sanitizeMessageContent(
message.author?.username || "Unknown"
)}](/@${message.author_id})\n` +
`**Infraction ID:** \`${infraction._id}\`` + `**Infraction ID:** \`${infraction._id}\`` +
(infraction.actionType == 'ban' && infraction.expires (infraction.actionType == "ban" && infraction.expires
? (infraction.expires == Infinity ? infraction.expires == Infinity
? '\n**Ban duration:** Permanent' ? "\n**Ban duration:** Permanent"
: `\n**Ban expires** <t:${Math.round(infraction.expires / 1000)}:R>`) : `\n**Ban expires** <t:${Math.round(
: '') + infraction.expires / 1000
(infraction.actionType == 'ban' )}:R>`
? '\n\n**Reminder:** Circumventing this ban by using another account is a violation of the Revolt [Terms of Service](<https://revolt.chat/terms>) ' + : "") +
'and may result in your accounts getting suspended from the platform.' (infraction.actionType == "ban"
: '') ? "\n\n**Reminder:** Circumventing this ban by using another account is a violation of the Revolt [Terms of Service](<https://revolt.chat/terms>) " +
} "and may result in your accounts getting suspended from the platform."
: ""),
};
if (serverConfig.contact) { if (serverConfig.contact) {
if (RE_MAILTO_URI.test(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 ` + embed.description +=
`[${serverConfig.contact.replace(RE_MAILTO_URI, '')}](${serverConfig.contact}).` `\n\nIf you wish to appeal this decision, you may contact the server's moderation team at ` +
} `[${serverConfig.contact.replace(RE_MAILTO_URI, "")}](${
else if (RE_HTTP_URI.test(serverConfig.contact)) { serverConfig.contact
embed.description += `\n\nIf you wish to appeal this decision, you may do so [here](${serverConfig.contact}).` }).`;
} } else if (RE_HTTP_URI.test(serverConfig.contact)) {
else { embed.description += `\n\nIf you wish to appeal this decision, you may do so [here](${serverConfig.contact}).`;
} else {
embed.description += `\n\n${serverConfig.contact}`; embed.description += `\n\n${serverConfig.contact}`;
} }
} }
return embed; return embed;
} };
// Copied from https://github.com/janderedev/feeds-bot/blob/master/src/util.ts
const yesNoMessage = (
channel: Channel,
allowedUser: string,
message: string,
title?: string,
messageYes?: string,
messageNo?: string
): Promise<boolean> =>
new Promise(async (resolve, reject) => {
const EMOJI_YES = "✅",
EMOJI_NO = "❌";
try {
const msg = await channel.sendMessage({
embeds: [
{
colour: "var(--status-streaming)",
title: title,
description: message,
},
],
interactions: {
reactions: [EMOJI_YES, EMOJI_NO],
restrict_reactions: true,
},
});
let destroyed = false;
const cb = async (packet: ClientboundNotification) => {
if (packet.type != "MessageReact") return;
if (packet.id != msg._id) return;
if (packet.user_id != allowedUser) return;
switch (packet.emoji_id) {
case EMOJI_YES:
channel.client.removeListener("packet", cb);
destroyed = true;
resolve(true);
msg.edit({
embeds: [
{
colour: "var(--success)",
title: title,
description: `${EMOJI_YES} ${
messageYes ?? "Confirmed!"
}`,
},
],
}).catch((e) => console.error(e));
break;
case EMOJI_NO:
channel.client.removeListener("packet", cb);
destroyed = true;
resolve(false);
msg.edit({
embeds: [
{
colour: "var(--error)",
title: title,
description: `${EMOJI_NO} ${
messageNo ?? "Cancelled."
}`,
},
],
}).catch((e) => console.error(e));
break;
default:
logger.warn(
"Received unexpected reaction: " + packet.emoji_id
);
}
};
channel.client.on("packet", cb);
setTimeout(() => {
if (!destroyed) {
resolve(false);
channel.client.removeListener("packet", cb);
msg.edit({
embeds: [
{
colour: "var(--error)",
title: title,
description: `${EMOJI_NO} Timed out`,
},
],
}).catch((e) => console.error(e));
}
}, 30000);
} catch (e) {
reject(e);
}
});
// Get all cached members of a server. Whoever put STRINGIFIED JSON as map keys is now on my hit list.
const getMembers = (id: string) =>
Array.from(client.members.entries())
.filter((item) => item[0].includes(`"${id}"`))
.map((entry) => entry[1]);
export { export {
getAutumnURL, getAutumnURL,
@ -423,9 +640,11 @@ export {
getMutualServers, getMutualServers,
getDmChannel, getDmChannel,
generateInfractionDMEmbed, generateInfractionDMEmbed,
yesNoMessage,
getMembers,
EmbedColor, EmbedColor,
NO_MANAGER_MSG, NO_MANAGER_MSG,
ULID_REGEX, ULID_REGEX,
USER_MENTION_REGEX, USER_MENTION_REGEX,
CHANNEL_MENTION_REGEX, CHANNEL_MENTION_REGEX,
} };