Initial Version

Signed-off-by: Etzelia <etzelia@hotmail.com>
query
Etzelia 2020-08-07 14:30:36 -05:00
parent 68fe6c1ee4
commit e41d54e1ed
No known key found for this signature in database
GPG Key ID: 3CAEB74806C4ADE5
15 changed files with 601 additions and 2 deletions

View File

@ -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
View File

@ -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>

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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()
);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,5 @@
# The port to run on
port: 8080
# (Optional) password
password: ''

View File

@ -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);
}
}

View File

@ -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, "");
}
}