diff --git a/api/src/main/java/org/btuk/proxy/api/impl/StatsApiImpl.java b/api/src/main/java/org/btuk/proxy/api/impl/StatsApiImpl.java new file mode 100644 index 0000000..a45a6a2 --- /dev/null +++ b/api/src/main/java/org/btuk/proxy/api/impl/StatsApiImpl.java @@ -0,0 +1,71 @@ +package org.btuk.proxy.api.impl; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.btuk.proxy.api.model.PlayerBaseStats; +import org.btuk.proxy.api.model.TotalBaseStats; +import org.btuk.proxy.database.sql.GlobalSQL; +import org.btuk.proxy.database.sql.PlotSQL; + +@Path("/stats") +@Produces(MediaType.APPLICATION_JSON) +public class StatsApiImpl { + + private final GlobalSQL globalSQL; + private final PlotSQL plotSQL; + + public StatsApiImpl(@Context GlobalSQL globalSQL, @Context PlotSQL plotSQL) { + this.globalSQL = globalSQL; + this.plotSQL = plotSQL; + } + + @GET + @Path("/total") + public Response getTotalStats() { + try { + // Fetch total stats from your database layer + org.btuk.proxy.database.dto.TotalBaseStats stats = globalSQL.getTotalBaseStats(); + if (stats == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(stats).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } + + @GET + @Path("/player") + public Response getPlayerStats(@QueryParam("uuid") String uuid) { + if (uuid == null || uuid.isBlank()) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("Player UUID is required") + .build(); + } + + try { + // Fetch individual player stats from your database layer + org.btuk.proxy.database.dto.PlayerBaseStats stats = globalSQL.getPlayerBaseStats(uuid); + if (stats == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + int reviews = plotSQL.getPlayerTotalReviews(uuid); + PlayerBaseStats combinedStats = new PlayerBaseStats(); + + combinedStats.buildings(stats.buildings()); + combinedStats.tplls(stats.tplls()); + combinedStats.messagesSent(stats.messagesSent()); + combinedStats.reviewsCompleted(reviews); + combinedStats.timePlayed(stats.timePlayed()); + + return Response.ok(combinedStats).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build(); + } + } +} \ No newline at end of file diff --git a/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java b/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java index 5723b78..60eed29 100644 --- a/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java +++ b/api/src/main/java/org/btuk/proxy/api/server/ProxyApi.java @@ -3,10 +3,12 @@ import lombok.extern.java.Log; import org.btuk.proxy.api.impl.BuildingsApiImpl; import org.btuk.proxy.api.impl.PlayerApiImpl; +import org.btuk.proxy.api.impl.StatsApiImpl; import org.btuk.proxy.api.impl.StatusApiImpl; import org.btuk.proxy.core.chat.ChatManager; import org.btuk.proxy.database.sql.GlobalSQL; +import org.btuk.proxy.database.sql.PlotSQL; import org.glassfish.grizzly.http.server.HttpServer; import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; import org.glassfish.jersey.internal.inject.AbstractBinder; @@ -22,13 +24,15 @@ public class ProxyApi { private final int port; private final GlobalSQL globalSQL; private final ChatManager chatManager; + private final PlotSQL plotSQL; - public ProxyApi(boolean enabled, int port, GlobalSQL globalSQL, ChatManager chatManager) { + public ProxyApi(boolean enabled, int port, GlobalSQL globalSQL, ChatManager chatManager, PlotSQL plotSQL) { this.enabled = enabled; this.port = port; this.globalSQL = globalSQL; this.chatManager = chatManager; + this.plotSQL = plotSQL; } public void start() { @@ -56,13 +60,14 @@ public void start() { protected void configure() { bind(globalSQL).to(GlobalSQL.class); bind(chatManager).to(ChatManager.class); + bind(plotSQL).to(PlotSQL.class); } }); - // 3. Register the implementation CLASSES (or scan the package) rc.register(StatusApiImpl.class); rc.register(PlayerApiImpl.class); rc.register(BuildingsApiImpl.class); + rc.register(StatsApiImpl.class); // ResourceConfig rc = new ResourceConfig() // .property("jersey.config.server.wadl.disableWadl", true) diff --git a/api/src/main/resources/openapi.yaml b/api/src/main/resources/openapi.yaml index 3eb16fe..7348f37 100644 --- a/api/src/main/resources/openapi.yaml +++ b/api/src/main/resources/openapi.yaml @@ -272,6 +272,41 @@ paths: schema: $ref: '#/components/schemas/BuildingGridResponse' + /stats/total: + get: + tags: + - Stats + summary: Get overall base statistics + operationId: getTotalStats + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/TotalBaseStats' + + /stats/player: + get: + tags: + - Stats + summary: Get individual player base statistics + operationId: getPlayerStats + parameters: + - name: uuid + in: query + required: false + description: The UUID of the player. + schema: + type: string + format: uuid + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/PlayerBaseStats' components: schemas: Status: @@ -367,4 +402,27 @@ components: cells: type: array items: - $ref: '#/components/schemas/GridCell' \ No newline at end of file + $ref: '#/components/schemas/GridCell' + TotalBaseStats: + type: object + properties: + buildings: + type: integer + recentBuildings: + type: integer + previousRecentBuildings: + type: integer + + PlayerBaseStats: + type: object + properties: + buildings: + type: integer + tplls: + type: integer + timePlayed: + type: integer + messagesSent: + type: integer + reviewsCompleted: + type: integer \ No newline at end of file diff --git a/app/src/main/java/org.btuk.proxy.app/ProxyController.java b/app/src/main/java/org.btuk.proxy.app/ProxyController.java index b7e9aab..04bb749 100644 --- a/app/src/main/java/org.btuk.proxy.app/ProxyController.java +++ b/app/src/main/java/org.btuk.proxy.app/ProxyController.java @@ -126,7 +126,7 @@ public void start(ChatHandler chatHandler, Scheduler scheduler, CoreServerManage new ReviewStatus(config, globalSQL, plotSQL, regionSQL, discord, scheduler); this.discord.addJDAEventListeners(chatManager, coreUserManager, tabManager, plotSQL); - this.proxyApi = new ProxyApi(config.getBoolean("api.enabled"), config.getInt("api.port"), globalSQL, chatManager); + this.proxyApi = new ProxyApi(config.getBoolean("api.enabled"), config.getInt("api.port"), globalSQL, chatManager,plotSQL); serverManager.initOnlineServers(); socketInitializer.accept(new ProxySocketHandler(chatManager, discord, userManager, serverManager)); diff --git a/database/src/main/java/org/btuk/proxy/database/dto/PlayerBaseStats.java b/database/src/main/java/org/btuk/proxy/database/dto/PlayerBaseStats.java new file mode 100644 index 0000000..a03917c --- /dev/null +++ b/database/src/main/java/org/btuk/proxy/database/dto/PlayerBaseStats.java @@ -0,0 +1,8 @@ +package org.btuk.proxy.database.dto; + +public record PlayerBaseStats( + int buildings, + int tplls, + int timePlayed, + int messagesSent +) {} \ No newline at end of file diff --git a/database/src/main/java/org/btuk/proxy/database/dto/TotalBaseStats.java b/database/src/main/java/org/btuk/proxy/database/dto/TotalBaseStats.java new file mode 100644 index 0000000..db1167b --- /dev/null +++ b/database/src/main/java/org/btuk/proxy/database/dto/TotalBaseStats.java @@ -0,0 +1,7 @@ +package org.btuk.proxy.database.dto; + +public record TotalBaseStats( + int buildings, + int recentBuildings, + int previousRecentBuildings +) {} \ No newline at end of file diff --git a/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java b/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java index 9ee7513..49a2fc4 100644 --- a/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java +++ b/database/src/main/java/org/btuk/proxy/database/sql/GlobalSQL.java @@ -6,6 +6,8 @@ import org.btuk.proxy.database.dto.BuildingDTO; import org.btuk.proxy.database.dto.GridCellDTO; import org.btuk.proxy.database.dto.PlayerDTO; +import org.btuk.proxy.database.dto.TotalBaseStats; +import org.btuk.proxy.database.dto.PlayerBaseStats; import javax.sql.DataSource; import java.sql.Connection; @@ -436,4 +438,73 @@ public List getBuildingGridCounts(double minLat, double maxLat, dou } return cells; } + + /** + * Retrieves overall base statistics. + * Counts total buildings and recent buildings added within the last 30 days. + */ + public TotalBaseStats getTotalBaseStats() { + final String sql = """ + SELECT + (SELECT COUNT(*) FROM buildings) AS total_buildings, + (SELECT COUNT(*) FROM buildings WHERE time_added >= NOW() - INTERVAL 30 DAY) AS recent_buildings, + (SELECT COUNT(*) FROM buildings WHERE time_added < NOW() - INTERVAL 30 DAY AND time_added >= NOW() - INTERVAL 60 DAY) AS previous_buildings; + """; + + try (Connection conn = conn(); + PreparedStatement statement = conn.prepareStatement(sql); + ResultSet results = statement.executeQuery()) { + + if (results.next()) { + return new TotalBaseStats( + results.getInt("total_buildings"), + results.getInt("recent_buildings"), + results.getInt("previous_buildings") + ); + } + } catch (SQLException e) { + log.severe("Failed to fetch total base stats: " + e.getMessage()); + } + return new TotalBaseStats(0, 0,0); + } + + /** + * Retrieves aggregated statistics for a specific player by UUID. + */ + public PlayerBaseStats getPlayerBaseStats(String uuid) { + if (uuid == null || uuid.isBlank()) { + log.warning("getPlayerBaseStats called with null or empty uuid"); + return null; + } + + final String sql = """ + SELECT + (SELECT COUNT(*) FROM buildings WHERE player_id = ?) AS buildings, + (SELECT COALESCE(SUM(tpll), 0) FROM statistics WHERE uuid = ?) AS tplls, + (SELECT COALESCE(SUM(playtime), 0) FROM statistics WHERE uuid = ?) AS time_played, + (SELECT COALESCE(SUM(messages), 0) FROM statistics WHERE uuid = ?) AS messages_sent; + """; + + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement(sql)) { + statement.setString(1, uuid); + statement.setString(2, uuid); + statement.setString(3, uuid); + statement.setString(4, uuid); + + try (ResultSet results = statement.executeQuery()) { + if (results.next()) { + return new PlayerBaseStats( + results.getInt("buildings"), + results.getInt("tplls"), + results.getInt("time_played"), + results.getInt("messages_sent") + ); + } + } + } catch (SQLException e) { + log.severe("Failed to fetch player base stats for " + uuid + ": " + e.getMessage()); + } + return null; + } + } diff --git a/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java b/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java index a277f92..6d80c3a 100644 --- a/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java +++ b/database/src/main/java/org/btuk/proxy/database/sql/PlotSQL.java @@ -187,4 +187,26 @@ private int[][] getOldPlotCorners(int[][] corners, int plotID) { return corners; } } + + public int getPlayerTotalReviews(String uuid){ + if (uuid == null) { + log.warning("getPlayerTotalReviews called with null uuid"); + return 0; + } + + final String sql = "SELECT COUNT(*) FROM plot_review WHERE reviewer = ?;"; + + try (Connection conn = conn(); PreparedStatement statement = conn.prepareStatement(sql)) { + statement.setString(1, uuid); + + try (ResultSet results = statement.executeQuery()) { + if (results.next()) { + return results.getInt(1); + } + } + } catch (SQLException e) { + log.severe("An error occurred while fetching total reviews for " + uuid + ": " + e.getMessage()); + } + return 0; + } }