Rework logging, various minor fixes
- /kick and /ban are now logged - Log messages can be received via Discord - Revolt log messages have 3 different styles - Requires manual database migration lol
This commit is contained in:
parent
8ae2533ca1
commit
648fe39fe6
12 changed files with 298 additions and 136 deletions
|
@ -5,9 +5,13 @@ import InfractionType from "../../struct/antispam/InfractionType";
|
|||
import Command from "../../struct/Command";
|
||||
import MessageCommandContext from "../../struct/MessageCommandContext";
|
||||
import TempBan from "../../struct/TempBan";
|
||||
import { fetchUsername } from "../modules/mod_logs";
|
||||
import { fetchUsername, logModAction } from "../modules/mod_logs";
|
||||
import { storeTempBan } from "../modules/tempbans";
|
||||
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
|
||||
import Day from 'dayjs';
|
||||
import RelativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
Day.extend(RelativeTime);
|
||||
|
||||
export default {
|
||||
name: 'ban',
|
||||
|
@ -18,10 +22,10 @@ export default {
|
|||
run: async (message: MessageCommandContext, args: string[]) => {
|
||||
if (!await isModerator(message.member!, message.serverContext))
|
||||
return message.reply(NO_MANAGER_MSG);
|
||||
|
||||
|
||||
if (args.length == 0)
|
||||
return message.reply(`You need to provide a target user!`);
|
||||
|
||||
|
||||
let targetUser = await parseUser(args.shift()!);
|
||||
if (!targetUser) return message.reply('Sorry, I can\'t find that user.');
|
||||
|
||||
|
@ -61,7 +65,7 @@ export default {
|
|||
|
||||
if (banDuration == 0) {
|
||||
let infId = ulid();
|
||||
let { userWarnCount } = await storeInfraction({
|
||||
let infraction: Infraction = {
|
||||
_id: infId,
|
||||
createdBy: message.author_id,
|
||||
date: Date.now(),
|
||||
|
@ -70,19 +74,23 @@ export default {
|
|||
type: InfractionType.Manual,
|
||||
user: targetUser._id,
|
||||
actionType: 'ban',
|
||||
} as Infraction);
|
||||
}
|
||||
let { userWarnCount } = await storeInfraction(infraction);
|
||||
|
||||
message.serverContext.banUser(targetUser._id, {
|
||||
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})`
|
||||
})
|
||||
.catch(e => message.reply(`Failed to ban user: \`${e}\``));
|
||||
|
||||
message.reply(`### @${targetUser.username} has been banned.\n`
|
||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
|
||||
await Promise.all([
|
||||
message.reply(`### @${targetUser.username} has been banned.\n`
|
||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`),
|
||||
logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction, `Ban duration: **Permanent**`),
|
||||
]);
|
||||
} else {
|
||||
let banUntil = Date.now() + banDuration;
|
||||
let infId = ulid();
|
||||
let { userWarnCount } = await storeInfraction({
|
||||
let infraction: Infraction = {
|
||||
_id: infId,
|
||||
createdBy: message.author_id,
|
||||
date: Date.now(),
|
||||
|
@ -91,7 +99,8 @@ export default {
|
|||
type: InfractionType.Manual,
|
||||
user: targetUser._id,
|
||||
actionType: 'ban',
|
||||
} as Infraction);
|
||||
}
|
||||
let { userWarnCount } = await storeInfraction(infraction);
|
||||
|
||||
message.serverContext.banUser(targetUser._id, {
|
||||
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id}) (${durationStr})`
|
||||
|
@ -105,8 +114,11 @@ export default {
|
|||
until: banUntil,
|
||||
} as TempBan);
|
||||
|
||||
message.reply(`### ${targetUser.username} has been temporarily banned.\n`
|
||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
|
||||
await Promise.all([
|
||||
message.reply(`### ${targetUser.username} has been temporarily banned.\n`
|
||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`),
|
||||
logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction, `Ban duration: **${Day(banUntil).fromNow(true)}**`),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} as Command;
|
||||
|
|
|
@ -21,7 +21,7 @@ export default {
|
|||
try {
|
||||
let serverConf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: message.serverContext._id });
|
||||
|
||||
if (!serverConf?.userScan?.enable) return message.reply(`User scanning is not enabled for this server.`);
|
||||
if (!serverConf?.enableUserScan) return message.reply(`User scanning is not enabled for this server.`);
|
||||
if (userscans.includes(message.serverContext._id)) return message.reply(`There is already a scan running for this server.`);
|
||||
userscans.push(message.serverContext._id);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import Infraction from "../../struct/antispam/Infraction";
|
|||
import InfractionType from "../../struct/antispam/InfractionType";
|
||||
import Command from "../../struct/Command";
|
||||
import MessageCommandContext from "../../struct/MessageCommandContext";
|
||||
import { logModAction } from "../modules/mod_logs";
|
||||
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
|
||||
|
||||
export default {
|
||||
|
@ -41,7 +42,7 @@ export default {
|
|||
}
|
||||
|
||||
let infId = ulid();
|
||||
let { userWarnCount } = await storeInfraction({
|
||||
let infraction: Infraction = {
|
||||
_id: infId,
|
||||
createdBy: message.author_id,
|
||||
date: Date.now(),
|
||||
|
@ -50,7 +51,8 @@ export default {
|
|||
type: InfractionType.Manual,
|
||||
user: targetUser._id,
|
||||
actionType: 'kick',
|
||||
} as Infraction);
|
||||
}
|
||||
let { userWarnCount } = await storeInfraction(infraction);
|
||||
|
||||
try {
|
||||
await targetMember.kick();
|
||||
|
@ -58,7 +60,10 @@ export default {
|
|||
return message.reply(`Failed to kick user: \`${e}\``);
|
||||
}
|
||||
|
||||
message.reply(`### @${targetUser.username} has been ${Math.random() > 0.8 ? 'ejected' : 'kicked'}.\n`
|
||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
|
||||
await Promise.all([
|
||||
message.reply(`### @${targetUser.username} has been ${Math.random() > 0.8 ? 'ejected' : 'kicked'}.\n`
|
||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`),
|
||||
logModAction('kick', message.serverContext, message.member!, targetUser._id, reason, infraction),
|
||||
]);
|
||||
}
|
||||
} as Command;
|
||||
|
|
|
@ -11,7 +11,7 @@ export default {
|
|||
message.reply(`Measuring...`)
|
||||
?.catch(console.error)
|
||||
.then(msg => {
|
||||
msg?.edit({ content: `## Ping Pong!\n`
|
||||
if (msg) msg.edit({ content: `## Ping Pong!\n`
|
||||
+ `WS: \`${client.websocket.ping ?? '--'}ms\`\n`
|
||||
+ `Msg: \`${Math.round(Date.now() - now) / 2}ms\`` });
|
||||
});
|
||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
|||
+ ` for ${await fetchUsername(user._id)}.\n`
|
||||
+ `**Infraction ID:** \`${infraction._id}\`\n`
|
||||
+ `**Reason:** \`${infraction.reason}\``),
|
||||
logModAction('warn', message.serverContext, message.member!, user._id, reason, `This is warn number **${userWarnCount}** for this user.`),
|
||||
logModAction('warn', message.serverContext, message.member!, user._id, reason, infraction, `This is warn number ${userWarnCount} for this user.`),
|
||||
]);
|
||||
}
|
||||
} as Command;
|
||||
|
|
|
@ -39,7 +39,7 @@ export default {
|
|||
+ `${inf[0].type == InfractionType.Manual ? `(${await fetchUsername(inf[0].createdBy ?? '')})` : ''}\n`;
|
||||
};
|
||||
|
||||
message.reply(msg.substr(0, 1999));
|
||||
message.reply(msg.substring(0, 1999));
|
||||
} else {
|
||||
switch(args[0]?.toLowerCase()) {
|
||||
case 'delete':
|
||||
|
@ -55,7 +55,7 @@ export default {
|
|||
|
||||
if (!inf) return message.reply('I can\'t find that ID.');
|
||||
|
||||
message.reply(`## Infraction deleted\n\u200b\n`
|
||||
message.reply(`## Infraction deleted\n`
|
||||
+ `ID: \`${inf._id}\`\n`
|
||||
+ `Reason: ${getInfEmoji(inf)}\`${inf.reason}\` `
|
||||
+ `(${inf.type == InfractionType.Manual ? await fetchUsername(inf.createdBy ?? '') : 'System'})\n`
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
|
||||
import { Server } from "@janderedev/revolt.js/dist/maps/Servers";
|
||||
import { client } from "../..";
|
||||
import Infraction from "../../struct/antispam/Infraction";
|
||||
import LogMessage from "../../struct/LogMessage";
|
||||
import ServerConfig from "../../struct/ServerConfig";
|
||||
import logger from "../logger";
|
||||
import { getAutumnURL, sanitizeMessageContent, uploadFile } from "../util";
|
||||
import { getAutumnURL, sanitizeMessageContent, sendLogMessage } from "../util";
|
||||
|
||||
// the `packet` event is emitted before the client's cache
|
||||
// is updated, which allows us to get the old message content
|
||||
|
@ -13,8 +15,6 @@ client.on('packet', async (packet) => {
|
|||
try {
|
||||
if (!packet.data.content) return;
|
||||
|
||||
logger.debug('Message updated');
|
||||
|
||||
let m = client.messages.get(packet.id);
|
||||
|
||||
if (m?.author_id == client.user?._id) return;
|
||||
|
@ -29,31 +29,36 @@ client.on('packet', async (packet) => {
|
|||
if (!server || !channel) return logger.warn('Received message update in unknown channel or server');
|
||||
|
||||
let config: ServerConfig = await client.db.get('servers').findOne({ id: server._id }) ?? {};
|
||||
if (!config?.logs?.messageUpdate) return;
|
||||
let logChannelID = config.logs.messageUpdate;
|
||||
let logChannel = client.channels.get(logChannelID);
|
||||
if (!logChannel) return logger.debug('Log channel deleted or not cached: ' + logChannelID);
|
||||
if (config?.logs?.messageUpdate) {
|
||||
const attachFullMessage = oldMsg.length > 800 || newMsg.length > 800;
|
||||
let embed: LogMessage = {
|
||||
title: `Message edited in ${server.name}`,
|
||||
description: `[\\[#${channel.name}\\]](/server/${server._id}/channel/${channel._id}) | `
|
||||
+ `[\\[Author\\]](/@${m?.author_id}) | `
|
||||
+ `[\\[Jump to message\\]](/server/${server._id}/channel/${channel._id}/${packet.id})`,
|
||||
fields: [],
|
||||
color: '#829dff',
|
||||
overrides: {
|
||||
discord: {
|
||||
description: `Author: @${m?.author?.username || m?.author_id || "Unknown"} | Channel: ${channel?.name || channel?._id}`
|
||||
},
|
||||
revoltRvembed: {
|
||||
description: `Author: @${m?.author?.username || m?.author_id || "Unknown"} | Channel: ${channel?.name || channel?._id}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let attachFullMessage = oldMsg.length > 800 || newMsg.length > 800;
|
||||
if (attachFullMessage) {
|
||||
embed.attachments = [
|
||||
{ name: 'old_message.txt', content: Buffer.from(oldMsgRaw) },
|
||||
{ name: 'new_message.txt', content: Buffer.from(newMsgRaw) },
|
||||
];
|
||||
} else {
|
||||
embed.fields!.push({ title: 'Old content', content: oldMsg });
|
||||
embed.fields!.push({ title: 'New content', content: newMsg });
|
||||
}
|
||||
|
||||
if (attachFullMessage) {
|
||||
logChannel.sendMessage({
|
||||
content: `### Message edited in ${server.name}\n`
|
||||
+ `[\\[Jump to message\\]](/server/${server._id}/channel/${channel._id}/${packet.id})\n`,
|
||||
attachments: await Promise.all([
|
||||
uploadFile(oldMsgRaw, 'Old message'),
|
||||
uploadFile(newMsgRaw, 'New message'),
|
||||
]),
|
||||
});
|
||||
} else {
|
||||
let logMsg = `### Message edited in ${server.name}\n`
|
||||
+ `[\\[Jump to message\\]](/server/${server._id}/channel/${channel._id}/${packet.id}) | `
|
||||
+ `[\\[Author\\]](/@${m?.author_id})\n`;
|
||||
logMsg += `#### Old Content\n${oldMsg}\n`;
|
||||
logMsg += `#### New Content\n${newMsg}`;
|
||||
|
||||
logChannel.sendMessage(logMsg)
|
||||
.catch(() => logger.warn(`Failed to send log message`));
|
||||
await sendLogMessage(config.logs.messageUpdate, embed);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
|
@ -71,42 +76,37 @@ client.on('packet', async (packet) => {
|
|||
let msg = sanitizeMessageContent(msgRaw);
|
||||
|
||||
let config: ServerConfig = await client.db.get('servers').findOne({ id: message.channel?.server?._id }) ?? {};
|
||||
if (!config?.logs?.messageUpdate) return;
|
||||
let logChannelID = config.logs.messageUpdate;
|
||||
let logChannel = client.channels.get(logChannelID);
|
||||
if (!logChannel) return logger.debug('Log channel deleted or not cached: ' + logChannelID);
|
||||
if (config.logs?.messageUpdate) {
|
||||
let embed: LogMessage = {
|
||||
title: `Message deleted in ${message.channel?.server?.name}`,
|
||||
description: `[\\[#${channel.name}\\]](/server/${channel.server_id}/channel/${channel._id}) | `
|
||||
+ `[\\[Author\\]](/@${message.author_id}) | `
|
||||
+ `[\\[Jump to context\\]](/server/${channel.server_id}/channel/${channel._id}/${packet.id})`,
|
||||
fields: [],
|
||||
color: '#ff6b6b',
|
||||
overrides: {
|
||||
discord: {
|
||||
description: `Author: @${message.author?.username || message.author_id} | Channel: ${message.channel?.name || message.channel_id}`
|
||||
},
|
||||
revoltRvembed: {
|
||||
description: `Author: @${message.author?.username || message.author_id} | Channel: ${message.channel?.name || message.channel_id}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (msg.length > 1000) {
|
||||
let logMsg = `### Message deleted in ${message.channel?.server?.name}\n`;
|
||||
if (msg.length > 1000) {
|
||||
embed.attachments?.push({ name: 'message.txt', content: Buffer.from(msgRaw) });
|
||||
} else {
|
||||
embed.fields!.push({ title: 'Content', content: msg || "(Empty)" });
|
||||
}
|
||||
|
||||
if (message.attachments?.length) {
|
||||
let autumnURL = await getAutumnURL();
|
||||
|
||||
logMsg += `\n\u200b\n#### Attachments\n` + message.attachments.map(a =>
|
||||
`[\\[${a.filename}\\]](<${autumnURL}/${a.tag}/${a._id}/${a.filename}>)`).join(' | ');
|
||||
embed.fields!.push({ title: 'Attachments', content: message.attachments.map(a =>
|
||||
`[\\[${a.filename}\\]](<${autumnURL}/${a.tag}/${a._id}/${a.filename}>)`).join(' | ') })
|
||||
}
|
||||
|
||||
logChannel.sendMessage({
|
||||
content: logMsg,
|
||||
attachments: [ await uploadFile(msgRaw, 'Message content') ],
|
||||
})
|
||||
.catch(() => logger.warn(`Failed to send log message`));
|
||||
} else {
|
||||
let logMsg = `### Message deleted in ${channel.server?.name}\n`
|
||||
+ `[\\[Jump to channel\\]](/server/${channel.server?._id}/channel/${channel._id}) | `
|
||||
+ `[\\[Author\\]](/@${message.author_id})\n`
|
||||
+ `#### Message content\n`
|
||||
+ msg;
|
||||
|
||||
if (message.attachments?.length) {
|
||||
let autumnURL = await getAutumnURL();
|
||||
|
||||
logMsg += `\n\u200b\n#### Attachments\n` + message.attachments.map(a =>
|
||||
`[\\[${a.filename}\\]](<${autumnURL}/${a.tag}/${a._id}/${a.filename}>)`).join(' | ');
|
||||
}
|
||||
|
||||
logChannel.sendMessage(logMsg)
|
||||
.catch(() => logger.warn(`Failed to send log message`));
|
||||
await sendLogMessage(config.logs.messageUpdate, embed);
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
|
@ -114,22 +114,36 @@ client.on('packet', async (packet) => {
|
|||
}
|
||||
});
|
||||
|
||||
async function logModAction(type: 'warn'|'kick'|'ban', server: Server, mod: Member, target: string, reason: string|null, extraText?: string|null): Promise<void> {
|
||||
async function logModAction(type: 'warn'|'kick'|'ban', server: Server, mod: Member, target: string, reason: string|null, infraction: Infraction, extraText?: string): Promise<void> {
|
||||
try {
|
||||
let config: ServerConfig = await client.db.get('servers').findOne({ id: server._id }) ?? {};
|
||||
let logChannelID = config.logs?.modAction;
|
||||
if (!logChannelID) return;
|
||||
let logChannel = client.channels.get(logChannelID);
|
||||
|
||||
let aType = type == 'ban' ? 'banned' : type + 'ed';
|
||||
let msg = `User ${aType}\n`
|
||||
+ `\`@${mod.user?.username}\` **${aType}** \`@`
|
||||
+ `${await fetchUsername(target)}\`${type == 'warn' ? '.' : ` from ${server.name}.`}\n`
|
||||
+ `**Reason**: \`${reason ? reason : 'No reason provided.'}\`\n`
|
||||
+ (extraText ?? '');
|
||||
|
||||
logChannel?.sendMessage(msg)
|
||||
.catch(() => logger.warn('Failed to send log message'));
|
||||
if (config.logs?.modAction) {
|
||||
let aType = type == 'ban' ? 'banned' : type + 'ed';
|
||||
let embedColor = '#0576ff';
|
||||
if (type == 'kick') embedColor = '#ff861d';
|
||||
if (type == 'ban') embedColor = '#ff2f05';
|
||||
|
||||
|
||||
sendLogMessage(config.logs.modAction, {
|
||||
title: `User ${aType}`,
|
||||
description: `\`@${mod.user?.username}\` **${aType}** \``
|
||||
+ `${await fetchUsername(target)}\`${type == 'warn' ? '.' : ` from ${server.name}.`}\n`
|
||||
+ `**Reason**: \`${reason ? reason : 'No reason provided.'}\`\n`
|
||||
+ `**Warn ID**: \`${infraction._id}\`\n`
|
||||
+ (extraText ?? ''),
|
||||
color: embedColor,
|
||||
overrides: {
|
||||
revoltRvembed: {
|
||||
description: `@${mod.user?.username} ${aType} `
|
||||
+ `${await fetchUsername(target)}${type == 'warn' ? '.' : ` from ${server.name}.`}\n`
|
||||
+ `Reason: ${reason ? reason : 'No reason provided.'}\n`
|
||||
+ `Warn ID: ${infraction._id}\n`
|
||||
+ (extraText ?? ''),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import { FindOneResult } from "monk";
|
|||
import ScannedUser from "../../struct/ScannedUser";
|
||||
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
|
||||
import ServerConfig from "../../struct/ServerConfig";
|
||||
import { MessageEmbed, WebhookClient } from "discord.js";
|
||||
import logger from "../logger";
|
||||
import { sendLogMessage } from "../util";
|
||||
|
||||
let { USERSCAN_WORDLIST_PATH } = process.env;
|
||||
|
||||
|
@ -16,6 +16,8 @@ let wordlist = USERSCAN_WORDLIST_PATH
|
|||
.filter(word => word.length > 0)
|
||||
: null;
|
||||
|
||||
if (wordlist) logger.info("Found word list; user scanning enabled");
|
||||
|
||||
let scannedUsers = client.db.get('scanned_users');
|
||||
let serverConfig: Map<string, ServerConfig> = new Map();
|
||||
let userScanTimeout: Map<string, number> = new Map();
|
||||
|
@ -24,7 +26,7 @@ async function scanServer(id: string, userScanned: () => void, done: () => void)
|
|||
if (!wordlist) return;
|
||||
let conf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: id });
|
||||
serverConfig.set(id, conf as ServerConfig);
|
||||
if (!conf?.userScan?.enable) return;
|
||||
if (!conf?.enableUserScan) return;
|
||||
|
||||
try {
|
||||
logger.debug(`Scanning user list for ${id}`);
|
||||
|
@ -101,44 +103,32 @@ async function scanUser(member: Member) {
|
|||
async function logUser(member: Member, profile: any) { // `Profile` type doesn't seem to be exported by revolt.js
|
||||
try {
|
||||
let conf = serverConfig.get(member.server!._id);
|
||||
if (!conf || !conf.userScan?.enable) return;
|
||||
if (!conf || !conf.enableUserScan) return;
|
||||
|
||||
if (conf.userScan.discordWebhook) {
|
||||
try {
|
||||
let embed = new MessageEmbed()
|
||||
.setTitle('Potentially suspicious user found')
|
||||
.setAuthor(`${member.user?.username ?? 'Unknown user'} | ${member._id.user}`, member.generateAvatarURL());
|
||||
logger.debug(`User ${member._id} matched word list; reporting`);
|
||||
|
||||
if (member.nickname) embed.addField('Nickname', member.nickname || 'None', true);
|
||||
if (member.user?.status?.text) embed.addField('Status', member.user.status.text || 'None', true);
|
||||
embed.addField('Profile', ((profile?.content || 'No about me text') as string).substr(0, 1000));
|
||||
if (conf.enableUserScan && conf.logs?.userScan) {
|
||||
let bannerUrl = client.generateFileURL({
|
||||
_id: profile.background._id,
|
||||
tag: profile.background.tag,
|
||||
content_type: profile.background.content_type,
|
||||
}, undefined, true);
|
||||
let embedFields: { title: string, content: string, inline?: boolean }[] = [];
|
||||
if (member.nickname) embedFields.push({ title: 'Nickname', content: member.nickname || 'None', inline: true });
|
||||
if (member.user?.status?.text) embedFields.push({ title: 'Status', content: member.user.status.text || 'None', inline: true });
|
||||
embedFields.push({ title: 'Profile', content: ((profile?.content || 'No about me text') as string).substring(0, 1000), inline: true });
|
||||
|
||||
if (profile.background) {
|
||||
let url = client.generateFileURL({
|
||||
_id: profile.background._id,
|
||||
tag: profile.background.tag,
|
||||
content_type: profile.background.content_type,
|
||||
}, undefined, true);
|
||||
sendLogMessage(conf.logs.userScan, {
|
||||
title: 'Potentially suspicious user found',
|
||||
description: `${member.user?.username ?? 'Unknown user'} | [${member._id.user}](/@${member._id.user}) | [Avatar](<${member.generateAvatarURL()}>)`,
|
||||
color: '#ff9c11',
|
||||
fields: embedFields,
|
||||
image: bannerUrl ? {
|
||||
type: 'BIG',
|
||||
url: bannerUrl
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
if (url) embed.setImage(url);
|
||||
}
|
||||
|
||||
let whClient = new WebhookClient({ url: conf.userScan.discordWebhook });
|
||||
await whClient.send({ embeds: [ embed ] });
|
||||
whClient.destroy();
|
||||
} catch(e) { console.error(e) }
|
||||
}
|
||||
|
||||
if (conf.userScan.logChannel) {
|
||||
try {
|
||||
let channel = client.channels.get(conf.userScan.logChannel)
|
||||
|| await client.channels.fetch(conf.userScan.logChannel);
|
||||
|
||||
let msg = `## Potentially suspicious user found\n`
|
||||
+ `The profile <@${member._id.user}> (${member._id.user}) might contain abusive content.`;
|
||||
|
||||
await channel.sendMessage(msg);
|
||||
} catch(e) { console.error(e) }
|
||||
}
|
||||
} catch(e) { console.error(e) }
|
||||
}
|
||||
|
@ -166,7 +156,7 @@ new Promise((res: (value: void) => void) => client.user ? res() : client.once('r
|
|||
let conf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: server._id });
|
||||
serverConfig.set(server._id, conf as ServerConfig);
|
||||
|
||||
if (conf?.userScan?.enable) {
|
||||
if (conf?.enableUserScan) {
|
||||
let member = await server.fetchMember(packet.id);
|
||||
let t = userScanTimeout.get(member._id.user);
|
||||
if (t && t > (Date.now() - 10000)) return;
|
||||
|
@ -191,7 +181,7 @@ new Promise((res: (value: void) => void) => client.user ? res() : client.once('r
|
|||
let conf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: server._id });
|
||||
serverConfig.set(server._id, conf as ServerConfig);
|
||||
|
||||
if (conf?.userScan?.enable) {
|
||||
if (conf?.enableUserScan) {
|
||||
let t = userScanTimeout.get(member._id.user);
|
||||
if (t && t > (Date.now() - 10000)) return;
|
||||
userScanTimeout.set(member._id.user, Date.now());
|
||||
|
|
110
src/bot/util.ts
110
src/bot/util.ts
|
@ -6,6 +6,11 @@ import ServerConfig from "../struct/ServerConfig";
|
|||
import FormData from 'form-data';
|
||||
import axios from 'axios';
|
||||
import { Server } from "@janderedev/revolt.js/dist/maps/Servers";
|
||||
import LogConfig from "../struct/LogConfig";
|
||||
import LogMessage from "../struct/LogMessage";
|
||||
import { ColorResolvable, MessageAttachment, MessageEmbed, WebhookClient } from "discord.js";
|
||||
import logger from "./logger";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
let ServerPermissions = {
|
||||
['View' as string]: 1 << 0,
|
||||
|
@ -127,6 +132,110 @@ async function uploadFile(file: any, filename: string): Promise<string> {
|
|||
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 }
|
||||
|
||||
const embed = new MessageEmbed();
|
||||
if (c.title) embed.setTitle(content.title);
|
||||
if (c.description) embed.setDescription(c.description);
|
||||
if (c.color) 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);
|
||||
}
|
||||
}
|
||||
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.attachments?.length) {
|
||||
embed.setFooter(`Attachments: ${content.attachments.map(a => a.name).join(', ')}`);
|
||||
}
|
||||
|
||||
let data = new FormData();
|
||||
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' });
|
||||
|
||||
axios.post(config.discord.webhookUrl, data, {headers: data.getHeaders() })
|
||||
.catch(e => {
|
||||
logger.error('Failed to fire Discord webhook: ' + 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);
|
||||
|
||||
let message = '';
|
||||
switch(config.revolt.type) {
|
||||
case 'RVEMBED':
|
||||
case 'DYNAMIC':
|
||||
c = { ...c, ...content.overrides?.revoltRvembed };
|
||||
let url = `https://rvembed.janderedev.xyz/embed`;
|
||||
let args = [];
|
||||
|
||||
let description = (c.description ?? '');
|
||||
if (c.fields?.length) {
|
||||
for (const field of c.fields) {
|
||||
description += `\n${field.title}\n` +
|
||||
`${field.content}`;
|
||||
}
|
||||
}
|
||||
|
||||
description = description.trim();
|
||||
|
||||
if (c.title) args.push(`title=${encodeURIComponent(c.title)}`);
|
||||
if (description) args.push(`description=${encodeURIComponent(description)}`);
|
||||
if (c.color) args.push(`color=${encodeURIComponent(c.color)}`);
|
||||
if (c.image) {
|
||||
args.push(`image=${encodeURIComponent(c.image.url)}`);
|
||||
args.push(`image_large=true`);
|
||||
}
|
||||
|
||||
if (!(config.revolt.type == 'DYNAMIC' && (description.length > 1000 || description.split('\n').length > 6))) {
|
||||
for (const i in args) url += `${i == '0' ? '?' : '&'}${args[i]}`;
|
||||
message = `[\u200b](${url})`;
|
||||
break;
|
||||
}
|
||||
default: // QUOTEBLOCK, PLAIN or unspecified
|
||||
|
||||
// please disregard this mess
|
||||
|
||||
c = { ...c, ...content.overrides?.revoltQuoteblock };
|
||||
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 = message.trim().split('\n').join('\n' + quote); // Wrap entire message in quotes
|
||||
if (c.image?.url) message += `\n[Attachment](${c.image.url})`;
|
||||
break;
|
||||
}
|
||||
|
||||
await channel.sendMessage({
|
||||
content: message,
|
||||
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 in ${config.revolt.channel}: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to escape a message's markdown content (qoutes, headers, **bold** / *italic*, etc)
|
||||
*/
|
||||
|
@ -175,6 +284,7 @@ export {
|
|||
storeInfraction,
|
||||
uploadFile,
|
||||
sanitizeMessageContent,
|
||||
sendLogMessage,
|
||||
NO_MANAGER_MSG,
|
||||
ULID_REGEX,
|
||||
USER_MENTION_REGEX,
|
||||
|
|
14
src/struct/LogConfig.ts
Normal file
14
src/struct/LogConfig.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export default class LogConfig {
|
||||
revolt?: {
|
||||
channel?: string,
|
||||
|
||||
// RVEMBED uses https://rvembed.janderedev.xyz to send a discord style embed, which doesn't
|
||||
// work properly with longer messages.
|
||||
// PLAIN is like QUOTEBLOCK but without the quotes.
|
||||
// DYNAMIC uses RVEMBED if the message is short enough, otherwise defaults to QUOTEBLOCK.
|
||||
type?: 'QUOTEBLOCK'|'PLAIN'|'RVEMBED'|'DYNAMIC';
|
||||
}
|
||||
discord?: {
|
||||
webhookUrl?: string,
|
||||
}
|
||||
}
|
21
src/struct/LogMessage.ts
Normal file
21
src/struct/LogMessage.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
type Override = {
|
||||
description?: string|null;
|
||||
};
|
||||
|
||||
export default class LogMessage {
|
||||
title: string;
|
||||
description?: string;
|
||||
fields?: { title: string, content: string, inline?: boolean }[];
|
||||
color?: string;
|
||||
image?: { type: 'BIG'|'THUMBNAIL', url: string };
|
||||
attachments?: { name: string, content: Buffer }[];
|
||||
overrides?: {
|
||||
// These take priority over `revolt`
|
||||
revoltRvembed?: Override,
|
||||
revoltQuoteblock?: Override,
|
||||
|
||||
revolt?: Override,
|
||||
|
||||
discord?: Override,
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import AutomodSettings from "./antispam/AutomodSettings";
|
||||
import LogConfig from "./LogConfig";
|
||||
|
||||
class ServerConfig {
|
||||
id: string | undefined;
|
||||
|
@ -14,16 +15,11 @@ class ServerConfig {
|
|||
managers: boolean | undefined,
|
||||
} | undefined;
|
||||
logs: {
|
||||
automod: string | undefined, // automod rule triggered
|
||||
messageUpdate: string | undefined, // Message edited or deleted
|
||||
modAction: string | undefined, // User warned, kicked or banned
|
||||
userUpdate: string | undefined, // Username/nickname/avatar changes
|
||||
} | undefined;
|
||||
userScan: {
|
||||
enable?: boolean;
|
||||
logChannel?: string;
|
||||
discordWebhook?: string;
|
||||
messageUpdate?: LogConfig, // Message edited or deleted
|
||||
modAction?: LogConfig, // User warned, kicked or banned
|
||||
userScan?: LogConfig // User profile matched word list
|
||||
} | undefined;
|
||||
enableUserScan?: boolean;
|
||||
}
|
||||
|
||||
export default ServerConfig;
|
||||
|
|
Loading…
Reference in a new issue