From ca132071d977d54cd8f70b07b1abbecea7607a41 Mon Sep 17 00:00:00 2001 From: JandereDev Date: Thu, 14 Oct 2021 13:25:13 +0200 Subject: [PATCH] **H** --- src/bot/commands/warns.ts | 12 +--- src/bot/modules/mod_logs.ts | 113 ++++++++++++++++++++++++++++++++++++ src/bot/util.ts | 59 +++++++++++++++++++ src/index.ts | 1 + src/struct/ServerConfig.ts | 7 +++ 5 files changed, 182 insertions(+), 10 deletions(-) create mode 100644 src/bot/modules/mod_logs.ts diff --git a/src/bot/commands/warns.ts b/src/bot/commands/warns.ts index f1f1484..09989bb 100644 --- a/src/bot/commands/warns.ts +++ b/src/bot/commands/warns.ts @@ -3,7 +3,7 @@ import { Message } from "revolt.js/dist/maps/Messages"; import { client } from "../.."; import Infraction from "../../struct/antispam/Infraction"; import InfractionType from "../../struct/antispam/InfractionType"; -import { isModerator, NO_MANAGER_MSG, parseUser } from "../util"; +import { isModerator, NO_MANAGER_MSG, parseUser, uploadFile } from "../util"; import Day from 'dayjs'; import RelativeTime from 'dayjs/plugin/relativeTime'; import Xlsx from 'xlsx'; @@ -108,17 +108,9 @@ export default { } let sheet = Xlsx.utils.aoa_to_sheet(csv_data); - let csv = Xlsx.utils.sheet_to_csv(sheet); - let apiConfig: any = (await axios.get(client.apiURL)).data; - let autumnURL = apiConfig.features.autumn.url; - - let data = new FormData(); - data.append("file", csv, { filename: `${user._id}.csv` }); - - let req = await axios.post(autumnURL + '/attachments', data, { headers: data.getHeaders() }); - message.reply({ content: msg, attachments: [ (req.data as any)['id'] as string ] }); + message.reply({ content: msg, attachments: [ await uploadFile(csv, `${user._id}.csv`) ] }); } catch(e) { console.error(e); message.reply(msg); diff --git a/src/bot/modules/mod_logs.ts b/src/bot/modules/mod_logs.ts new file mode 100644 index 0000000..0238d56 --- /dev/null +++ b/src/bot/modules/mod_logs.ts @@ -0,0 +1,113 @@ +import { client } from "../.."; +import ServerConfig from "../../struct/ServerConfig"; +import logger from "../logger"; +import { getAutumnURL, sanitizeMessageContent, uploadFile } from "../util"; + +// the `packet` event is emitted before the client's cache +// is updated, which allows us to get the old message content +// if it was cached before +client.on('packet', async (packet) => { + if (packet.type == 'MessageUpdate') { + 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; + + let oldMsgRaw = String(m?.content ?? '(Unknown)'); + let newMsgRaw = String(packet.data.content); + let oldMsg = sanitizeMessageContent(oldMsgRaw) || '(Empty)'; + let newMsg = sanitizeMessageContent(newMsgRaw) || '(Empty)'; + + let channel = client.channels.get(packet.channel); + let server = channel?.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 }) ?? {}; + 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); + + let attachFullMessage = oldMsg.length > 800 || newMsg.length > 800; + + 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`)); + } + } catch(e) { + console.error(e); + } + } + + if (packet.type == 'MessageDelete') { + try { + let channel = client.channels.get(packet.channel); + if (!channel) return; + let message = client.messages.get(packet.id); + if (!message) return; + + let msgRaw = String(message.content ?? '(Unknown)'); + 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 (msg.length > 1000) { + let logMsg = `### Message deleted in ${message.channel?.server?.name}\n`; + + 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 { + 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`)); + } + } catch(e) { + console.error(e); + } + } +}); diff --git a/src/bot/util.ts b/src/bot/util.ts index 60e02ab..2a5818e 100644 --- a/src/bot/util.ts +++ b/src/bot/util.ts @@ -3,6 +3,8 @@ import { User } from "revolt.js/dist/maps/Users"; import { client } from ".."; import Infraction from "../struct/antispam/Infraction"; import ServerConfig from "../struct/ServerConfig"; +import FormData from 'form-data'; +import axios from 'axios'; let ServerPermissions = { ['View' as string]: 1 << 0, @@ -20,6 +22,14 @@ let ServerPermissions = { const NO_MANAGER_MSG = '🔒 Missing permission'; 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; +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; +} /** * Parses user input and returns an user object. @@ -96,12 +106,61 @@ async function storeInfraction(infraction: Infraction): Promise<{ userWarnCount: return { userWarnCount: (r[1].length ?? 0) + 1 } } +async function uploadFile(file: any, filename: string): Promise { + 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; +} + +/** + * 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')) { + + line = line.trim(); + + if (line.startsWith('#') || // headers + line.startsWith('>') || // quotes + line.startsWith('|') || // tables + line.startsWith('*') || // unordered lists + line.startsWith('-') || // ^ + line.startsWith('+') // ^ + ) { + line = `\\${line}`; + } + + // Ordered lists can't be escaped using `\`, + // so we just put an invisible character \u200b + if (/^[0-9]+[)\.].*/gi.test(line)) { + line = `\u200b${line}`; + } + + for (const char of ['_', '!!', '~', '`', '*', '^', '$']) { + line = line.replace(new RegExp(`(?