Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions zander-hub/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,6 @@
<artifactId>zander-hub</artifactId>
<version>1.0</version>

<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
</build>

<repositories>
<repository>
<id>papermc</id>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,17 @@
import io.github.ModularEnigma.Request;
import io.github.ModularEnigma.Response;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer;
import org.modularsoft.zander.velocity.ZanderVelocityMain;
import org.modularsoft.zander.velocity.model.Filter;
import org.modularsoft.zander.velocity.model.discord.DiscordChat;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Optional;

public class UserChatEvent {

private final MiniMessage miniMessage = MiniMessage.miniMessage();

@Subscribe
public void UserChatEvent(PlayerChatEvent event) {
Player player = event.getPlayer();
String rawMessage = event.getMessage();
Component originalMessage = Component.text(rawMessage);
String BaseAPIURL = ZanderVelocityMain.getConfig().getString(Route.from("BaseAPIURL"));
String APIKey = ZanderVelocityMain.getConfig().getString(Route.from("APIKey"));

Expand Down Expand Up @@ -79,182 +68,11 @@ public void UserChatEvent(PlayerChatEvent event) {
Response discordChatReqRes = discordChatReq.execute();
ZanderVelocityMain.getLogger().info("Response (" + discordChatReqRes.getStatusCode() + "): " + discordChatReqRes.getBody());
}

Component formattedMessage = formatChatMessage(player, originalMessage);
player.getCurrentServer()
.map(serverConnection -> serverConnection.getServer())
.ifPresentOrElse(
server -> server.getPlayersConnected().forEach(target -> target.sendMessage(formattedMessage)),
() -> player.sendMessage(formattedMessage)
);
event.setResult(PlayerChatEvent.ChatResult.denied());
} catch (Exception e) {
Component builder = Component.text("The chat filter could not be reached at this time, there maybe an issue with the API.").color(NamedTextColor.YELLOW);
player.sendMessage(builder);
event.setResult(PlayerChatEvent.ChatResult.denied());
ZanderVelocityMain.getLogger().error("Chat filter error for player {}", player.getUsername(), e);
}
}

private Component formatChatMessage(Player player, Component originalMessage) {
LuckPermsMeta metaData = resolveLuckPermsMeta(player).orElse(null);
Component rankPrefix = buildRankPrefix(metaData);

return Component.text()
.append(rankPrefix)
.append(Component.space())
.append(Component.text(player.getUsername()))
.append(Component.text(": "))
.append(originalMessage)
.build();
}

private Component buildRankPrefix(LuckPermsMeta metaData) {
String prefix = metaData != null ? metaData.prefix : null;
String rankNameMeta = getMetaValue(metaData, "displayname");
String rankDescriptionMeta = getMetaValue(metaData, "rank_description");

String rankName = (rankNameMeta != null && !rankNameMeta.isBlank())
? rankNameMeta
: (prefix != null && !prefix.isBlank() ? prefix : "Member");
String rankDescription = (rankDescriptionMeta != null && !rankDescriptionMeta.isBlank())
? rankDescriptionMeta
: "No description set for this rank.";

rankName = stripLegacy(rankName);
rankDescription = stripLegacy(rankDescription);

Component prefixComponent = buildPrefixComponent(prefix, rankName);
Component hoverText = Component.text()
.append(prefixComponent)
.append(Component.space())
.append(Component.text(rankName).color(NamedTextColor.GOLD))
.append(Component.newline())
.append(Component.text(rankDescription).color(NamedTextColor.GRAY))
.build();

return prefixComponent.hoverEvent(HoverEvent.showText(hoverText));
}

private String getMetaValue(LuckPermsMeta metaData, String baseKey) {
if (metaData == null) {
return null;
}
String scopedKey = null;
if (metaData.primaryGroup != null && !metaData.primaryGroup.isBlank()) {
scopedKey = baseKey + "." + metaData.primaryGroup;
}
if (scopedKey != null) {
String scopedValue = metaData.metaValues.get(scopedKey);
if (scopedValue != null && !scopedValue.isBlank()) {
return scopedValue;
}
}
String directValue = metaData.metaValues.get(baseKey);
if (directValue != null && !directValue.isBlank()) {
return directValue;
}
return null;
}

private Component buildPrefixComponent(String prefix, String rankName) {
if (prefix != null && !prefix.isBlank()) {
return LegacyComponentSerializer.legacyAmpersand().deserialize(prefix);
}

String miniMessagePrefix = "<dark_gray>[</dark_gray><yellow>"
+ escapeMiniMessageContent(rankName)
+ "</yellow><dark_gray>]</dark_gray>";
return miniMessage.deserialize(miniMessagePrefix);
}

private String escapeMiniMessageContent(String input) {
return input.replace("<", "\\<").replace(">", "\\>");
}

private String stripLegacy(String input) {
return input.replaceAll("§.", "").replaceAll("&.", "");
}

private Optional<LuckPermsMeta> resolveLuckPermsMeta(Player player) {
try {
Class<?> providerClass = Class.forName("net.luckperms.api.LuckPermsProvider");
Object luckPerms = providerClass.getMethod("get").invoke(null);
Object playerAdapter = luckPerms.getClass().getMethod("getPlayerAdapter", Class.class)
.invoke(luckPerms, Player.class);
Object user = playerAdapter.getClass().getMethod("getUser", Player.class).invoke(playerAdapter, player);
Object metaData = playerAdapter.getClass().getMethod("getMetaData", Player.class).invoke(playerAdapter, player);

String prefix = null;
String primaryGroup = null;
Map<String, List<String>> metaMap = Map.of();

if (user != null) {
Method primaryGroupMethod = user.getClass().getMethod("getPrimaryGroup");
Object primaryGroupResult = primaryGroupMethod.invoke(user);
if (primaryGroupResult instanceof String) {
primaryGroup = (String) primaryGroupResult;
}
}

if (metaData != null) {
Method prefixMethod = metaData.getClass().getMethod("getPrefix");
Object prefixResult = prefixMethod.invoke(metaData);
if (prefixResult instanceof String) {
prefix = (String) prefixResult;
}
Method metaMethod = metaData.getClass().getMethod("getMeta");
Object metaResult = metaMethod.invoke(metaData);
if (metaResult instanceof Map<?, ?> rawMeta) {
metaMap = castMetaMap(rawMeta);
}
}

return Optional.of(new LuckPermsMeta(prefix, primaryGroup, flattenMeta(metaMap)));
} catch (ReflectiveOperationException | LinkageError ignored) {
return Optional.empty();
}
}

private Map<String, String> flattenMeta(Map<String, List<String>> metaMap) {
Map<String, String> flattened = new java.util.HashMap<>();
for (Map.Entry<String, List<String>> entry : metaMap.entrySet()) {
List<String> values = entry.getValue();
if (values == null) {
continue;
}
for (String value : values) {
if (value != null && !value.isBlank()) {
flattened.put(entry.getKey(), value);
break;
}
}
}
return flattened;
}

@SuppressWarnings("unchecked")
private Map<String, List<String>> castMetaMap(Map<?, ?> rawMeta) {
Map<String, List<String>> casted = new java.util.HashMap<>();
for (Map.Entry<?, ?> entry : rawMeta.entrySet()) {
Object key = entry.getKey();
Object value = entry.getValue();
if (key instanceof String && value instanceof List<?> listValue) {
casted.put((String) key, (List<String>) listValue);
}
}
return casted;
}

private static class LuckPermsMeta {
private final String prefix;
private final String primaryGroup;
private final Map<String, String> metaValues;

private LuckPermsMeta(String prefix, String primaryGroup, Map<String, String> metaValues) {
this.prefix = prefix;
this.primaryGroup = primaryGroup;
this.metaValues = metaValues;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,6 @@ public void UserLoginEvent (PostLoginEvent event) {
ZanderVelocityMain.getPrivateMessageService().updateNameCache(player.getUniqueId(), player.getUsername());

ZanderVelocityMain.getProxy().getScheduler().buildTask(ZanderVelocityMain.getInstance(), () -> {
String BaseAPIURL = ZanderVelocityMain.getConfig().getString(Route.from("BaseAPIURL"));
String APIKey = ZanderVelocityMain.getConfig().getString(Route.from("APIKey"));

try {
//
// Send User Creation API POST for new user
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,49 +8,60 @@
import net.kyori.adventure.text.format.NamedTextColor;
import org.modularsoft.zander.velocity.ZanderVelocityMain;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Heartbeat {
private static final int MAX_CONSECUTIVE_FAILURES = 3;
private static final int REQUEST_TIMEOUT_SECONDS = 10;

public static void startHeartbeatTask() {
String BaseAPIURL = ZanderVelocityMain.getConfig().getString(Route.from("BaseAPIURL"));
String APIKey = ZanderVelocityMain.getConfig().getString(Route.from("APIKey"));

AtomicInteger consecutiveFailures = new AtomicInteger(0);

ZanderVelocityMain.getProxy().getScheduler().buildTask(ZanderVelocityMain.getInstance(), () -> {
try {
// Your existing code here
// GET request to link to rules.
Request req = Request.builder()
.setURL(BaseAPIURL + "/heartbeat")
.setMethod(Request.Method.GET)
.addHeader("x-access-token", APIKey)
.build();

Response res = req.execute();
Response res = CompletableFuture.supplyAsync(req::execute)
.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS);

String json = res.getBody();
Boolean heartbeat = JsonPath.read(json, "$.success");
System.out.println("API Heartbeat Success");

// Check if the heartbeat is not successful
if (!heartbeat) {
// Kick all players
ZanderVelocityMain.getProxy().getAllPlayers().forEach(player -> {
Component message = Component.text("API Heartbeat Failed, the server is temporarily offline.")
.color(NamedTextColor.RED);
player.disconnect(message);
});

if (heartbeat) {
consecutiveFailures.set(0);
System.out.println("API Heartbeat Success");
} else {
int failures = consecutiveFailures.incrementAndGet();
ZanderVelocityMain.getLogger().warn("API Heartbeat returned failure ({}/{})", failures, MAX_CONSECUTIVE_FAILURES);
if (failures >= MAX_CONSECUTIVE_FAILURES) {
kickAllPlayers();
}
}
} catch (Exception e) {
// Handle exceptions here
e.printStackTrace();
ZanderVelocityMain.getLogger().error("API Heartbeat Failed, kicking all players until back online.");

// Kick all players
ZanderVelocityMain.getProxy().getAllPlayers().forEach(player -> {
Component message = Component.text("API Heartbeat Failed, the server is temporarily offline.")
.color(NamedTextColor.RED);
player.disconnect(message);
});
int failures = consecutiveFailures.incrementAndGet();
ZanderVelocityMain.getLogger().error("API Heartbeat error ({}/{}): {}", failures, MAX_CONSECUTIVE_FAILURES, e.getMessage());
if (failures >= MAX_CONSECUTIVE_FAILURES) {
ZanderVelocityMain.getLogger().error("API Heartbeat failed {} consecutive times, kicking all players.", MAX_CONSECUTIVE_FAILURES);
kickAllPlayers();
}
}
}).repeat(60, TimeUnit.SECONDS).schedule();
}
}

private static void kickAllPlayers() {
ZanderVelocityMain.getProxy().getAllPlayers().forEach(player -> {
Component message = Component.text("API Heartbeat Failed, the server is temporarily offline.")
.color(NamedTextColor.RED);
player.disconnect(message);
});
}
}
Loading