Skip to content

Commit 09c8ebf

Browse files
authored
Merge pull request #117 from Matyrobbrt/3.0
Add a scam link detector
2 parents c312435 + 2c1cce7 commit 09c8ebf

File tree

5 files changed

+224
-1
lines changed

5 files changed

+224
-1
lines changed

src/main/java/com/mcmoddev/mmdbot/core/TaskScheduler.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121
package com.mcmoddev.mmdbot.core;
2222

2323
import com.mcmoddev.mmdbot.MMDBot;
24+
import com.mcmoddev.mmdbot.modules.logging.misc.ScamDetector;
2425
import com.mcmoddev.mmdbot.utilities.oldchannels.ChannelMessageChecker;
2526
import com.mcmoddev.mmdbot.utilities.updatenotifiers.fabric.FabricApiUpdateNotifier;
2627
import com.mcmoddev.mmdbot.utilities.updatenotifiers.forge.ForgeUpdateNotifier;
2728
import com.mcmoddev.mmdbot.utilities.updatenotifiers.minecraft.MinecraftUpdateNotifier;
2829

2930
import java.util.Timer;
31+
import java.util.TimerTask;
3032

3133
/**
3234
* The type Task scheduler.
@@ -63,5 +65,15 @@ public static void init() {
6365
TIMER.scheduleAtFixedRate(new MinecraftUpdateNotifier(), 0, fifteenMinutes);
6466
TIMER.scheduleAtFixedRate(new FabricApiUpdateNotifier(), 0, fifteenMinutes);
6567
TIMER.scheduleAtFixedRate(new ChannelMessageChecker(), 0, 1000 * 60 * 60 * 24);
68+
TIMER.scheduleAtFixedRate(new TimerTask() {
69+
@Override
70+
public void run() {
71+
if (ScamDetector.setupScamLinks()) {
72+
MMDBot.LOGGER.info("Successfully refreshed scam links");
73+
} else {
74+
MMDBot.LOGGER.warn("Scam links could not be automatically refreshed");
75+
}
76+
}
77+
}, 0, 1000 * 60 * 60 * 24 * 7);
6678
}
6779
}

src/main/java/com/mcmoddev/mmdbot/modules/commands/CommandModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.mcmoddev.mmdbot.modules.commands.bot.info.CmdHelp;
2929
import com.mcmoddev.mmdbot.modules.commands.bot.info.CmdUptime;
3030
import com.mcmoddev.mmdbot.modules.commands.bot.management.CmdAvatar;
31+
import com.mcmoddev.mmdbot.modules.commands.bot.management.CmdRefreshScamLinks;
3132
import com.mcmoddev.mmdbot.modules.commands.bot.management.CmdRename;
3233
import com.mcmoddev.mmdbot.modules.commands.bot.management.CmdShutdown;
3334
import com.mcmoddev.mmdbot.modules.commands.general.info.CmdCatFacts;
@@ -125,6 +126,7 @@ public static void setupCommandModule() {
125126
.addSlashCommands(CmdMappings.createCommands()) // TODO: This is broken beyond belief. Consider moving away from linkie. - Curle
126127
.addSlashCommands(CmdTranslateMappings.createCommands())
127128
.addSlashCommand(new CmdQuote())
129+
.addCommand(new CmdRefreshScamLinks())
128130
.build();
129131

130132
if (MMDBot.getConfig().isCommandModuleEnabled()) {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.commands.bot.management;
22+
23+
import com.jagrosh.jdautilities.command.Command;
24+
import com.jagrosh.jdautilities.command.CommandEvent;
25+
import com.mcmoddev.mmdbot.modules.logging.misc.ScamDetector;
26+
27+
/**
28+
* Refreshes the {@link com.mcmoddev.mmdbot.modules.logging.misc.ScamDetector#SCAM_LINKS}
29+
*
30+
* @author matyrobbrt
31+
*/
32+
public class CmdRefreshScamLinks extends Command {
33+
34+
public CmdRefreshScamLinks() {
35+
name = "refresh-scam-links";
36+
aliases = new String[]{"refreshscamlinks"};
37+
help = "Refreshes the scam links";
38+
category = new Category("Management");
39+
hidden = true;
40+
guildOnly = false;
41+
ownerCommand = true;
42+
}
43+
44+
@Override
45+
protected void execute(final CommandEvent event) {
46+
event.getMessage().reply("Refreshing scam links...").mentionRepliedUser(false).queue(msg -> {
47+
new Thread(() -> {
48+
if (ScamDetector.setupScamLinks()) {
49+
msg.editMessage("Scam links successfully refreshed!").queue();
50+
} else {
51+
msg.editMessage("Scam links could not be refreshed! This is most likely caused by a connection issue.").queue();
52+
}
53+
}, "RefreshingScamLinks").start();
54+
});
55+
}
56+
}

src/main/java/com/mcmoddev/mmdbot/modules/logging/LoggingModule.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import com.mcmoddev.mmdbot.MMDBot;
2424
import com.mcmoddev.mmdbot.modules.logging.misc.EventReactionAdded;
25+
import com.mcmoddev.mmdbot.modules.logging.misc.ScamDetector;
2526
import com.mcmoddev.mmdbot.modules.logging.users.EventNicknameChanged;
2627
import com.mcmoddev.mmdbot.modules.logging.users.EventRoleAdded;
2728
import com.mcmoddev.mmdbot.modules.logging.users.EventRoleRemoved;
@@ -50,7 +51,8 @@ public static void setupLoggingModule() {
5051
new EventRoleAdded(),
5152
new EventRoleRemoved(),
5253
new EventReactionAdded(),
53-
new UserBanned());
54+
new UserBanned(),
55+
new ScamDetector());
5456
MMDBot.LOGGER.warn("Event logging module enabled and loaded.");
5557
} else {
5658
MMDBot.LOGGER.warn("Event logging module disabled via config, Discord event logging won't work right now!");
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
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

Comments
 (0)