|
| 1 | +package org.secverse.secVersEssentialsXMySQLConnector.SecVersCom; |
| 2 | + |
| 3 | +import org.bukkit.Bukkit; |
| 4 | +import org.bukkit.plugin.Plugin; |
| 5 | +import org.bukkit.scheduler.BukkitRunnable; |
| 6 | + |
| 7 | +import java.io.*; |
| 8 | +import java.net.HttpURLConnection; |
| 9 | +import java.net.URL; |
| 10 | +import java.nio.charset.StandardCharsets; |
| 11 | +import java.time.Instant; |
| 12 | +import java.util.HashMap; |
| 13 | +import java.util.Map; |
| 14 | +import java.util.UUID; |
| 15 | + |
| 16 | +/** |
| 17 | + * Telemetry helper for a Bukkit/Spigot/Paper plugin. |
| 18 | + * |
| 19 | + * Responsibilities: |
| 20 | + * - Generate and persist a unique HWID per plugin installation. |
| 21 | + * - Provide server name and basic metadata. |
| 22 | + * - Build telemetry JSON payloads. |
| 23 | + * - Send telemetry POST requests asynchronously using Bukkit scheduler. |
| 24 | + * |
| 25 | + * Note: This class keeps network I/O off the main thread by using Bukkit's scheduler. |
| 26 | + * Telemetry is opt-in/opt-out via the plugin config (telemetry.enabled). |
| 27 | + */ |
| 28 | +public class Telemetry { |
| 29 | + |
| 30 | + private static final String HWID_FILENAME = "hwid.txt"; |
| 31 | + private final Plugin plugin; |
| 32 | + private final File dataFolder; |
| 33 | + private final UUID hwid; |
| 34 | + private final String endpoint; |
| 35 | + private final boolean enabled; |
| 36 | + |
| 37 | + /** |
| 38 | + * Initialize Telemetry for a plugin. Will create the plugin data folder if missing |
| 39 | + * and persist a generated HWID in hwid.txt. |
| 40 | + * |
| 41 | + * @param plugin your main plugin instance (for scheduler & data folder) |
| 42 | + */ |
| 43 | + public Telemetry(Plugin plugin) { |
| 44 | + this.plugin = plugin; |
| 45 | + this.dataFolder = plugin.getDataFolder(); |
| 46 | + if (!dataFolder.exists()) { |
| 47 | + boolean created = dataFolder.mkdirs(); |
| 48 | + if (!created) { |
| 49 | + plugin.getLogger().warning("Could not create plugin data folder for telemetry persistence."); |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + // load config values (fall back to safe defaults) |
| 54 | + this.enabled = plugin.getConfig().getBoolean("telemetry.enabled", true); |
| 55 | + this.endpoint = plugin.getConfig().getString("telemetry.endpoint", "").trim(); |
| 56 | + |
| 57 | + // load or generate HWID |
| 58 | + this.hwid = loadOrCreateHwid(); |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * Returns the persisted HWID for this plugin installation. |
| 63 | + * |
| 64 | + * @return UUID representing the installation HWID. |
| 65 | + */ |
| 66 | + public UUID getHwid() { |
| 67 | + return hwid; |
| 68 | + } |
| 69 | + |
| 70 | + /** |
| 71 | + * Returns the server name as reported by Bukkit. If that is empty or generic, |
| 72 | + * additional heuristics could be added. |
| 73 | + * |
| 74 | + * @return server name string |
| 75 | + */ |
| 76 | + public String getServerName() { |
| 77 | + String name = Bukkit.getServer().getName(); |
| 78 | + if (name == null || name.isEmpty()) { |
| 79 | + // fallback: try to read server.properties server-name variants or return "unknown" |
| 80 | + return "unknown"; |
| 81 | + } |
| 82 | + return name; |
| 83 | + } |
| 84 | + |
| 85 | + /** |
| 86 | + * Build a simple telemetry payload. Extend the map for custom fields. |
| 87 | + * |
| 88 | + * @param additional optional extra key/value pairs to include |
| 89 | + * @return JSON string representing the payload |
| 90 | + */ |
| 91 | + public String buildPayload(Map<String, Object> additional) { |
| 92 | + Map<String, Object> payload = new HashMap<>(); |
| 93 | + payload.put("hwid", hwid.toString()); |
| 94 | + payload.put("serverName", getServerName()); |
| 95 | + payload.put("pluginName", plugin.getDescription().getName()); |
| 96 | + payload.put("pluginVersion", plugin.getDescription().getVersion()); |
| 97 | + payload.put("timestamp", Instant.now().toString()); |
| 98 | + |
| 99 | + if (additional != null) { |
| 100 | + payload.putAll(additional); |
| 101 | + } |
| 102 | + |
| 103 | + return toJson(payload); |
| 104 | + } |
| 105 | + |
| 106 | + /** |
| 107 | + * Send telemetry to configured endpoint asynchronously. |
| 108 | + * If telemetry is disabled or endpoint missing, the call returns without network IO. |
| 109 | + * |
| 110 | + * @param additional optional extra fields to include in the payload |
| 111 | + */ |
| 112 | + public void sendTelemetryAsync(Map<String, Object> additional) { |
| 113 | + if (!enabled) { |
| 114 | + plugin.getLogger().info("Telemetry disabled in config; skipping send."); |
| 115 | + return; |
| 116 | + } |
| 117 | + if (endpoint == null || endpoint.isEmpty()) { |
| 118 | + plugin.getLogger().warning("Telemetry endpoint not configured; skipping send."); |
| 119 | + return; |
| 120 | + } |
| 121 | + |
| 122 | + final String payload = buildPayload(additional); |
| 123 | + |
| 124 | + |
| 125 | + new BukkitRunnable() { |
| 126 | + @Override |
| 127 | + public void run() { |
| 128 | + try { |
| 129 | + sendPost(payload); |
| 130 | + } catch (Exception e) { |
| 131 | + plugin.getLogger().warning("Failed to send telemetry: " + e.getMessage()); |
| 132 | + } |
| 133 | + } |
| 134 | + }.runTaskAsynchronously(plugin); |
| 135 | + } |
| 136 | + |
| 137 | + /** |
| 138 | + * Sends a JSON payload to a URL using POST. |
| 139 | + * |
| 140 | + * @param jsonPayload the JSON string |
| 141 | + * @throws IOException on network or IO errors |
| 142 | + */ |
| 143 | + private void sendPost(String jsonPayload) throws IOException { |
| 144 | + URL url = new URL("https://api.secvers.org/v1/telemetry/EssentailsXMySQL"); |
| 145 | + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); |
| 146 | + try { |
| 147 | + conn.setRequestMethod("POST"); |
| 148 | + conn.setDoOutput(true); |
| 149 | + conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); |
| 150 | + conn.setConnectTimeout(8000); |
| 151 | + conn.setReadTimeout(8000); |
| 152 | + |
| 153 | + byte[] out = jsonPayload.getBytes(StandardCharsets.UTF_8); |
| 154 | + conn.setFixedLengthStreamingMode(out.length); |
| 155 | + conn.connect(); |
| 156 | + |
| 157 | + try (OutputStream os = conn.getOutputStream()) { |
| 158 | + os.write(out); |
| 159 | + } |
| 160 | + |
| 161 | + int status = conn.getResponseCode(); |
| 162 | + if (status < 200 || status >= 300) { |
| 163 | + // read error stream for debugging |
| 164 | + String err = readStream(conn.getErrorStream()); |
| 165 | + plugin.getLogger().warning("Telemetry server returned HTTP " + status + ": " + err); |
| 166 | + } else { |
| 167 | + plugin.getLogger().fine("Telemetry sent successfully."); |
| 168 | + } |
| 169 | + } finally { |
| 170 | + conn.disconnect(); |
| 171 | + } |
| 172 | + } |
| 173 | + |
| 174 | + /** |
| 175 | + * Convert a Map to a minimally escaped JSON string. This avoids external libs. |
| 176 | + * This simple serializer supports string/number/boolean values and nested maps. |
| 177 | + * |
| 178 | + * Note: For complex payloads, prefer a proper JSON library (Gson/Jackson). |
| 179 | + * |
| 180 | + * @param map data map |
| 181 | + * @return JSON string |
| 182 | + */ |
| 183 | + private static String toJson(Map<String, Object> map) { |
| 184 | + StringBuilder sb = new StringBuilder(); |
| 185 | + sb.append("{"); |
| 186 | + boolean first = true; |
| 187 | + for (Map.Entry<String, Object> e : map.entrySet()) { |
| 188 | + if (!first) sb.append(","); |
| 189 | + first = false; |
| 190 | + sb.append("\"").append(escapeJson(e.getKey())).append("\":"); |
| 191 | + sb.append(valueToJson(e.getValue())); |
| 192 | + } |
| 193 | + sb.append("}"); |
| 194 | + return sb.toString(); |
| 195 | + } |
| 196 | + |
| 197 | + private static String valueToJson(Object val) { |
| 198 | + if (val == null) return "null"; |
| 199 | + if (val instanceof Number || val instanceof Boolean) return val.toString(); |
| 200 | + if (val instanceof Map) return toJson((Map<String, Object>) val); |
| 201 | + return "\"" + escapeJson(val.toString()) + "\""; |
| 202 | + } |
| 203 | + |
| 204 | + private static String escapeJson(String s) { |
| 205 | + return s.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r"); |
| 206 | + } |
| 207 | + |
| 208 | + private static String readStream(InputStream is) { |
| 209 | + if (is == null) return ""; |
| 210 | + try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) { |
| 211 | + StringBuilder out = new StringBuilder(); |
| 212 | + String line; |
| 213 | + while ((line = br.readLine()) != null) { |
| 214 | + out.append(line); |
| 215 | + } |
| 216 | + return out.toString(); |
| 217 | + } catch (IOException e) { |
| 218 | + return ""; |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + /** |
| 223 | + * Try to load an existing HWID from hwid.txt or create a new UUID and persist it. |
| 224 | + * |
| 225 | + * @return UUID used as hwid |
| 226 | + */ |
| 227 | + private UUID loadOrCreateHwid() { |
| 228 | + File file = new File(dataFolder, HWID_FILENAME); |
| 229 | + if (file.exists()) { |
| 230 | + try (BufferedReader br = new BufferedReader(new FileReader(file))) { |
| 231 | + String line = br.readLine(); |
| 232 | + if (line != null && !line.trim().isEmpty()) { |
| 233 | + try { |
| 234 | + return UUID.fromString(line.trim()); |
| 235 | + } catch (IllegalArgumentException ignore) { |
| 236 | + // fall through -> regenerate |
| 237 | + } |
| 238 | + } |
| 239 | + } catch (IOException ignored) { |
| 240 | + // ignore and generate new |
| 241 | + } |
| 242 | + } |
| 243 | + |
| 244 | + UUID newId = UUID.randomUUID(); |
| 245 | + try (BufferedWriter bw = new BufferedWriter(new FileWriter(file))) { |
| 246 | + bw.write(newId.toString()); |
| 247 | + } catch (IOException e) { |
| 248 | + plugin.getLogger().warning("Failed to persist hwid to file: " + e.getMessage()); |
| 249 | + } |
| 250 | + return newId; |
| 251 | + } |
| 252 | +} |
0 commit comments