Skip to content
This repository has been archived by the owner on Mar 26, 2024. It is now read-only.

Feat/message logs #277

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"id-blacklist": "off",
"id-length": "off",
"id-match": "off",
"indent": ["error", 2],
"indent": ["error", 2, { "SwitchCase": 1 }],
"init-declarations": "off",
"line-comment-position": "off",
"lines-between-class-members": [
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@
"axios": "^0.26.0",
"bull": "^4.6.2",
"date-fns": "^2.28.0",
"diff": "^5.0.0",
"discord.js": "^13.6.0",
"fs-extra": "^10.0.1",
"mime-types": "^2.1.35",
"mongodb": "^4.4.0",
"node-cron": "^3.0.0",
"puppeteer-core": "^13.4.0",
Expand All @@ -48,7 +50,9 @@
"@sapphire/ts-config": "^3.3.1",
"@types/chai": "^4.3.0",
"@types/chai-as-promised": "^7.1.5",
"@types/diff": "^5.0.2",
"@types/fs-extra": "^9.0.13",
"@types/mime-types": "^2.1.1",
"@types/mocha": "^9.1.0",
"@types/node": "^16.11.26",
"@types/node-cron": "^3.0.1",
Expand Down
74 changes: 74 additions & 0 deletions src/commands/config/Logs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Command, type CommandOptions } from '@sapphire/framework';
import { type CommandInteraction } from 'discord.js';
import { env } from '../../lib';

@ApplyOptions<CommandOptions>({
chatInputApplicationOptions: {
defaultPermission: false,
options: [
{
description: 'Especifica el canal donde se enviarán las notificaciones.',
name: 'canal',
options: [
{
channelTypes: [
'GUILD_NEWS', 'GUILD_TEXT'
],
description: 'Mención del canal',
name: 'canal',
required: true,
type: 'CHANNEL'
}
],
type: 'SUB_COMMAND'
},
],
permissions: [
{
id: env.STAFF_ROLE,
permission: true,
type: 'ROLE'
}
]
},
description: 'Configura el sistema de logging del servidor.',
name: 'logs'
})
export class LogsCommand extends Command {
// eslint-disable-next-line @typescript-eslint/no-empty-function
public messageRun(): void {

}

public async chatInputApplicationRun(interaction: CommandInteraction<'present'>): Promise<void> {
await interaction.deferReply();

const subcommand = interaction.options.getSubcommand();

switch (subcommand) {
case 'canal': {
await this.setLogsChannel(interaction);
break;
}

default: {
await interaction.editReply('No reconozco el comando que has utilizado.');
}
}
}

private async setLogsChannel(interaction: CommandInteraction<'present'>): Promise<void> {
const channel = interaction.options.getChannel('canal', true);
const guild = this.container.stores.get('models').get('guild');

await guild.setChannel(interaction.guildId, 'logs', channel.id);

await interaction.editReply({
embeds: [{
color: 0x1b5e20,
description: `Canal de logs cambiado a: <#${ channel.id }>`
}]
});
}
}
9 changes: 5 additions & 4 deletions src/lib/util/discord/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './get-interaction-channel';
export * from './get-interaction-guild';
export * from './get-interaction-member';
export * from './get-interaction-member-roles';
export * from './get-interaction-channel';
export * from './get-interaction-guild';
export * from './get-interaction-member';
export * from './get-interaction-member-roles';
export * from './message-utils';
36 changes: 36 additions & 0 deletions src/lib/util/discord/message-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { diffChars } from 'diff';
import { MessageAttachment } from 'discord.js';
import mime from 'mime-types';

/**
* Escapes markdown formatting from a string.
* @param text The text to escape.
*/
export const escapeMarkdown = (text: string): string => {
return text.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&');
};

/**
* Get the difference between two messages, as a markdown formatted string.
* @param oldMessage The old message content.
* @param newMessage The new message content.
*/
export const messageDiff = (oldMessage: string, newMessage: string): string => {
const changes = diffChars(oldMessage, newMessage);

const diffText = changes.map((change) => {
if (change.added) {
return `**${escapeMarkdown(change.value)}**`;
} else if (change.removed) {
return `~~${escapeMarkdown(change.value)}~~`;
}
return escapeMarkdown(change.value);
}).join('');

return diffText;
};

export const reuploadAttachment = (attachment: MessageAttachment): MessageAttachment => {
const newAttachment = new MessageAttachment(attachment.url, attachment.name ?? `${attachment.id}.${mime.extension(attachment.contentType ?? 'application/octet-stream')}`);
return newAttachment;
};
76 changes: 76 additions & 0 deletions src/listeners/MessageDelete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
import { MessageEmbed, type BaseGuildTextChannel, type Message } from 'discord.js';
import { stringSimilarity } from 'string-similarity-js';
import { sleep } from '../lib';

@ApplyOptions<ListenerOptions>({
event: Events.MessageDelete,
})
export class MessageDeleteListener extends Listener {
public async run(message: Message): Promise<void> {
const { client } = this.container;

if (!message.guild) return;
if (message.author.bot) return;

const guild = this.container.stores.get('models').get('guild'),
logsChannelId = await guild.getChannel(message.guild.id, 'logs'),
logsChannel = message.guild.channels.cache.get(logsChannelId!) as BaseGuildTextChannel;

// TODO: log to database

if (!logsChannel) {
client.logger.info(`No logs channel configured for ${message.guild.name}`);
return;
}

if (message.content.length) {
/**
* Try to find a similar message sent after the deleted one, and from a webhook with the same name as the deleted message's author
* If found, compare the content
* If the content is similar, we can be 99% sure that the message was deleted by NQN, so we don't log it
*/

await sleep(2000);

const webhookMessages = message.channel.messages.cache.filter((m) => m.webhookId !== null && m.createdTimestamp > message.createdTimestamp),
webhookMessagesSameUser = webhookMessages.filter((m) => m.author.username === message.member?.displayName);

const contentMatch = webhookMessagesSameUser.find((m) => {
const messageContentWithoutEmojiIDs = m.content.replace(/<(a)?(:[^:]+:)([0-9]+)>/g, '$2'),
similarityScore = stringSimilarity(messageContentWithoutEmojiIDs, message.content);

return similarityScore > 0.99;
});

if (contentMatch) return;
}

const embed = new MessageEmbed()
.setColor('#ff0000')
.setAuthor({
name: message.author.tag,
iconURL: message.author.displayAvatarURL()
})
.setDescription(`Mensaje de <@${message.author.id}> eliminado en <#${message.channel.id}>`);

if (message.content.length) {
embed.addField('Mensaje', message.content.length > 1024 ? `${message.content.slice(0, 1021)}...` : message.content, false);
}

if (message.attachments.size) {
embed.addField('Archivos', message.attachments.map(file => file.url).join('\n'), false);
}

const previousMessages = await message.channel.messages.fetch({ limit: 10, before: message.id }),
previousMessagesNotAuthor = previousMessages.filter(msg => msg.author.id !== message.author.id),
previousMessage = previousMessagesNotAuthor.size ? previousMessagesNotAuthor.first() : previousMessages.first();

embed.addField('Mensaje anterior', `[Ver mensaje](${previousMessage!.url})`);

await logsChannel.send({
embeds: [embed]
});
}
}
55 changes: 55 additions & 0 deletions src/listeners/MessageUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { ApplyOptions } from '@sapphire/decorators';
import { Events, Listener, type ListenerOptions } from '@sapphire/framework';
import { type BaseGuildTextChannel, type Message, MessageEmbed } from 'discord.js';
import { messageDiff } from '../lib/util';

@ApplyOptions<ListenerOptions>({
event: Events.MessageUpdate,
})
export class MessageUpdateListener extends Listener {
public async run(oldMessage: Message, newMessage: Message): Promise<void> {
const { client } = this.container;

if (!oldMessage.guild || !newMessage.guild) return;
if (newMessage.author.bot) return;

const guild = this.container.stores.get('models').get('guild'),
logsChannelId = await guild.getChannel(newMessage.guild.id, 'logs'),
logsChannel = newMessage.guild.channels.cache.get(logsChannelId!) as BaseGuildTextChannel;

// TODO: log to database

if (!logsChannel) {
client.logger.info(`No logs channel configured for ${newMessage.guild.name}`);
return;
}

const diff = messageDiff(oldMessage.content, newMessage.content);

const embed = new MessageEmbed()
.setTitle(`Mensaje editado en **#${(newMessage.channel as BaseGuildTextChannel).name}**`)
.setColor('#00bcd4')
.setAuthor({
name: newMessage.author.tag,
iconURL: newMessage.author.displayAvatarURL()
})
.setDescription(`[Ver mensaje](${newMessage.url})`)
.setTimestamp();

if (oldMessage.content.length) {
embed.addField('Antes', oldMessage.content.length > 1024 ? `${oldMessage.content.slice(0, 1021)}...` : oldMessage.content, false);
}

if (newMessage.content.length) {
embed.addField('Después', newMessage.content.length > 1024 ? `${newMessage.content.slice(0, 1021)}...` : newMessage.content, false);
}

if (oldMessage.content.length && newMessage.content.length) {
embed.addField('Cambios', diff);
}

await logsChannel.send({
embeds: [embed]
});
}
}
5 changes: 3 additions & 2 deletions src/models/Guild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { ApplyOptions } from '@sapphire/decorators';
import { Model } from '../lib';
import type { PieceOptions } from '@sapphire/pieces';

type SettingsChannels = 'starboard' | 'roles';
type SettingsChannels = 'logs' | 'roles' | 'starboard';

export interface IGuild {
id: string;
settings?: {
prefix?: string;
channels?: {
starboard?: string;
logs?: string;
roles?: string;
starboard?: string;
};
};
stats?: {
Expand Down
36 changes: 35 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,13 @@ __metadata:
languageName: node
linkType: hard

"@types/diff@npm:^5.0.2":
version: 5.0.2
resolution: "@types/diff@npm:5.0.2"
checksum: 8fbc419b5aca33f494026bf5f70e026f76367689677ef114f9c078ac738d7dbe96e6dda3fd8290e4a7c35281e2b60b034e3d7e3c968b850cf06a21279e7ddcbe
languageName: node
linkType: hard

"@types/express-serve-static-core@npm:^4.17.18":
version: 4.17.28
resolution: "@types/express-serve-static-core@npm:4.17.28"
Expand Down Expand Up @@ -499,6 +506,13 @@ __metadata:
languageName: node
linkType: hard

"@types/mime-types@npm:^2.1.1":
version: 2.1.1
resolution: "@types/mime-types@npm:2.1.1"
checksum: 106b5d556add46446a579ad25ff15d6b421851790d887edcad558c90c1e64b1defc72bfbaf4b08f208916e21d9cc45cdb951d77be51268b18221544cfe054a3c
languageName: node
linkType: hard

"@types/mime@npm:^1":
version: 1.3.2
resolution: "@types/mime@npm:1.3.2"
Expand Down Expand Up @@ -1304,7 +1318,9 @@ __metadata:
"@sapphire/ts-config": ^3.3.1
"@types/chai": ^4.3.0
"@types/chai-as-promised": ^7.1.5
"@types/diff": ^5.0.2
"@types/fs-extra": ^9.0.13
"@types/mime-types": ^2.1.1
"@types/mocha": ^9.1.0
"@types/node": ^16.11.26
"@types/node-cron": ^3.0.1
Expand All @@ -1316,12 +1332,14 @@ __metadata:
chai: ^4.3.6
chai-as-promised: ^7.1.1
date-fns: ^2.28.0
diff: ^5.0.0
discord.js: ^13.6.0
erlpack: "github:discord/erlpack"
eslint: ~8.10.0
fs-extra: ^10.0.1
husky: ^7.0.4
lint-staged: ^12.3.4
mime-types: ^2.1.35
mocha: ^9.2.1
mongodb: ^4.4.0
node-cron: ^3.0.0
Expand Down Expand Up @@ -1482,7 +1500,7 @@ __metadata:
languageName: node
linkType: hard

"diff@npm:5.0.0":
"diff@npm:5.0.0, diff@npm:^5.0.0":
version: 5.0.0
resolution: "diff@npm:5.0.0"
checksum: f19fe29284b633afdb2725c2a8bb7d25761ea54d321d8e67987ac851c5294be4afeab532bd84531e02583a3fe7f4014aa314a3eda84f5590e7a9e6b371ef3b46
Expand Down Expand Up @@ -2750,6 +2768,13 @@ __metadata:
languageName: node
linkType: hard

"mime-db@npm:1.52.0":
version: 1.52.0
resolution: "mime-db@npm:1.52.0"
checksum: 0d99a03585f8b39d68182803b12ac601d9c01abfa28ec56204fa330bc9f3d1c5e14beb049bafadb3dbdf646dfb94b87e24d4ec7b31b7279ef906a8ea9b6a513f
languageName: node
linkType: hard

"mime-types@npm:^2.1.12":
version: 2.1.34
resolution: "mime-types@npm:2.1.34"
Expand All @@ -2759,6 +2784,15 @@ __metadata:
languageName: node
linkType: hard

"mime-types@npm:^2.1.35":
version: 2.1.35
resolution: "mime-types@npm:2.1.35"
dependencies:
mime-db: 1.52.0
checksum: 89a5b7f1def9f3af5dad6496c5ed50191ae4331cc5389d7c521c8ad28d5fdad2d06fd81baf38fed813dc4e46bb55c8145bb0ff406330818c9cf712fb2e9b3836
languageName: node
linkType: hard

"mimic-fn@npm:^2.1.0":
version: 2.1.0
resolution: "mimic-fn@npm:2.1.0"
Expand Down