feat: store keys per-user using discord instead of having a global key

refactor: use language on DirectMessageEventHandler
refactor: some misc refactors (i forgot what i did lol)
This commit is contained in:
ChomeNS
2025-05-26 19:46:38 +07:00
parent 9afbdc26b3
commit f68ce719f9
13 changed files with 358 additions and 152 deletions

View File

@@ -90,7 +90,6 @@ public class Bot extends SessionAdapter {
public final TabCompletePlugin tabComplete;
public final CommandHandlerPlugin commandHandler;
public final ChatCommandHandlerPlugin chatCommandHandler;
public final HashingPlugin hashing;
public final BossbarManagerPlugin bossbar;
public final MusicPlayerPlugin music;
public final TPSPlugin tps;
@@ -151,7 +150,6 @@ public class Bot extends SessionAdapter {
this.tabComplete = new TabCompletePlugin(this);
this.commandHandler = new CommandHandlerPlugin(this);
this.chatCommandHandler = new ChatCommandHandlerPlugin(this);
this.hashing = new HashingPlugin(this);
this.bossbar = new BossbarManagerPlugin(this);
this.music = new MusicPlayerPlugin(this);
this.tps = new TPSPlugin(this);

View File

@@ -9,7 +9,6 @@ public class Configuration {
public String consoleCommandPrefix;
public Keys keys = new Keys();
public Backup backup = new Backup();
public Database database = new Database();
@@ -69,22 +68,10 @@ public class Configuration {
public List<String> players = new ArrayList<>();
}
public static class Keys {
public String trustedKey;
public String adminKey;
public String ownerKey;
}
public static class Core {
public String customName = "{\"text\":\"@\"}";
}
public static class Position {
public int x = 0;
public int y = 0;
public int z = 0;
}
public static class ColorPalette {
public String primary = "yellow";
public String secondary = "gold";

View File

@@ -6,10 +6,7 @@ import me.chayapak1.chomens_bot.plugins.ConsolePlugin;
import me.chayapak1.chomens_bot.plugins.DatabasePlugin;
import me.chayapak1.chomens_bot.plugins.DiscordPlugin;
import me.chayapak1.chomens_bot.plugins.IRCPlugin;
import me.chayapak1.chomens_bot.util.ArrayUtilities;
import me.chayapak1.chomens_bot.util.HttpUtilities;
import me.chayapak1.chomens_bot.util.I18nUtilities;
import me.chayapak1.chomens_bot.util.LoggerUtilities;
import me.chayapak1.chomens_bot.util.*;
import net.dv8tion.jda.api.requests.restaction.MessageCreateAction;
import org.yaml.snakeyaml.LoaderOptions;
import org.yaml.snakeyaml.Yaml;
@@ -137,6 +134,7 @@ public class Main {
try {
if (config.database.enabled) database = new DatabasePlugin(config);
HashingUtilities.init();
final Configuration.BotOption[] botsOptions = config.bots;

View File

@@ -0,0 +1,24 @@
package me.chayapak1.chomens_bot.data.keys;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import me.chayapak1.chomens_bot.command.TrustLevel;
import org.jetbrains.annotations.NotNull;
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonSerialize
public record Key(
@JsonProperty TrustLevel trustLevel,
@JsonProperty String key,
@JsonProperty long createdAt
) {
@Override
public @NotNull String toString () {
return "Key{" +
"trustLevel=" + trustLevel +
", key='" + key + '\'' +
", createdAt=" + createdAt +
'}';
}
}

View File

@@ -0,0 +1,23 @@
package me.chayapak1.chomens_bot.data.keys;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
@JsonSerialize
public record KeysData(
@JsonProperty ArrayList<Key> keys,
@JsonProperty String userId
) {
@Override
public @NotNull String toString () {
return "KeysData{" +
"keys=" + keys +
", userId='" + userId + '\'' +
'}';
}
}

View File

@@ -1,8 +1,10 @@
package me.chayapak1.chomens_bot.data.mail;
import org.jetbrains.annotations.NotNull;
public record Mail(String sentBy, String sentTo, long timeSent, String server, String contents) {
@Override
public String toString () {
public @NotNull String toString () {
return "Mail{" +
"sentBy='" + sentBy + '\'' +
", sentTo='" + sentTo + '\'' +

View File

@@ -3,7 +3,8 @@ package me.chayapak1.chomens_bot.discord;
import me.chayapak1.chomens_bot.Configuration;
import me.chayapak1.chomens_bot.command.TrustLevel;
import me.chayapak1.chomens_bot.data.logging.LogType;
import me.chayapak1.chomens_bot.plugins.HashingPlugin;
import me.chayapak1.chomens_bot.util.HashingUtilities;
import me.chayapak1.chomens_bot.util.I18nUtilities;
import me.chayapak1.chomens_bot.util.LoggerUtilities;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.entities.Guild;
@@ -19,6 +20,8 @@ import org.jetbrains.annotations.NotNull;
public class DirectMessageEventHandler extends ListenerAdapter {
private static final String HASH_MESSAGE = "hash";
private static final String KEY_MESSAGE = "key";
private static final String FORCED_KEY_MESSAGE = "key force";
private final JDA jda;
@@ -40,7 +43,11 @@ public class DirectMessageEventHandler extends ListenerAdapter {
final Message message = event.getMessage();
if (!message.getContentDisplay().equalsIgnoreCase(HASH_MESSAGE)) return;
if (
!message.getContentDisplay().equalsIgnoreCase(HASH_MESSAGE)
&& !message.getContentDisplay().equalsIgnoreCase(KEY_MESSAGE)
&& !message.getContentDisplay().equalsIgnoreCase(FORCED_KEY_MESSAGE)
) return;
final Guild guild;
@@ -68,16 +75,21 @@ public class DirectMessageEventHandler extends ListenerAdapter {
LoggerUtilities.log(
LogType.DISCORD,
Component.translatable(
"User %s tried to get hash in Discord without any trusted roles!",
I18nUtilities.get("hashing.discord_direct_message.error.no_roles.log"),
Component.text(member.toString())
)
);
message.reply("You do not have any trusted roles!")
message
.reply(I18nUtilities.get("hashing.discord_direct_message.error.no_roles"))
.queue();
return;
}
sendHash(trustLevel, message, member);
switch (message.getContentDisplay().toLowerCase()) {
case HASH_MESSAGE -> sendHash(trustLevel, message, member);
case KEY_MESSAGE -> sendKey(trustLevel, message, member, false);
case FORCED_KEY_MESSAGE -> sendKey(trustLevel, message, member, true);
}
},
exception -> {
if (!(exception instanceof final ErrorResponseException error)) return;
@@ -85,13 +97,18 @@ public class DirectMessageEventHandler extends ListenerAdapter {
final ErrorResponse errorResponse = error.getErrorResponse();
if (errorResponse == ErrorResponse.UNKNOWN_MEMBER) {
message.reply("You are not in " + guild.getName() + "!")
message
.reply(
String.format(
I18nUtilities.get("hashing.discord_direct_message.error.not_in_guild"),
guild.getName()
)
)
.queue();
} else if (errorResponse == ErrorResponse.UNKNOWN_USER) {
LoggerUtilities.error(
Component.translatable(
"Got ErrorResponse.UNKNOWN_USER while trying to " +
"retrieve member! Weird user. User: %s",
I18nUtilities.get("hashing.discord_direct_message.error.log_unknown_user"),
Component.text(message.getAuthor().toString())
)
);
@@ -102,12 +119,12 @@ public class DirectMessageEventHandler extends ListenerAdapter {
}
private void sendHash (final TrustLevel trustLevel, final Message message, final Member member) {
final String result = HashingPlugin.generateDiscordHash(member.getIdLong(), trustLevel);
final String result = HashingUtilities.generateDiscordHash(member.getIdLong(), trustLevel);
message
.reply(
String.format(
"Hash for %s trust level: **%s**",
I18nUtilities.get("hashing.discord_direct_message.hash_generated"),
trustLevel,
result
)
@@ -117,11 +134,40 @@ public class DirectMessageEventHandler extends ListenerAdapter {
LoggerUtilities.log(
LogType.DISCORD,
Component.translatable(
"Generated hash %s (%s) for user %s",
I18nUtilities.get("hashing.discord_direct_message.hash_generated.log"),
Component.text(result),
Component.text(trustLevel.toString()),
Component.text(member.getEffectiveName())
)
);
}
private void sendKey (final TrustLevel trustLevel, final Message message, final Member member, final boolean force) {
try {
final String generatedKey = HashingUtilities.KEY_MANAGER.generate(
trustLevel,
member.getId(),
force,
String.format(
// long ahh
I18nUtilities.get("hashing.discord_direct_message.error.key_for_trust_level_already_exists"),
trustLevel,
FORCED_KEY_MESSAGE
)
);
message
.reply(
String.format(
I18nUtilities.get("hashing.discord_direct_message.key_generated"),
trustLevel,
generatedKey
)
)
.queue();
} catch (final IllegalStateException e) {
message.reply(e.getMessage()).queue();
}
}
}

View File

@@ -11,6 +11,7 @@ import me.chayapak1.chomens_bot.commands.*;
import me.chayapak1.chomens_bot.data.chat.ChatPacketType;
import me.chayapak1.chomens_bot.data.listener.Listener;
import me.chayapak1.chomens_bot.util.ExceptionUtilities;
import me.chayapak1.chomens_bot.util.HashingUtilities;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Role;
import net.kyori.adventure.text.Component;
@@ -215,7 +216,7 @@ public class CommandHandlerPlugin implements Listener {
context.trustLevel = authenticatedTrustLevel;
} else {
final TrustLevel userTrustLevel = bot.hashing.getTrustLevel(userHash, splitInput[0], context.sender);
final TrustLevel userTrustLevel = HashingUtilities.getTrustLevel(userHash, splitInput[0], context.sender);
if (trustLevel.level > userTrustLevel.level) {
context.sendOutput(

View File

@@ -1,112 +0,0 @@
package me.chayapak1.chomens_bot.plugins;
import com.google.common.hash.Hashing;
import me.chayapak1.chomens_bot.Bot;
import me.chayapak1.chomens_bot.command.TrustLevel;
import me.chayapak1.chomens_bot.data.player.PlayerEntry;
import me.chayapak1.chomens_bot.util.RandomStringUtilities;
import org.apache.commons.lang3.tuple.Pair;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class HashingPlugin {
public static final Map<Long, Pair<TrustLevel, String>> discordHashes = new ConcurrentHashMap<>();
private final Bot bot;
public HashingPlugin (final Bot bot) {
this.bot = bot;
}
public String getHash (final String prefix, final PlayerEntry sender, final boolean sectionSigns) { return getGenericHash(bot.config.keys.trustedKey, prefix, sender, sectionSigns); }
public String getAdminHash (final String prefix, final PlayerEntry sender, final boolean sectionSigns) { return getGenericHash(bot.config.keys.adminKey, prefix, sender, sectionSigns); }
public String getOwnerHash (final String prefix, final PlayerEntry sender, final boolean sectionSigns) { return getGenericHash(bot.config.keys.ownerKey, prefix, sender, sectionSigns); }
// should this be public?
public String getGenericHash (final String key, final String prefix, final PlayerEntry sender, final boolean sectionSigns) {
final long time = System.currentTimeMillis() / 5_000;
final String hashValue = sender.profile.getIdAsString() + prefix + time + key;
final String hash = Hashing.sha256()
.hashString(hashValue, StandardCharsets.UTF_8)
.toString()
.substring(0, 16);
return sectionSigns ?
String.join("",
Arrays.stream(hash.split(""))
.map((letter) -> "§" + letter)
.toArray(String[]::new)
) :
hash;
}
private boolean checkHash (final String hash, String input) {
// removes reset section sign
if (input.length() == (16 * 2 /* <-- don't forget, we have the section signs */) + 2 && input.endsWith("§r"))
input = input.substring(0, input.length() - 2);
return input.equals(hash);
}
public boolean isCorrectHash (final String input, final String prefix, final PlayerEntry sender) {
return checkHash(getHash(prefix, sender, true), input) ||
checkHash(getHash(prefix, sender, false), input);
}
public boolean isCorrectAdminHash (final String input, final String prefix, final PlayerEntry sender) {
return checkHash(getAdminHash(prefix, sender, true), input) ||
checkHash(getAdminHash(prefix, sender, false), input);
}
public boolean isCorrectOwnerHash (final String input, final String prefix, final PlayerEntry sender) {
return checkHash(getOwnerHash(prefix, sender, true), input) ||
checkHash(getOwnerHash(prefix, sender, false), input);
}
public boolean isCorrectDiscordHash (final String input) {
for (final Pair<TrustLevel, String> pair : discordHashes.values()) {
if (checkHash(pair.getRight(), input)) return true;
}
return false;
}
public TrustLevel getDiscordHashTrustLevel (final String input) {
for (final Map.Entry<Long, Pair<TrustLevel, String>> entry : new ArrayList<>(discordHashes.entrySet())) {
final Pair<TrustLevel, String> pair = entry.getValue();
if (!pair.getRight().equals(input)) continue;
discordHashes.remove(entry.getKey());
return pair.getLeft();
}
return TrustLevel.PUBLIC;
}
public TrustLevel getTrustLevel (final String input, final String prefix, final PlayerEntry sender) {
if (isCorrectOwnerHash(input, prefix, sender)) return TrustLevel.OWNER;
else if (isCorrectAdminHash(input, prefix, sender)) return TrustLevel.ADMIN;
else if (isCorrectHash(input, prefix, sender)) return TrustLevel.TRUSTED;
else if (isCorrectDiscordHash(input)) return getDiscordHashTrustLevel(input);
else return TrustLevel.PUBLIC;
}
public static String generateDiscordHash (final long userId, final TrustLevel trustLevel) {
// i wouldn't say it's a hash, it's just a random string
final String string = RandomStringUtilities.generate(16);
discordHashes.putIfAbsent(userId, Pair.of(trustLevel, string));
return discordHashes.get(userId).getRight();
}
}

View File

@@ -0,0 +1,235 @@
package me.chayapak1.chomens_bot.util;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.hash.Hashing;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.chayapak1.chomens_bot.Main;
import me.chayapak1.chomens_bot.command.TrustLevel;
import me.chayapak1.chomens_bot.data.keys.Key;
import me.chayapak1.chomens_bot.data.keys.KeysData;
import me.chayapak1.chomens_bot.data.player.PlayerEntry;
import org.apache.commons.lang3.tuple.Pair;
import javax.annotation.Nullable;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class HashingUtilities {
public static final KeyManager KEY_MANAGER = new KeyManager();
public static final Map<Long, Pair<TrustLevel, String>> discordHashes = new ConcurrentHashMap<>();
public static void init () {
}
public static String getHash (final String key, final String prefix, final PlayerEntry sender) {
final long time = System.currentTimeMillis() / 5_000;
final String hashInput = sender.profile.getIdAsString() + prefix + time + key;
return Hashing.sha256()
.hashString(hashInput, StandardCharsets.UTF_8)
.toString()
.substring(0, 16);
}
private static String getFixedHashInput (final String input) {
String sanitizedInput = input;
// removes reset section sign
if (input.length() == (16 * 2) + 2 && input.endsWith("§r"))
sanitizedInput = input.substring(0, input.length() - 2);
// removes all the section signs
sanitizedInput = sanitizedInput.replace("§", "");
return sanitizedInput;
}
public static TrustLevel getPlayerHashTrustLevel (
final String input,
final String prefix,
final PlayerEntry sender
) {
final String fixedInput = getFixedHashInput(input);
final List<KeysData> keys = KEY_MANAGER.keys;
synchronized (keys) {
for (final KeysData keysData : keys) {
for (final Key keyObject : keysData.keys()) {
final String hashed = getHash(keyObject.key(), prefix, sender);
if (fixedInput.equals(hashed)) return keyObject.trustLevel();
}
}
}
// not TrustLevel.PUBLIC !
return null;
}
public static boolean isCorrectDiscordHash (final String input) {
final String fixedInput = getFixedHashInput(input);
for (final Pair<TrustLevel, String> pair : discordHashes.values()) {
if (pair.getValue().equals(fixedInput)) return true;
}
return false;
}
public static TrustLevel getDiscordHashTrustLevel (final String input) {
for (final Map.Entry<Long, Pair<TrustLevel, String>> entry : new ArrayList<>(discordHashes.entrySet())) {
final Pair<TrustLevel, String> pair = entry.getValue();
if (!pair.getRight().equals(input)) continue;
discordHashes.remove(entry.getKey());
return pair.getLeft();
}
return TrustLevel.PUBLIC;
}
public static TrustLevel getTrustLevel (final String input, final String prefix, final PlayerEntry sender) {
final TrustLevel playerHashTrustLevel = getPlayerHashTrustLevel(input, prefix, sender);
if (playerHashTrustLevel != null) return playerHashTrustLevel;
else if (isCorrectDiscordHash(input)) return getDiscordHashTrustLevel(input);
else return TrustLevel.PUBLIC;
}
public static String generateDiscordHash (final long userId, final TrustLevel trustLevel) {
// i wouldn't say it's a hash, it's just a random string
final String string = RandomStringUtilities.generate(16);
discordHashes.putIfAbsent(userId, Pair.of(trustLevel, string));
return discordHashes.get(userId).getRight();
}
public static class KeyManager {
private static final Path KEY_PATH = Path.of("keys.json");
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public List<KeysData> keys = null;
public KeyManager () {
try {
initialLoad();
} catch (final IOException e) {
LoggerUtilities.error("Failed to load the keys!");
LoggerUtilities.error(e);
return;
}
Main.EXECUTOR.scheduleAtFixedRate(this::write, 1, 1, TimeUnit.MINUTES);
Runtime.getRuntime().addShutdownHook(new Thread(this::write));
}
public @Nullable String generate (
final TrustLevel level,
final String userId,
final boolean force,
final String alreadyExistsMessage // so it's flexible
) throws IllegalStateException {
if (keys == null) return null;
// is this useless? although it prevents the synchronization on non-final field warning by IDEA
final List<KeysData> keys = this.keys;
KeysData data = null;
synchronized (keys) {
for (final KeysData keysData : keys) {
if (keysData.userId().equals(userId)) {
data = keysData;
break;
}
}
}
final String generatedKey = RandomStringUtilities.generate(48);
if (data == null) {
data = new KeysData(new ArrayList<>(), userId);
data.keys().add(
new Key(
level,
generatedKey,
System.currentTimeMillis()
)
);
keys.add(data);
} else {
for (final Key key : new ArrayList<>(data.keys())) {
if (!key.trustLevel().equals(level)) continue;
if (!force) throw new IllegalStateException(alreadyExistsMessage);
data.keys().remove(key);
}
data.keys().add(
new Key(
level,
generatedKey,
System.currentTimeMillis()
)
);
}
write();
return generatedKey;
}
private void initialLoad () throws IOException {
if (Files.exists(KEY_PATH)) {
try (final BufferedReader reader = Files.newBufferedReader(KEY_PATH)) {
keys = Collections.synchronizedList(
OBJECT_MAPPER.readValue(
reader,
OBJECT_MAPPER
.getTypeFactory()
.constructCollectionType(ObjectArrayList.class, KeysData.class)
)
);
}
} else {
Files.createFile(KEY_PATH);
keys = Collections.synchronizedList(new ObjectArrayList<>());
}
}
private void write () {
try (
final BufferedWriter writer = Files.newBufferedWriter(
KEY_PATH,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
) {
writer.write(OBJECT_MAPPER.writeValueAsString(keys));
} catch (final IOException e) {
LoggerUtilities.error("Failed to write the keys file!");
LoggerUtilities.error(e);
}
}
}
}