From f579e654628cf1882df5528280e9694f74a6cda7 Mon Sep 17 00:00:00 2001 From: Telesphoreo Date: Sun, 26 May 2024 13:16:46 -0500 Subject: [PATCH] add storage for reports --- build.gradle.kts | 3 + gradle/libs.versions.toml | 4 + src/main/java/dev/plex/medina/Medina.java | 16 ++- .../java/dev/plex/medina/data/Report.java | 38 +++++++ .../plex/medina/storage/SQLConnection.java | 104 ++++++++++++++++++ .../dev/plex/medina/storage/SQLReports.java | 101 +++++++++++++++++ .../medina/storage/annotation/PrimaryKey.java | 13 +++ .../medina/storage/annotation/TableName.java | 13 +++ .../dev/plex/medina/util/MedinaUtils.java | 25 +++++ .../util/adapter/ZonedDateTimeAdapter.java | 27 +++++ src/main/resources/config.yml | 13 ++- 11 files changed, 350 insertions(+), 7 deletions(-) create mode 100644 src/main/java/dev/plex/medina/data/Report.java create mode 100644 src/main/java/dev/plex/medina/storage/SQLConnection.java create mode 100644 src/main/java/dev/plex/medina/storage/SQLReports.java create mode 100644 src/main/java/dev/plex/medina/storage/annotation/PrimaryKey.java create mode 100644 src/main/java/dev/plex/medina/storage/annotation/TableName.java create mode 100644 src/main/java/dev/plex/medina/util/adapter/ZonedDateTimeAdapter.java diff --git a/build.gradle.kts b/build.gradle.kts index feace10..a97d3e2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,8 +26,11 @@ java { } dependencies { + library(libs.lombok) + library(libs.hikari) compileOnly(libs.paperApi) implementation(libs.bundles.bstats) { isTransitive = false } + annotationProcessor(libs.lombok) } tasks { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f6e85d7..f625a7c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,11 +1,15 @@ [versions] paper = "1.20.4-R0.1-SNAPSHOT" bstats = "3.0.2" +lombok = "1.18.32" +hikari = "5.1.0" [libraries] paperApi = { group = "io.papermc.paper", name = "paper-api", version.ref = "paper" } bstatsBase = { group = "org.bstats", name = "bstats-base", version.ref = "bstats" } bstatsBukkit = { group = "org.bstats", name = "bstats-bukkit", version.ref = "bstats" } +lombok = { group = "org.projectlombok", name = "lombok", version.ref = "lombok" } +hikari = { group = "com.zaxxer", name = "HikariCP", version.ref = "hikari" } [bundles] bstats = ["bstatsBase", "bstatsBukkit"] \ No newline at end of file diff --git a/src/main/java/dev/plex/medina/Medina.java b/src/main/java/dev/plex/medina/Medina.java index 2f110c6..342fd45 100644 --- a/src/main/java/dev/plex/medina/Medina.java +++ b/src/main/java/dev/plex/medina/Medina.java @@ -2,17 +2,24 @@ package dev.plex.medina; import dev.plex.medina.config.Config; import dev.plex.medina.registration.CommandRegistration; +import dev.plex.medina.storage.SQLConnection; +import dev.plex.medina.util.MedinaUtils; +import lombok.Getter; import org.bstats.bukkit.Metrics; import org.bukkit.plugin.java.JavaPlugin; public class Medina extends JavaPlugin { + @Getter private static Medina plugin; public Config config; public Config messages; + @Getter + private SQLConnection sqlConnection; + @Override public void onLoad() { @@ -30,12 +37,9 @@ public class Medina extends JavaPlugin // Metrics @ https://bstats.org/plugin/bukkit/Medina/22026 Metrics metrics = new Metrics(this, 22026); + sqlConnection = new SQLConnection(); + MedinaUtils.testConnection(); + new CommandRegistration(); } - - public static Medina getPlugin() - { - return plugin; - } - } diff --git a/src/main/java/dev/plex/medina/data/Report.java b/src/main/java/dev/plex/medina/data/Report.java new file mode 100644 index 0000000..7853bfe --- /dev/null +++ b/src/main/java/dev/plex/medina/data/Report.java @@ -0,0 +1,38 @@ +package dev.plex.medina.data; + +import com.google.gson.GsonBuilder; +import dev.plex.medina.storage.annotation.PrimaryKey; +import dev.plex.medina.storage.annotation.TableName; +import dev.plex.medina.util.adapter.ZonedDateTimeAdapter; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; + +import java.time.ZonedDateTime; +import java.util.UUID; + +@Data +@TableName("reports") +public class Report +{ + @PrimaryKey + private int reportId; // This will be automatically set from addReport + + @Getter + private final UUID reporterUUID; + private final String reporterName; + + private final UUID reportedUUID; + private final String reportedName; + + private final ZonedDateTime timestamp; + + private final String reason; + + private final boolean deleted; + + public String toJSON() + { + return new GsonBuilder().registerTypeAdapter(ZonedDateTime.class, new ZonedDateTimeAdapter()).create().toJson(this); + } +} diff --git a/src/main/java/dev/plex/medina/storage/SQLConnection.java b/src/main/java/dev/plex/medina/storage/SQLConnection.java new file mode 100644 index 0000000..0bc4b37 --- /dev/null +++ b/src/main/java/dev/plex/medina/storage/SQLConnection.java @@ -0,0 +1,104 @@ +package dev.plex.medina.storage; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import dev.plex.medina.MedinaBase; +import lombok.Getter; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +@Getter +public class SQLConnection implements MedinaBase +{ + private HikariDataSource dataSource; + + public SQLConnection() + { + String host = plugin.config.getString("database.hostname"); + int port = plugin.config.getInt("database.port"); + String username = plugin.config.getString("database.username"); + String password = plugin.config.getString("database.password"); + String database = plugin.config.getString("database.name"); + + HikariConfig config = new HikariConfig(); + config.addDataSourceProperty("cachePrepStmts", "true"); + config.addDataSourceProperty("prepStmtCacheSize", "250"); + config.addDataSourceProperty("prepStmtCacheSqlLimit", "2048"); + + this.dataSource = new HikariDataSource(); + dataSource.setMaxLifetime(15000); + dataSource.setIdleTimeout(15000 * 2); + dataSource.setConnectionTimeout(15000 * 4); + dataSource.setMinimumIdle(2); + dataSource.setMaximumPoolSize(10); + try + { + Class.forName("org.mariadb.jdbc.Driver"); + dataSource.setJdbcUrl("jdbc:mariadb://" + host + ":" + port + "/" + database); + dataSource.setUsername(username); + dataSource.setPassword(password); + } + catch (ClassNotFoundException throwables) + { + throwables.printStackTrace(); + } + + try (Connection con = getCon()) + { + con.prepareStatement("CREATE TABLE IF NOT EXISTS `reports` (" + + "`reportId` INT NOT NULL AUTOINCREMENT, " + + "`reporterUUID` VARCHAR(46) NOT NULL, " + + "`reporterName` VARCHAR(18), " + + "`reportedUUID` VARCHAR(46) NOT NULL, " + + "`reportedName` VARCHAR(18), " + + "`timestamp` BIGINT, " + + "`reason` VARCHAR(2000), " + + "`deleted` BOOLEAN, " + + "PRIMARY KEY (`reportId`));").execute(); + } + catch (SQLException throwables) + { + throwables.printStackTrace(); + } + } + + private boolean tableExistsSQL(String tableName) throws SQLException + { + try (Connection connection = getCon()) + { + PreparedStatement preparedStatement = connection.prepareStatement("SELECT count(*) " + + "FROM information_schema.tables " + + "WHERE table_name = ?" + + "LIMIT 1;"); + preparedStatement.setString(1, tableName); + + ResultSet resultSet = preparedStatement.executeQuery(); + resultSet.next(); + return resultSet.getInt(1) != 0; + } + catch (SQLException ignored) + { + return false; + } + } + + public Connection getCon() + { + if (this.dataSource == null) + { + return null; + } + try + { + return dataSource.getConnection(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + return null; + } +} diff --git a/src/main/java/dev/plex/medina/storage/SQLReports.java b/src/main/java/dev/plex/medina/storage/SQLReports.java new file mode 100644 index 0000000..eb8dfc2 --- /dev/null +++ b/src/main/java/dev/plex/medina/storage/SQLReports.java @@ -0,0 +1,101 @@ +package dev.plex.medina.storage; + +import com.google.common.collect.Lists; +import dev.plex.medina.Medina; +import dev.plex.medina.MedinaBase; +import dev.plex.medina.data.Report; +import dev.plex.medina.util.MedinaUtils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class SQLReports implements MedinaBase +{ + private static final String SELECT = "SELECT * FROM `reports` WHERE reportedUUID=?"; + private static final String INSERT = "INSERT INTO `reports` (`reportId`, `reporterUUID`, `reporterName`, `reportedUUID`, `reportedName`, `timestamp`, `reason`, `deleted`) VALUES(?, ?, ?, ?, ?, ?, ?, ?)"; + private static final String DELETE = "DELETE FROM `reports` WHERE reportId=? AND reportedUUID=?"; + + public CompletableFuture> getReports(UUID reportedUUID) + { + return CompletableFuture.supplyAsync(() -> + { + List reports = Lists.newArrayList(); + try (Connection con = plugin.getSqlConnection().getCon()) + { + PreparedStatement statement = con.prepareStatement(SELECT); + statement.setString(1, reportedUUID.toString()); + ResultSet set = statement.executeQuery(); + while (set.next()) + { + Report report = new Report( + UUID.fromString(set.getString("reporterUUID")), + set.getString("reporterName"), + reportedUUID, + set.getString("reportedName"), + ZonedDateTime.ofInstant(Instant.ofEpochMilli(set.getLong("timestamp")), ZoneId.of(MedinaUtils.TIMEZONE)), + set.getString("reason"), + set.getBoolean("deleted")); + reports.add(report); + } + } + catch (SQLException e) + { + e.printStackTrace(); + return reports; + } + return reports; + }); + } + + public CompletableFuture deleteReport(int reportId, UUID reportedUUID) + { + return CompletableFuture.runAsync(() -> + { + try (Connection con = plugin.getSqlConnection().getCon()) + { + PreparedStatement statement = con.prepareStatement(DELETE); + statement.setInt(1, reportId); + statement.setString(2, reportedUUID.toString()); + statement.execute(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + }); + } + + public CompletableFuture addReport(Report report) + { + return CompletableFuture.runAsync(() -> + { + getReports(report.getReportedUUID()).whenComplete((reports, throwable) -> + { + try (Connection con = plugin.getSqlConnection().getCon()) + { + PreparedStatement statement = con.prepareStatement(INSERT); + statement.setString(1, report.getReporterUUID().toString()); + statement.setString(2, report.getReporterName()); + statement.setString(3, report.getReportedUUID().toString()); + statement.setString(4, report.getReportedName()); + statement.setLong(5, report.getTimestamp().toInstant().toEpochMilli()); + statement.setString(6, report.getReason()); + statement.setBoolean(7, report.isDeleted()); + statement.execute(); + } + catch (SQLException e) + { + e.printStackTrace(); + } + }); + }); + } +} diff --git a/src/main/java/dev/plex/medina/storage/annotation/PrimaryKey.java b/src/main/java/dev/plex/medina/storage/annotation/PrimaryKey.java new file mode 100644 index 0000000..4c380c7 --- /dev/null +++ b/src/main/java/dev/plex/medina/storage/annotation/PrimaryKey.java @@ -0,0 +1,13 @@ +package dev.plex.medina.storage.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface PrimaryKey +{ + boolean dontSet() default false; +} \ No newline at end of file diff --git a/src/main/java/dev/plex/medina/storage/annotation/TableName.java b/src/main/java/dev/plex/medina/storage/annotation/TableName.java new file mode 100644 index 0000000..8d52a56 --- /dev/null +++ b/src/main/java/dev/plex/medina/storage/annotation/TableName.java @@ -0,0 +1,13 @@ +package dev.plex.medina.storage.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface TableName +{ + String value(); +} \ No newline at end of file diff --git a/src/main/java/dev/plex/medina/util/MedinaUtils.java b/src/main/java/dev/plex/medina/util/MedinaUtils.java index 924b9bf..c6796e7 100644 --- a/src/main/java/dev/plex/medina/util/MedinaUtils.java +++ b/src/main/java/dev/plex/medina/util/MedinaUtils.java @@ -1,14 +1,19 @@ package dev.plex.medina.util; +import dev.plex.medina.Medina; import dev.plex.medina.MedinaBase; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.minimessage.MiniMessage; import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; +import java.sql.Connection; +import java.sql.SQLException; + public class MedinaUtils implements MedinaBase { private static final MiniMessage MINI_MESSAGE = MiniMessage.miniMessage(); + public static String TIMEZONE = plugin.config.getString("timezone"); public static Component mmDeserialize(String input) { @@ -54,4 +59,24 @@ public class MedinaUtils implements MedinaBase } return f; } + + public static void testConnection() + { + MedinaLog.log("Attempting to connect to DB: {0}", plugin.config.getString("database.name")); + if (plugin.getSqlConnection().getDataSource() != null) + { + try (Connection ignored = plugin.getSqlConnection().getCon()) + { + MedinaLog.log("Connected to " + plugin.config.getString("database.name")); + } + catch (SQLException e) + { + MedinaLog.error("Failed to connect to " + plugin.config.getString("database.name")); + } + } + else + { + MedinaLog.error("Unable to initialize Hikari data source!"); + } + } } diff --git a/src/main/java/dev/plex/medina/util/adapter/ZonedDateTimeAdapter.java b/src/main/java/dev/plex/medina/util/adapter/ZonedDateTimeAdapter.java new file mode 100644 index 0000000..d6ca7ac --- /dev/null +++ b/src/main/java/dev/plex/medina/util/adapter/ZonedDateTimeAdapter.java @@ -0,0 +1,27 @@ +package dev.plex.medina.util.adapter; + +import com.google.gson.*; +import dev.plex.medina.Medina; + +import java.lang.reflect.Type; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; + +public class ZonedDateTimeAdapter implements JsonSerializer, JsonDeserializer +{ + private static final String TIMEZONE = Medina.getPlugin().config.getString("timezone"); + + @Override + public JsonElement serialize(ZonedDateTime src, Type typeOfSrc, JsonSerializationContext context) + { + return new JsonPrimitive(src.toInstant().toEpochMilli()); + } + + @Override + public ZonedDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException + { + Instant instant = Instant.ofEpochMilli(json.getAsJsonPrimitive().getAsLong()); + return ZonedDateTime.ofInstant(instant, ZoneId.of(TIMEZONE)); + } +} diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index eb0e276..10a18a9 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1 +1,12 @@ -# Medina Configuration File \ No newline at end of file +# Medina Configuration File + +# Database configuration +database: + name: medina + hostname: 127.0.0.1 + port: 3306 + username: minecraft + password: medina + +# The timezone the reports will show up as +timezone: Etc/UTC \ No newline at end of file