Confirmation prompt when kicking/banning via reply
This commit is contained in:
parent
2f9792c616
commit
5c3479268d
4 changed files with 783 additions and 291 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnSaveMode": "modifications",
|
||||
"prettier.tabWidth": 4
|
||||
}
|
||||
|
|
|
@ -5,9 +5,22 @@ import InfractionType from "automod/dist/types/antispam/InfractionType";
|
|||
import SimpleCommand from "../../../struct/commands/SimpleCommand";
|
||||
import { fetchUsername, logModAction } from "../../modules/mod_logs";
|
||||
import { storeTempBan } from "../../modules/tempbans";
|
||||
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 {
|
||||
dedupeArray,
|
||||
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 { SendableEmbed } from "revolt-api";
|
||||
import { User } from "@janderedev/revolt.js";
|
||||
|
@ -16,30 +29,40 @@ import logger from "../../logger";
|
|||
Day.extend(RelativeTime);
|
||||
|
||||
export default {
|
||||
name: 'ban',
|
||||
aliases: [ 'eject' ],
|
||||
description: 'Ban a member from the server',
|
||||
syntax: '/ban @username [10m|1h|...?] [reason?]',
|
||||
name: "ban",
|
||||
aliases: ["eject"],
|
||||
description: "Ban a member from the server",
|
||||
syntax: "/ban @username [10m|1h|...?] [reason?]",
|
||||
removeEmptyArgs: true,
|
||||
category: CommandCategory.Moderation,
|
||||
run: async (message, args, serverConfig) => {
|
||||
if (!await isModerator(message))
|
||||
return message.reply(NO_MANAGER_MSG);
|
||||
if (!message.serverContext.havePermission('BanMembers')) {
|
||||
return await message.reply({ embeds: [
|
||||
embed(`Sorry, I do not have \`BanMembers\` permission.`, '', EmbedColor.SoftError)
|
||||
] });
|
||||
if (!(await isModerator(message))) return message.reply(NO_MANAGER_MSG);
|
||||
if (!message.serverContext.havePermission("BanMembers")) {
|
||||
return await message.reply({
|
||||
embeds: [
|
||||
embed(
|
||||
`Sorry, I do not have \`BanMembers\` permission.`,
|
||||
"",
|
||||
EmbedColor.SoftError
|
||||
),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
const userInput = !message.reply_ids?.length ? args.shift() || '' : undefined;
|
||||
if (!userInput && !message.reply_ids?.length) return message.reply({ embeds: [
|
||||
embed(
|
||||
`Please specify one or more users by replying to their message while running this command or ` +
|
||||
`by specifying a comma-separated list of usernames.`,
|
||||
'No target user specified',
|
||||
EmbedColor.SoftError,
|
||||
),
|
||||
] });
|
||||
const userInput = !message.reply_ids?.length
|
||||
? args.shift() || ""
|
||||
: undefined;
|
||||
if (!userInput && !message.reply_ids?.length)
|
||||
return message.reply({
|
||||
embeds: [
|
||||
embed(
|
||||
`Please specify one or more users by replying to their message while running this command or ` +
|
||||
`by specifying a comma-separated list of usernames.`,
|
||||
"No target user specified",
|
||||
EmbedColor.SoftError
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
let banDuration = 0;
|
||||
let durationStr = args.shift();
|
||||
|
@ -49,48 +72,84 @@ export default {
|
|||
// 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 [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;
|
||||
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.unshift(durationStr);
|
||||
|
||||
let reason = args.join(' ')
|
||||
?.replace(new RegExp('`', 'g'), '\'')
|
||||
?.replace(new RegExp('\n', 'g'), ' ');
|
||||
let reason = args
|
||||
.join(" ")
|
||||
?.replace(new RegExp("`", "g"), "'")
|
||||
?.replace(new RegExp("\n", "g"), " ");
|
||||
|
||||
if (reason.length > 500) return message.reply({
|
||||
embeds: [ embed('Ban reason may not be longer than 500 characters.', null, EmbedColor.SoftError) ]
|
||||
});
|
||||
if (reason.length > 500)
|
||||
return message.reply({
|
||||
embeds: [
|
||||
embed(
|
||||
"Ban reason may not be longer than 500 characters.",
|
||||
null,
|
||||
EmbedColor.SoftError
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const embeds: SendableEmbed[] = [];
|
||||
const handledUsers: string[] = [];
|
||||
const targetUsers: User|{ _id: string }[] = [];
|
||||
const targetUsers: User | { _id: string }[] = [];
|
||||
|
||||
const targetInput = dedupeArray(
|
||||
message.reply_ids?.length
|
||||
? (await Promise.allSettled(
|
||||
message.reply_ids.map(msg => message.channel?.fetchMessage(msg))
|
||||
))
|
||||
.filter(m => m.status == 'fulfilled').map(m => (m as any).value.author_id)
|
||||
: userInput!.split(','),
|
||||
? (
|
||||
await Promise.allSettled(
|
||||
message.reply_ids.map((msg) =>
|
||||
message.channel?.fetchMessage(msg)
|
||||
)
|
||||
)
|
||||
)
|
||||
.filter((m) => m.status == "fulfilled")
|
||||
.map((m) => (m as any).value.author_id)
|
||||
: userInput!.split(",")
|
||||
);
|
||||
|
||||
for (const userStr of targetInput) {
|
||||
try {
|
||||
let user = await parseUserOrId(userStr);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -99,27 +158,57 @@ export default {
|
|||
handledUsers.push(user._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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
targetUsers.push(user);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
embeds.push(embed(
|
||||
`Failed to ban target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`,
|
||||
`Failed to ban: An error has occurred`,
|
||||
EmbedColor.Error,
|
||||
));
|
||||
embeds.push(
|
||||
embed(
|
||||
`Failed to ban target \`${sanitizeMessageContent(
|
||||
userStr
|
||||
).trim()}\`: ${e}`,
|
||||
`Failed to ban: An error has occurred`,
|
||||
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) {
|
||||
try {
|
||||
|
@ -129,64 +218,106 @@ export default {
|
|||
_id: infId,
|
||||
createdBy: message.author_id,
|
||||
date: Date.now(),
|
||||
reason: reason || 'No reason provided',
|
||||
reason: reason || "No reason provided",
|
||||
server: message.serverContext._id,
|
||||
type: InfractionType.Manual,
|
||||
user: user._id,
|
||||
actionType: 'ban',
|
||||
actionType: "ban",
|
||||
expires: Infinity,
|
||||
}
|
||||
};
|
||||
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)) {
|
||||
embeds.push(embed(
|
||||
`\`${member.user?.username}\` has an equally or higher ranked role than you; refusing to ban.`,
|
||||
'Missing permission',
|
||||
EmbedColor.SoftError
|
||||
));
|
||||
if (
|
||||
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.`,
|
||||
"Missing permission",
|
||||
EmbedColor.SoftError
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (member && !member.bannable) {
|
||||
embeds.push(embed(
|
||||
`I don't have permission to ban \`${member?.user?.username || user._id}\`.`,
|
||||
null,
|
||||
EmbedColor.SoftError
|
||||
));
|
||||
embeds.push(
|
||||
embed(
|
||||
`I don't have permission to ban \`${
|
||||
member?.user?.username || user._id
|
||||
}\`.`,
|
||||
null,
|
||||
EmbedColor.SoftError
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (serverConfig?.dmOnKick) {
|
||||
try {
|
||||
const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message);
|
||||
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) {
|
||||
|
||||
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})`
|
||||
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({
|
||||
title: `User ${Math.random() > 0.8 ? 'ejected' : 'banned'}`,
|
||||
icon_url: user instanceof User ? user.generateAvatarURL() : undefined,
|
||||
title: `User ${
|
||||
Math.random() > 0.8 ? "ejected" : "banned"
|
||||
}`,
|
||||
icon_url:
|
||||
user instanceof User
|
||||
? user.generateAvatarURL()
|
||||
: undefined,
|
||||
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` +
|
||||
`**User ID:** \`${user._id}\`\n` +
|
||||
`**Infraction ID:** \`${infraction._id}\`\n` +
|
||||
`**Reason:** \`${infraction.reason}\``
|
||||
`**Reason:** \`${infraction.reason}\``,
|
||||
});
|
||||
} else {
|
||||
const banUntil = Date.now() + banDuration;
|
||||
|
@ -196,31 +327,47 @@ export default {
|
|||
_id: infId,
|
||||
createdBy: message.author_id,
|
||||
date: Date.now(),
|
||||
reason: (reason || 'No reason provided') + ` (${durationStr})`,
|
||||
reason:
|
||||
(reason || "No reason provided") +
|
||||
` (${durationStr})`,
|
||||
server: message.serverContext._id,
|
||||
type: InfractionType.Manual,
|
||||
user: user._id,
|
||||
actionType: 'ban',
|
||||
actionType: "ban",
|
||||
expires: banUntil,
|
||||
}
|
||||
};
|
||||
const { userWarnCount } = await storeInfraction(infraction);
|
||||
|
||||
if (serverConfig?.dmOnKick) {
|
||||
try {
|
||||
const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message);
|
||||
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) {
|
||||
|
||||
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})`
|
||||
reason:
|
||||
reason +
|
||||
` (by ${await fetchUsername(message.author_id)} ${
|
||||
message.author_id
|
||||
}) (${durationStr})`,
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
|
@ -231,7 +378,7 @@ export default {
|
|||
until: banUntil,
|
||||
}),
|
||||
logModAction(
|
||||
'ban',
|
||||
"ban",
|
||||
message.serverContext,
|
||||
message.member!,
|
||||
user._id,
|
||||
|
@ -243,23 +390,36 @@ export default {
|
|||
|
||||
embeds.push({
|
||||
title: `User temporarily banned`,
|
||||
icon_url: user instanceof User ? user.generateAvatarURL() : undefined,
|
||||
icon_url:
|
||||
user instanceof User
|
||||
? user.generateAvatarURL()
|
||||
: undefined,
|
||||
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` +
|
||||
`**Ban duration:** ${banDurationFancy}\n` +
|
||||
`**User ID:** \`${user._id}\`\n` +
|
||||
`**Infraction ID:** \`${infraction._id}\`\n` +
|
||||
`**Reason:** \`${infraction.reason}\``
|
||||
`**Reason:** \`${infraction.reason}\``,
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
embeds.push(embed(
|
||||
`Failed to ban target \`${await fetchUsername(user._id, user._id)}\`: ${e}`,
|
||||
'Failed to ban: An error has occurred',
|
||||
EmbedColor.Error,
|
||||
));
|
||||
embeds.push(
|
||||
embed(
|
||||
`Failed to ban target \`${await fetchUsername(
|
||||
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);
|
||||
|
||||
if (firstMsg) {
|
||||
await message.reply({ embeds: targetEmbeds, content: 'Operation completed.' }, false);
|
||||
await message.reply(
|
||||
{ embeds: targetEmbeds, content: "Operation completed." },
|
||||
false
|
||||
);
|
||||
} else {
|
||||
await message.channel?.sendMessage({ embeds: targetEmbeds });
|
||||
}
|
||||
firstMsg = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
} as SimpleCommand;
|
||||
|
|
|
@ -8,58 +8,99 @@ import CommandCategory from "../../../struct/commands/CommandCategory";
|
|||
import SimpleCommand from "../../../struct/commands/SimpleCommand";
|
||||
import logger from "../../logger";
|
||||
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 {
|
||||
name: 'kick',
|
||||
aliases: [ 'yeet', 'vent' ],
|
||||
description: 'Kick a member from the server',
|
||||
syntax: '/kick @username [reason?]',
|
||||
name: "kick",
|
||||
aliases: ["yeet", "vent"],
|
||||
description: "Kick a member from the server",
|
||||
syntax: "/kick @username [reason?]",
|
||||
removeEmptyArgs: true,
|
||||
category: CommandCategory.Moderation,
|
||||
run: async (message, args, serverConfig) => {
|
||||
if (!await isModerator(message))
|
||||
return message.reply(NO_MANAGER_MSG);
|
||||
if (!message.serverContext.havePermission('KickMembers')) {
|
||||
return await message.reply(`Sorry, I do not have \`KickMembers\` permission.`);
|
||||
if (!(await isModerator(message))) return message.reply(NO_MANAGER_MSG);
|
||||
if (!message.serverContext.havePermission("KickMembers")) {
|
||||
return await message.reply(
|
||||
`Sorry, I do not have \`KickMembers\` permission.`
|
||||
);
|
||||
}
|
||||
|
||||
const userInput = !message.reply_ids?.length ? args.shift() || '' : undefined;
|
||||
if (!userInput && !message.reply_ids?.length) return message.reply({ embeds: [
|
||||
embed(
|
||||
`Please specify one or more users by replying to their message while running this command or ` +
|
||||
`by specifying a comma-separated list of usernames.`,
|
||||
'No target user specified',
|
||||
EmbedColor.SoftError,
|
||||
),
|
||||
] });
|
||||
|
||||
let reason = args.join(' ')
|
||||
?.replace(new RegExp('`', 'g'), '\'')
|
||||
?.replace(new RegExp('\n', 'g'), ' ');
|
||||
const userInput = !message.reply_ids?.length
|
||||
? args.shift() || ""
|
||||
: undefined;
|
||||
if (!userInput && !message.reply_ids?.length)
|
||||
return message.reply({
|
||||
embeds: [
|
||||
embed(
|
||||
`Please specify one or more users by replying to their message while running this command or ` +
|
||||
`by specifying a comma-separated list of usernames.`,
|
||||
"No target user specified",
|
||||
EmbedColor.SoftError
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
if (reason.length > 500) return message.reply({
|
||||
embeds: [ embed('Kick reason may not be longer than 500 characters.', null, EmbedColor.SoftError) ]
|
||||
});
|
||||
let reason = args
|
||||
.join(" ")
|
||||
?.replace(new RegExp("`", "g"), "'")
|
||||
?.replace(new RegExp("\n", "g"), " ");
|
||||
|
||||
if (reason.length > 500)
|
||||
return message.reply({
|
||||
embeds: [
|
||||
embed(
|
||||
"Kick reason may not be longer than 500 characters.",
|
||||
null,
|
||||
EmbedColor.SoftError
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
const embeds: SendableEmbed[] = [];
|
||||
const handledUsers: string[] = [];
|
||||
const targetUsers: User|{ _id: string }[] = [];
|
||||
const targetUsers: User | { _id: string }[] = [];
|
||||
|
||||
const targetInput = dedupeArray(
|
||||
message.reply_ids?.length
|
||||
? (await Promise.allSettled(
|
||||
message.reply_ids.map(msg => message.channel?.fetchMessage(msg))
|
||||
))
|
||||
.filter(m => m.status == 'fulfilled').map(m => (m as any).value.author_id)
|
||||
: userInput!.split(','),
|
||||
? (
|
||||
await Promise.allSettled(
|
||||
message.reply_ids.map((msg) =>
|
||||
message.channel?.fetchMessage(msg)
|
||||
)
|
||||
)
|
||||
)
|
||||
.filter((m) => m.status == "fulfilled")
|
||||
.map((m) => (m as any).value.author_id)
|
||||
: userInput!.split(",")
|
||||
);
|
||||
|
||||
for (const userStr of targetInput) {
|
||||
try {
|
||||
let user = await parseUserOrId(userStr);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -68,33 +109,69 @@ export default {
|
|||
handledUsers.push(user._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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
targetUsers.push(user);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
embeds.push(embed(
|
||||
`Failed to kick target \`${sanitizeMessageContent(userStr).trim()}\`: ${e}`,
|
||||
`Failed to kick: An error has occurred`,
|
||||
EmbedColor.Error,
|
||||
));
|
||||
embeds.push(
|
||||
embed(
|
||||
`Failed to kick target \`${sanitizeMessageContent(
|
||||
userStr
|
||||
).trim()}\`: ${e}`,
|
||||
`Failed to kick: An error has occurred`,
|
||||
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) {
|
||||
try {
|
||||
const member = members.members.find(m => m._id.user == user._id);
|
||||
const member = members.find((m) => m._id.user == user._id);
|
||||
if (!member) {
|
||||
embeds.push(embed(''));
|
||||
embeds.push(
|
||||
embed(
|
||||
`\`${await fetchUsername(
|
||||
user._id
|
||||
)}\` is not a member of this server.`
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -103,45 +180,75 @@ export default {
|
|||
_id: infId,
|
||||
createdBy: message.author_id,
|
||||
date: Date.now(),
|
||||
reason: reason || 'No reason provided',
|
||||
reason: reason || "No reason provided",
|
||||
server: message.serverContext._id,
|
||||
type: InfractionType.Manual,
|
||||
user: user._id,
|
||||
actionType: 'kick',
|
||||
}
|
||||
actionType: "kick",
|
||||
};
|
||||
|
||||
if (serverConfig?.dmOnKick) {
|
||||
try {
|
||||
const embed = generateInfractionDMEmbed(message.serverContext, serverConfig, infraction, message);
|
||||
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) {
|
||||
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),
|
||||
logModAction('kick', message.serverContext, message.member!, user._id, reason, infraction._id),
|
||||
logModAction(
|
||||
"kick",
|
||||
message.serverContext,
|
||||
message.member!,
|
||||
user._id,
|
||||
reason,
|
||||
infraction._id
|
||||
),
|
||||
member.kick(),
|
||||
]);
|
||||
|
||||
embeds.push({
|
||||
title: `User kicked`,
|
||||
icon_url: user instanceof User ? user.generateAvatarURL() : undefined,
|
||||
icon_url:
|
||||
user instanceof User
|
||||
? user.generateAvatarURL()
|
||||
: undefined,
|
||||
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` +
|
||||
`**User ID:** \`${user._id}\`\n` +
|
||||
`**Infraction ID:** \`${infraction._id}\`\n` +
|
||||
`**Reason:** \`${infraction.reason}\``
|
||||
`**Reason:** \`${infraction.reason}\``,
|
||||
});
|
||||
} catch(e) {
|
||||
embeds.push(embed(`Failed to kick user ${await fetchUsername(user._id)}: ${e}`, 'Failed to kick user', EmbedColor.Error));
|
||||
} catch (e) {
|
||||
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);
|
||||
|
||||
if (firstMsg) {
|
||||
await message.reply({ embeds: targetEmbeds, content: 'Operation completed.' }, false);
|
||||
await message.reply(
|
||||
{ embeds: targetEmbeds, content: "Operation completed." },
|
||||
false
|
||||
);
|
||||
} else {
|
||||
await message.channel?.sendMessage({ embeds: targetEmbeds });
|
||||
}
|
||||
firstMsg = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
} as SimpleCommand;
|
||||
|
|
|
@ -17,21 +17,25 @@ import { isSudo } from "./commands/admin/botadm";
|
|||
import { SendableEmbed } from "revolt-api";
|
||||
import MessageCommandContext from "../struct/MessageCommandContext";
|
||||
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 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 => {
|
||||
let autumn_url: string | null = null;
|
||||
let apiConfig: any = axios.get(client.apiURL).then((res) => {
|
||||
autumn_url = (res.data as any).features.autumn.url;
|
||||
});
|
||||
|
||||
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
|
||||
* @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;
|
||||
|
||||
let uid: string|null = null;
|
||||
let uid: string | null = null;
|
||||
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)) {
|
||||
uid = text.toUpperCase();
|
||||
} else {
|
||||
if (text.startsWith('@')) text = text.substr(1);
|
||||
if (text.startsWith("@")) text = text.substr(1);
|
||||
|
||||
// Why is there no .find() or .filter()
|
||||
let user: User|null = null;
|
||||
client.users.forEach(u => {
|
||||
let user: User | null = null;
|
||||
client.users.forEach((u) => {
|
||||
if (u.username?.toLowerCase() == text.toLowerCase()) {
|
||||
user = u;
|
||||
}
|
||||
|
@ -63,16 +67,20 @@ async function parseUser(text: string): Promise<User|null> {
|
|||
}
|
||||
|
||||
try {
|
||||
if (uid) return await client.users.fetch(uid) || null;
|
||||
if (uid) return (await client.users.fetch(uid)) || null;
|
||||
else return null;
|
||||
} catch(e) { return null; }
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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);
|
||||
if (parsed) return parsed;
|
||||
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) {
|
||||
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),
|
||||
dbs.SERVERS.findOne({ id: server._id }),
|
||||
checkSudoPermission(message, announceSudo),
|
||||
]);
|
||||
|
||||
return isManager
|
||||
|| (mods?.moderators?.indexOf(member.user?._id!) ?? -1) > -1
|
||||
|| isSudo;
|
||||
return (
|
||||
isManager ||
|
||||
(mods?.moderators?.indexOf(member.user?._id!) ?? -1) > -1 ||
|
||||
isSudo
|
||||
);
|
||||
}
|
||||
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 }),
|
||||
checkSudoPermission(message, announceSudo),
|
||||
]);
|
||||
|
||||
return (managers?.botManagers?.indexOf(member.user?._id!) ?? -1) > -1
|
||||
|| isSudo;
|
||||
return (
|
||||
(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!);
|
||||
if (!hasPerm) return false;
|
||||
else {
|
||||
if (announce !== false) {
|
||||
await message.reply(`# :unlock: Bypassed permission check\n`
|
||||
+ `Sudo mode is enabled for @${message.author!.username}.\n`);
|
||||
await message.reply(
|
||||
`# :unlock: Bypassed permission check\n` +
|
||||
`Sudo mode is enabled for @${message.author!.username}.\n`
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
async function getPermissionLevel(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;
|
||||
async function getPermissionLevel(
|
||||
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;
|
||||
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 });
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function getPermissionBasedOnRole(member: Member): 0|1|2|3 {
|
||||
if (hasPerm(member, 'ManageServer')) return 3;
|
||||
if (hasPerm(member, 'KickMembers')) return 1;
|
||||
function getPermissionBasedOnRole(member: Member): 0 | 1 | 2 | 3 {
|
||||
if (hasPerm(member, "ManageServer")) return 3;
|
||||
if (hasPerm(member, "KickMembers")) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
@ -153,85 +185,117 @@ function hasPerm(member: Member, perm: keyof typeof Permission): boolean {
|
|||
/**
|
||||
* @deprecated Unnecessary
|
||||
*/
|
||||
function hasPermForChannel(member: Member, channel: Channel, perm: keyof typeof Permission): boolean {
|
||||
if (!member.server) throw 'hasPermForChannel(): Server is undefined';
|
||||
function hasPermForChannel(
|
||||
member: Member,
|
||||
channel: Channel,
|
||||
perm: keyof typeof Permission
|
||||
): boolean {
|
||||
if (!member.server) throw "hasPermForChannel(): Server is undefined";
|
||||
return member.hasPermission(channel, perm);
|
||||
}
|
||||
|
||||
async function getOwnMemberInServer(server: Server): Promise<Member> {
|
||||
return client.members.getKey({ server: server._id, user: client.user!._id })
|
||||
|| await server.fetchMember(client.user!._id);
|
||||
return (
|
||||
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([
|
||||
dbs.INFRACTIONS.insert(infraction, { castIds: false }),
|
||||
dbs.INFRACTIONS.find({
|
||||
server: infraction.server,
|
||||
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> {
|
||||
let data = new FormData();
|
||||
data.append("file", file, { filename: filename });
|
||||
|
||||
let req = await axios.post(await getAutumnURL() + '/attachments', data, { headers: data.getHeaders() });
|
||||
return (req.data as any)['id'] as string;
|
||||
let req = await axios.post((await getAutumnURL()) + "/attachments", data, {
|
||||
headers: data.getHeaders(),
|
||||
});
|
||||
return (req.data as any)["id"] as string;
|
||||
}
|
||||
|
||||
async function sendLogMessage(config: LogConfig, content: LogMessage) {
|
||||
if (config.discord?.webhookUrl) {
|
||||
let c = { ...content, ...content.overrides?.discord }
|
||||
let c = { ...content, ...content.overrides?.discord };
|
||||
|
||||
const embed = new MessageEmbed();
|
||||
if (c.title) embed.setTitle(content.title);
|
||||
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) {
|
||||
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.type == 'THUMBNAIL') embed.setThumbnail(content.image.url);
|
||||
else if (content.image.type == 'BIG') embed.setImage(content.image.url);
|
||||
if (content.image.type == "THUMBNAIL")
|
||||
embed.setThumbnail(content.image.url);
|
||||
else if (content.image.type == "BIG")
|
||||
embed.setImage(content.image.url);
|
||||
}
|
||||
|
||||
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();
|
||||
content.attachments?.forEach(a => {
|
||||
content.attachments?.forEach((a) => {
|
||||
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() })
|
||||
.catch(e => logger.error(`Failed to send log message (discord): ${e}`));
|
||||
axios
|
||||
.post(config.discord.webhookUrl, data, {
|
||||
headers: data.getHeaders(),
|
||||
})
|
||||
.catch((e) =>
|
||||
logger.error(`Failed to send log message (discord): ${e}`)
|
||||
);
|
||||
}
|
||||
|
||||
if (config.revolt?.channel) {
|
||||
let c = { ...content, ...content.overrides?.revolt };
|
||||
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 embed: SendableEmbed|undefined = undefined;
|
||||
switch(config.revolt.type) {
|
||||
case 'EMBED':
|
||||
let message = "";
|
||||
let embed: SendableEmbed | undefined = undefined;
|
||||
switch (config.revolt.type) {
|
||||
case "EMBED":
|
||||
c = { ...c, ...content.overrides?.revoltEmbed };
|
||||
embed = {
|
||||
title: c.title,
|
||||
description: c.description,
|
||||
colour: c.color,
|
||||
}
|
||||
};
|
||||
|
||||
if (c.fields?.length) {
|
||||
for (const field of c.fields) {
|
||||
|
@ -241,35 +305,55 @@ async function sendLogMessage(config: LogConfig, content: LogMessage) {
|
|||
break;
|
||||
|
||||
default: // QUOTEBLOCK, PLAIN or unspecified
|
||||
|
||||
// Wrap entire message in quotes
|
||||
// please disregard this mess
|
||||
|
||||
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.description) message += `${c.description}\n`;
|
||||
if (c.fields?.length) {
|
||||
for (const field of c.fields) {
|
||||
message += `${quote ? '\u200b\n' : ''}${quote}### ${field.title}\n` +
|
||||
`${quote}${field.content.trim().split('\n').join('\n' + quote)}\n${quote ? '\n' : ''}`;
|
||||
message +=
|
||||
`${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
|
||||
if (c.image?.url) message += `\n[Attachment](${c.image.url})`;
|
||||
message = message
|
||||
.trim()
|
||||
.split("\n")
|
||||
.join("\n" + quote);
|
||||
if (c.image?.url)
|
||||
message += `\n[Attachment](${c.image.url})`;
|
||||
break;
|
||||
}
|
||||
|
||||
channel.sendMessage({
|
||||
content: message,
|
||||
embeds: embed ? [ embed ] : undefined,
|
||||
attachments: content.attachments ?
|
||||
await Promise.all(content.attachments?.map(a => uploadFile(a.content, a.name))) :
|
||||
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}`);
|
||||
channel
|
||||
.sendMessage({
|
||||
content: message,
|
||||
embeds: embed ? [embed] : undefined,
|
||||
attachments: content.attachments
|
||||
? await Promise.all(
|
||||
content.attachments?.map((a) =>
|
||||
uploadFile(a.content, a.name)
|
||||
)
|
||||
)
|
||||
: 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)
|
||||
*/
|
||||
function sanitizeMessageContent(msg: string): string {
|
||||
let str = '';
|
||||
for (let line of msg.split('\n')) {
|
||||
|
||||
let str = "";
|
||||
for (let line of msg.split("\n")) {
|
||||
line = line.trim();
|
||||
|
||||
if (line.startsWith('#') || // headers
|
||||
line.startsWith('>') || // quotes
|
||||
line.startsWith('|') || // tables
|
||||
line.startsWith('*') || // unordered lists
|
||||
line.startsWith('-') || // ^
|
||||
line.startsWith('+') // ^
|
||||
if (
|
||||
line.startsWith("#") || // headers
|
||||
line.startsWith(">") || // quotes
|
||||
line.startsWith("|") || // tables
|
||||
line.startsWith("*") || // unordered lists
|
||||
line.startsWith("-") || // ^
|
||||
line.startsWith("+") // ^
|
||||
) {
|
||||
line = `\\${line}`;
|
||||
}
|
||||
|
@ -299,14 +383,17 @@ function sanitizeMessageContent(msg: string): string {
|
|||
line = `\u200b${line}`;
|
||||
}
|
||||
|
||||
for (const char of ['_', '!!', '~', '`', '*', '^', '$']) {
|
||||
line = line.replace(new RegExp(`(?<!\\\\)\\${char}`, 'g'), `\\${char}`);
|
||||
for (const char of ["_", "!!", "~", "`", "*", "^", "$"]) {
|
||||
line = line.replace(
|
||||
new RegExp(`(?<!\\\\)\\${char}`, "g"),
|
||||
`\\${char}`
|
||||
);
|
||||
}
|
||||
|
||||
// Mentions
|
||||
line = line.replace(/<@/g, `<\\@`);
|
||||
|
||||
str += line + '\n';
|
||||
str += line + "\n";
|
||||
}
|
||||
|
||||
return str;
|
||||
|
@ -319,12 +406,16 @@ enum EmbedColor {
|
|||
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 {
|
||||
description: content,
|
||||
title: title,
|
||||
colour: color,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function dedupeArray<T>(...arrays: T[][]): T[] {
|
||||
|
@ -342,65 +433,191 @@ function dedupeArray<T>(...arrays: T[][]): T[] {
|
|||
function getMutualServers(user: User) {
|
||||
const servers: Server[] = [];
|
||||
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;
|
||||
}
|
||||
|
||||
const awaitClient = () => new Promise<void>(async resolve => {
|
||||
if (!client.user) client.once('ready', () => resolve());
|
||||
else resolve();
|
||||
});
|
||||
const awaitClient = () =>
|
||||
new Promise<void>(async (resolve) => {
|
||||
if (!client.user) client.once("ready", () => 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);
|
||||
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();
|
||||
}
|
||||
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 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',
|
||||
colour: "#ff9e2f",
|
||||
url: message.url,
|
||||
description: 'You have been ' +
|
||||
description:
|
||||
"You have been " +
|
||||
(infraction.actionType
|
||||
? `**${infraction.actionType == 'ban' ? 'banned' : 'kicked'}** from `
|
||||
? `**${
|
||||
infraction.actionType == "ban" ? "banned" : "kicked"
|
||||
}** from `
|
||||
: `**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` +
|
||||
`**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.actionType == 'ban' && infraction.expires
|
||||
? (infraction.expires == Infinity
|
||||
? '\n**Ban duration:** Permanent'
|
||||
: `\n**Ban expires** <t:${Math.round(infraction.expires / 1000)}:R>`)
|
||||
: '') +
|
||||
(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.'
|
||||
: '')
|
||||
}
|
||||
(infraction.actionType == "ban" && infraction.expires
|
||||
? infraction.expires == Infinity
|
||||
? "\n**Ban duration:** Permanent"
|
||||
: `\n**Ban expires** <t:${Math.round(
|
||||
infraction.expires / 1000
|
||||
)}:R>`
|
||||
: "") +
|
||||
(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 (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\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;
|
||||
}
|
||||
};
|
||||
|
||||
// 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 {
|
||||
getAutumnURL,
|
||||
|
@ -423,9 +640,11 @@ export {
|
|||
getMutualServers,
|
||||
getDmChannel,
|
||||
generateInfractionDMEmbed,
|
||||
yesNoMessage,
|
||||
getMembers,
|
||||
EmbedColor,
|
||||
NO_MANAGER_MSG,
|
||||
ULID_REGEX,
|
||||
USER_MENTION_REGEX,
|
||||
CHANNEL_MENTION_REGEX,
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue