-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
314 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
312 changes: 312 additions & 0 deletions
312
ChunkBusterPlugin/src/main/java/codes/biscuit/chunkbuster/hooks/MetricsLite.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,312 @@ | ||
package codes.biscuit.chunkbuster.hooks; | ||
|
||
import org.bukkit.Bukkit; | ||
import org.bukkit.configuration.file.YamlConfiguration; | ||
import org.bukkit.plugin.Plugin; | ||
import org.bukkit.plugin.RegisteredServiceProvider; | ||
import org.bukkit.plugin.ServicePriority; | ||
import org.json.simple.JSONArray; | ||
import org.json.simple.JSONObject; | ||
|
||
import javax.net.ssl.HttpsURLConnection; | ||
import java.io.*; | ||
import java.lang.reflect.InvocationTargetException; | ||
import java.net.URL; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.Timer; | ||
import java.util.TimerTask; | ||
import java.util.UUID; | ||
import java.util.logging.Level; | ||
import java.util.zip.GZIPOutputStream; | ||
|
||
/** | ||
* bStats collects some data for plugin authors. | ||
* <p> | ||
* Check out https://bStats.org/ to learn more about bStats! | ||
*/ | ||
@SuppressWarnings({"WeakerAccess", "unchecked", "unused"}) | ||
public class MetricsLite { | ||
|
||
// The version of this bStats class | ||
public static final int B_STATS_VERSION = 1; | ||
|
||
// The url to which the data is sent | ||
private static final String URL = "https://bStats.org/submitData/bukkit"; | ||
|
||
// Is bStats enabled on this server? | ||
private boolean enabled; | ||
|
||
// Should failed requests be logged? | ||
private static boolean logFailedRequests; | ||
|
||
// Should the sent data be logged? | ||
private static boolean logSentData; | ||
|
||
// Should the response text be logged? | ||
private static boolean logResponseStatusText; | ||
|
||
// The uuid of the server | ||
private static String serverUUID; | ||
|
||
// The plugin | ||
private final Plugin plugin; | ||
|
||
/** | ||
* Class constructor. | ||
* | ||
* @param plugin The plugin which stats should be submitted. | ||
*/ | ||
public MetricsLite(Plugin plugin) { | ||
if (plugin == null) { | ||
throw new IllegalArgumentException("Plugin cannot be null!"); | ||
} | ||
this.plugin = plugin; | ||
|
||
// Get the config file | ||
File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); | ||
File configFile = new File(bStatsFolder, "config.yml"); | ||
YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); | ||
|
||
// Check if the config file exists | ||
if (!config.isSet("serverUuid")) { | ||
|
||
// Add default values | ||
config.addDefault("enabled", true); | ||
// Every server gets it's unique random id. | ||
config.addDefault("serverUuid", UUID.randomUUID().toString()); | ||
// Should failed request be logged? | ||
config.addDefault("logFailedRequests", false); | ||
// Should the sent data be logged? | ||
config.addDefault("logSentData", false); | ||
// Should the response text be logged? | ||
config.addDefault("logResponseStatusText", false); | ||
|
||
// Inform the server owners about bStats | ||
config.options().header( | ||
"bStats collects some data for plugin authors like how many servers are using their plugins.\n" + | ||
"To honor their work, you should not disable it.\n" + | ||
"This has nearly no effect on the server performance!\n" + | ||
"Check out https://bStats.org/ to learn more :)" | ||
).copyDefaults(true); | ||
try { | ||
config.save(configFile); | ||
} catch (IOException ignored) { } | ||
} | ||
|
||
// Load the data | ||
serverUUID = config.getString("serverUuid"); | ||
logFailedRequests = config.getBoolean("logFailedRequests", false); | ||
enabled = config.getBoolean("enabled", true); | ||
if (enabled) { | ||
boolean found = false; | ||
// Search for all other bStats Metrics classes to see if we are the first one | ||
for (Class<?> service : Bukkit.getServicesManager().getKnownServices()) { | ||
try { | ||
service.getField("B_STATS_VERSION"); // Our identifier :) | ||
found = true; // We aren't the first | ||
break; | ||
} catch (NoSuchFieldException ignored) { } | ||
} | ||
// Register our service | ||
Bukkit.getServicesManager().register(MetricsLite.class, this, plugin, ServicePriority.Normal); | ||
if (!found) { | ||
// We are the first! | ||
startSubmitting(); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Checks if bStats is enabled. | ||
* | ||
* @return Whether bStats is enabled or not. | ||
*/ | ||
public boolean isEnabled() { | ||
return enabled; | ||
} | ||
|
||
/** | ||
* Starts the Scheduler which submits our data every 30 minutes. | ||
*/ | ||
private void startSubmitting() { | ||
final Timer timer = new Timer(true); // We use a timer cause the Bukkit scheduler is affected by server lags | ||
timer.scheduleAtFixedRate(new TimerTask() { | ||
@Override | ||
public void run() { | ||
if (!plugin.isEnabled()) { // Plugin was disabled | ||
timer.cancel(); | ||
return; | ||
} | ||
// Nevertheless we want our code to run in the Bukkit main thread, so we have to use the Bukkit scheduler | ||
// Don't be afraid! The connection to the bStats server is still async, only the stats collection is sync ;) | ||
Bukkit.getScheduler().runTask(plugin, () -> submitData()); | ||
} | ||
}, 1000 * 60 * 5, 1000 * 60 * 30); | ||
// Submit the data every 30 minutes, first time after 5 minutes to give other plugins enough time to start | ||
// WARNING: Changing the frequency has no effect but your plugin WILL be blocked/deleted! | ||
// WARNING: Just don't do it! | ||
} | ||
|
||
/** | ||
* Gets the plugin specific data. | ||
* This method is called using Reflection. | ||
* | ||
* @return The plugin specific data. | ||
*/ | ||
public JSONObject getPluginData() { | ||
JSONObject data = new JSONObject(); | ||
|
||
String pluginName = plugin.getDescription().getName(); | ||
String pluginVersion = plugin.getDescription().getVersion(); | ||
|
||
data.put("pluginName", pluginName); // Append the name of the plugin | ||
data.put("pluginVersion", pluginVersion); // Append the version of the plugin | ||
JSONArray customCharts = new JSONArray(); | ||
data.put("customCharts", customCharts); | ||
|
||
return data; | ||
} | ||
|
||
/** | ||
* Gets the server specific data. | ||
* | ||
* @return The server specific data. | ||
*/ | ||
private JSONObject getServerData() { | ||
// Minecraft specific data | ||
int playerAmount = Bukkit.getOnlinePlayers().size(); | ||
int onlineMode = Bukkit.getOnlineMode() ? 1 : 0; | ||
String bukkitVersion = Bukkit.getVersion(); | ||
|
||
// OS/Java specific data | ||
String javaVersion = System.getProperty("java.version"); | ||
String osName = System.getProperty("os.name"); | ||
String osArch = System.getProperty("os.arch"); | ||
String osVersion = System.getProperty("os.version"); | ||
int coreCount = Runtime.getRuntime().availableProcessors(); | ||
|
||
JSONObject data = new JSONObject(); | ||
|
||
data.put("serverUUID", serverUUID); | ||
|
||
data.put("playerAmount", playerAmount); | ||
data.put("onlineMode", onlineMode); | ||
data.put("bukkitVersion", bukkitVersion); | ||
|
||
data.put("javaVersion", javaVersion); | ||
data.put("osName", osName); | ||
data.put("osArch", osArch); | ||
data.put("osVersion", osVersion); | ||
data.put("coreCount", coreCount); | ||
|
||
return data; | ||
} | ||
|
||
/** | ||
* Collects the data and sends it afterwards. | ||
*/ | ||
private void submitData() { | ||
final JSONObject data = getServerData(); | ||
|
||
JSONArray pluginData = new JSONArray(); | ||
// Search for all other bStats Metrics classes to get their plugin data | ||
for (Class<?> service : Bukkit.getServicesManager().getKnownServices()) { | ||
try { | ||
service.getField("B_STATS_VERSION"); // Our identifier :) | ||
|
||
for (RegisteredServiceProvider<?> provider : Bukkit.getServicesManager().getRegistrations(service)) { | ||
try { | ||
pluginData.add(provider.getService().getMethod("getPluginData").invoke(provider.getProvider())); | ||
} catch (NullPointerException | NoSuchMethodException | IllegalAccessException | InvocationTargetException ignored) { | ||
} | ||
} | ||
} catch (NoSuchFieldException ignored) { } | ||
} | ||
|
||
data.put("plugins", pluginData); | ||
|
||
// Create a new thread for the connection to the bStats server | ||
new Thread(() -> { | ||
try { | ||
// Send the data | ||
sendData(plugin, data); | ||
} catch (Exception e) { | ||
// Something went wrong! :( | ||
if (logFailedRequests) { | ||
plugin.getLogger().log(Level.WARNING, "Could not submit plugin stats of " + plugin.getName(), e); | ||
} | ||
} | ||
}).start(); | ||
} | ||
|
||
/** | ||
* Sends the data to the bStats server. | ||
* | ||
* @param plugin Any plugin. It's just used to get a logger instance. | ||
* @param data The data to send. | ||
* @throws Exception If the request failed. | ||
*/ | ||
private static void sendData(Plugin plugin, JSONObject data) throws Exception { | ||
if (data == null) { | ||
throw new IllegalArgumentException("Data cannot be null!"); | ||
} | ||
if (Bukkit.isPrimaryThread()) { | ||
throw new IllegalAccessException("This method must not be called from the main thread!"); | ||
} | ||
if (logSentData) { | ||
plugin.getLogger().info("Sending data to bStats: " + data.toString()); | ||
} | ||
HttpsURLConnection connection = (HttpsURLConnection) new URL(URL).openConnection(); | ||
|
||
// Compress the data to save bandwidth | ||
byte[] compressedData = compress(data.toString()); | ||
|
||
// Add headers | ||
connection.setRequestMethod("POST"); | ||
connection.addRequestProperty("Accept", "application/json"); | ||
connection.addRequestProperty("Connection", "close"); | ||
connection.addRequestProperty("Content-Encoding", "gzip"); // We gzip our request | ||
connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); | ||
connection.setRequestProperty("Content-Type", "application/json"); // We send our data in JSON format | ||
connection.setRequestProperty("User-Agent", "MC-Server/" + B_STATS_VERSION); | ||
|
||
// Send data | ||
connection.setDoOutput(true); | ||
DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream()); | ||
outputStream.write(compressedData); | ||
outputStream.flush(); | ||
outputStream.close(); | ||
|
||
InputStream inputStream = connection.getInputStream(); | ||
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); | ||
|
||
StringBuilder builder = new StringBuilder(); | ||
String line; | ||
while ((line = bufferedReader.readLine()) != null) { | ||
builder.append(line); | ||
} | ||
bufferedReader.close(); | ||
if (logResponseStatusText) { | ||
plugin.getLogger().info("Sent data to bStats and received response: " + builder.toString()); | ||
} | ||
} | ||
|
||
/** | ||
* Gzips the given String. | ||
* | ||
* @param str The string to gzip. | ||
* @return The gzipped String. | ||
* @throws IOException If the compression failed. | ||
*/ | ||
private static byte[] compress(final String str) throws IOException { | ||
if (str == null) { | ||
return null; | ||
} | ||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); | ||
GZIPOutputStream gzip = new GZIPOutputStream(outputStream); | ||
gzip.write(str.getBytes(StandardCharsets.UTF_8)); | ||
gzip.close(); | ||
return outputStream.toByteArray(); | ||
} | ||
|
||
} |