diff --git a/.env.example b/.env.example index 6a3f672..7c6cbb5 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ SAY_LOGS_CHANNEL= LOGS_CHANNEL= SIMULATED_BAN_SHARE_LOGS_CHANNEL= BAN_LOGS_CHANNEL= +MESSAGE_LOGS_CHANNEL= # These arent needed outside of production MAVEN_REPO= diff --git a/src/handlers/spam.handler.ts b/src/handlers/spam.handler.ts new file mode 100644 index 0000000..2d747ed --- /dev/null +++ b/src/handlers/spam.handler.ts @@ -0,0 +1,181 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + EmbedBuilder, + GuildMember, + GuildNSFWLevel, + Message, + ModalActionRowComponentBuilder, + ModalBuilder, + PermissionsBitField, + TextBasedChannel, + TextInputBuilder, + TextInputStyle, +} from 'discord.js'; +import { Handler } from '..'; +import { Button } from './button.handler'; + +const banButton = new Button( + 'ban-spammer', + async (interaction, data: { userId: string }) => { + const user = await interaction.client.users.fetch(data.userId); + if ( + !(interaction.member as GuildMember)?.permissions.has( + PermissionsBitField.Flags.BanMembers + ) + ) { + return await interaction.reply({ + content: 'You do not have permission to ban this user', + ephemeral: true, + }); + } + + const modal = new ModalBuilder() + .setCustomId(`ban`) + .setTitle(`Ban ${user.username}`) + + .addComponents( + new ActionRowBuilder().addComponents( + new TextInputBuilder() + .setCustomId('banReason') + .setLabel('Ban reason') + .setStyle(TextInputStyle.Paragraph) + .setValue('spam (autodetected)') + ) + ); + await interaction.showModal(modal); + interaction + .awaitModalSubmit({ + filter: (interaction) => + interaction.customId == modal.data.custom_id, + time: 300_000, + }) + .then(async (modalResponse) => { + interaction.guild?.bans.create(data.userId, { + reason: modalResponse.components[0].components[0].value, + }); + await modalResponse.reply({ + content: `<@${data.userId}> (\`${data.userId}\`) was banned.`, + ephemeral: true, + }); + await interaction.message.edit({ + components: [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('fakeBanButton') + .setLabel('Ban') + .setStyle(ButtonStyle.Danger) + .setDisabled(true) + ), + ], + }); + if (interaction.guild != null) { + const channel = await interaction.guild.channels.fetch( + process.env.BAN_LOGS_CHANNEL + ); + if (channel?.isTextBased()) { + channel.send({ + embeds: [ + new EmbedBuilder() + .setTitle('User Banned for spam') + .setDescription( + `<@!${data.userId}> was banned!` + ) + .setFields([ + { + name: 'Reason', + value: modalResponse.components[0] + .components[0].value, + }, + ]) + .setAuthor({ + iconURL: + interaction.user.avatar ?? + undefined, + name: interaction.user.username, + }), + ], + }); + } + } + }); + } +); + +const getMessageSuspicion = async (message: Message) => { + let suspicionLevel = 0; + const links = message.content.matchAll( + /(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?\/[a-zA-Z0-9]{2,}|((https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z]{2,}(\.[a-zA-Z]{2,})(\.[a-zA-Z]{2,})?)|(https:\/\/www\.|http:\/\/www\.|https:\/\/|http:\/\/)?[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}\.[a-zA-Z0-9]{2,}(\.[a-zA-Z0-9]{2,})?/g + ); + if (message.content.toLowerCase().includes('nitro')) suspicionLevel += 5; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + for (const _ of links) suspicionLevel += 5; + + const invites = message.content.matchAll( + /\s*(?:https:\/\/)?(?:discord\.gg\/|discord\.com\/invite\/)[a-zA-Z0-9]+\s*/g + ); + for (const inviteLink of invites) { + const invite = await message.client.fetchInvite(inviteLink[0]); + suspicionLevel += 10; + if (invite.guild?.name.includes('18')) suspicionLevel += 20; + if ( + invite.guild?.nsfwLevel == GuildNSFWLevel.Explicit || + invite.guild?.nsfwLevel == GuildNSFWLevel.AgeRestricted + ) + suspicionLevel += 20; + } + return suspicionLevel; +}; + +export const spamHandler: Handler = (client) => { + client.on('messageCreate', async (message: Message) => { + let suspicionLevel = await getMessageSuspicion(message); + if (suspicionLevel > 10) { + if (!message.inGuild()) return; + + suspicionLevel += ( + await Promise.all( + ( + await Promise.all( + message.guild.channels.cache + .filter((channel) => channel.isTextBased()) + .map(async (channel) => { + return ( + await ( + channel as TextBasedChannel + ).messages.fetch({ limit: 10 }) + ).filter( + (msg) => + msg.author.id == message.author.id + ); + }) + ) + ) + .reduce((a, b) => a.concat(b)) + .map(getMessageSuspicion) + ) + ).reduce((a, b) => a + b); + + const logChannel = await message.guild?.channels.fetch( + process.env.MESSAGE_LOGS_CHANNEL + ); + if (logChannel?.isTextBased()) + logChannel.send({ + embeds: [ + new EmbedBuilder({ + description: `suspicion level ${suspicionLevel} for ${message.author}, from message ${message.url}`, + }), + ], + components: [ + new ActionRowBuilder().addComponents( + banButton.button( + { label: 'Ban', style: ButtonStyle.Danger }, + { userId: message.author.id } + ) + ), + ], + }); + } + }); +}; diff --git a/src/index.ts b/src/index.ts index 5571f72..42a6290 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ import { reloadGlobalSlashCommands } from './handlers/command.handler'; import './webserver'; import { buttonHandler } from './handlers/button.handler'; import textCommandHandler from './handlers/textCommand.handler'; +import { spamHandler } from './handlers/spam.handler'; export const client = new Client({ intents: [ @@ -89,6 +90,7 @@ const handlers: Handler[] = [ textCommandHandler, logHandler, buttonHandler, + spamHandler, ]; for (const handler of handlers) { diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts index 37bd4b2..5cc6bd0 100644 --- a/src/types/environment.d.ts +++ b/src/types/environment.d.ts @@ -8,6 +8,7 @@ declare global { SAY_LOGS_CHANNEL: string; LOGS_CHANNEL: string; BAN_LOGS_CHANNEL: string; + MESSAGE_LOGS_CHANNEL: string; MAVEN_REPO: string; GITHUB_STATUS_CHANNEL: string; GITHUB_SECRET: string; diff --git a/src/webserver/banshare.ts b/src/webserver/banshare.ts index 3019f3c..2a6cf92 100644 --- a/src/webserver/banshare.ts +++ b/src/webserver/banshare.ts @@ -18,7 +18,7 @@ import { Button } from '../handlers/button.handler'; const banButton = new Button( 'ban', async (interaction, data: { userId: string }) => { - const user = await interaction.client.users.fetch(data.userId) + const user = await interaction.client.users.fetch(data.userId); if ( !(interaction.member as GuildMember)?.permissions.has( PermissionsBitField.Flags.BanMembers @@ -36,7 +36,7 @@ const banButton = new Button( const modal = new ModalBuilder() .setCustomId(`ban`) .setTitle(`Ban ${user.username}`) - + .addComponents( new ActionRowBuilder().addComponents( new TextInputBuilder() @@ -158,7 +158,9 @@ const handleBan = async (client: Client, req: Request) => { { label: 'Ban', style: ButtonStyle.Danger, - disabled: guildMember ? !guildMember.bannable : false + disabled: guildMember + ? !guildMember.bannable + : false, }, { userId } )