|
| 1 | +/* |
| 2 | + * MMDBot - https://github.com/MinecraftModDevelopment/MMDBot |
| 3 | + * Copyright (C) 2016-2022 <MMD - MinecraftModDevelopment> |
| 4 | + * |
| 5 | + * This library is free software; you can redistribute it and/or |
| 6 | + * modify it under the terms of the GNU Lesser General Public |
| 7 | + * License as published by the Free Software Foundation; either |
| 8 | + * version 2.1 of the License, or (at your option) any later version. |
| 9 | + * |
| 10 | + * This library is distributed in the hope that it will be useful, |
| 11 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 12 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 13 | + * Lesser General Public License for more details. |
| 14 | + * |
| 15 | + * You should have received a copy of the GNU Lesser General Public |
| 16 | + * License along with this library; if not, write to the Free Software |
| 17 | + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 |
| 18 | + * USA |
| 19 | + * https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html |
| 20 | + */ |
| 21 | +package com.mcmoddev.mmdbot.modules.logging.misc; |
| 22 | + |
| 23 | +import com.google.gson.Gson; |
| 24 | +import com.google.gson.GsonBuilder; |
| 25 | +import com.google.gson.JsonArray; |
| 26 | +import com.google.gson.JsonElement; |
| 27 | +import com.mcmoddev.mmdbot.MMDBot; |
| 28 | +import com.mcmoddev.mmdbot.utilities.Utils; |
| 29 | +import com.mcmoddev.mmdbot.utilities.console.MMDMarkers; |
| 30 | +import net.dv8tion.jda.api.EmbedBuilder; |
| 31 | +import net.dv8tion.jda.api.Permission; |
| 32 | +import net.dv8tion.jda.api.entities.Guild; |
| 33 | +import net.dv8tion.jda.api.entities.Member; |
| 34 | +import net.dv8tion.jda.api.entities.Message; |
| 35 | +import net.dv8tion.jda.api.entities.MessageEmbed; |
| 36 | +import net.dv8tion.jda.api.entities.TextChannel; |
| 37 | +import net.dv8tion.jda.api.events.message.guild.GenericGuildMessageEvent; |
| 38 | +import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent; |
| 39 | +import net.dv8tion.jda.api.events.message.guild.GuildMessageUpdateEvent; |
| 40 | +import net.dv8tion.jda.api.hooks.ListenerAdapter; |
| 41 | +import net.dv8tion.jda.api.utils.MarkdownUtil; |
| 42 | + |
| 43 | +import javax.annotation.Nonnull; |
| 44 | +import java.awt.Color; |
| 45 | +import java.io.IOException; |
| 46 | +import java.net.URL; |
| 47 | +import java.nio.charset.StandardCharsets; |
| 48 | +import java.time.Instant; |
| 49 | +import java.util.Collections; |
| 50 | +import java.util.HashSet; |
| 51 | +import java.util.Locale; |
| 52 | +import java.util.Set; |
| 53 | +import java.util.Timer; |
| 54 | +import java.util.TimerTask; |
| 55 | +import java.util.function.Consumer; |
| 56 | +import java.util.stream.StreamSupport; |
| 57 | + |
| 58 | +/** |
| 59 | + * Scam detection system |
| 60 | + * @author matyrobbrt |
| 61 | + */ |
| 62 | +public class ScamDetector extends ListenerAdapter { |
| 63 | + |
| 64 | + public static final Gson GSON = new GsonBuilder().disableHtmlEscaping().create(); |
| 65 | + public static final String SCAM_LINKS_DATA_URL = "https://phish.sinking.yachts/v2/all"; |
| 66 | + |
| 67 | + public static final Set<String> SCAM_LINKS = Collections.synchronizedSet(new HashSet<>()); |
| 68 | + |
| 69 | + static { |
| 70 | + new Thread(ScamDetector::setupScamLinks, "Scam link collector").start(); |
| 71 | + } |
| 72 | + |
| 73 | + @Override |
| 74 | + public void onGuildMessageReceived(@Nonnull final GuildMessageReceivedEvent event) { |
| 75 | + takeActionIfScam(event.getMessage(), ""); |
| 76 | + } |
| 77 | + |
| 78 | + @Override |
| 79 | + public void onGuildMessageUpdate(@Nonnull final GuildMessageUpdateEvent event) { |
| 80 | + takeActionIfScam(event.getMessage(), ", by editing an old message"); |
| 81 | + } |
| 82 | + |
| 83 | + public static void takeActionIfScam(@Nonnull final Message msg, @Nonnull final String loggingReason) { |
| 84 | + final var member = msg.getMember(); |
| 85 | + if (member == null || msg.getAuthor().isBot() || msg.getAuthor().isSystem() || |
| 86 | + member.hasPermission(Permission.MANAGE_CHANNEL)) { |
| 87 | + return; |
| 88 | + } |
| 89 | + if (containsScam(msg)) { |
| 90 | + final var guild = msg.getGuild(); |
| 91 | + final var embed = getLoggingEmbed(msg, loggingReason); |
| 92 | + msg.delete().reason("Scam link").queue($ -> { |
| 93 | + executeInLoggingChannel(channel -> channel.sendMessageEmbeds(embed).queue()); |
| 94 | + mute(guild, member); |
| 95 | + }); |
| 96 | + } |
| 97 | + } |
| 98 | + |
| 99 | + private static void mute(final Guild guild, final Member member) { |
| 100 | + final var mutedRoleID = MMDBot.getConfig().getRole("muted"); |
| 101 | + // TODO once JDA is updated, maybe timeout the user. It seems a better idea |
| 102 | + final var mutedRole = guild.getRoleById(mutedRoleID); |
| 103 | + if (mutedRole == null) { |
| 104 | + MMDBot.LOGGER.error(MMDMarkers.MUTING, "Unable to find muted role {}", mutedRoleID); |
| 105 | + return; |
| 106 | + } |
| 107 | + guild.addRoleToMember(member, mutedRole).queue(); |
| 108 | + } |
| 109 | + |
| 110 | + private static MessageEmbed getLoggingEmbed(final Message message, final String extraDescription) { |
| 111 | + final var member = message.getMember(); |
| 112 | + return new EmbedBuilder().setTitle("Scam link detected!") |
| 113 | + .setDescription(String.format("User %s sent a scam link in %s%s. Their message was deleted, and they were muted.", member.getUser().getAsTag(), |
| 114 | + message.getTextChannel().getAsMention(), extraDescription)) |
| 115 | + .addField("Message Content", MarkdownUtil.codeblock(message.getContentRaw()), false) |
| 116 | + .setColor(Color.RED) |
| 117 | + .setTimestamp(Instant.now()) |
| 118 | + .setFooter("User ID: " + member.getIdLong()) |
| 119 | + .setThumbnail(member.getEffectiveAvatarUrl()).build(); |
| 120 | + } |
| 121 | + |
| 122 | + private static void executeInLoggingChannel(Consumer<TextChannel> channel) { |
| 123 | + Utils.getChannelIfPresent(MMDBot.getConfig().getChannel("events.requests_deletion"), channel); |
| 124 | + } |
| 125 | + |
| 126 | + public static boolean containsScam(final Message message) { |
| 127 | + final String msgContent = message.getContentRaw().toLowerCase(Locale.ROOT); |
| 128 | + synchronized (SCAM_LINKS) { |
| 129 | + for (final var link : SCAM_LINKS) { |
| 130 | + if (msgContent.contains(link)) { |
| 131 | + return true; |
| 132 | + } |
| 133 | + } |
| 134 | + } |
| 135 | + return false; |
| 136 | + } |
| 137 | + |
| 138 | + public static boolean setupScamLinks() { |
| 139 | + MMDBot.LOGGER.debug("Setting up scam links! Receiving data from {}.", SCAM_LINKS_DATA_URL); |
| 140 | + try (var is = new URL(SCAM_LINKS_DATA_URL).openStream()) { |
| 141 | + final String result = new String(is.readAllBytes(), StandardCharsets.UTF_8); |
| 142 | + SCAM_LINKS.clear(); |
| 143 | + SCAM_LINKS.addAll(StreamSupport.stream(GSON.fromJson(result, JsonArray.class).spliterator(), false) |
| 144 | + .map(JsonElement::getAsString).filter(s -> !s.contains("discordapp.co")).toList()); |
| 145 | + return true; |
| 146 | + } catch (final IOException e) { |
| 147 | + MMDBot.LOGGER.error("Error while setting up scam links!", e); |
| 148 | + } |
| 149 | + return false; |
| 150 | + } |
| 151 | +} |
0 commit comments