parent
68fe6c1ee4
commit
e41d54e1ed
15
README.md
15
README.md
|
@ -1,6 +1,19 @@
|
|||
# ServerAPI
|
||||
|
||||
TBD
|
||||
An API for your Minecraft server!
|
||||
|
||||
Use REST API or GraphQL.
|
||||
|
||||
## Available endpoints
|
||||
|
||||
All endpoints can optionally be password-secured via config.
|
||||
|
||||
* bans
|
||||
* players
|
||||
* query
|
||||
* tps
|
||||
* worlds
|
||||
* world (by name)
|
||||
|
||||
## License
|
||||
|
||||
|
|
15
pom.xml
15
pom.xml
|
@ -44,6 +44,21 @@
|
|||
<artifactId>javalin</artifactId>
|
||||
<version>3.9.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.javalin</groupId>
|
||||
<artifactId>javalin-graphql</artifactId>
|
||||
<version>3.9.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>1.7.30</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>2.10.3</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
|
|
@ -1,26 +1,59 @@
|
|||
package xyz.etztech.serverapi;
|
||||
|
||||
|
||||
import org.bukkit.OfflinePlayer;
|
||||
import org.bukkit.World;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import xyz.etztech.serverapi.commands.MainCommand;
|
||||
import xyz.etztech.serverapi.tps.TPS;
|
||||
import xyz.etztech.serverapi.web.IProvider;
|
||||
import xyz.etztech.serverapi.web.Web;
|
||||
import xyz.etztech.serverapi.web.api.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
public class ServerAPI extends JavaPlugin {
|
||||
public class ServerAPI extends JavaPlugin implements IProvider {
|
||||
|
||||
private static ServerAPI instance;
|
||||
private static TPS tps;
|
||||
private Web web = new Web(this);
|
||||
private final Logger log = Logger.getLogger( "Minecraft" );
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
instance = this;
|
||||
web = new Web(this);
|
||||
saveDefaultConfig();
|
||||
reloadConfig();
|
||||
|
||||
if (isEnabled()) {
|
||||
new MainCommand(this);
|
||||
|
||||
tps = new TPS();
|
||||
getServer().getScheduler().scheduleSyncRepeatingTask(this, tps, 1000, 50);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
web.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadConfig() {
|
||||
super.reloadConfig();
|
||||
web.stop();
|
||||
web.start(
|
||||
getConfig().getInt("port", 8080),
|
||||
getConfig().getString("password", "")
|
||||
);
|
||||
}
|
||||
|
||||
public void log(String message) {
|
||||
log.info( "[ServerAPI]: " + message );
|
||||
}
|
||||
|
@ -28,5 +61,56 @@ public class ServerAPI extends JavaPlugin {
|
|||
public static ServerAPI getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static TPS getTPS() {
|
||||
return tps;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TPSAPI TPS() {
|
||||
return new TPSAPI(tps.getHistory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldAPI world(String name) {
|
||||
for (World world : getServer().getWorlds()) {
|
||||
if (world.getName().equalsIgnoreCase(name)) {
|
||||
return WorldAPI.fromMinecraft(world);
|
||||
}
|
||||
}
|
||||
return new WorldAPI("unknown", 0, WorldAPI.WEATHER_UNKNOWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WorldAPI> worlds() {
|
||||
List<WorldAPI> worldAPIS = new ArrayList<>();
|
||||
for (World world : getServer().getWorlds()) {
|
||||
worldAPIS.add(WorldAPI.fromMinecraft(world));
|
||||
}
|
||||
return worldAPIS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<PlayerAPI> players() {
|
||||
Set<PlayerAPI> players = new HashSet<>();
|
||||
for (Player player : getServer().getOnlinePlayers()) {
|
||||
players.add(PlayerAPI.fromMinecraft(player));
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<PlayerAPI> bans() {
|
||||
Set<PlayerAPI> players = new HashSet<>();
|
||||
for (OfflinePlayer player : getServer().getBannedPlayers()) {
|
||||
players.add(PlayerAPI.fromMinecraft(player));
|
||||
}
|
||||
return players;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryAPI query() {
|
||||
return QueryAPI.fromMinecraft(getServer());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package xyz.etztech.serverapi.tps;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
public class TPS implements Runnable {
|
||||
private transient long lastPoll = System.nanoTime();
|
||||
private final LinkedList<Float> history = new LinkedList<>();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final long startTime = System.nanoTime();
|
||||
long timeSpent = (startTime - lastPoll) / 1000;
|
||||
if (timeSpent == 0) {
|
||||
timeSpent = 1;
|
||||
}
|
||||
if (history.size() > 10) {
|
||||
history.remove();
|
||||
}
|
||||
long tickInterval = 50;
|
||||
float tps = tickInterval * 1000000.0f / timeSpent;
|
||||
if (tps <= 21) {
|
||||
history.add(tps);
|
||||
}
|
||||
lastPoll = startTime;
|
||||
}
|
||||
|
||||
public LinkedList<Float> getHistory() {
|
||||
return history;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package xyz.etztech.serverapi.web;
|
||||
|
||||
import com.expediagroup.graphql.annotations.GraphQLName;
|
||||
import io.javalin.plugin.graphql.graphql.QueryGraphql;
|
||||
import xyz.etztech.serverapi.web.api.PlayerAPI;
|
||||
import xyz.etztech.serverapi.web.api.QueryAPI;
|
||||
import xyz.etztech.serverapi.web.api.TPSAPI;
|
||||
import xyz.etztech.serverapi.web.api.WorldAPI;
|
||||
|
||||
public class GraphQL implements QueryGraphql {
|
||||
private final IProvider provider;
|
||||
|
||||
public GraphQL(IProvider provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
@GraphQLName("players")
|
||||
public PlayerAPI[] getPlayers() {
|
||||
return provider.players().toArray(new PlayerAPI[0]);
|
||||
}
|
||||
|
||||
@GraphQLName("query")
|
||||
public QueryAPI getQuery() {
|
||||
return provider.query();
|
||||
}
|
||||
|
||||
@GraphQLName("world")
|
||||
public WorldAPI getWorld(@GraphQLName("world") String name) {
|
||||
return provider.world(name);
|
||||
}
|
||||
|
||||
@GraphQLName("worlds")
|
||||
public WorldAPI[] getWorlds() {
|
||||
return provider.worlds().toArray(new WorldAPI[0]);
|
||||
}
|
||||
|
||||
@GraphQLName("tps")
|
||||
public TPSAPI getTps() {
|
||||
return provider.TPS();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package xyz.etztech.serverapi.web;
|
||||
|
||||
import xyz.etztech.serverapi.web.api.PlayerAPI;
|
||||
import xyz.etztech.serverapi.web.api.QueryAPI;
|
||||
import xyz.etztech.serverapi.web.api.TPSAPI;
|
||||
import xyz.etztech.serverapi.web.api.WorldAPI;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public interface IProvider {
|
||||
Set<PlayerAPI> bans();
|
||||
Set<PlayerAPI> players();
|
||||
QueryAPI query();
|
||||
TPSAPI TPS();
|
||||
List<WorldAPI> worlds();
|
||||
WorldAPI world(String name);
|
||||
|
||||
void log(String message);
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package xyz.etztech.serverapi.web;
|
||||
|
||||
import io.javalin.Javalin;
|
||||
import io.javalin.core.event.EventListener;
|
||||
import io.javalin.core.plugin.Plugin;
|
||||
import io.javalin.core.util.RouteOverviewPlugin;
|
||||
import io.javalin.http.Context;
|
||||
import io.javalin.http.UnauthorizedResponse;
|
||||
import io.javalin.plugin.graphql.GraphQLOptions;
|
||||
import io.javalin.plugin.graphql.GraphQLPlugin;
|
||||
import io.javalin.plugin.json.JavalinJson;
|
||||
import org.apache.commons.lang.exception.ExceptionUtils;
|
||||
import org.slf4j.helpers.NOPLogger;
|
||||
import xyz.etztech.serverapi.ServerAPI;
|
||||
import xyz.etztech.serverapi.web.api.ErrorAPI;
|
||||
|
||||
public class Web {
|
||||
private final IProvider provider;
|
||||
private String password;
|
||||
private Javalin app;
|
||||
|
||||
public Web(IProvider provider) {
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
public void start(int port, String password) {
|
||||
Javalin.log = NOPLogger.NOP_LOGGER;
|
||||
app = Javalin.create(config -> {
|
||||
config.registerPlugin(graphql());
|
||||
config.registerPlugin(new RouteOverviewPlugin("/"));
|
||||
config.enableCorsForAllOrigins();
|
||||
}).events(this::events).exception(Exception.class, this::exception);
|
||||
|
||||
this.password = password;
|
||||
if (!"".equals(password)) {
|
||||
app.before(this::access);
|
||||
}
|
||||
|
||||
app.get("/bans", this::bans);
|
||||
app.get("/players", this::players);
|
||||
app.get("/query", this::query);
|
||||
app.get("/tps", this::tps);
|
||||
app.get("/worlds", this::worlds);
|
||||
app.get("/worlds/:name", this::world);
|
||||
|
||||
// What in the actual fuck... https://github.com/tipsy/javalin/issues/358
|
||||
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
|
||||
Thread.currentThread().setContextClassLoader(ServerAPI.class.getClassLoader());
|
||||
app.start(port);
|
||||
Thread.currentThread().setContextClassLoader(classLoader);
|
||||
}
|
||||
|
||||
|
||||
public void stop() {
|
||||
if (app != null) {
|
||||
app.stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void events(EventListener event) {
|
||||
event.serverStarting(() -> provider.log("Starting API..."));
|
||||
event.serverStarted(() -> provider.log("API is ready!"));
|
||||
event.serverStartFailed(() -> provider.log("Could not start API..."));
|
||||
event.serverStopping(() -> provider.log("Stopping API..."));
|
||||
event.serverStopped(() -> provider.log("API is stopped!"));
|
||||
}
|
||||
|
||||
public void exception(Exception exception, Context ctx) {
|
||||
provider.log(String.format("API Exception at %s:\n%s",
|
||||
ctx.req.getRequestURI(),
|
||||
ExceptionUtils.getStackTrace(exception)));
|
||||
ctx.status(500);
|
||||
ctx.json(new ErrorAPI(500, "Internal Error"));
|
||||
}
|
||||
|
||||
public void access(Context ctx) {
|
||||
String pw = ctx.header("X-ServerAPI-Password");
|
||||
if (pw == null) {
|
||||
pw = ctx.queryParam("password");
|
||||
}
|
||||
|
||||
if (!password.equals(pw)) {
|
||||
throw new UnauthorizedResponse(JavalinJson.toJson(new ErrorAPI(401, "Unauthorized")));
|
||||
}
|
||||
}
|
||||
|
||||
public Plugin graphql() {
|
||||
GraphQLOptions options = new GraphQLOptions("/graphql", null)
|
||||
.addPackage("xyz.etztech.serverapi.web.api")
|
||||
.register(new GraphQL(provider));
|
||||
return new GraphQLPlugin(options);
|
||||
}
|
||||
|
||||
public void tps(Context ctx) {
|
||||
ctx.json(provider.TPS());
|
||||
}
|
||||
|
||||
public void worlds(Context ctx) {
|
||||
ctx.json(provider.worlds());
|
||||
}
|
||||
|
||||
public void world(Context ctx) {
|
||||
ctx.json(provider.world(ctx.pathParam("name")));
|
||||
}
|
||||
|
||||
public void players(Context ctx) {
|
||||
ctx.json(provider.players());
|
||||
}
|
||||
|
||||
public void bans(Context ctx) {
|
||||
ctx.json(provider.bans());
|
||||
}
|
||||
|
||||
public void query(Context ctx) {
|
||||
ctx.json(provider.query());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package xyz.etztech.serverapi.web.api;
|
||||
|
||||
public class ErrorAPI {
|
||||
private final int status;
|
||||
private final String message;
|
||||
|
||||
public ErrorAPI(int status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package xyz.etztech.serverapi.web.api;
|
||||
|
||||
import com.expediagroup.graphql.annotations.GraphQLDescription;
|
||||
import com.expediagroup.graphql.annotations.GraphQLName;
|
||||
import org.bukkit.OfflinePlayer;
|
||||
|
||||
@GraphQLName("Player")
|
||||
@GraphQLDescription("Player GraphQL")
|
||||
public class PlayerAPI {
|
||||
private final String name;
|
||||
private final String uuid;
|
||||
|
||||
public PlayerAPI(String name, String uuid) {
|
||||
this.name = name;
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
@GraphQLName("name")
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@GraphQLName("uuid")
|
||||
public String getUUID() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public static PlayerAPI fromMinecraft(OfflinePlayer player) {
|
||||
return new PlayerAPI(player.getName(), player.getUniqueId().toString());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package xyz.etztech.serverapi.web.api;
|
||||
|
||||
import com.expediagroup.graphql.annotations.GraphQLName;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.bukkit.Server;
|
||||
|
||||
public class QueryAPI {
|
||||
private final String type;
|
||||
private final String version;
|
||||
private final String motd;
|
||||
@JsonProperty("current_players")
|
||||
private final int currentPlayers;
|
||||
@JsonProperty("max_players")
|
||||
private final int maxPlayers;
|
||||
|
||||
public QueryAPI(String type, String version, String motd, int currentPlayers, int maxPlayers) {
|
||||
this.type = type;
|
||||
this.version = version;
|
||||
this.motd = motd;
|
||||
this.currentPlayers = currentPlayers;
|
||||
this.maxPlayers = maxPlayers;
|
||||
}
|
||||
|
||||
@GraphQLName("type")
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@GraphQLName("version")
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
@GraphQLName("motd")
|
||||
public String getMotd() {
|
||||
return motd;
|
||||
}
|
||||
|
||||
@GraphQLName("current_players")
|
||||
public Integer getCurrentPlayers() {
|
||||
return currentPlayers;
|
||||
}
|
||||
|
||||
@GraphQLName("max_players")
|
||||
public Integer getMaxPlayers() {
|
||||
return maxPlayers;
|
||||
}
|
||||
|
||||
public static QueryAPI fromMinecraft(Server server) {
|
||||
return new QueryAPI(
|
||||
server.getName(),
|
||||
server.getBukkitVersion().split("-")[0], // 1.x.x-R0.1-SNAPSHOT
|
||||
server.getMotd(),
|
||||
server.getOnlinePlayers().size(),
|
||||
server.getMaxPlayers()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package xyz.etztech.serverapi.web.api;
|
||||
|
||||
import com.expediagroup.graphql.annotations.GraphQLDescription;
|
||||
import com.expediagroup.graphql.annotations.GraphQLName;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
@GraphQLName("TPS")
|
||||
@GraphQLDescription("TPS GraphQL")
|
||||
public class TPSAPI {
|
||||
private final LinkedList<Float> history;
|
||||
|
||||
public TPSAPI(LinkedList<Float> history) {
|
||||
this.history = history;
|
||||
}
|
||||
|
||||
@GraphQLName("history")
|
||||
public LinkedList<Float> getHistory() {
|
||||
return history;
|
||||
}
|
||||
|
||||
@JsonProperty("average")
|
||||
@GraphQLName("average")
|
||||
public float getAverageTPS() {
|
||||
float avg = 0;
|
||||
if (history.size() == 0) return avg;
|
||||
for (Float f : history) {
|
||||
if (f != null) {
|
||||
avg += f;
|
||||
}
|
||||
}
|
||||
return avg / history.size();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package xyz.etztech.serverapi.web.api;
|
||||
|
||||
import com.expediagroup.graphql.annotations.GraphQLDescription;
|
||||
import com.expediagroup.graphql.annotations.GraphQLName;
|
||||
import org.bukkit.World;
|
||||
|
||||
@GraphQLName("World")
|
||||
@GraphQLDescription("World GraphQL")
|
||||
public class WorldAPI {
|
||||
public static int WEATHER_UNKNOWN = -1;
|
||||
public static int WEATHER_CLEAR = 0;
|
||||
public static int WEATHER_STORM = 1;
|
||||
public static int WEATHER_THUNDER = 2;
|
||||
|
||||
private final String name;
|
||||
private final long time;
|
||||
private final int weather;
|
||||
|
||||
public WorldAPI(String name, long time, int weather) {
|
||||
this.name = name;
|
||||
this.time = time;
|
||||
this.weather = weather;
|
||||
}
|
||||
|
||||
@GraphQLName("name")
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@GraphQLName("time")
|
||||
public long getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
@GraphQLName("weather")
|
||||
public int getWeather() {
|
||||
return weather;
|
||||
}
|
||||
|
||||
public static WorldAPI fromMinecraft(World world) {
|
||||
int state = WEATHER_CLEAR;
|
||||
if (world.hasStorm()) {
|
||||
state = WEATHER_STORM;
|
||||
if (world.isThundering()) {
|
||||
state = WEATHER_THUNDER;
|
||||
}
|
||||
}
|
||||
return new WorldAPI(world.getName(), world.getFullTime(), state);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
# The port to run on
|
||||
port: 8080
|
||||
|
||||
# (Optional) password
|
||||
password: ''
|
|
@ -0,0 +1,69 @@
|
|||
package xyz.etztech.serverapi;
|
||||
|
||||
import xyz.etztech.serverapi.web.IProvider;
|
||||
import xyz.etztech.serverapi.web.api.PlayerAPI;
|
||||
import xyz.etztech.serverapi.web.api.QueryAPI;
|
||||
import xyz.etztech.serverapi.web.api.TPSAPI;
|
||||
import xyz.etztech.serverapi.web.api.WorldAPI;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MockProvider implements IProvider {
|
||||
List<WorldAPI> worlds;
|
||||
|
||||
public MockProvider() {
|
||||
worlds = Arrays.asList(
|
||||
new WorldAPI("overworld", 1000, WorldAPI.WEATHER_CLEAR),
|
||||
new WorldAPI("nether", 1500, WorldAPI.WEATHER_STORM),
|
||||
new WorldAPI("end", 2000, WorldAPI.WEATHER_THUNDER)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TPSAPI TPS() {
|
||||
return new TPSAPI(new LinkedList<>(Arrays.asList(20.0f, 15.0f, 10.0f, 18.0f, 20.0f)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public WorldAPI world(String name) {
|
||||
for (WorldAPI mock : worlds) {
|
||||
if (mock.getName().equalsIgnoreCase(name)) {
|
||||
return mock;
|
||||
}
|
||||
}
|
||||
return new WorldAPI("unknown", 0, WorldAPI.WEATHER_CLEAR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WorldAPI> worlds() {
|
||||
return worlds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<PlayerAPI> bans() {
|
||||
return new HashSet<>(Arrays.asList(
|
||||
new PlayerAPI("Badzelia", "bf0446a8-9695-4c41-aa4c-7ff45bfd1171"),
|
||||
new PlayerAPI("LessThanZeroSD", "fe7e8413-2570-4588-9203-2b69ff188bc3"),
|
||||
new PlayerAPI("Vakbuttzel", "7afbf663-2bf0-49ef-915f-22e81b298d17")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<PlayerAPI> players() {
|
||||
return new HashSet<>(Arrays.asList(
|
||||
new PlayerAPI("Etzelia", "bf0446a8-9695-4c41-aa4c-7ff45bfd1171"),
|
||||
new PlayerAPI("Zero", "fe7e8413-2570-4588-9203-2b69ff188bc3"),
|
||||
new PlayerAPI("Vak", "7afbf663-2bf0-49ef-915f-22e81b298d17")
|
||||
));
|
||||
}
|
||||
|
||||
@Override
|
||||
public QueryAPI query() {
|
||||
return new QueryAPI("Mock", "0.0.1", "Hello, world!", 0, 100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void log(String message) {
|
||||
System.out.println(message);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package xyz.etztech.serverapi;
|
||||
|
||||
import xyz.etztech.serverapi.web.Web;
|
||||
|
||||
public class ServerRunner {
|
||||
|
||||
public static void main(String[] args) {
|
||||
Web web = new Web(new MockProvider());
|
||||
|
||||
web.start(8080, "");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue