feat: websocket with events!!!

This commit is contained in:
TheClashFruit 2024-04-28 21:26:34 +02:00
parent 836d9f62ee
commit db42422328
Signed by: TheClashFruit
GPG key ID: 09BB24C34C2F3204
15 changed files with 381 additions and 13 deletions

View file

@ -2,12 +2,17 @@ package me.theclashfruit.crss;
import me.theclashfruit.crss.backend.error.GoneServlet;
import me.theclashfruit.crss.backend.IndexServlet;
import me.theclashfruit.crss.backend.error.NotFoundServlet;
import me.theclashfruit.crss.backend.v2.StatusServlet;
import me.theclashfruit.crss.backend.v2.gateway.GatewayServlet;
import me.theclashfruit.crss.listeners.PlayerListener;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
import org.eclipse.jetty.ee10.websocket.server.config.JettyWebSocketServletContainerInitializer;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.websocket.core.server.WebSocketUpgradeHandler;
public class CRSSPlugin extends JavaPlugin {
Server server = new Server(25580);
@ -20,26 +25,42 @@ public class CRSSPlugin extends JavaPlugin {
saveDefaultConfig();
PluginManager pluginManager = getServer().getPluginManager();
context.setContextPath("/api");
context.addServlet(IndexServlet.class, "/");
// v2 api
context.addServlet(StatusServlet.class, "/v2/status");
context.addServlet(GatewayServlet.class, "/v2/gateway");
// errors
context.addServlet(GoneServlet.class, "/v1/*");
JettyWebSocketServletContainerInitializer.configure(context, null);
try {
server.setHandler(context);
server.addConnector(connector);
server.start();
} catch (Exception e) {
getLogger().severe(e.getMessage());
getLogger().throwing(this.getName(), "onEnable", e);
}
pluginManager.registerEvents(new PlayerListener(), this);
}
@Override
public void onDisable() {
getLogger().info("Plugin disabled!");
try {
server.stop();
} catch (Exception e) {
getLogger().throwing(this.getName(), "onDisable", e);
}
}
}

View file

@ -4,6 +4,7 @@ import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import me.theclashfruit.crss.models.ErrorResponse;
import java.io.IOException;
@ -13,7 +14,14 @@ public class GoneServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
res.setContentType("application/json");
ErrorResponse error = new ErrorResponse(
410,
"This version of the API is no longer available."
);
String resJson = error.toJson();
res.setStatus(410);
res.getWriter().println("{\"error\":410,\"message\":\"This version of the API is no longer available.\"}");
res.getWriter().println(resJson);
}
}

View file

@ -4,6 +4,7 @@ import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import me.theclashfruit.crss.models.ErrorResponse;
import java.io.IOException;
@ -13,7 +14,14 @@ public class NotFoundServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
res.setContentType("application/json");
ErrorResponse error = new ErrorResponse(
404,
"This endpoint does not exist."
);
String resJson = error.toJson();
res.setStatus(404);
res.getWriter().println("{\"error\":404,\"message\":\"This endpoint does not exist.\"}");
res.getWriter().println(resJson);
}
}

View file

@ -0,0 +1,30 @@
package me.theclashfruit.crss.backend.v2;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import me.theclashfruit.crss.models.StatusResponse;
import org.bukkit.Bukkit;
import org.bukkit.World;
import java.io.IOException;
@WebServlet(name = "Status API")
public class StatusServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
res.setContentType("application/json");
StatusResponse response = new StatusResponse(
Bukkit.getBukkitVersion(),
Bukkit.getOnlinePlayers().toArray().length,
Bukkit.getWorlds().stream().map(World::getName).toArray(String[]::new)
);
String resJson = response.toJson();
res.setStatus(200);
res.getWriter().println(resJson);
}
}

View file

@ -0,0 +1,13 @@
package me.theclashfruit.crss.backend.v2.gateway;
import jakarta.servlet.annotation.WebServlet;
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServlet;
import org.eclipse.jetty.ee10.websocket.server.JettyWebSocketServletFactory;
@WebServlet(name = "Gateway WebSocket API")
public class GatewayServlet extends JettyWebSocketServlet {
@Override
protected void configure(JettyWebSocketServletFactory factory) {
factory.register(GatewaySocket.class);
}
}

View file

@ -0,0 +1,86 @@
package me.theclashfruit.crss.backend.v2.gateway;
import me.theclashfruit.crss.enums.Channels;
import me.theclashfruit.crss.models.ErrorResponse;
import me.theclashfruit.crss.models.SocketReqMessage;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import static org.bukkit.Bukkit.getLogger;
@WebSocket
public class GatewaySocket {
private static final Set<GatewaySocket> gatewayConnections = new CopyOnWriteArraySet<>();
private Session session;
private final ArrayList<Channels> subscriptions = new ArrayList<>();
@OnWebSocketOpen
public void onOpen(Session session) {
this.session = session;
gatewayConnections.add(this);
}
@OnWebSocketClose
public void onClose(int statusCode, String reason) {
gatewayConnections.remove(this);
}
@OnWebSocketMessage
public void onMessage(String message) {
try {
SocketReqMessage reqMessage = (SocketReqMessage) SocketReqMessage.fromJson(message, SocketReqMessage.class);
if (Objects.equals(reqMessage.getType(), "subscribe")) {
for (String channel : reqMessage.getChannels()) {
this.subscriptions.add(Channels.getChannel(channel));
}
}
} catch (Exception e) {
getLogger().throwing(GatewaySocket.class.getName(), "onMessage", e);
String[] stackTrace = Arrays.stream(e.getStackTrace()).map(StackTraceElement::toString).toArray(String[]::new);
StringBuilder stackTraceString = new StringBuilder();
for (String stackTraceElement : stackTrace) {
stackTraceString
.append(stackTraceElement)
.append("\n");
}
ErrorResponse error = new ErrorResponse(
1011,
stackTraceString.toString()
);
this.session.sendText(error.toJson(), null);
}
}
public static void broadcastToChannel(Channels channel, String message) {
gatewayConnections.stream()
.filter(gateway -> gateway.subscriptions != null)
.filter(gateway -> {
for (Channels subscription : gateway.subscriptions) {
if (subscription.equals(channel)) {
return true;
}
}
return false;
})
.forEach(gateway -> {
gateway.session.sendText(message, null);
});
}
}

View file

@ -0,0 +1,15 @@
package me.theclashfruit.crss.enums;
public enum Channels {
PLAYER_MOVE,
PLAYER_JOIN,
PLAYER_LEAVE,
PLAYER_CHAT,
PLAYER_DEATH;
// TODO: add more
public static Channels getChannel(String channel) {
return valueOf(channel.toUpperCase());
}
}

View file

@ -0,0 +1,74 @@
package me.theclashfruit.crss.listeners;
import me.theclashfruit.crss.backend.v2.gateway.GatewaySocket;
import me.theclashfruit.crss.enums.Channels;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
import org.bukkit.event.player.PlayerMoveEvent;
import org.bukkit.event.player.PlayerQuitEvent;
public class PlayerListener implements Listener {
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
String msg = "{" +
"\"type\":\"player_join\"," +
"\"player\":{" +
"\"name\":\"" + player.getName() + "\"," +
"\"uuid\":\"" + player.getUniqueId() + "\"" +
"}" +
"}";
GatewaySocket.broadcastToChannel(Channels.PLAYER_JOIN, msg);
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
Player player = event.getPlayer();
String msg = "{" +
"\"type\":\"player_leave\"," +
"\"player\":{" +
"\"name\":\"" + player.getName() + "\"," +
"\"uuid\":\"" + player.getUniqueId() + "\"" +
"}" +
"}";
GatewaySocket.broadcastToChannel(Channels.PLAYER_LEAVE, msg);
}
@EventHandler
public void onPlayerMove(PlayerMoveEvent event) {
Player player = event.getPlayer();
// too lazy to make classes for these lmao
String msg = "{" +
"\"type\":\"player_move\"," +
"\"player\":{" +
"\"name\":\"" + player.getName() + "\"," +
"\"uuid\":\"" + player.getUniqueId() + "\"" +
"}," +
"\"position\":{" +
"\"distance\":" + event.getFrom().distance(event.getTo()) + "," +
"\"from\":{" +
"\"world\":\"" + event.getFrom().getWorld().getName() + "\"," +
"\"x\":" + event.getFrom().getX() + "," +
"\"y\":" + event.getFrom().getY() + "," +
"\"z\":" + event.getFrom().getZ() +
"}," +
"\"to\":{" +
"\"world\":\"" + event.getTo().getWorld().getName() + "\"," +
"\"x\":" + event.getTo().getX() + "," +
"\"y\":" + event.getTo().getY() + "," +
"\"z\":" + event.getTo().getZ() +
"}" +
"}" +
"}";
if (event.getFrom().distance(event.getTo()) != 0)
GatewaySocket.broadcastToChannel(Channels.PLAYER_MOVE, msg);
}
}

View file

@ -0,0 +1,27 @@
package me.theclashfruit.crss.models;
public class ErrorResponse extends Model {
private Integer error;
private String message;
public ErrorResponse(Integer error, String message) {
this.error = error;
this.message = message;
}
public Integer getError() {
return error;
}
public void setError(Integer error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}

View file

@ -2,21 +2,21 @@ package me.theclashfruit.crss.models;
import java.util.ArrayList;
public class IndexResponse {
private String latest_version;
public class IndexResponse extends Model {
private String latestVersion;
public ArrayList<Version> versions;
public IndexResponse(String latest_version, ArrayList<Version> versions) {
this.latest_version = latest_version;
public IndexResponse(String latestVersion, ArrayList<Version> versions) {
this.latestVersion = latestVersion;
this.versions = versions;
}
public String getLatestVersion() {
return latest_version;
return latestVersion;
}
public void setLatestVersion(String latest_version) {
this.latest_version = latest_version;
public void setLatestVersion(String latestVersion) {
this.latestVersion = latestVersion;
}
public ArrayList<Version> getVersions() {

View file

@ -0,0 +1,13 @@
package me.theclashfruit.crss.models;
import me.theclashfruit.crss.utils.JsonUtil;
public class Model {
public String toJson() {
return JsonUtil.gson.toJson(this);
}
public static Object fromJson(String json, Class<? extends Model> clazz) {
return JsonUtil.gson.fromJson(json, clazz);
}
}

View file

@ -0,0 +1,32 @@
package me.theclashfruit.crss.models;
import me.theclashfruit.crss.enums.Channels;
import java.util.ArrayList;
import java.util.stream.Collectors;
public class SocketReqMessage extends Model {
private String type;
private String[] channels;
public SocketReqMessage(String type, String[] channels) {
this.type = type;
this.channels = channels;
}
public String getType() {
return type;
}
public String[] getChannels() {
return channels;
}
public void setType(String type) {
this.type = type;
}
public void setChannels(String[] channels) {
this.channels = channels;
}
}

View file

@ -0,0 +1,37 @@
package me.theclashfruit.crss.models;
public class StatusResponse extends Model {
private String version;
private Integer online;
private String[] worlds;
public StatusResponse(String version, Integer online, String[] worlds) {
this.version = version;
this.online = online;
this.worlds = worlds;
}
public String getVersion() {
return version;
}
public Integer getOnline() {
return online;
}
public String[] getWorlds() {
return worlds;
}
public void setVersion(String version) {
this.version = version;
}
public void setOnline(Integer online) {
this.online = online;
}
public void setWorlds(String[] worlds) {
this.worlds = worlds;
}
}

View file

@ -1,6 +1,6 @@
package me.theclashfruit.crss.models;
public class Version {
public class Version extends Model {
private String name;
private String path;
private Boolean deprecated;

View file

@ -1,7 +1,11 @@
package me.theclashfruit.crss.utils;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
public class JsonUtil {
public static Gson gson = new Gson();
public static Gson gson = new GsonBuilder()
.setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
.create();
}