diff --git a/README.md b/README.md index 26740d0..b2a0df3 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +<<<<<<< HEAD # An API to expose player information for Janet (or other discord bots). @@ -131,3 +132,6 @@ Note, the plugin will not run unless you change the secret. This is explained ab # Legal Mumbo Jumbo The idea and base code for this project came from [TristanSMPAPI](https://github.com/twisttaan/TristanSMPAPI). It has been extensively added to by myself with much technical help from the developers of [DiscordSRV](https://github.com/DiscordSRV/DiscordSRV/) +======= +A remake of BoredManCodes' SMP-API to fit my needs. +>>>>>>> 5b19083 (file add) diff --git a/pom.xml b/pom.xml index d5d5f4a..8a699cc 100644 --- a/pom.xml +++ b/pom.xml @@ -4,9 +4,15 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 +<<<<<<< HEAD net.boredman SMP-API 2.0.3 +======= + quest.safecloud + SMP-API + 1.0 +>>>>>>> 5b19083 (file add) jar SMP-Api @@ -16,7 +22,11 @@ 17 UTF-8 +<<<<<<< HEAD https://boredman.net +======= + https://safecloud.quest +>>>>>>> 5b19083 (file add) @@ -89,7 +99,7 @@ com.discordsrv discordsrv - 1.21.1 + 1.27.0 provided diff --git a/src/main/java/net/boredman/api.java b/src/main/java/net/boredman/api.java index f4d53ea..cfb4414 100644 --- a/src/main/java/net/boredman/api.java +++ b/src/main/java/net/boredman/api.java @@ -44,6 +44,4 @@ public void start() { } public static Express express = new Express(); - - } diff --git a/src/main/java/net/boredman/routes/DiscordRoute.java b/src/main/java/net/boredman/routes/DiscordRoute.java index 951d5ff..44c3877 100644 --- a/src/main/java/net/boredman/routes/DiscordRoute.java +++ b/src/main/java/net/boredman/routes/DiscordRoute.java @@ -30,26 +30,23 @@ public DiscordRoute(Express app) { API.getPlugin(API.class).getLogger().info("A request was made to access " + req.getParams().get("username") + "'s Discord data"); } - if (!secret.equals(req.getHeader("secret").get(0))) { + if (secret!= null &&!secret.equals(req.getHeader("secret").get(0))) { obj.put("error", true); obj.put("message", "You are not authorised to access this resource"); res.send(obj.toJSONString()); API.getPlugin(API.class).getLogger().warning("A request to access Discord info from " + req.getIp() + " was rejected as they did not pass the correct secret in the header"); - return; } else { if (discordId == null) { obj.put("error", true); obj.put("message", "Player not linked to discord"); res.send(obj.toJSONString()); - return; } else { User user = DiscordUtil.getJda().getUserById(discordId); if (user == null) { obj.put("error", true); obj.put("message", "Couldn't find Discord User by ID. Maybe they left the server?"); res.send(obj.toJSONString()); - return; } else { obj.put("error", false); obj.put("username", username); @@ -58,7 +55,6 @@ public DiscordRoute(Express app) { obj.put("discordTag", user.getAsTag()); obj.put("discordName", user.getName()); res.send(obj.toJSONString()); - return; } } } @@ -74,7 +70,7 @@ public DiscordRoute(Express app) { API.getPlugin(API.class).getLogger().info("A request was made to access " + req.getParams().get("id") + "'s Discord data"); } - if (!secret.equals(req.getHeader("secret").get(0))) { + if (secret!= null &&!secret.equals(req.getHeader("secret").get(0))) { obj.put("error", true); obj.put("message", "You are not authorised to access this resource"); res.send(obj.toJSONString()); @@ -85,14 +81,12 @@ public DiscordRoute(Express app) { obj.put("error", true); obj.put("message", "Player not linked to discord"); res.send(obj.toJSONString()); - return; } else { User user = DiscordUtil.getJda().getUserById(discordId); if (user == null) { obj.put("error", true); obj.put("message", "Couldn't find Discord User by ID. Maybe they left the server?"); res.send(obj.toJSONString()); - return; } else { obj.put("error", false); obj.put("username", username); @@ -101,7 +95,6 @@ public DiscordRoute(Express app) { obj.put("discordTag", user.getAsTag()); obj.put("discordName", user.getName()); res.send(obj.toJSONString()); - return; } } } diff --git a/src/main/java/quest/safecloud/API.java b/src/main/java/quest/safecloud/API.java new file mode 100644 index 0000000..57d2018 --- /dev/null +++ b/src/main/java/quest/safecloud/API.java @@ -0,0 +1,49 @@ +package quest.safecloud; + +import express.Express; +import github.scarsz.discordsrv.objects.managers.AccountLinkManager; +import quest.safecloud.events.QuitEvent; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.event.Listener; +import org.bukkit.plugin.java.JavaPlugin; + + +public class API extends JavaPlugin implements Listener { + private static API plugin; + final FileConfiguration config = getConfig(); + + public static Express getApp() { + return express; + } + + @Override + public void onEnable() { + config.addDefault("port", 25567); + config.addDefault("secret", "CHANGE THIS!"); + config.addDefault("debug", false); + config.options().copyDefaults(true); + saveConfig(); + start(); + plugin = this; + if (config.getString("secret").equals("CHANGE THIS!")) { + getLogger().warning("--------------------------------------------"); + getLogger().severe("You MUST change the secret in the config.yml for this plugin to work. " + + "This prevents exposing player IP addresses to the world"); + getLogger().warning("--------------------------------------------"); + this.getPluginLoader().disablePlugin(this); + } + this.getServer().getPluginManager().registerEvents(new QuitEvent(), this); + } + + @Override + public void onDisable() { + } + + public void start() { + getLogger().info("Starting API on port " + config.getInt("port")); + new ReqHandler(express); + getLogger().info("API Started"); + } + + public static Express express = new Express(); +} diff --git a/src/main/java/quest/safecloud/ReqHandler.java b/src/main/java/quest/safecloud/ReqHandler.java new file mode 100644 index 0000000..5b875b3 --- /dev/null +++ b/src/main/java/quest/safecloud/ReqHandler.java @@ -0,0 +1,16 @@ +package quest.safecloud; + +import express.Express; +import quest.safecloud.routes.DiscordRoute; +import quest.safecloud.routes.PlayersRoute; + +public class ReqHandler { + + public ReqHandler(Express app) { + int port = API.getPlugin(API.class).getConfig().getInt("port"); + new PlayersRoute(app); + new DiscordRoute(app); + app.get("/", (req, res) -> res.send("Error 418: The server refuses to brew coffee because it is, permanently, a teapot.\n" + + "For more information: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418")).listen(port); + } +} \ No newline at end of file diff --git a/src/main/java/quest/safecloud/events/QuitEvent.java b/src/main/java/quest/safecloud/events/QuitEvent.java new file mode 100644 index 0000000..16b04b3 --- /dev/null +++ b/src/main/java/quest/safecloud/events/QuitEvent.java @@ -0,0 +1,78 @@ +package quest.safecloud.events; + +import quest.safecloud.API; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerQuitEvent; +import org.json.simple.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import static org.bukkit.Bukkit.getPlayer; + +public class QuitEvent implements Listener { + @EventHandler + public void playerQuitEvent(PlayerQuitEvent event) throws IOException { + boolean debug = API.getPlugin(API.class).getConfig().getBoolean("debug"); + if (debug) { + System.out.println(event.getPlayer().getName().toString() + " left the server, saving their player data."); + } + File dir = new File(API.class.getProtectionDomain().getCodeSource().getLocation().getPath().replaceAll("%20", " ")); + File plugins = new File(dir.getParentFile().getPath()); + String playerDataFolder = plugins + "\\SMP-API\\playerdata\\"; + if (!Files.exists(Path.of(playerDataFolder))) { + Files.createDirectory(Path.of(playerDataFolder)); + } + String filename = playerDataFolder + event.getPlayer().getName() + ".json"; + String username = event.getPlayer().getName(); + JSONObject obj = new JSONObject(); + Player player = getPlayer(username); + String bed; + String[] arrOfBed; + String location; + String[] arrOfLocation; + String address; + String[] arrOfAddress; + try { + obj.put((Object) "username", (Object) player.getName()); + obj.put((Object) "uuid", (Object) player.getUniqueId().toString()); + obj.put((Object) "health", (Object) String.valueOf(player.getHealth())); + obj.put((Object) "food", (Object) String.valueOf(player.getFoodLevel())); + obj.put((Object) "world", (Object) player.getWorld().getName()); + obj.put((Object) "experience", (Object) String.valueOf(player.getExp())); + obj.put((Object) "level", (Object) String.valueOf(player.getLevel())); + obj.put((Object) "deaths", (Object) String.valueOf(player.getStatistic(Statistic.DEATHS))); + obj.put((Object) "kills", (Object) String.valueOf(player.getStatistic(Statistic.MOB_KILLS))); + obj.put((Object) "jumps", (Object) String.valueOf(player.getStatistic(Statistic.JUMP))); + obj.put((Object) "gamemode", (Object) player.getGameMode().toString()); + if (player.getBedSpawnLocation() != null) { + bed = player.getBedSpawnLocation().toString(); + arrOfBed = bed.split(","); + obj.put((Object) "bed", (Object) (arrOfBed[1] + "," + arrOfBed[2] + "," + arrOfBed[3])); + } + obj.put((Object) "time", (Object) String.valueOf(player.getStatistic(Statistic.PLAY_ONE_MINUTE) / 20)); + obj.put((Object) "death", (Object) String.valueOf(player.getStatistic(Statistic.TIME_SINCE_DEATH) / 20)); + address = String.valueOf(player.getAddress()).replace("/", ""); + arrOfAddress = address.split(":"); + obj.put((Object) "address", (Object) arrOfAddress[0]); + obj.put((Object) "lastJoined", (Object) System.currentTimeMillis()); + obj.put((Object) "online", (Object) false); + location = player.getLocation().toString(); + arrOfLocation = location.split(","); + obj.put((Object) "location", (Object) (arrOfLocation[1] + "," + arrOfLocation[2] + "," + arrOfLocation[3])); + Files.write(Paths.get(filename), obj.toJSONString().getBytes()); + if (debug) { + API.getPlugin(API.class).getLogger().info("Saved " + event.getPlayer().getName() + "'s player data"); + } + } catch (Exception e) { + API.getPlugin(API.class).getLogger().severe("Ran into an error trying to save player data"); + API.getPlugin(API.class).getLogger().severe(String.valueOf(e)); + } + } +} diff --git a/src/main/java/quest/safecloud/routes/DiscordRoute.java b/src/main/java/quest/safecloud/routes/DiscordRoute.java new file mode 100644 index 0000000..2c30e8c --- /dev/null +++ b/src/main/java/quest/safecloud/routes/DiscordRoute.java @@ -0,0 +1,108 @@ +package quest.safecloud.routes; + +import express.Express; +import github.scarsz.discordsrv.dependencies.jda.api.entities.User; +import github.scarsz.discordsrv.util.DiscordUtil; +import quest.safecloud.API; +import org.bukkit.OfflinePlayer; +import org.json.simple.JSONObject; + +import java.util.UUID; + +import static github.scarsz.discordsrv.DiscordSRV.getPlugin; +import static org.bukkit.Bukkit.getOfflinePlayer; + + +@SuppressWarnings("unchecked") +public class DiscordRoute { + public DiscordRoute(Express app) { + + // Read config + boolean debug = API.getPlugin(API.class).getConfig().getBoolean("debug"); + String secret = API.getPlugin(API.class).getConfig().getString("secret"); + + if (secret == null) { + API.getPlugin(API.class).getLogger().warning("Secret not set in config.yml. This is a security risk."); + return; + } + + // Lookup Discord via username + app.get("/minecraft/name/:username", (req, res) -> { + final String username = req.getParams().get("username"); + final OfflinePlayer player = getOfflinePlayer(username); + final String discordId = getPlugin().getAccountLinkManager().getDiscordId(player.getUniqueId()); + final JSONObject obj = new JSONObject(); + if (debug) { + API.getPlugin(API.class).getLogger().info("A request was made to access " + + req.getParams().get("username") + "'s Discord data"); + } + if (!secret.equals(req.getHeader("secret").get(0))) { + obj.put("error", true); + obj.put("message", "You are not authorised to access this resource"); + res.send(obj.toJSONString()); + API.getPlugin(API.class).getLogger().warning("A request to access Discord info from " + req.getIp() + + " was rejected as they did not pass the correct secret in the header"); + } else { + if (discordId == null) { + obj.put("error", true); + obj.put("message", "Player not linked to discord"); + } else { + User user = DiscordUtil.getJda().getUserById(discordId); + if (user == null) { + obj.put("error", true); + obj.put("message", "Couldn't find Discord User by ID. Maybe they left the server?"); + } else { + obj.put("error", false); + obj.put("username", username); + obj.put("uuid", player.getUniqueId().toString()); + obj.put("discordId", discordId); + obj.put("discordTag", user.getAsTag()); + obj.put("discordName", user.getName()); + } + } + res.send(obj.toJSONString()); + } + }); + + // Lookup Discord via ID + app.get("/discord/id/:id", (req, res) -> { + final String discordId = req.getParams().get("id"); + final UUID playerUUID = getPlugin().getAccountLinkManager().getUuid(discordId); + final JSONObject obj = new JSONObject(); + if (debug) { + API.getPlugin(API.class).getLogger().info("A request was made to access " + + req.getParams().get("id") + "'s Discord data"); + } + if (!secret.equals(req.getHeader("secret").get(0))) { + obj.put("error", true); + obj.put("message", "You are not authorised to access this resource"); + res.send(obj.toJSONString()); + API.getPlugin(API.class).getLogger().warning("A request to access Discord info from " + req.getIp() + + " was rejected as they did not pass the correct secret in the header"); + } else { + if (playerUUID == null) { + obj.put("error", true); + obj.put("message", "Player not linked to discord"); + res.send(obj.toJSONString()); + } else { + User user = DiscordUtil.getJda().getUserById(discordId); + if (user == null) { + obj.put("error", true); + obj.put("message", "Couldn't find Discord User by ID. Maybe they left the server?"); + res.send(obj.toJSONString()); + } else { + OfflinePlayer player = getOfflinePlayer(playerUUID); + obj.put("error", false); + obj.put("username", player.getName()); + obj.put("uuid", player.getUniqueId().toString()); + obj.put("discordId", discordId); + obj.put("discordTag", user.getAsTag()); + obj.put("discordName", user.getName()); + res.send(obj.toJSONString()); + } + } + } + }); + + } +} \ No newline at end of file diff --git a/src/main/java/quest/safecloud/routes/PlayersRoute.java b/src/main/java/quest/safecloud/routes/PlayersRoute.java new file mode 100644 index 0000000..1b5910f --- /dev/null +++ b/src/main/java/quest/safecloud/routes/PlayersRoute.java @@ -0,0 +1,130 @@ +package net.boredman.routes; + +import express.Express; +import github.scarsz.discordsrv.dependencies.jda.api.entities.User; +import github.scarsz.discordsrv.util.DiscordUtil; +import net.boredman.API; +import org.bukkit.OfflinePlayer; +import org.bukkit.Statistic; +import org.bukkit.entity.Player; +import org.json.simple.JSONObject; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.UUID; + +import static github.scarsz.discordsrv.DiscordSRV.getPlugin; +import static org.bukkit.Bukkit.*; + +public class PlayersRoute { + public PlayersRoute(Express app) { + + // Online Player Stats // + boolean debug = API.getPlugin(API.class).getConfig().getBoolean("debug"); + String secret = API.getPlugin(API.class).getConfig().getString("secret"); + app.get("/players/:username", (req, res) -> { + if (debug) { + API.getPlugin(API.class).getLogger().info("A request was made to access " + + req.getParams().get("username") + "'s player data"); + } + if (secret.equals(req.getHeader("secret").get(0))) { + File dir = new File(API.class.getProtectionDomain().getCodeSource().getLocation().getPath().replaceAll("%20", " ")); + File plugins = new File(dir.getParentFile().getPath()); + String playerDataFolder = plugins + "\\SMP-API\\playerdata\\"; + if (!Files.exists(Path.of(playerDataFolder))) { + try { + Files.createDirectory(Path.of(playerDataFolder)); + } catch (IOException e) { + e.printStackTrace(); + } + } + String username = req.getParams().get("username"); + JSONObject obj = new JSONObject(); + Player player = getPlayer(username); + String bed; + String[] arrOfBed; + String location; + String[] arrOfLocation; + String address; + String[] arrOfAddress; + try { + obj.put((Object)"username", (Object)player.getName()); + obj.put((Object)"uuid", (Object)player.getUniqueId().toString()); + obj.put((Object)"health", (Object)String.valueOf(player.getHealth())); + obj.put((Object)"food", (Object)String.valueOf(player.getFoodLevel())); + obj.put((Object)"world", (Object)player.getWorld().getName()); + obj.put((Object)"experience", (Object)String.valueOf(player.getExp())); + obj.put((Object)"level", (Object)String.valueOf(player.getLevel())); + obj.put((Object)"deaths", (Object)String.valueOf(player.getStatistic(Statistic.DEATHS))); + obj.put((Object)"kills", (Object)String.valueOf(player.getStatistic(Statistic.MOB_KILLS))); + obj.put((Object)"jumps", (Object)String.valueOf(player.getStatistic(Statistic.JUMP))); + obj.put((Object)"gamemode", (Object)player.getGameMode().toString()); + if (player.getBedSpawnLocation() != null) { + bed = player.getBedSpawnLocation().toString(); + arrOfBed = bed.split(","); + obj.put((Object)"bed", (Object)(arrOfBed[1] + "," + arrOfBed[2] + "," + arrOfBed[3])); + } + obj.put((Object)"time", (Object)String.valueOf(player.getStatistic(Statistic.PLAY_ONE_MINUTE) / 20)); + obj.put((Object)"death", (Object)String.valueOf(player.getStatistic(Statistic.TIME_SINCE_DEATH) / 20)); + address = String.valueOf(player.getAddress()).replace("/", ""); + arrOfAddress = address.split(":"); + obj.put((Object)"address", (Object)arrOfAddress[0]); + obj.put((Object)"lastJoined", (Object)player.getLastPlayed()); + obj.put((Object) "online", (Object) true); + location = player.getLocation().toString(); + arrOfLocation = location.split(","); + obj.put((Object) "location", (Object) (arrOfLocation[1] + "," + arrOfLocation[2] + "," + arrOfLocation[3])); + res.send(obj.toJSONString()); + } catch (Exception e) { + if (e.getMessage().contains("Cannot invoke \"org.bukkit.entity.Player.getName()\" because \"player\" is null")) { + if (debug) { + API.getPlugin(API.class).getLogger().info("Player offline, attempting to serve cached player data"); + } + String filename = playerDataFolder + username + ".json"; + if (Files.exists(Path.of(filename))) { + String content = "blank"; + try { + content = Files.readString(Path.of(filename)); + } catch (IOException ex) { + ex.printStackTrace(); + } + res.send(content); + BasicFileAttributes attr = null; + try { + attr = Files.readAttributes(Path.of(filename), BasicFileAttributes.class); + } catch (IOException ex) { + ex.printStackTrace(); + } + + if (debug) { + if (attr != null) { + API.getPlugin(API.class).getLogger().info("Served cached data from " + attr.lastModifiedTime()); + } else { + API.getPlugin(API.class).getLogger().info("Served cached data"); + } + } + return; + } + obj.put("error", true); + obj.put("message", "Player not online, or not found"); + res.send(obj.toJSONString()); + } else { + getLogger().info("Error: " + e.getMessage()); + res.send("Error: " + e.getMessage()); + } + } + } else { + final JSONObject obj = new JSONObject(); + obj.put("error", true); + obj.put("message", "You are not authorised to access this resource"); + res.send(obj.toJSONString()); + API.getPlugin(API.class).getLogger().warning("A request to access Minecraft info from " + req.getIp() + + " was rejected as they did not pass the correct secret in the header"); + } + }); + + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 1340749..1b70789 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,10 +1,10 @@ name: SMP-API version: '${project.version}' -main: net.boredman.API -api-version: 1.18 +main: quest.safecloud.API +api-version: 1.20 prefix: SMP-API -authors: [ BoredManCodes, twisttaan ] +authors: [ Wingdingderp, BoredManCodes, twisttaan ] softdepend: - 'DiscordSRV' description: A SMP API for exposing player information for Discord bots to use -website: https://boredman.net +website: https://safecloud.quest