Working Prototype

This commit is contained in:
coral
2026-04-16 21:45:12 -06:00
parent 3228088cd1
commit 6a92832b48
10 changed files with 434 additions and 18 deletions

1
.idea/gradle.xml generated
View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>

View File

@@ -13,8 +13,16 @@ dependencies {
testImplementation(platform("org.junit:junit-bom:6.0.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
implementation("net.dv8tion:JDA:6.4.1")
implementation("com.google.code.gson:gson:2.7")
}
tasks.test {
useJUnitPlatform()
}
}
tasks.jar {
manifest {
attributes["Main-Class"] = "me.trouper.antispambot.Init"
}
}

View File

@@ -1,17 +0,0 @@
package me.trouper;
//TIP To <b>Run</b> code, press <shortcut actionId="Run"/> or
// click the <icon src="AllIcons.Actions.Execute"/> icon in the gutter.
public class Main {
static void main() {
//TIP Press <shortcut actionId="ShowIntentionActions"/> with your caret at the highlighted text
// to see how IntelliJ IDEA suggests fixing it.
IO.println(String.format("Hello and welcome!"));
for (int i = 1; i <= 5; i++) {
//TIP Press <shortcut actionId="Debug"/> to start debugging your code. We have set one <icon src="AllIcons.Debugger.Db_set_breakpoint"/> breakpoint
// for you, but you can always add more by pressing <shortcut actionId="ToggleLineBreakpoint"/>.
IO.println("i = " + i);
}
}
}

View File

@@ -0,0 +1,15 @@
package me.trouper.antispambot;
import me.trouper.antispambot.bot.AntiSpamBot;
import me.trouper.antispambot.data.Config;
public interface Context {
public default AntiSpamBot getBot() {
return Init.getBot();
}
public default Config getConfig() {
return getBot().getConfig();
}
}

View File

@@ -0,0 +1,25 @@
package me.trouper.antispambot;
import me.trouper.antispambot.bot.AntiSpamBot;
public class Init {
private static AntiSpamBot bot;
static void main() {
try {
bot = new AntiSpamBot(System.getenv("TOKEN"));
} catch (InterruptedException ex) {
System.err.println("Failed to start AntiSpamBot: ");
ex.printStackTrace();
System.exit(-1);
return;
}
bot.startBot();
}
public static AntiSpamBot getBot() {
return bot;
}
}

View File

@@ -0,0 +1,122 @@
package me.trouper.antispambot.bot;
import me.trouper.antispambot.bot.commands.CleanseCommand;
import me.trouper.antispambot.bot.events.HoneyPotListener;
import me.trouper.antispambot.data.Config;
import me.trouper.antispambot.data.JsonSerializable;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.SlashCommandData;
import net.dv8tion.jda.api.requests.GatewayIntent;
import java.io.File;
import java.util.concurrent.TimeUnit;
public class AntiSpamBot {
private final String CONFIG_FILE_NAME = "config.json";
private File configFile;
private Guild guild;
private TextChannel channel;
private Role modRole;
private Config config;
private final JDA jda;
public AntiSpamBot(String token) throws InterruptedException {
this.jda = JDABuilder.create(token, GatewayIntent.getIntents(GatewayIntent.ALL_INTENTS))
.addEventListeners(
new HoneyPotListener(),
new CleanseCommand()
)
.build()
.awaitReady();
}
public void startBot() {
jda.updateCommands().addCommands(
Commands.slash("cleanse","Ban all users in this channel who sent 4 attachments at once.")
).queue();
configFile = new File(CONFIG_FILE_NAME);
config = JsonSerializable.load(configFile,Config.class,new Config());
guild = jda.getGuildById(config.targetServerID);
if (guild == null) throw new RuntimeException("The bot could not find the target server (" + config.targetServerID + ").");
channel = guild.getTextChannelById(config.targetChanelID);
if (channel == null) throw new RuntimeException("The bot could not find the target channel (" + config.targetChanelID + ").");
modRole = guild.getRoleById(config.moderatorRoleID);
if (modRole == null) throw new RuntimeException("The bot could not find the moderator role (" + config.moderatorRoleID + ").");
verifyChannel();
}
private void verifyChannel() {
channel.getHistory().retrievePast(20).submit().thenAccept((messages -> {
channel.deleteMessages(messages).queue();
}));
channel.sendMessageEmbeds(new EmbedBuilder()
.setTitle("Notice")
.setDescription("Sending a message in this channel will result in an immediate ban.")
.build()).queue();
}
public void banNotice(Member member) {
User user = member.getUser();
user.openPrivateChannel().submit().thenAccept(privateChannel -> {
privateChannel.sendMessageEmbeds(new EmbedBuilder()
.setTitle("Notice of Hacked Account")
.setDescription("""
ClickCrystals.xyz has detected your account as being compromised.
A 3rd party actor has been using it to spam a Crypto Scam/Token Stealer
in all of the servers you are a member of. If you still have access to
your account...
**CHANGE YOUR PASSWORD __IMMEDIATLY__**
Doing so will refresh your token, kicking the malicious actors out.
Do *not* download, run apps, or paste console scripts from any unestablished sources.
If you believe this notice to be in error, you may rejoin the ClickCrystals
discord server after verifing your account's security.
Thank you for your cooperation.
""")
.build()).setContent("!!! OPEN DM IMMEDIATELY !!!").submit().thenAccept((message)->{
member.ban(3, TimeUnit.DAYS).submit().thenAccept((v)->{
guild.unban(user).queue();
});
});
});
}
public Config getConfig() {
return config;
}
public JDA getJda() {
return jda;
}
public File getConfigFile() {
return configFile;
}
public Guild getGuild() {
return guild;
}
public TextChannel getChannel() {
return channel;
}
public Role getModRole() {
return modRole;
}
}

View File

@@ -0,0 +1,55 @@
package me.trouper.antispambot.bot.commands;
import me.trouper.antispambot.Context;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.Role;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
public class CleanseCommand extends ListenerAdapter implements Context {
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
if (!event.isFromGuild() || event.getGuild().getIdLong() != getConfig().targetServerID) return;
if (!event.getInteraction().getName().equals("cleanse")) return;
Member member = event.getMember();
if (member == null) return;
if (!member.getRoles().contains(getBot().getModRole())) {
event.reply("You do not have permission to use this command.").setEphemeral(true).queue();
return;
}
event.reply("Scanning channel...").submit().thenAccept(reply -> {
TextChannel target = event.getChannel().asTextChannel();
target.getHistory().retrievePast(20).submit().thenAccept(messages -> {
reply.editOriginal("Retrieved 20 messages...").queue();
AtomicInteger count = new AtomicInteger();
messages.stream()
.filter(message -> {
List<Message.Attachment> jpgs = message.getAttachments().stream()
.filter(a -> "jpg".equals(a.getFileExtension()))
.toList();
return jpgs.size() == 4;
})
.forEach(message -> {
Member suspect = message.getMember();
if (suspect == null) return;
getBot().banNotice(suspect);
count.incrementAndGet();
});
reply.editOriginal("Done cleansing compromised accounts. Banned: " + count.get()).queue();
});
});
}
}

View File

@@ -0,0 +1,24 @@
package me.trouper.antispambot.bot.events;
import me.trouper.antispambot.Context;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.util.concurrent.TimeUnit;
public class HoneyPotListener extends ListenerAdapter implements Context {
@Override
public void onMessageReceived(MessageReceivedEvent event) {
if (!event.isFromGuild() || event.getGuild().getIdLong() != getConfig().targetServerID) return;
if (event.getChannel().getIdLong() != getConfig().targetChanelID) return;
Member member = event.getMember();
if (member == null) return;
if (member.getUser().isBot()) return;
event.getMessage().delete().queue();
getBot().banNotice(member);
}
}

View File

@@ -0,0 +1,17 @@
package me.trouper.antispambot.data;
import me.trouper.antispambot.Context;
import java.io.File;
public class Config implements JsonSerializable, Context {
@Override
public File getFile() {
return getBot().getConfigFile();
}
public long targetServerID = 1095079504516493404L;
public long targetChanelID = 1494468792775086172L;
public long moderatorRoleID = 1100292475932917841L;
}

View File

@@ -0,0 +1,166 @@
package me.trouper.antispambot.data;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import java.io.*;
/**
* This is taken from ImproperIssues (ItziSpyder on GitHub) don't touch it unless its broken somehow.
*/
public interface JsonSerializable<T> {
Gson gson = new GsonBuilder().setPrettyPrinting().serializeNulls().setLenient().create();
File getFile();
default String serialize(boolean pretty) {
Gson gson;
if (pretty) {
gson = new GsonBuilder().setPrettyPrinting().setLenient().create();
}
else {
gson = new Gson();
}
try {
String json = gson.toJson(this);
if (json == null) {
throw new IllegalStateException("json parse failed for " + this.getClass().getSimpleName());
}
return json;
}
catch (Exception ex) {
return "{}";
}
}
@SuppressWarnings("unchecked")
default T deserialize(String json) {
try {
JsonSerializable<?> v = gson.fromJson(json, this.getClass());
if (v == null) {
throw new IllegalStateException("json parse failed");
}
return (T)v;
}
catch (Exception ex) {
return null;
}
}
default JsonObject getJson() {
return gson.toJsonTree(this).getAsJsonObject();
}
/**
* Gets a json element given the specified member path
* @param path Path separated by a period . between each member name
* @return the JsonElement at the end of the path, otherwise null
*/
default JsonElement get(String path) {
JsonElement root = gson.toJsonTree(this);
JsonElement json = root;
for (String memberName : path.split("\\.")) {
JsonElement e = json.getAsJsonObject().get(memberName);
if (e != null)
json = e;
else
break;
}
return json == root ? null : json;
}
/**
* Gets a json element given the specified member path
* @param path Path separated by a period . between each member name
*/
default boolean set(String path, Object obj) {
JsonElement root = gson.toJsonTree(this);
JsonElement json = root;
String[] paths = path.split("\\.");
if (paths.length == 0)
return false;
if (paths.length == 1) {
root.getAsJsonObject().add(path, gson.toJsonTree(obj));
return true;
}
for (int i = 0; i < paths.length - 1; i++) {
JsonElement e = json.getAsJsonObject().get(paths[i]);
if (e != null)
json = e;
else
break;
}
if (json != root) {
json.getAsJsonObject().add(paths[paths.length - 1], gson.toJsonTree(obj));
return true;
}
return false;
}
default void save() {
String json = serialize(true);
File f = getFile();
if (validate(f)) {
try {
FileWriter fw = new FileWriter(f);
BufferedWriter bw = new BufferedWriter(fw);
bw.write(json);
bw.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
default <O> O getOrDef(O val, O def) {
return val != null ? val : def;
}
static <T extends JsonSerializable<?>> T load(File file, Class<T> jsonSerializable, T fallback) {
if (validate(file)) {
try {
FileReader fr = new FileReader(file);
BufferedReader br = new BufferedReader(fr);
T t = gson.fromJson(br, jsonSerializable);
if (t == null) {
throw new IllegalStateException("json parse failed!");
}
return t;
} catch (Throwable ex) {
ex.printStackTrace();
System.out.println("The above stacktrace is expected. Falling back to default json file.");
}
}
return fallback;
}
static <T extends JsonSerializable<?>> T load(String path, Class<T> jsonSerializable, T fallback) {
return load(new File(path), jsonSerializable, fallback);
}
public static boolean validate(File file) {
try {
if (!file.getParentFile().exists())
if (!file.getParentFile().mkdirs())
return false;
if (!file.exists())
if (!file.createNewFile())
return false;
return true;
}
catch (Exception ex) {
return false;
}
}
}