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 Command from "../../struct/Command";
|
||||||
import MessageCommandContext from "../../struct/MessageCommandContext";
|
import MessageCommandContext from "../../struct/MessageCommandContext";
|
||||||
import TempBan from "../../struct/TempBan";
|
import TempBan from "../../struct/TempBan";
|
||||||
import { fetchUsername } from "../modules/mod_logs";
|
import { fetchUsername, logModAction } from "../modules/mod_logs";
|
||||||
import { storeTempBan } from "../modules/tempbans";
|
import { storeTempBan } from "../modules/tempbans";
|
||||||
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
|
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
|
||||||
|
import Day from 'dayjs';
|
||||||
|
import RelativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
Day.extend(RelativeTime);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ban',
|
name: 'ban',
|
||||||
|
@ -61,7 +65,7 @@ export default {
|
||||||
|
|
||||||
if (banDuration == 0) {
|
if (banDuration == 0) {
|
||||||
let infId = ulid();
|
let infId = ulid();
|
||||||
let { userWarnCount } = await storeInfraction({
|
let infraction: Infraction = {
|
||||||
_id: infId,
|
_id: infId,
|
||||||
createdBy: message.author_id,
|
createdBy: message.author_id,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
|
@ -70,19 +74,23 @@ export default {
|
||||||
type: InfractionType.Manual,
|
type: InfractionType.Manual,
|
||||||
user: targetUser._id,
|
user: targetUser._id,
|
||||||
actionType: 'ban',
|
actionType: 'ban',
|
||||||
} as Infraction);
|
}
|
||||||
|
let { userWarnCount } = await storeInfraction(infraction);
|
||||||
|
|
||||||
message.serverContext.banUser(targetUser._id, {
|
message.serverContext.banUser(targetUser._id, {
|
||||||
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})`
|
reason: reason + ` (by ${await fetchUsername(message.author_id)} ${message.author_id})`
|
||||||
})
|
})
|
||||||
.catch(e => message.reply(`Failed to ban user: \`${e}\``));
|
.catch(e => message.reply(`Failed to ban user: \`${e}\``));
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
message.reply(`### @${targetUser.username} has been banned.\n`
|
message.reply(`### @${targetUser.username} has been banned.\n`
|
||||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
|
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`),
|
||||||
|
logModAction('ban', message.serverContext, message.member!, targetUser._id, reason, infraction, `Ban duration: **Permanent**`),
|
||||||
|
]);
|
||||||
} else {
|
} else {
|
||||||
let banUntil = Date.now() + banDuration;
|
let banUntil = Date.now() + banDuration;
|
||||||
let infId = ulid();
|
let infId = ulid();
|
||||||
let { userWarnCount } = await storeInfraction({
|
let infraction: Infraction = {
|
||||||
_id: infId,
|
_id: infId,
|
||||||
createdBy: message.author_id,
|
createdBy: message.author_id,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
|
@ -91,7 +99,8 @@ export default {
|
||||||
type: InfractionType.Manual,
|
type: InfractionType.Manual,
|
||||||
user: targetUser._id,
|
user: targetUser._id,
|
||||||
actionType: 'ban',
|
actionType: 'ban',
|
||||||
} as Infraction);
|
}
|
||||||
|
let { userWarnCount } = await storeInfraction(infraction);
|
||||||
|
|
||||||
message.serverContext.banUser(targetUser._id, {
|
message.serverContext.banUser(targetUser._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})`
|
||||||
|
@ -105,8 +114,11 @@ export default {
|
||||||
until: banUntil,
|
until: banUntil,
|
||||||
} as TempBan);
|
} as TempBan);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
message.reply(`### ${targetUser.username} has been temporarily banned.\n`
|
message.reply(`### ${targetUser.username} has been temporarily banned.\n`
|
||||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
|
+ `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;
|
} as Command;
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
let serverConf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: message.serverContext._id });
|
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.`);
|
if (userscans.includes(message.serverContext._id)) return message.reply(`There is already a scan running for this server.`);
|
||||||
userscans.push(message.serverContext._id);
|
userscans.push(message.serverContext._id);
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import Infraction from "../../struct/antispam/Infraction";
|
||||||
import InfractionType from "../../struct/antispam/InfractionType";
|
import InfractionType from "../../struct/antispam/InfractionType";
|
||||||
import Command from "../../struct/Command";
|
import Command from "../../struct/Command";
|
||||||
import MessageCommandContext from "../../struct/MessageCommandContext";
|
import MessageCommandContext from "../../struct/MessageCommandContext";
|
||||||
|
import { logModAction } from "../modules/mod_logs";
|
||||||
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
|
import { isModerator, NO_MANAGER_MSG, parseUser, storeInfraction } from "../util";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -41,7 +42,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
let infId = ulid();
|
let infId = ulid();
|
||||||
let { userWarnCount } = await storeInfraction({
|
let infraction: Infraction = {
|
||||||
_id: infId,
|
_id: infId,
|
||||||
createdBy: message.author_id,
|
createdBy: message.author_id,
|
||||||
date: Date.now(),
|
date: Date.now(),
|
||||||
|
@ -50,7 +51,8 @@ export default {
|
||||||
type: InfractionType.Manual,
|
type: InfractionType.Manual,
|
||||||
user: targetUser._id,
|
user: targetUser._id,
|
||||||
actionType: 'kick',
|
actionType: 'kick',
|
||||||
} as Infraction);
|
}
|
||||||
|
let { userWarnCount } = await storeInfraction(infraction);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await targetMember.kick();
|
await targetMember.kick();
|
||||||
|
@ -58,7 +60,10 @@ export default {
|
||||||
return message.reply(`Failed to kick user: \`${e}\``);
|
return message.reply(`Failed to kick user: \`${e}\``);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
message.reply(`### @${targetUser.username} has been ${Math.random() > 0.8 ? 'ejected' : 'kicked'}.\n`
|
message.reply(`### @${targetUser.username} has been ${Math.random() > 0.8 ? 'ejected' : 'kicked'}.\n`
|
||||||
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`);
|
+ `Infraction ID: \`${infId}\` (**#${userWarnCount}** for this user)`),
|
||||||
|
logModAction('kick', message.serverContext, message.member!, targetUser._id, reason, infraction),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
} as Command;
|
} as Command;
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default {
|
||||||
message.reply(`Measuring...`)
|
message.reply(`Measuring...`)
|
||||||
?.catch(console.error)
|
?.catch(console.error)
|
||||||
.then(msg => {
|
.then(msg => {
|
||||||
msg?.edit({ content: `## Ping Pong!\n`
|
if (msg) msg.edit({ content: `## Ping Pong!\n`
|
||||||
+ `WS: \`${client.websocket.ping ?? '--'}ms\`\n`
|
+ `WS: \`${client.websocket.ping ?? '--'}ms\`\n`
|
||||||
+ `Msg: \`${Math.round(Date.now() - now) / 2}ms\`` });
|
+ `Msg: \`${Math.round(Date.now() - now) / 2}ms\`` });
|
||||||
});
|
});
|
||||||
|
|
|
@ -42,7 +42,7 @@ export default {
|
||||||
+ ` for ${await fetchUsername(user._id)}.\n`
|
+ ` for ${await fetchUsername(user._id)}.\n`
|
||||||
+ `**Infraction ID:** \`${infraction._id}\`\n`
|
+ `**Infraction ID:** \`${infraction._id}\`\n`
|
||||||
+ `**Reason:** \`${infraction.reason}\``),
|
+ `**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;
|
} as Command;
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default {
|
||||||
+ `${inf[0].type == InfractionType.Manual ? `(${await fetchUsername(inf[0].createdBy ?? '')})` : ''}\n`;
|
+ `${inf[0].type == InfractionType.Manual ? `(${await fetchUsername(inf[0].createdBy ?? '')})` : ''}\n`;
|
||||||
};
|
};
|
||||||
|
|
||||||
message.reply(msg.substr(0, 1999));
|
message.reply(msg.substring(0, 1999));
|
||||||
} else {
|
} else {
|
||||||
switch(args[0]?.toLowerCase()) {
|
switch(args[0]?.toLowerCase()) {
|
||||||
case 'delete':
|
case 'delete':
|
||||||
|
@ -55,7 +55,7 @@ export default {
|
||||||
|
|
||||||
if (!inf) return message.reply('I can\'t find that ID.');
|
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`
|
+ `ID: \`${inf._id}\`\n`
|
||||||
+ `Reason: ${getInfEmoji(inf)}\`${inf.reason}\` `
|
+ `Reason: ${getInfEmoji(inf)}\`${inf.reason}\` `
|
||||||
+ `(${inf.type == InfractionType.Manual ? await fetchUsername(inf.createdBy ?? '') : 'System'})\n`
|
+ `(${inf.type == InfractionType.Manual ? await fetchUsername(inf.createdBy ?? '') : 'System'})\n`
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
|
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
|
||||||
import { Server } from "@janderedev/revolt.js/dist/maps/Servers";
|
import { Server } from "@janderedev/revolt.js/dist/maps/Servers";
|
||||||
import { client } from "../..";
|
import { client } from "../..";
|
||||||
|
import Infraction from "../../struct/antispam/Infraction";
|
||||||
|
import LogMessage from "../../struct/LogMessage";
|
||||||
import ServerConfig from "../../struct/ServerConfig";
|
import ServerConfig from "../../struct/ServerConfig";
|
||||||
import logger from "../logger";
|
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
|
// the `packet` event is emitted before the client's cache
|
||||||
// is updated, which allows us to get the old message content
|
// is updated, which allows us to get the old message content
|
||||||
|
@ -13,8 +15,6 @@ client.on('packet', async (packet) => {
|
||||||
try {
|
try {
|
||||||
if (!packet.data.content) return;
|
if (!packet.data.content) return;
|
||||||
|
|
||||||
logger.debug('Message updated');
|
|
||||||
|
|
||||||
let m = client.messages.get(packet.id);
|
let m = client.messages.get(packet.id);
|
||||||
|
|
||||||
if (m?.author_id == client.user?._id) return;
|
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');
|
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 }) ?? {};
|
let config: ServerConfig = await client.db.get('servers').findOne({ id: server._id }) ?? {};
|
||||||
if (!config?.logs?.messageUpdate) return;
|
if (config?.logs?.messageUpdate) {
|
||||||
let logChannelID = config.logs.messageUpdate;
|
const attachFullMessage = oldMsg.length > 800 || newMsg.length > 800;
|
||||||
let logChannel = client.channels.get(logChannelID);
|
let embed: LogMessage = {
|
||||||
if (!logChannel) return logger.debug('Log channel deleted or not cached: ' + logChannelID);
|
title: `Message edited in ${server.name}`,
|
||||||
|
description: `[\\[#${channel.name}\\]](/server/${server._id}/channel/${channel._id}) | `
|
||||||
let attachFullMessage = oldMsg.length > 800 || newMsg.length > 800;
|
+ `[\\[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}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (attachFullMessage) {
|
if (attachFullMessage) {
|
||||||
logChannel.sendMessage({
|
embed.attachments = [
|
||||||
content: `### Message edited in ${server.name}\n`
|
{ name: 'old_message.txt', content: Buffer.from(oldMsgRaw) },
|
||||||
+ `[\\[Jump to message\\]](/server/${server._id}/channel/${channel._id}/${packet.id})\n`,
|
{ name: 'new_message.txt', content: Buffer.from(newMsgRaw) },
|
||||||
attachments: await Promise.all([
|
];
|
||||||
uploadFile(oldMsgRaw, 'Old message'),
|
|
||||||
uploadFile(newMsgRaw, 'New message'),
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
let logMsg = `### Message edited in ${server.name}\n`
|
embed.fields!.push({ title: 'Old content', content: oldMsg });
|
||||||
+ `[\\[Jump to message\\]](/server/${server._id}/channel/${channel._id}/${packet.id}) | `
|
embed.fields!.push({ title: 'New content', content: newMsg });
|
||||||
+ `[\\[Author\\]](/@${m?.author_id})\n`;
|
}
|
||||||
logMsg += `#### Old Content\n${oldMsg}\n`;
|
|
||||||
logMsg += `#### New Content\n${newMsg}`;
|
|
||||||
|
|
||||||
logChannel.sendMessage(logMsg)
|
await sendLogMessage(config.logs.messageUpdate, embed);
|
||||||
.catch(() => logger.warn(`Failed to send log message`));
|
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -71,42 +76,37 @@ client.on('packet', async (packet) => {
|
||||||
let msg = sanitizeMessageContent(msgRaw);
|
let msg = sanitizeMessageContent(msgRaw);
|
||||||
|
|
||||||
let config: ServerConfig = await client.db.get('servers').findOne({ id: message.channel?.server?._id }) ?? {};
|
let config: ServerConfig = await client.db.get('servers').findOne({ id: message.channel?.server?._id }) ?? {};
|
||||||
if (!config?.logs?.messageUpdate) return;
|
if (config.logs?.messageUpdate) {
|
||||||
let logChannelID = config.logs.messageUpdate;
|
let embed: LogMessage = {
|
||||||
let logChannel = client.channels.get(logChannelID);
|
title: `Message deleted in ${message.channel?.server?.name}`,
|
||||||
if (!logChannel) return logger.debug('Log channel deleted or not cached: ' + logChannelID);
|
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) {
|
if (msg.length > 1000) {
|
||||||
let logMsg = `### Message deleted in ${message.channel?.server?.name}\n`;
|
embed.attachments?.push({ name: 'message.txt', content: Buffer.from(msgRaw) });
|
||||||
|
|
||||||
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({
|
|
||||||
content: logMsg,
|
|
||||||
attachments: [ await uploadFile(msgRaw, 'Message content') ],
|
|
||||||
})
|
|
||||||
.catch(() => logger.warn(`Failed to send log message`));
|
|
||||||
} else {
|
} else {
|
||||||
let logMsg = `### Message deleted in ${channel.server?.name}\n`
|
embed.fields!.push({ title: 'Content', content: msg || "(Empty)" });
|
||||||
+ `[\\[Jump to channel\\]](/server/${channel.server?._id}/channel/${channel._id}) | `
|
}
|
||||||
+ `[\\[Author\\]](/@${message.author_id})\n`
|
|
||||||
+ `#### Message content\n`
|
|
||||||
+ msg;
|
|
||||||
|
|
||||||
if (message.attachments?.length) {
|
if (message.attachments?.length) {
|
||||||
let autumnURL = await getAutumnURL();
|
let autumnURL = await getAutumnURL();
|
||||||
|
embed.fields!.push({ title: 'Attachments', content: message.attachments.map(a =>
|
||||||
logMsg += `\n\u200b\n#### Attachments\n` + message.attachments.map(a =>
|
`[\\[${a.filename}\\]](<${autumnURL}/${a.tag}/${a._id}/${a.filename}>)`).join(' | ') })
|
||||||
`[\\[${a.filename}\\]](<${autumnURL}/${a.tag}/${a._id}/${a.filename}>)`).join(' | ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logChannel.sendMessage(logMsg)
|
await sendLogMessage(config.logs.messageUpdate, embed);
|
||||||
.catch(() => logger.warn(`Failed to send log message`));
|
|
||||||
}
|
}
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(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 {
|
try {
|
||||||
let config: ServerConfig = await client.db.get('servers').findOne({ id: server._id }) ?? {};
|
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);
|
|
||||||
|
|
||||||
|
if (config.logs?.modAction) {
|
||||||
let aType = type == 'ban' ? 'banned' : type + 'ed';
|
let aType = type == 'ban' ? 'banned' : type + 'ed';
|
||||||
let msg = `User ${aType}\n`
|
let embedColor = '#0576ff';
|
||||||
+ `\`@${mod.user?.username}\` **${aType}** \`@`
|
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`
|
+ `${await fetchUsername(target)}\`${type == 'warn' ? '.' : ` from ${server.name}.`}\n`
|
||||||
+ `**Reason**: \`${reason ? reason : 'No reason provided.'}\`\n`
|
+ `**Reason**: \`${reason ? reason : 'No reason provided.'}\`\n`
|
||||||
+ (extraText ?? '');
|
+ `**Warn ID**: \`${infraction._id}\`\n`
|
||||||
|
+ (extraText ?? ''),
|
||||||
logChannel?.sendMessage(msg)
|
color: embedColor,
|
||||||
.catch(() => logger.warn('Failed to send log message'));
|
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) {
|
} catch(e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,8 @@ import { FindOneResult } from "monk";
|
||||||
import ScannedUser from "../../struct/ScannedUser";
|
import ScannedUser from "../../struct/ScannedUser";
|
||||||
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
|
import { Member } from "@janderedev/revolt.js/dist/maps/Members";
|
||||||
import ServerConfig from "../../struct/ServerConfig";
|
import ServerConfig from "../../struct/ServerConfig";
|
||||||
import { MessageEmbed, WebhookClient } from "discord.js";
|
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
|
import { sendLogMessage } from "../util";
|
||||||
|
|
||||||
let { USERSCAN_WORDLIST_PATH } = process.env;
|
let { USERSCAN_WORDLIST_PATH } = process.env;
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ let wordlist = USERSCAN_WORDLIST_PATH
|
||||||
.filter(word => word.length > 0)
|
.filter(word => word.length > 0)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
if (wordlist) logger.info("Found word list; user scanning enabled");
|
||||||
|
|
||||||
let scannedUsers = client.db.get('scanned_users');
|
let scannedUsers = client.db.get('scanned_users');
|
||||||
let serverConfig: Map<string, ServerConfig> = new Map();
|
let serverConfig: Map<string, ServerConfig> = new Map();
|
||||||
let userScanTimeout: Map<string, number> = 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;
|
if (!wordlist) return;
|
||||||
let conf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: id });
|
let conf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: id });
|
||||||
serverConfig.set(id, conf as ServerConfig);
|
serverConfig.set(id, conf as ServerConfig);
|
||||||
if (!conf?.userScan?.enable) return;
|
if (!conf?.enableUserScan) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.debug(`Scanning user list for ${id}`);
|
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
|
async function logUser(member: Member, profile: any) { // `Profile` type doesn't seem to be exported by revolt.js
|
||||||
try {
|
try {
|
||||||
let conf = serverConfig.get(member.server!._id);
|
let conf = serverConfig.get(member.server!._id);
|
||||||
if (!conf || !conf.userScan?.enable) return;
|
if (!conf || !conf.enableUserScan) return;
|
||||||
|
|
||||||
if (conf.userScan.discordWebhook) {
|
logger.debug(`User ${member._id} matched word list; reporting`);
|
||||||
try {
|
|
||||||
let embed = new MessageEmbed()
|
|
||||||
.setTitle('Potentially suspicious user found')
|
|
||||||
.setAuthor(`${member.user?.username ?? 'Unknown user'} | ${member._id.user}`, member.generateAvatarURL());
|
|
||||||
|
|
||||||
if (member.nickname) embed.addField('Nickname', member.nickname || 'None', true);
|
if (conf.enableUserScan && conf.logs?.userScan) {
|
||||||
if (member.user?.status?.text) embed.addField('Status', member.user.status.text || 'None', true);
|
let bannerUrl = client.generateFileURL({
|
||||||
embed.addField('Profile', ((profile?.content || 'No about me text') as string).substr(0, 1000));
|
|
||||||
|
|
||||||
if (profile.background) {
|
|
||||||
let url = client.generateFileURL({
|
|
||||||
_id: profile.background._id,
|
_id: profile.background._id,
|
||||||
tag: profile.background.tag,
|
tag: profile.background.tag,
|
||||||
content_type: profile.background.content_type,
|
content_type: profile.background.content_type,
|
||||||
}, undefined, true);
|
}, 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 (url) embed.setImage(url);
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
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) }
|
} 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 });
|
let conf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: server._id });
|
||||||
serverConfig.set(server._id, conf as ServerConfig);
|
serverConfig.set(server._id, conf as ServerConfig);
|
||||||
|
|
||||||
if (conf?.userScan?.enable) {
|
if (conf?.enableUserScan) {
|
||||||
let member = await server.fetchMember(packet.id);
|
let member = await server.fetchMember(packet.id);
|
||||||
let t = userScanTimeout.get(member._id.user);
|
let t = userScanTimeout.get(member._id.user);
|
||||||
if (t && t > (Date.now() - 10000)) return;
|
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 });
|
let conf: FindOneResult<ServerConfig> = await client.db.get('servers').findOne({ id: server._id });
|
||||||
serverConfig.set(server._id, conf as ServerConfig);
|
serverConfig.set(server._id, conf as ServerConfig);
|
||||||
|
|
||||||
if (conf?.userScan?.enable) {
|
if (conf?.enableUserScan) {
|
||||||
let t = userScanTimeout.get(member._id.user);
|
let t = userScanTimeout.get(member._id.user);
|
||||||
if (t && t > (Date.now() - 10000)) return;
|
if (t && t > (Date.now() - 10000)) return;
|
||||||
userScanTimeout.set(member._id.user, Date.now());
|
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 FormData from 'form-data';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { Server } from "@janderedev/revolt.js/dist/maps/Servers";
|
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 = {
|
let ServerPermissions = {
|
||||||
['View' as string]: 1 << 0,
|
['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;
|
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)
|
* Attempts to escape a message's markdown content (qoutes, headers, **bold** / *italic*, etc)
|
||||||
*/
|
*/
|
||||||
|
@ -175,6 +284,7 @@ export {
|
||||||
storeInfraction,
|
storeInfraction,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
sanitizeMessageContent,
|
sanitizeMessageContent,
|
||||||
|
sendLogMessage,
|
||||||
NO_MANAGER_MSG,
|
NO_MANAGER_MSG,
|
||||||
ULID_REGEX,
|
ULID_REGEX,
|
||||||
USER_MENTION_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 AutomodSettings from "./antispam/AutomodSettings";
|
||||||
|
import LogConfig from "./LogConfig";
|
||||||
|
|
||||||
class ServerConfig {
|
class ServerConfig {
|
||||||
id: string | undefined;
|
id: string | undefined;
|
||||||
|
@ -14,16 +15,11 @@ class ServerConfig {
|
||||||
managers: boolean | undefined,
|
managers: boolean | undefined,
|
||||||
} | undefined;
|
} | undefined;
|
||||||
logs: {
|
logs: {
|
||||||
automod: string | undefined, // automod rule triggered
|
messageUpdate?: LogConfig, // Message edited or deleted
|
||||||
messageUpdate: string | undefined, // Message edited or deleted
|
modAction?: LogConfig, // User warned, kicked or banned
|
||||||
modAction: string | undefined, // User warned, kicked or banned
|
userScan?: LogConfig // User profile matched word list
|
||||||
userUpdate: string | undefined, // Username/nickname/avatar changes
|
|
||||||
} | undefined;
|
|
||||||
userScan: {
|
|
||||||
enable?: boolean;
|
|
||||||
logChannel?: string;
|
|
||||||
discordWebhook?: string;
|
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
enableUserScan?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ServerConfig;
|
export default ServerConfig;
|
||||||
|
|
Loading…
Reference in a new issue