diff --git a/core/src/main/java/dev/faststats/core/Metrics.java b/core/src/main/java/dev/faststats/core/Metrics.java index 01eb2d6..ccd26d5 100644 --- a/core/src/main/java/dev/faststats/core/Metrics.java +++ b/core/src/main/java/dev/faststats/core/Metrics.java @@ -146,7 +146,7 @@ interface Factory { * * @since 0.1.0 */ - interface Config { + sealed interface Config permits SimpleMetrics.Config { /** * The server id. * diff --git a/core/src/main/java/dev/faststats/core/SimpleMetrics.java b/core/src/main/java/dev/faststats/core/SimpleMetrics.java index 8d98da3..f8f4412 100644 --- a/core/src/main/java/dev/faststats/core/SimpleMetrics.java +++ b/core/src/main/java/dev/faststats/core/SimpleMetrics.java @@ -60,14 +60,14 @@ public abstract class SimpleMetrics implements Metrics { @Contract(mutates = "io") @SuppressWarnings("PatternValidation") - protected SimpleMetrics(Factory factory, Path config) throws IllegalStateException { + protected SimpleMetrics(Factory factory, Config config) throws IllegalStateException { if (factory.token == null) throw new IllegalStateException("Token must be specified"); - this.config = new Config(config); - this.charts = this.config.additionalMetrics ? Set.copyOf(factory.charts) : Set.of(); - this.debug = factory.debug || Boolean.getBoolean("faststats.debug") || this.config.debug(); + this.config = config; + this.charts = config.additionalMetrics ? Set.copyOf(factory.charts) : Set.of(); + this.debug = factory.debug || Boolean.getBoolean("faststats.debug") || config.debug(); this.token = factory.token; - this.tracker = this.config.errorTracking ? factory.tracker : null; + this.tracker = config.errorTracking ? factory.tracker : null; this.url = factory.url; Runtime.getRuntime().addShutdownHook(new Thread(() -> { @@ -80,6 +80,11 @@ protected SimpleMetrics(Factory factory, Path config) throws IllegalStateExce }, "metrics-shutdown-thread " + getClass().getName())); } + @Contract(mutates = "io") + protected SimpleMetrics(Factory factory, Path config) throws IllegalStateException { + this(factory, Config.read(config)); + } + @VisibleForTesting protected SimpleMetrics(Config config, Set> charts, @Token String token, @Nullable ErrorTracker tracker, URI url, boolean debug) { if (!token.matches(Token.PATTERN)) { @@ -339,21 +344,43 @@ public Metrics.Factory url(URI url) { } } - protected static final class Config implements Metrics.Config { - private final UUID serverId; - private final boolean additionalMetrics; - private final boolean debug; - private final boolean enabled; - private final boolean errorTracking; - private final boolean firstRun; + public record Config( + UUID serverId, + boolean additionalMetrics, + boolean debug, + boolean enabled, + boolean errorTracking, + boolean firstRun + ) implements Metrics.Config { + + public static final String DEFAULT_COMMENT = """ + FastStats (https://faststats.dev) collects anonymous usage statistics for plugin developers. + # This helps developers understand how their projects are used in the real world. + # + # No IP addresses, player data, or personal information is collected. + # The server ID below is randomly generated and can be regenerated at any time. + # + # Enabling metrics has no noticeable performance impact. + # Keeping metrics enabled is recommended, but you can disable them by setting 'enabled=false'. + # + # If you suspect a plugin is collecting personal data or bypassing the "enabled" option, + # please report it at: https://faststats.dev/abuse + # + # For more information, visit: https://faststats.dev/info + """; @Contract(mutates = "io") - protected Config(Path file) { + public static Config read(Path file) throws RuntimeException { + return read(file, DEFAULT_COMMENT, false, false); + } + + @Contract(mutates = "io") + public static Config read(Path file, String comment, boolean externallyManaged, boolean externallyEnabled) throws RuntimeException { var properties = readOrEmpty(file); - this.firstRun = properties.isEmpty(); - var saveConfig = new AtomicBoolean(this.firstRun); + var firstRun = properties.isEmpty(); + var saveConfig = new AtomicBoolean(firstRun); - this.serverId = properties.map(object -> object.getProperty("serverId")).map(string -> { + var serverId = properties.map(object -> object.getProperty("serverId")).map(string -> { try { var trimmed = string.trim(); var corrected = trimmed.length() > 36 ? trimmed.substring(0, 36) : trimmed; @@ -375,54 +402,21 @@ protected Config(Path file) { }); }; - this.enabled = predicate.test("enabled", true); - this.errorTracking = predicate.test("submitErrors", true); - this.additionalMetrics = predicate.test("submitAdditionalMetrics", true); - this.debug = predicate.test("debug", false); + var enabled = externallyManaged ? externallyEnabled : predicate.test("enabled", true); + var errorTracking = predicate.test("submitErrors", true); + var additionalMetrics = predicate.test("submitAdditionalMetrics", true); + var debug = predicate.test("debug", false); if (saveConfig.get()) try { - save(file, serverId, enabled, errorTracking, additionalMetrics, debug); + save(file, externallyManaged, comment, serverId, enabled, errorTracking, additionalMetrics, debug); } catch (IOException e) { throw new RuntimeException("Failed to save metrics config", e); } - } - @VisibleForTesting - public Config(UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) { - this.serverId = serverId; - this.enabled = enabled; - this.debug = debug; - this.errorTracking = errorTracking; - this.additionalMetrics = additionalMetrics; - this.firstRun = false; - } - - @Override - public UUID serverId() { - return serverId; - } - - @Override - public boolean enabled() { - return enabled; - } - - @Override - public boolean errorTracking() { - return errorTracking; - } - - @Override - public boolean additionalMetrics() { - return additionalMetrics; - } - - @Override - public boolean debug() { - return debug; + return new Config(serverId, additionalMetrics, debug, enabled, errorTracking, firstRun); } - private static Optional readOrEmpty(Path file) { + private static Optional readOrEmpty(Path file) throws RuntimeException { if (!Files.isRegularFile(file)) return Optional.empty(); try (var reader = Files.newBufferedReader(file, UTF_8)) { var properties = new Properties(); @@ -433,33 +427,18 @@ private static Optional readOrEmpty(Path file) { } } - private static void save(Path file, UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) throws IOException { + private static void save(Path file, boolean externallyManaged, String comment, UUID serverId, boolean enabled, boolean errorTracking, boolean additionalMetrics, boolean debug) throws IOException { Files.createDirectories(file.getParent()); try (var out = Files.newOutputStream(file); var writer = new OutputStreamWriter(out, UTF_8)) { var properties = new Properties(); properties.setProperty("serverId", serverId.toString()); - properties.setProperty("enabled", Boolean.toString(enabled)); + if (!externallyManaged) properties.setProperty("enabled", Boolean.toString(enabled)); properties.setProperty("submitErrors", Boolean.toString(errorTracking)); properties.setProperty("submitAdditionalMetrics", Boolean.toString(additionalMetrics)); properties.setProperty("debug", Boolean.toString(debug)); - var comment = """ - FastStats (https://faststats.dev) collects anonymous usage statistics for plugin developers. - # This helps developers understand how their projects are used in the real world. - # - # No IP addresses, player data, or personal information is collected. - # The server ID below is randomly generated and can be regenerated at any time. - # - # Enabling metrics has no noticeable performance impact. - # Keeping metrics enabled is recommended, but you can disable them by setting 'enabled=false'. - # - # If you suspect a plugin is collecting personal data or bypassing the "enabled" option, - # please report it at: https://faststats.dev/abuse - # - # For more information, visit: https://faststats.dev/info - """; properties.store(writer, comment); } } diff --git a/core/src/test/java/dev/faststats/MockMetrics.java b/core/src/test/java/dev/faststats/MockMetrics.java index 174b9d6..a1dabf1 100644 --- a/core/src/test/java/dev/faststats/MockMetrics.java +++ b/core/src/test/java/dev/faststats/MockMetrics.java @@ -16,7 +16,7 @@ @NullMarked public class MockMetrics extends SimpleMetrics { public MockMetrics(UUID serverId, @Token String token, @Nullable ErrorTracker tracker, boolean debug) { - super(new SimpleMetrics.Config(serverId, true, true, true, debug), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug); + super(new Config(serverId, true, debug, true, true, false), Set.of(), token, tracker, URI.create("http://localhost:5000/v1/collect"), debug); } @Override diff --git a/gradle.properties b/gradle.properties index 94b3b6e..753eb4c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=0.11.1 +version=0.12.0 diff --git a/settings.gradle.kts b/settings.gradle.kts index 4546526..fdff68c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,4 +11,5 @@ include("hytale") include("hytale:example-plugin") include("minestom") include("nukkit") +include("sponge") include("velocity") \ No newline at end of file diff --git a/sponge/build.gradle.kts b/sponge/build.gradle.kts new file mode 100644 index 0000000..68990e6 --- /dev/null +++ b/sponge/build.gradle.kts @@ -0,0 +1,10 @@ +val moduleName by extra("dev.faststats.sponge") + +repositories { + maven("https://repo.spongepowered.org/repository/maven-public/") +} + +dependencies { + api(project(":core")) + compileOnly("org.spongepowered:spongeapi:8.0.0") +} diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java new file mode 100644 index 0000000..a8c4047 --- /dev/null +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetrics.java @@ -0,0 +1,30 @@ +package dev.faststats.sponge; + +import com.google.inject.Inject; +import dev.faststats.core.Metrics; +import org.apache.logging.log4j.Logger; +import org.spongepowered.api.config.ConfigDir; + +import java.nio.file.Path; + +/** + * Sponge metrics implementation. + * + * @since 0.12.0 + */ +public sealed interface SpongeMetrics extends Metrics permits SpongeMetricsImpl { + final class Factory extends SpongeMetricsImpl.Factory { + /** + * Creates a new metrics factory for Sponge. + * + * @param logger the logger + * @param dataDirectory the data directory + * @apiNote This instance is automatically injected into your plugin. + * @since 0.12.0 + */ + @Inject + private Factory(Logger logger, @ConfigDir(sharedRoot = true) Path dataDirectory) { + super(logger, dataDirectory); + } + } +} diff --git a/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java new file mode 100644 index 0000000..6144ad5 --- /dev/null +++ b/sponge/src/main/java/dev/faststats/sponge/SpongeMetricsImpl.java @@ -0,0 +1,93 @@ +package dev.faststats.sponge; + +import com.google.gson.JsonObject; +import dev.faststats.core.Metrics; +import dev.faststats.core.SimpleMetrics; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Async; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.Nullable; +import org.spongepowered.api.Platform; +import org.spongepowered.api.Sponge; +import org.spongepowered.api.config.ConfigDir; +import org.spongepowered.plugin.PluginContainer; + +import java.nio.file.Path; + +final class SpongeMetricsImpl extends SimpleMetrics implements SpongeMetrics { + public static final String COMMENT = """ + FastStats (https://faststats.dev) collects anonymous usage statistics for plugin developers. + # This helps developers understand how their projects are used in the real world. + # + # No IP addresses, player data, or personal information is collected. + # The server ID below is randomly generated and can be regenerated at any time. + # + # Enabling metrics has no noticeable performance impact. + # Enabling metrics is recommended, you can do so in the Sponge config. + # + # If you suspect a plugin is collecting personal data or bypassing the Sponge config, + # please report it at: https://faststats.dev/abuse + # + # For more information, visit: https://faststats.dev/info + """; + + private final Logger logger; + private final PluginContainer plugin; + + @Async.Schedule + @Contract(mutates = "io") + private SpongeMetricsImpl( + SimpleMetrics.Factory factory, + Logger logger, + PluginContainer plugin, + Path config + ) throws IllegalStateException { + super(factory, SimpleMetrics.Config.read(config, COMMENT, true, Sponge.metricsConfigManager() + .effectiveCollectionState(plugin).asBoolean())); + + this.logger = logger; + this.plugin = plugin; + + startSubmitting(); + } + + @Override + protected void appendDefaultData(JsonObject charts) { + charts.addProperty("online_mode", Sponge.server().isOnlineModeEnabled()); + charts.addProperty("player_count", Sponge.server().onlinePlayers().size()); + charts.addProperty("plugin_version", plugin.metadata().version().toString()); + charts.addProperty("minecraft_version", Sponge.platform().minecraftVersion().name()); + charts.addProperty("server_type", Sponge.platform().container(Platform.Component.IMPLEMENTATION).metadata().id()); + } + + @Override + protected void printError(String message, @Nullable Throwable throwable) { + logger.error(message, throwable); + } + + @Override + protected void printInfo(String message) { + logger.info(message); + } + + @Override + protected void printWarning(String message) { + logger.warn(message); + } + + static class Factory extends SimpleMetrics.Factory { + protected final Logger logger; + protected final Path dataDirectory; + + public Factory(Logger logger, @ConfigDir(sharedRoot = true) Path dataDirectory) { + this.logger = logger; + this.dataDirectory = dataDirectory; + } + + @Override + public Metrics create(PluginContainer plugin) throws IllegalStateException, IllegalArgumentException { + var faststats = dataDirectory.resolveSibling("faststats"); + return new SpongeMetricsImpl(this, logger, plugin, faststats.resolve("config.properties")); + } + } +} diff --git a/sponge/src/main/java/module-info.java b/sponge/src/main/java/module-info.java new file mode 100644 index 0000000..b01a156 --- /dev/null +++ b/sponge/src/main/java/module-info.java @@ -0,0 +1,13 @@ +import org.jspecify.annotations.NullMarked; + +@NullMarked +module dev.faststats.sponge { + exports dev.faststats.sponge; + + requires com.google.gson; + requires com.google.guice; + requires dev.faststats.core; + + requires static org.jetbrains.annotations; + requires static org.jspecify; +} \ No newline at end of file