Skip to content
Draft
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
16 changes: 16 additions & 0 deletions zander-velocity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Zander Velocity

Zander Velocity formats network-wide chat directly on the proxy. To surface readable rank data, ensure each LuckPerms group defines the following metadata:

```
lp group admin meta set displayname "Admin"
lp group admin meta set rank_description "Has full access to staff & server tools."
```

`displayname` appears in the hover title (falling back to the prefix text or `Member`), and `rank_description` appears in the hover description (falling back to `No description set for this rank.`). The proxy resolves the highest-priority prefix for each player and renders chat as:

```
[Rank] Username: message
```

The bracketed rank prefix is hoverable and shows the rank name and description.
11 changes: 11 additions & 0 deletions zander-velocity/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@
<version>3.4.0-SNAPSHOT</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.luckperms</groupId>
<artifactId>api</artifactId>
<version>5.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>net.kyori</groupId>
<artifactId>adventure-text-minimessage</artifactId>
<version>4.17.0</version>
</dependency>
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
name = "zander-velocity",
version = "1.2.0",
dependencies = {
@Dependency(id = "signedvelocity")
@Dependency(id = "signedvelocity"),
@Dependency(id = "luckperms")
}
)
public class ZanderVelocityMain {
Expand Down Expand Up @@ -113,6 +114,5 @@ public ZanderVelocityMain(
container.ifPresent(pluginContainer -> pluginContainer.getExecutorService().shutdown());
}

logger.info("Zander Proxy has started.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.velocitypowered.api.command.SimpleCommand;
import dev.dejvokep.boostedyaml.route.Route;
import io.github.ModularEnigma.Request;
import io.github.ModularEnigma.Response;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
Expand Down Expand Up @@ -38,7 +37,6 @@ public void execute(final Invocation invocation) {
} catch (Exception e) {
Component builder = Component.text("An error has occurred. Is the API down?").color(NamedTextColor.RED);
source.sendMessage(builder);
System.out.println(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ public void execute(Invocation invocation) {

} catch (Exception e) {
player.sendMessage(Component.text("An error has occurred. Is the API down?").color(NamedTextColor.RED));
System.out.println(e);
}
} else {
source.sendMessage(Component.text("Only players can use this command.").color(NamedTextColor.RED));
Expand All @@ -112,4 +111,4 @@ public List<String> suggest(Invocation invocation) {

return completions;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.velocitypowered.api.command.SimpleCommand;
import dev.dejvokep.boostedyaml.route.Route;
import io.github.ModularEnigma.Request;
import io.github.ModularEnigma.Response;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
Expand Down Expand Up @@ -37,7 +36,6 @@ public void execute(final Invocation invocation) {
} catch (Exception e) {
Component builder = Component.text("An error has occurred. Is the API down?").color(NamedTextColor.RED);
source.sendMessage(builder);
System.out.println(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.velocitypowered.api.command.SimpleCommand;
import dev.dejvokep.boostedyaml.route.Route;
import io.github.ModularEnigma.Request;
import io.github.ModularEnigma.Response;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
Expand Down Expand Up @@ -37,7 +36,6 @@ public void execute(final Invocation invocation) {
} catch (Exception e) {
Component builder = Component.text("An error has occurred. Is the API down?").color(NamedTextColor.RED);
source.sendMessage(builder);
System.out.println(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
package org.modularsoft.zander.velocity.events;

import com.jayway.jsonpath.JsonPath;
import com.velocitypowered.api.event.PostOrder;
import com.velocitypowered.api.event.Subscribe;
import com.velocitypowered.api.event.player.PlayerChatEvent;
import com.velocitypowered.api.proxy.Player;
import dev.dejvokep.boostedyaml.route.Route;
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 net.luckperms.api.LuckPerms;
import net.luckperms.api.LuckPermsProvider;
import net.luckperms.api.cacheddata.CachedMetaData;
import net.luckperms.api.model.user.User;
import net.luckperms.api.node.NodeType;
import net.luckperms.api.node.types.MetaNode;
import org.modularsoft.zander.velocity.ZanderVelocityMain;
import org.modularsoft.zander.velocity.model.Filter;
import org.modularsoft.zander.velocity.model.discord.DiscordChat;

public class UserChatEvent {

@Subscribe
private final LuckPerms luckPerms = LuckPermsProvider.get();
private final MiniMessage miniMessage = MiniMessage.miniMessage();

@Subscribe(order = PostOrder.LAST)
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"));

// Filter out commands.
if (event.getMessage().startsWith("/")) return;
if (rawMessage.startsWith("/")) return;

// Check chat for blocked content
try {
Filter phrase = Filter.builder()
.content(event.getMessage().toString())
.content(rawMessage)
.build();

Request phraseReq = Request.builder()
Expand All @@ -43,17 +58,16 @@ public void UserChatEvent(PlayerChatEvent event) {
Boolean success = JsonPath.parse(phraseJson).read("$.success");
String phraseCaughtMessage = JsonPath.read(phraseJson, "$.message");

ZanderVelocityMain.getLogger().info("[FILTER] Response (" + phraseRes.getStatusCode() + "): " + phraseRes.getBody());

if (!success) {
Component builder = Component.text(phraseCaughtMessage).color(NamedTextColor.RED);
player.sendMessage(builder);
event.setResult(PlayerChatEvent.ChatResult.denied());
return;
} else {
DiscordChat chat = DiscordChat.builder()
.username(player.getUsername())
.server(player.getCurrentServer().get().getServer().getServerInfo().getName())
.content(event.getMessage().toString())
.content(rawMessage)
.build();

Request discordChatReq = Request.builder()
Expand All @@ -64,12 +78,110 @@ public void UserChatEvent(PlayerChatEvent event) {
.build();

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);
System.out.println(e);
}
}

private Component formatChatMessage(Player player, Component originalMessage) {
User user = luckPerms.getPlayerAdapter(Player.class).getUser(player);
CachedMetaData metaData = user.getCachedData().getMetaData();
Component rankPrefix = buildRankPrefix(user, metaData);

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

private Component buildRankPrefix(User user, CachedMetaData metaData) {
String prefix = metaData.getPrefix();
String rankNameMeta = getMetaValue(user, metaData, "displayname");
String rankDescriptionMeta = getMetaValue(user, 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(User user, CachedMetaData metaData, String baseKey) {
MetaNode bestNode = null;
for (MetaNode node : user.getNodes(NodeType.META)) {
String key = node.getMetaKey();
if (matchesMetaKey(key, baseKey)) {
if (bestNode == null || node.getPriority() > bestNode.getPriority()) {
bestNode = node;
}
}
}

if (bestNode != null) {
String value = bestNode.getMetaValue();
if (value != null && !value.isBlank()) {
return value;
}
}

return metaData.getMetaValue(baseKey);
}

private boolean matchesMetaKey(String key, String baseKey) {
if (key == null) {
return false;
}
if (key.equalsIgnoreCase(baseKey)) {
return true;
}
return key.regionMatches(true, 0, baseKey + ".", 0, baseKey.length() + 1);
}

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("&.", "");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.velocitypowered.api.proxy.Player;
import dev.dejvokep.boostedyaml.route.Route;
import io.github.ModularEnigma.Request;
import io.github.ModularEnigma.Response;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.modularsoft.zander.velocity.ZanderVelocityMain;
Expand All @@ -25,8 +24,6 @@ public void onPlayerCommand(CommandExecuteEvent event) {
String APIKey = ZanderVelocityMain.getConfig().getString(Route.from("APIKey"));
String command = event.getCommand(); // Get the full command

ZanderVelocityMain.getLogger().info("Command: {}", command);

// Check if the command is one we need to log (ignore direct message commands)
if (command.startsWith("msg") || command.startsWith("tell") || command.startsWith("w")
|| command.startsWith("message") || command.startsWith("r")) {
Expand All @@ -50,12 +47,10 @@ public void onPlayerCommand(CommandExecuteEvent event) {
.setRequestBody(commandSpy.toString())
.build();

Response commandSpyRes = commandSpyReq.execute();
ZanderVelocityMain.getLogger().info("Response (" + commandSpyRes.getStatusCode() + "): " + commandSpyRes.getBody());
commandSpyReq.execute();
} catch (Exception e) {
Component builder = Component.text("An error has occurred. Is the API down?").color(NamedTextColor.RED);
player.disconnect(builder);
System.out.println(e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ public void onProxyPingEvent(ProxyPingEvent event) {
pingBuilder.description(serverPingDescription);

} catch (Exception e) {
System.out.print(e);

// Fallback MOTD in case of an exception
String motdTopLine = ZanderVelocityMain.getConfig().getString(Route.from("announcementMOTDTopLine"));
Component fallbackDescription = LegacyComponentSerializer.builder()
Expand All @@ -68,4 +66,4 @@ public void onProxyPingEvent(ProxyPingEvent event) {
// Set the modified ServerPing back to the event
event.setPing(pingBuilder.build());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import com.velocitypowered.api.proxy.ServerConnection;
import dev.dejvokep.boostedyaml.route.Route;
import io.github.ModularEnigma.Request;
import io.github.ModularEnigma.Response;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import org.modularsoft.zander.velocity.ZanderVelocityMain;
Expand All @@ -30,8 +29,6 @@ public void onUserChatDMEvent(CommandExecuteEvent event) {
Player player = (Player) event.getCommandSource();
String command = event.getCommand();

logger.info("Command: {}", command);

// Check if the command is a direct message command
if (command.contains("msg") || command.contains("tell") || command.contains("w")
|| command.contains("message") || command.contains("r")) {
Expand Down Expand Up @@ -76,13 +73,12 @@ public void onUserChatDMEvent(CommandExecuteEvent event) {
.setRequestBody(socialSpy.toString()) // Ensure proper serialization to JSON
.build();

Response socialSpyRes = socialSpyReq.execute();
logger.info("Social Spy Response ({}): {}", socialSpyRes.getStatusCode(), socialSpyRes.getBody());
socialSpyReq.execute();

} catch (Exception e) {
logger.error("Error occurred while handling social spy request", e);
player.sendMessage(Component.text("An error occurred, but you can continue playing.").color(NamedTextColor.RED));
}
}
}
}
}
Loading