From c9f75fbcf2041e0f7184ba97e8e04f05ee62317f Mon Sep 17 00:00:00 2001 From: loki5512344 Date: Sun, 12 Jul 2026 13:17:42 +0200 Subject: [PATCH] feat: RP support, config-driven GUI, rich Discord embeds, i18n messages.yml --- build.gradle.kts | 1 + .../dev/loki/lorep/command/ReportCommand.java | 133 ++++-- .../lorep/command/ReportStatsCommand.java | 12 +- .../dev/loki/lorep/config/ConfigManager.java | 62 ++- .../dev/loki/lorep/config/GuiItemConfig.java | 42 ++ .../loki/lorep/database/DatabaseManager.java | 8 +- .../database/PostgreSQLDatabaseManager.java | 70 +++- .../java/dev/loki/lorep/database/Report.java | 8 +- .../lorep/database/SQLiteDatabaseManager.java | 67 ++- .../dev/loki/lorep/gui/GuiClickListener.java | 120 +++++- .../java/dev/loki/lorep/gui/ReportGui.java | 382 ++++++++++++++---- .../loki/lorep/webhook/DiscordWebhook.java | 198 +++++++-- src/main/resources/config.yml | 165 ++++++-- src/main/resources/messages.yml | 50 +++ src/main/resources/plugin.yml | 2 +- .../lorep/database/DatabaseManagerTest.java | 13 +- .../lorep/webhook/DiscordWebhookTest.java | 8 +- 17 files changed, 1106 insertions(+), 235 deletions(-) create mode 100644 src/main/java/dev/loki/lorep/config/GuiItemConfig.java create mode 100644 src/main/resources/messages.yml diff --git a/build.gradle.kts b/build.gradle.kts index 97ce7a3..c78b0c6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { // Paper API для 1.19.4+ compileOnly("io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT") + implementation("com.google.code.gson:gson:2.11.0") implementation("com.squareup.okhttp3:okhttp:4.12.0") implementation("com.zaxxer:HikariCP:6.2.1") implementation("org.postgresql:postgresql:42.7.4") diff --git a/src/main/java/dev/loki/lorep/command/ReportCommand.java b/src/main/java/dev/loki/lorep/command/ReportCommand.java index ee61ac5..4cdf8f3 100644 --- a/src/main/java/dev/loki/lorep/command/ReportCommand.java +++ b/src/main/java/dev/loki/lorep/command/ReportCommand.java @@ -16,127 +16,177 @@ import java.util.List; import java.util.stream.Collectors; public class ReportCommand implements CommandExecutor, TabCompleter { - + private final LorepPlugin plugin; - + public ReportCommand(LorepPlugin plugin) { this.plugin = plugin; } - + @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!(sender instanceof Player player)) { - sender.sendMessage("§cЭта команда только для игроков!"); + sender.sendMessage(plugin.getConfigManager().getMessage("console-only")); return true; } - + if (args.length == 0) { player.sendMessage(plugin.getConfigManager().getMessage("usage")); return true; } - - // Handle subcommands for admins + if (args[0].equalsIgnoreCase("gui")) { return handleGui(player); } - + if (args[0].equalsIgnoreCase("stats") && args.length >= 2) { return handleStats(player, args[1]); } - - // Regular report command + + if (args[0].equalsIgnoreCase("image") && args.length >= 3) { + return handleReportImage(player, args[1], args[2]); + } + if (!player.hasPermission("lorep.report")) { player.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; } - + if (args.length < 2) { player.sendMessage(plugin.getConfigManager().getMessage("usage")); return true; } - - String targetName = args[0]; - String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); - // Validate reason + String targetName = args[0]; + String reason; + String imageUrl = null; + + if (args.length >= 3 && args[2].startsWith("http")) { + reason = args[1]; + imageUrl = args[2]; + } else { + reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); + } + if (reason.trim().isEmpty()) { player.sendMessage(plugin.getConfigManager().getMessage("usage")); return true; } if (reason.length() > 500) { - player.sendMessage("§cПричина слишком длинная! Максимум 500 символов."); + player.sendMessage(plugin.getConfigManager().getMessage("reason-too-long", "%max%", "500")); return true; } - return handleReport(player, targetName, reason); + return handleReport(player, targetName, reason, imageUrl); } - + private boolean handleReport(Player reporter, String targetName, String reason) { - // Check if target exists + return handleReport(reporter, targetName, reason, null); + } + + private boolean handleReport(Player reporter, String targetName, String reason, String imageUrl) { @SuppressWarnings("deprecation") OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); - + if (!target.hasPlayedBefore() && !target.isOnline()) { reporter.sendMessage(plugin.getConfigManager().getMessage("player-not-found")); return true; } - - // Check self-report + if (target.getUniqueId().equals(reporter.getUniqueId())) { reporter.sendMessage(plugin.getConfigManager().getMessage("self-report")); return true; } - - // Check duplicate report + if (plugin.getDatabaseManager().hasReported(reporter.getUniqueId(), target.getUniqueId())) { reporter.sendMessage(plugin.getConfigManager().getMessage("already-reported")); return true; } - - // Create and save report + Report report = new Report( reporter.getUniqueId(), reporter.getName(), target.getUniqueId(), target.getName() != null ? target.getName() : targetName, - reason + reason, + imageUrl ); - + plugin.getDatabaseManager().saveReport(report); - - // Send to Discord webhook + if (plugin.getDiscordWebhook() != null) { plugin.getDiscordWebhook().sendReport(report); } - + reporter.sendMessage(plugin.getConfigManager().getMessage("report-sent")); return true; } + private boolean handleReportImage(Player player, String idStr, String imageUrl) { + if (!player.hasPermission("lorep.admin")) { + player.sendMessage(plugin.getConfigManager().getMessage("no-permission")); + return true; + } + + int reportId; + try { + reportId = Integer.parseInt(idStr); + } catch (NumberFormatException e) { + player.sendMessage(plugin.getConfigManager().getMessage("invalid-report-id")); + return true; + } + + Report report = plugin.getDatabaseManager().getReportById(reportId); + if (report == null) { + player.sendMessage(plugin.getConfigManager().getMessage("report-not-found")); + return true; + } + + plugin.getDatabaseManager().updateReportImage(reportId, imageUrl); + + Report updated = new Report( + report.id(), + report.reporterUuid(), + report.reporterName(), + report.targetUuid(), + report.targetName(), + report.reason(), + imageUrl, + report.status(), + report.createdAt() + ); + + if (plugin.getDiscordWebhook() != null) { + plugin.getDiscordWebhook().sendReport(updated); + } + + player.sendMessage(plugin.getConfigManager().getMessage("report-image-updated", "%id%", String.valueOf(reportId))); + return true; + } + private boolean handleGui(Player player) { if (!player.hasPermission("lorep.admin")) { player.sendMessage(plugin.getConfigManager().getMessage("no-permission")); return true; } - + new dev.loki.lorep.gui.ReportGui(plugin, 0).open(player); return true; } - + private boolean handleStats(Player player, String targetName) { new ReportStatsCommand(plugin).execute(player, targetName); return true; } - + @Override public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { List completions = new ArrayList<>(); - + if (args.length == 1) { - // First argument: player names or subcommands String input = args[0].toLowerCase(); - + if (sender.hasPermission("lorep.admin")) { if ("gui".startsWith(input)) { completions.add("gui"); @@ -144,9 +194,11 @@ public class ReportCommand implements CommandExecutor, TabCompleter { if ("stats".startsWith(input)) { completions.add("stats"); } + if ("image".startsWith(input)) { + completions.add("image"); + } } - - // Add online player names + completions.addAll( Bukkit.getOnlinePlayers().stream() .map(Player::getName) @@ -154,7 +206,6 @@ public class ReportCommand implements CommandExecutor, TabCompleter { .collect(Collectors.toList()) ); } else if (args.length == 2 && args[0].equalsIgnoreCase("stats")) { - // Second argument for stats: player names String input = args[1].toLowerCase(); completions.addAll( Bukkit.getOnlinePlayers().stream() @@ -163,7 +214,7 @@ public class ReportCommand implements CommandExecutor, TabCompleter { .collect(Collectors.toList()) ); } - + return completions; } } diff --git a/src/main/java/dev/loki/lorep/command/ReportStatsCommand.java b/src/main/java/dev/loki/lorep/command/ReportStatsCommand.java index baea4c7..545cb2a 100644 --- a/src/main/java/dev/loki/lorep/command/ReportStatsCommand.java +++ b/src/main/java/dev/loki/lorep/command/ReportStatsCommand.java @@ -49,14 +49,13 @@ public class ReportStatsCommand { player.sendMessage(plugin.getConfigManager().getMessage("stats-count", "%count%", String.valueOf(reportCount))); - // Last online - String lastOnline = target.isOnline() ? "Сейчас онлайн" : - TimeUtil.formatTimeAgo(target.getLastPlayed()); + String lastOnline = target.isOnline() + ? plugin.getConfigManager().getMessage("online-status") + : TimeUtil.formatTimeAgo(target.getLastPlayed()); player.sendMessage(plugin.getConfigManager().getMessage("stats-last-online", "%time%", lastOnline)); - // Recent reports (max 5) - player.sendMessage("§7Последние репорты:"); + player.sendMessage(plugin.getConfigManager().getMessage("stats-recent-header")); int shown = Math.min(MAX_RECENT_REPORTS, reports.size()); for (int i = 0; i < shown; i++) { Report report = reports.get(i); @@ -67,7 +66,8 @@ public class ReportStatsCommand { } if (reports.size() > MAX_RECENT_REPORTS) { - player.sendMessage("§7... и ещё " + (reports.size() - MAX_RECENT_REPORTS) + " репортов"); + player.sendMessage(plugin.getConfigManager().getMessage("stats-more-reports", + "%count%", String.valueOf(reports.size() - MAX_RECENT_REPORTS))); } } } diff --git a/src/main/java/dev/loki/lorep/config/ConfigManager.java b/src/main/java/dev/loki/lorep/config/ConfigManager.java index c0ac690..e9f2fd2 100644 --- a/src/main/java/dev/loki/lorep/config/ConfigManager.java +++ b/src/main/java/dev/loki/lorep/config/ConfigManager.java @@ -2,12 +2,21 @@ package dev.loki.lorep.config; import dev.loki.lorep.LorepPlugin; import org.bukkit.ChatColor; +import org.bukkit.Material; +import org.bukkit.configuration.ConfigurationSection; import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.io.File; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; public class ConfigManager { private final LorepPlugin plugin; private FileConfiguration config; + private FileConfiguration messages; public ConfigManager(LorepPlugin plugin) { this.plugin = plugin; @@ -18,6 +27,15 @@ public class ConfigManager { plugin.saveDefaultConfig(); plugin.reloadConfig(); this.config = plugin.getConfig(); + loadMessages(); + } + + private void loadMessages() { + File messagesFile = new File(plugin.getDataFolder(), "messages.yml"); + if (!messagesFile.exists()) { + plugin.saveResource("messages.yml", false); + } + this.messages = YamlConfiguration.loadConfiguration(messagesFile); } public String getWebhookUrl() { @@ -57,10 +75,52 @@ public class ConfigManager { } public String getMessage(String key) { - String message = config.getString("messages." + key, "&cMessage not found: " + key); + String message = messages.getString(key); + if (message == null) { + message = config.getString("messages." + key); + } + if (message == null) { + message = "&cMessage not found: " + key; + } return ChatColor.translateAlternateColorCodes('&', message); } + public GuiItemConfig getGuiItem(String path) { + ConfigurationSection section = config.getConfigurationSection("gui.items." + path); + if (section == null) { + return null; + } + + Material material = Material.getMaterial(section.getString("material", "STONE").toUpperCase()); + if (material == null) { + material = Material.STONE; + } + + int customModelData = section.contains("custom-model-data") ? section.getInt("custom-model-data") : GuiItemConfig.NO_CUSTOM_MODEL; + + String displayName = section.contains("name") ? ChatColor.translateAlternateColorCodes('&', section.getString("name")) : null; + + List lore = section.contains("lore") + ? section.getStringList("lore").stream() + .map(line -> ChatColor.translateAlternateColorCodes('&', line)) + .collect(Collectors.toList()) + : Collections.emptyList(); + + List slots = section.contains("slots") ? section.getIntegerList("slots") : Collections.emptyList(); + + boolean glowing = section.getBoolean("glowing", false); + + return new GuiItemConfig(material, customModelData, displayName, lore, slots, glowing); + } + + public List getItemSlots(String path) { + GuiItemConfig config = getGuiItem(path); + if (config != null && !config.slots().isEmpty()) { + return config.slots(); + } + return Collections.emptyList(); + } + public String getMessage(String key, String... replacements) { String message = getMessage(key); if (replacements == null || replacements.length == 0) { diff --git a/src/main/java/dev/loki/lorep/config/GuiItemConfig.java b/src/main/java/dev/loki/lorep/config/GuiItemConfig.java new file mode 100644 index 0000000..e92433c --- /dev/null +++ b/src/main/java/dev/loki/lorep/config/GuiItemConfig.java @@ -0,0 +1,42 @@ +package dev.loki.lorep.config; + +import org.bukkit.Material; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.inventory.ItemFlag; +import org.bukkit.inventory.ItemStack; +import org.bukkit.inventory.meta.ItemMeta; + +import java.util.List; + +public record GuiItemConfig( + Material material, + int customModelData, + String displayName, + List lore, + List slots, + boolean glowing +) { + public static final int NO_CUSTOM_MODEL = -1; + + public ItemStack createItem() { + ItemStack item = new ItemStack(material); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + if (customModelData != NO_CUSTOM_MODEL) { + meta.setCustomModelData(customModelData); + } + if (displayName != null) { + meta.setDisplayName(displayName); + } + if (lore != null && !lore.isEmpty()) { + meta.setLore(lore); + } + if (glowing) { + meta.addEnchant(Enchantment.DURABILITY, 1, true); + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + } + item.setItemMeta(meta); + } + return item; + } +} diff --git a/src/main/java/dev/loki/lorep/database/DatabaseManager.java b/src/main/java/dev/loki/lorep/database/DatabaseManager.java index 2ecfeda..48de964 100644 --- a/src/main/java/dev/loki/lorep/database/DatabaseManager.java +++ b/src/main/java/dev/loki/lorep/database/DatabaseManager.java @@ -20,6 +20,12 @@ public interface DatabaseManager { boolean hasReported(UUID reporterUuid, UUID targetUuid); List getReportsPaginated(int page, int pageSize); - + int getTotalReportCount(); + + void updateReportStatus(int reportId, String status); + + void updateReportImage(int reportId, String imageUrl); + + Report getReportById(int id); } diff --git a/src/main/java/dev/loki/lorep/database/PostgreSQLDatabaseManager.java b/src/main/java/dev/loki/lorep/database/PostgreSQLDatabaseManager.java index b9d90cc..8f2314d 100644 --- a/src/main/java/dev/loki/lorep/database/PostgreSQLDatabaseManager.java +++ b/src/main/java/dev/loki/lorep/database/PostgreSQLDatabaseManager.java @@ -73,6 +73,8 @@ public class PostgreSQLDatabaseManager implements DatabaseManager { target_uuid TEXT NOT NULL, target_name TEXT NOT NULL, reason TEXT NOT NULL, + image_url TEXT, + status TEXT NOT NULL DEFAULT 'PENDING', created_at BIGINT NOT NULL ) """; @@ -98,7 +100,8 @@ public class PostgreSQLDatabaseManager implements DatabaseManager { @Override public void saveReport(Report report) { - String sql = "INSERT INTO reports (reporter_uuid, reporter_name, target_uuid, target_name, reason, created_at) VALUES (?, ?, ?, ?, ?, ?)"; + String sql = "INSERT INTO reports (reporter_uuid, reporter_name, target_uuid, " + + "target_name, reason, image_url, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) { @@ -107,7 +110,13 @@ public class PostgreSQLDatabaseManager implements DatabaseManager { stmt.setString(3, report.targetUuid().toString()); stmt.setString(4, report.targetName()); stmt.setString(5, report.reason()); - stmt.setLong(6, report.createdAt().toEpochMilli()); + if (report.imageUrl() != null) { + stmt.setString(6, report.imageUrl()); + } else { + stmt.setNull(6, java.sql.Types.VARCHAR); + } + stmt.setString(7, report.status()); + stmt.setLong(8, report.createdAt().toEpochMilli()); stmt.executeUpdate(); } catch (SQLException e) { logger.log(Level.SEVERE, "Failed to save report", e); @@ -230,6 +239,10 @@ public class PostgreSQLDatabaseManager implements DatabaseManager { } private Report mapReport(ResultSet rs) throws SQLException { + String imageUrl = rs.getString("image_url"); + if (rs.wasNull()) { + imageUrl = null; + } return new Report( rs.getInt("id"), UUID.fromString(rs.getString("reporter_uuid")), @@ -237,7 +250,60 @@ public class PostgreSQLDatabaseManager implements DatabaseManager { UUID.fromString(rs.getString("target_uuid")), rs.getString("target_name"), rs.getString("reason"), + imageUrl, + rs.getString("status"), Instant.ofEpochMilli(rs.getLong("created_at")) ); } + + @Override + public void updateReportStatus(int reportId, String status) { + String sql = "UPDATE reports SET status = ? WHERE id = ?"; + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setString(1, status); + stmt.setInt(2, reportId); + stmt.executeUpdate(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to update report status", e); + } + } + + @Override + public void updateReportImage(int reportId, String imageUrl) { + String sql = "UPDATE reports SET image_url = ? WHERE id = ?"; + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + if (imageUrl != null) { + stmt.setString(1, imageUrl); + } else { + stmt.setNull(1, java.sql.Types.VARCHAR); + } + stmt.setInt(2, reportId); + stmt.executeUpdate(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to update report image", e); + } + } + + @Override + public Report getReportById(int id) { + String sql = "SELECT * FROM reports WHERE id = ?"; + + try (Connection conn = dataSource.getConnection(); + PreparedStatement stmt = conn.prepareStatement(sql)) { + stmt.setInt(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return mapReport(rs); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get report by id", e); + } + + return null; + } } diff --git a/src/main/java/dev/loki/lorep/database/Report.java b/src/main/java/dev/loki/lorep/database/Report.java index e1ada60..643e281 100644 --- a/src/main/java/dev/loki/lorep/database/Report.java +++ b/src/main/java/dev/loki/lorep/database/Report.java @@ -10,9 +10,15 @@ public record Report( UUID targetUuid, String targetName, String reason, + String imageUrl, + String status, Instant createdAt ) { public Report(UUID reporterUuid, String reporterName, UUID targetUuid, String targetName, String reason) { - this(0, reporterUuid, reporterName, targetUuid, targetName, reason, Instant.now()); + this(0, reporterUuid, reporterName, targetUuid, targetName, reason, null, "PENDING", Instant.now()); + } + + public Report(UUID reporterUuid, String reporterName, UUID targetUuid, String targetName, String reason, String imageUrl) { + this(0, reporterUuid, reporterName, targetUuid, targetName, reason, imageUrl, "PENDING", Instant.now()); } } diff --git a/src/main/java/dev/loki/lorep/database/SQLiteDatabaseManager.java b/src/main/java/dev/loki/lorep/database/SQLiteDatabaseManager.java index dcd315d..02434f9 100644 --- a/src/main/java/dev/loki/lorep/database/SQLiteDatabaseManager.java +++ b/src/main/java/dev/loki/lorep/database/SQLiteDatabaseManager.java @@ -54,6 +54,8 @@ public class SQLiteDatabaseManager implements DatabaseManager { target_uuid TEXT NOT NULL, target_name TEXT NOT NULL, reason TEXT NOT NULL, + image_url TEXT, + status TEXT NOT NULL DEFAULT 'PENDING', created_at INTEGER NOT NULL ) """; @@ -82,7 +84,8 @@ public class SQLiteDatabaseManager implements DatabaseManager { @Override public void saveReport(Report report) { - String sql = "INSERT INTO reports (reporter_uuid, reporter_name, target_uuid, target_name, reason, created_at) VALUES (?, ?, ?, ?, ?, ?)"; + String sql = "INSERT INTO reports (reporter_uuid, reporter_name, target_uuid, " + + "target_name, reason, image_url, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"; try (PreparedStatement stmt = connection.prepareStatement(sql)) { stmt.setString(1, report.reporterUuid().toString()); @@ -90,7 +93,13 @@ public class SQLiteDatabaseManager implements DatabaseManager { stmt.setString(3, report.targetUuid().toString()); stmt.setString(4, report.targetName()); stmt.setString(5, report.reason()); - stmt.setLong(6, report.createdAt().toEpochMilli()); + if (report.imageUrl() != null) { + stmt.setString(6, report.imageUrl()); + } else { + stmt.setNull(6, java.sql.Types.VARCHAR); + } + stmt.setString(7, report.status()); + stmt.setLong(8, report.createdAt().toEpochMilli()); stmt.executeUpdate(); } catch (SQLException e) { logger.log(Level.SEVERE, "Failed to save report", e); @@ -207,6 +216,10 @@ public class SQLiteDatabaseManager implements DatabaseManager { } private Report mapReport(ResultSet rs) throws SQLException { + String imageUrl = rs.getString("image_url"); + if (rs.wasNull()) { + imageUrl = null; + } return new Report( rs.getInt("id"), UUID.fromString(rs.getString("reporter_uuid")), @@ -214,7 +227,57 @@ public class SQLiteDatabaseManager implements DatabaseManager { UUID.fromString(rs.getString("target_uuid")), rs.getString("target_name"), rs.getString("reason"), + imageUrl, + rs.getString("status"), Instant.ofEpochMilli(rs.getLong("created_at")) ); } + + @Override + public void updateReportStatus(int reportId, String status) { + String sql = "UPDATE reports SET status = ? WHERE id = ?"; + + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setString(1, status); + stmt.setInt(2, reportId); + stmt.executeUpdate(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to update report status", e); + } + } + + @Override + public void updateReportImage(int reportId, String imageUrl) { + String sql = "UPDATE reports SET image_url = ? WHERE id = ?"; + + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + if (imageUrl != null) { + stmt.setString(1, imageUrl); + } else { + stmt.setNull(1, java.sql.Types.VARCHAR); + } + stmt.setInt(2, reportId); + stmt.executeUpdate(); + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to update report image", e); + } + } + + @Override + public Report getReportById(int id) { + String sql = "SELECT * FROM reports WHERE id = ?"; + + try (PreparedStatement stmt = connection.prepareStatement(sql)) { + stmt.setInt(1, id); + try (ResultSet rs = stmt.executeQuery()) { + if (rs.next()) { + return mapReport(rs); + } + } + } catch (SQLException e) { + logger.log(Level.SEVERE, "Failed to get report by id", e); + } + + return null; + } } diff --git a/src/main/java/dev/loki/lorep/gui/GuiClickListener.java b/src/main/java/dev/loki/lorep/gui/GuiClickListener.java index 6ff6129..e7d1873 100644 --- a/src/main/java/dev/loki/lorep/gui/GuiClickListener.java +++ b/src/main/java/dev/loki/lorep/gui/GuiClickListener.java @@ -1,7 +1,9 @@ package dev.loki.lorep.gui; import dev.loki.lorep.LorepPlugin; +import dev.loki.lorep.database.Report; import dev.loki.lorep.util.PaginationUtil; +import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; @@ -9,43 +11,119 @@ import org.bukkit.event.inventory.InventoryClickEvent; import org.bukkit.event.inventory.InventoryDragEvent; public class GuiClickListener implements Listener { - + private final LorepPlugin plugin; - + public GuiClickListener(LorepPlugin plugin) { this.plugin = plugin; } - + @EventHandler public void onInventoryClick(InventoryClickEvent event) { if (!(event.getInventory().getHolder() instanceof ReportGui gui)) { return; } - + event.setCancelled(true); - + if (!(event.getWhoClicked() instanceof Player player)) { return; } - - int slot = event.getRawSlot(); - // Navigation buttons are in bottom row - if (slot == ReportGui.SLOT_PREV_PAGE) { - if (gui.getPage() > 0) { - new ReportGui(plugin, gui.getPage() - 1).open(player); - } - } else if (slot == ReportGui.SLOT_CLOSE) { - player.closeInventory(); - } else if (slot == ReportGui.SLOT_NEXT_PAGE) { - int totalReports = plugin.getDatabaseManager().getTotalReportCount(); - int totalPages = PaginationUtil.calculateTotalPages(totalReports, ReportGui.PAGE_SIZE); - if (gui.getPage() < totalPages - 1) { - new ReportGui(plugin, gui.getPage() + 1).open(player); - } + String action = gui.slotActions.get(event.getRawSlot()); + + if (action == null) { + return; + } + + switch (action) { + case "prev_page" -> handlePrevPage(player, gui); + case "next_page" -> handleNextPage(player, gui); + case "close" -> handleClose(player, gui); + case "teleport" -> handleTeleport(player, gui); + case "screenshot" -> handleScreenshot(player, gui); + default -> handleDefaultAction(player, gui, action); } } - + + private void handlePrevPage(Player player, ReportGui gui) { + if (gui.page > 0) { + new ReportGui(plugin, gui.page - 1).open(player); + } + } + + private void handleNextPage(Player player, ReportGui gui) { + int totalReports = plugin.getDatabaseManager().getTotalReportCount(); + int pageSize = plugin.getConfig().getInt("gui.reports-per-page", 45); + int totalPages = PaginationUtil.calculateTotalPages(totalReports, pageSize); + if (gui.page < totalPages - 1) { + new ReportGui(plugin, gui.page + 1).open(player); + } + } + + private void handleClose(Player player, ReportGui gui) { + if (gui.detailView) { + new ReportGui(plugin, gui.page).open(player); + } else { + player.closeInventory(); + } + } + + private void handleTeleport(Player player, ReportGui gui) { + Report report = gui.currentReport; + if (report == null) { + return; + } + Player target = Bukkit.getPlayer(report.targetUuid()); + if (target != null && target.isOnline()) { + player.teleport(target.getLocation()); + player.sendMessage(plugin.getConfigManager().getMessage("teleported-to", "%player%", report.targetName())); + } else { + player.sendMessage(plugin.getConfigManager().getMessage("player-offline", "%player%", report.targetName())); + } + } + + private void handleScreenshot(Player player, ReportGui gui) { + Report report = gui.currentReport; + if (report != null && report.imageUrl() != null && !report.imageUrl().isEmpty()) { + player.sendMessage(plugin.getConfigManager().getMessage("screenshot-link", "%url%", report.imageUrl())); + } + } + + private void handleDefaultAction(Player player, ReportGui gui, String action) { + if (action.startsWith("status_set:")) { + handleStatusSet(player, gui, action.substring("status_set:".length())); + } else if (action.startsWith("report_detail:")) { + handleReportDetail(player, gui, action.substring("report_detail:".length())); + } + } + + private void handleStatusSet(Player player, ReportGui gui, String status) { + Report report = gui.currentReport; + if (report == null) { + return; + } + plugin.getDatabaseManager().updateReportStatus(report.id(), status); + Report updated = plugin.getDatabaseManager().getReportById(report.id()); + if (updated != null && plugin.getDiscordWebhook() != null) { + plugin.getDiscordWebhook().sendStatusUpdate(updated); + } + if (updated != null) { + new ReportGui(plugin, gui.page).openDetail(player, updated); + } + } + + private void handleReportDetail(Player player, ReportGui gui, String idStr) { + try { + int reportId = Integer.parseInt(idStr); + Report report = plugin.getDatabaseManager().getReportById(reportId); + if (report != null) { + new ReportGui(plugin, gui.page).openDetail(player, report); + } + } catch (NumberFormatException ignored) { + } + } + @EventHandler public void onInventoryDrag(InventoryDragEvent event) { if (event.getInventory().getHolder() instanceof ReportGui) { diff --git a/src/main/java/dev/loki/lorep/gui/ReportGui.java b/src/main/java/dev/loki/lorep/gui/ReportGui.java index d56f36d..301c390 100644 --- a/src/main/java/dev/loki/lorep/gui/ReportGui.java +++ b/src/main/java/dev/loki/lorep/gui/ReportGui.java @@ -1,140 +1,356 @@ package dev.loki.lorep.gui; import dev.loki.lorep.LorepPlugin; +import dev.loki.lorep.config.GuiItemConfig; import dev.loki.lorep.database.Report; +import dev.loki.lorep.util.PaginationUtil; import dev.loki.lorep.util.TimeUtil; import org.bukkit.Bukkit; import org.bukkit.Material; +import org.bukkit.OfflinePlayer; +import org.bukkit.enchantments.Enchantment; +import org.bukkit.entity.Player; import org.bukkit.inventory.Inventory; import org.bukkit.inventory.InventoryHolder; +import org.bukkit.inventory.ItemFlag; import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.SkullMeta; -import org.bukkit.entity.Player; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; public class ReportGui implements InventoryHolder { - public static final int PAGE_SIZE = 45; // 5 rows for reports - public static final int INVENTORY_SIZE = 54; // 6 rows total + public static final int INVENTORY_SIZE = 54; - public static final int SLOT_PREV_PAGE = 45; - public static final int SLOT_CLOSE = 47; - public static final int SLOT_PAGE_INFO = 49; - public static final int SLOT_NEXT_PAGE = 53; + final int page; + boolean detailView; + Report currentReport; + final Map slotActions = new HashMap<>(); private final LorepPlugin plugin; - private final int page; private Inventory inventory; - + public ReportGui(LorepPlugin plugin, int page) { this.plugin = plugin; this.page = page; + this.detailView = false; + this.currentReport = null; } - + public void open(Player player) { - String title = plugin.getConfigManager().getMessage("gui-title", "%page%", String.valueOf(page + 1)); + detailView = false; + currentReport = null; + slotActions.clear(); + + String title = plugin.getConfigManager().getMessage("gui-title", + "%page%", String.valueOf(page + 1)); this.inventory = Bukkit.createInventory(this, INVENTORY_SIZE, title); - - List reports = plugin.getDatabaseManager().getReportsPaginated(page, PAGE_SIZE); + + int pageSize = plugin.getConfig().getInt("gui.reports-per-page", 45); + List reports = plugin.getDatabaseManager().getReportsPaginated(page, pageSize); int totalReports = plugin.getDatabaseManager().getTotalReportCount(); - - // Add report items + int totalPages = PaginationUtil.calculateTotalPages(totalReports, pageSize); + + GuiItemConfig entryConfig = plugin.getConfigManager().getGuiItem("report-entry"); + for (int i = 0; i < reports.size(); i++) { Report report = reports.get(i); int targetReportCount = plugin.getDatabaseManager().getReportCount(report.targetUuid()); - inventory.setItem(i, createReportItem(report, targetReportCount)); + + ItemStack item; + if (entryConfig != null) { + item = entryConfig.createItem(); + if (item.getItemMeta() instanceof SkullMeta skullMeta) { + OfflinePlayer offlinePlayer = Bukkit.getOfflinePlayer(report.targetUuid()); + skullMeta.setOwningPlayer(offlinePlayer); + item.setItemMeta(skullMeta); + } + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(replacePlaceholders(meta.getDisplayName(), report, targetReportCount)); + if (meta.getLore() != null) { + List lore = meta.getLore(); + List newLore = new ArrayList<>(); + for (String line : lore) { + newLore.add(replacePlaceholders(line, report, targetReportCount)); + } + meta.setLore(newLore); + } + item.setItemMeta(meta); + } + } else { + item = createFallbackReportItem(report, targetReportCount); + } + + List slots = entryConfig != null ? entryConfig.slots() : List.of(); + if (!slots.isEmpty() && i < slots.size()) { + int slot = slots.get(i); + inventory.setItem(slot, item); + slotActions.put(slot, "report_detail:" + report.id()); + } else { + inventory.setItem(i, item); + slotActions.put(i, "report_detail:" + report.id()); + } } - - // Add navigation buttons - addNavigationButtons(totalReports); - + + addNavigation(totalReports, totalPages); player.openInventory(inventory); } - - private ItemStack createReportItem(Report report, int totalReports) { + + public void openDetail(Player player, Report report) { + detailView = true; + currentReport = report; + slotActions.clear(); + + String title = plugin.getConfigManager().getMessage("gui-title-detail", "%id%", String.valueOf(report.id())); + this.inventory = Bukkit.createInventory(this, INVENTORY_SIZE, title); + + int targetReportCount = plugin.getDatabaseManager().getReportCount(report.targetUuid()); + + GuiItemConfig entryConfig = plugin.getConfigManager().getGuiItem("report-entry"); + ItemStack item; + if (entryConfig != null) { + item = entryConfig.createItem(); + if (item.getItemMeta() instanceof SkullMeta skullMeta) { + skullMeta.setOwningPlayer(Bukkit.getOfflinePlayer(report.targetUuid())); + item.setItemMeta(skullMeta); + } + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(replacePlaceholders(meta.getDisplayName(), report, targetReportCount)); + if (meta.getLore() != null) { + List lore = meta.getLore(); + List newLore = new ArrayList<>(); + for (String line : lore) { + newLore.add(replacePlaceholders(line, report, targetReportCount)); + } + meta.setLore(newLore); + } + item.setItemMeta(meta); + } + } else { + item = createFallbackReportItem(report, targetReportCount); + } + inventory.setItem(22, item); + + placeDetailButton("teleport", "teleport"); + if (report.imageUrl() != null && !report.imageUrl().isEmpty()) { + placeDetailButton("screenshot", "screenshot"); + } + + String currentStatus = report.status() != null ? report.status() : "PENDING"; + placeStatusButton("status-pending", "status_set:PENDING", "PENDING".equals(currentStatus)); + placeStatusButton("status-reviewing", "status_set:REVIEWING", "REVIEWING".equals(currentStatus)); + placeStatusButton("status-resolved", "status_set:RESOLVED", "RESOLVED".equals(currentStatus)); + placeStatusButton("status-dismissed", "status_set:DISMISSED", "DISMISSED".equals(currentStatus)); + + GuiItemConfig closeConfig = plugin.getConfigManager().getGuiItem("close"); + if (closeConfig != null) { + List slots = closeConfig.slots(); + ItemStack closeItem = closeConfig.createItem(); + if (slots.isEmpty()) { + inventory.setItem(49, closeItem); + slotActions.put(49, "close"); + } else { + for (int slot : slots) { + inventory.setItem(slot, closeItem); + slotActions.put(slot, "close"); + } + } + } + + player.openInventory(inventory); + } + + private void placeDetailButton(String configPath, String action) { + GuiItemConfig config = plugin.getConfigManager().getGuiItem(configPath); + if (config == null) { + return; + } + + List slots = config.slots(); + ItemStack button = config.createItem(); + if (slots.isEmpty()) { + int slot = switch (action) { + case "teleport" -> 20; + case "screenshot" -> 24; + default -> 22; + }; + inventory.setItem(slot, button); + slotActions.put(slot, action); + } else { + for (int slot : slots) { + inventory.setItem(slot, button); + slotActions.put(slot, action); + } + } + } + + private void placeStatusButton(String configPath, String action, boolean active) { + GuiItemConfig config = plugin.getConfigManager().getGuiItem(configPath); + if (config == null) { + return; + } + + List slots = config.slots(); + ItemStack button = config.createItem(); + if (active) { + button.addUnsafeEnchantment(Enchantment.ARROW_DAMAGE, 1); + ItemMeta meta = button.getItemMeta(); + if (meta != null) { + meta.addItemFlags(ItemFlag.HIDE_ENCHANTS); + button.setItemMeta(meta); + } + } + if (slots.isEmpty()) { + int slot = switch (action) { + case "status_set:PENDING" -> 29; + case "status_set:REVIEWING" -> 30; + case "status_set:RESOLVED" -> 31; + case "status_set:DISMISSED" -> 32; + default -> 29; + }; + inventory.setItem(slot, button); + slotActions.put(slot, action); + } else { + for (int slot : slots) { + inventory.setItem(slot, button); + slotActions.put(slot, action); + } + } + } + + private void addNavigation(int totalReports, int totalPages) { + if (page > 0) { + GuiItemConfig prevConfig = plugin.getConfigManager().getGuiItem("prev-page"); + if (prevConfig != null) { + List slots = prevConfig.slots(); + ItemStack item = prevConfig.createItem(); + if (slots.isEmpty()) { + inventory.setItem(45, item); + slotActions.put(45, "prev_page"); + } else { + for (int slot : slots) { + inventory.setItem(slot, item); + slotActions.put(slot, "prev_page"); + } + } + } + } + + GuiItemConfig pageInfoConfig = plugin.getConfigManager().getGuiItem("page-info"); + if (pageInfoConfig != null) { + List slots = pageInfoConfig.slots(); + ItemStack item = pageInfoConfig.createItem(); + ItemMeta meta = item.getItemMeta(); + if (meta != null) { + meta.setDisplayName(meta.getDisplayName() + .replace("%page%", String.valueOf(page + 1)) + .replace("%total%", String.valueOf(Math.max(1, totalPages))) + .replace("%total_reports%", String.valueOf(totalReports))); + if (meta.getLore() != null) { + List lore = meta.getLore(); + List newLore = new ArrayList<>(); + for (String line : lore) { + newLore.add(line + .replace("%page%", String.valueOf(page + 1)) + .replace("%total%", String.valueOf(Math.max(1, totalPages))) + .replace("%total_reports%", String.valueOf(totalReports))); + } + meta.setLore(newLore); + } + item.setItemMeta(meta); + } + if (slots.isEmpty()) { + inventory.setItem(49, item); + } else { + for (int slot : slots) { + inventory.setItem(slot, item); + } + } + } + + if (page < totalPages - 1) { + GuiItemConfig nextConfig = plugin.getConfigManager().getGuiItem("next-page"); + if (nextConfig != null) { + List slots = nextConfig.slots(); + ItemStack item = nextConfig.createItem(); + if (slots.isEmpty()) { + inventory.setItem(53, item); + slotActions.put(53, "next_page"); + } else { + for (int slot : slots) { + inventory.setItem(slot, item); + slotActions.put(slot, "next_page"); + } + } + } + } + + GuiItemConfig closeConfig = plugin.getConfigManager().getGuiItem("close"); + if (closeConfig != null) { + List slots = closeConfig.slots(); + ItemStack item = closeConfig.createItem(); + if (slots.isEmpty()) { + inventory.setItem(47, item); + slotActions.put(47, "close"); + } else { + for (int slot : slots) { + inventory.setItem(slot, item); + slotActions.put(slot, "close"); + } + } + } + } + + private String replacePlaceholders(String text, Report report, int reportCount) { + String statusDisplay = plugin.getConfigManager().getMessage("status-" + report.status().toLowerCase()); + return text + .replace("%target_name%", report.targetName() != null ? report.targetName() : "") + .replace("%reporter_name%", report.reporterName() != null ? report.reporterName() : "") + .replace("%reason%", report.reason() != null ? report.reason() : "") + .replace("%time%", TimeUtil.formatTimeAgo(report.createdAt())) + .replace("%status%", statusDisplay) + .replace("%report_count%", String.valueOf(reportCount)); + } + + private ItemStack createFallbackReportItem(Report report, int totalReports) { ItemStack head = new ItemStack(Material.PLAYER_HEAD); SkullMeta meta = (SkullMeta) head.getItemMeta(); - if (meta != null) { - // Set skull owner for skin meta.setOwningPlayer(Bukkit.getOfflinePlayer(report.targetUuid())); - - // Display name meta.setDisplayName("§c" + report.targetName()); - - // Lore with report info List lore = new ArrayList<>(); lore.add("§7"); - lore.add("§eОтправитель: §f" + report.reporterName()); - lore.add("§ePричина: §f" + report.reason()); - lore.add("§eВремя: §f" + TimeUtil.formatTimeAgo(report.createdAt().toEpochMilli())); + lore.add("§eBy: §f" + report.reporterName()); + lore.add("§eReason: §f" + report.reason()); + lore.add("§eTime: §f" + TimeUtil.formatTimeAgo(report.createdAt())); lore.add("§7"); - lore.add("§eВсего репортов: §c" + totalReports); - + lore.add("§eReports: §c" + totalReports); meta.setLore(lore); head.setItemMeta(meta); } - return head; } - private void addNavigationButtons(int totalReports) { - int totalPages = (int) Math.ceil((double) totalReports / PAGE_SIZE); - - // Previous page button - if (page > 0) { - ItemStack prevButton = new ItemStack(Material.ARROW); - ItemMeta prevMeta = prevButton.getItemMeta(); - if (prevMeta != null) { - prevMeta.setDisplayName("§a← Предыдущая страница"); - prevButton.setItemMeta(prevMeta); - } - inventory.setItem(SLOT_PREV_PAGE, prevButton); - } - - // Page info - ItemStack pageInfo = new ItemStack(Material.PAPER); - ItemMeta pageMeta = pageInfo.getItemMeta(); - if (pageMeta != null) { - pageMeta.setDisplayName("§eСтраница " + (page + 1) + " из " + Math.max(1, totalPages)); - List lore = new ArrayList<>(); - lore.add("§7Всего репортов: " + totalReports); - pageMeta.setLore(lore); - pageInfo.setItemMeta(pageMeta); - } - inventory.setItem(SLOT_PAGE_INFO, pageInfo); - - // Next page button - if (page < totalPages - 1) { - ItemStack nextButton = new ItemStack(Material.ARROW); - ItemMeta nextMeta = nextButton.getItemMeta(); - if (nextMeta != null) { - nextMeta.setDisplayName("§aСледующая страница →"); - nextButton.setItemMeta(nextMeta); - } - inventory.setItem(SLOT_NEXT_PAGE, nextButton); - } - - // Close button - ItemStack closeButton = new ItemStack(Material.BARRIER); - ItemMeta closeMeta = closeButton.getItemMeta(); - if (closeMeta != null) { - closeMeta.setDisplayName("§cЗакрыть"); - closeButton.setItemMeta(closeMeta); - } - inventory.setItem(SLOT_CLOSE, closeButton); - } - public int getPage() { return page; } - + + public boolean isDetailView() { + return detailView; + } + + public Report getCurrentReport() { + return currentReport; + } + @Override public Inventory getInventory() { return inventory; } - } diff --git a/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java b/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java index c0e2aa0..95832a9 100644 --- a/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java +++ b/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java @@ -1,5 +1,8 @@ package dev.loki.lorep.webhook; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; import dev.loki.lorep.database.Report; import okhttp3.Call; import okhttp3.Callback; @@ -16,42 +19,42 @@ import java.util.logging.Level; import java.util.logging.Logger; public class DiscordWebhook { - + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); private static final DateTimeFormatter FORMATTER = DateTimeFormatter .ofPattern("dd.MM.yyyy HH:mm:ss") .withZone(ZoneId.systemDefault()); - + private final String webhookUrl; private final Logger logger; private final OkHttpClient client; - + public DiscordWebhook(String webhookUrl, Logger logger) { this.webhookUrl = webhookUrl; this.logger = logger; this.client = new OkHttpClient(); } - + public void sendReport(Report report) { if (webhookUrl == null || webhookUrl.isEmpty()) { logger.warning("Discord webhook URL is not configured"); return; } - + String json = buildPayload(report); - + RequestBody body = RequestBody.create(json, JSON); Request request = new Request.Builder() .url(webhookUrl) .post(body) .build(); - + client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { logger.log(Level.WARNING, "Failed to send Discord webhook", e); } - + @Override public void onResponse(Call call, Response response) { if (!response.isSuccessful()) { @@ -61,31 +64,164 @@ public class DiscordWebhook { } }); } - - public String buildPayload(Report report) { - String timestamp = FORMATTER.format(report.createdAt()); - - return String.format(""" - { - "embeds": [{ - "title": "📢 Новый репорт", - "color": 16711680, - "fields": [ - {"name": "👤 Нарушитель", "value": "%s", "inline": true}, - {"name": "📝 Отправитель", "value": "%s", "inline": true}, - {"name": "📋 Причина", "value": "%s", "inline": false}, - {"name": "🕐 Время", "value": "%s", "inline": false} - ] - }] + + public void sendStatusUpdate(Report report) { + if (webhookUrl == null || webhookUrl.isEmpty()) { + logger.warning("Discord webhook URL is not configured"); + return; + } + + String json = buildStatusPayload(report); + + RequestBody body = RequestBody.create(json, JSON); + Request request = new Request.Builder() + .url(webhookUrl) + .post(body) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + logger.log(Level.WARNING, "Failed to send status update webhook", e); } - """, - escapeJson(report.targetName()), - escapeJson(report.reporterName()), - escapeJson(report.reason()), - timestamp - ); + + @Override + public void onResponse(Call call, Response response) { + if (!response.isSuccessful()) { + logger.warning("Status update webhook returned: " + response.code()); + } + response.close(); + } + }); } - + + public String buildPayload(Report report) { + JsonObject embed = new JsonObject(); + embed.addProperty("title", "\uD83D\uDCE2 Новый репорт #" + report.id()); + embed.addProperty("color", getColorForStatus(report.status())); + + JsonObject thumbnail = new JsonObject(); + thumbnail.addProperty("url", "https://mc-heads.net/avatar/" + report.targetUuid()); + embed.add("thumbnail", thumbnail); + + JsonObject author = new JsonObject(); + author.addProperty("name", report.reporterName()); + author.addProperty("icon_url", "https://mc-heads.net/avatar/" + report.reporterUuid()); + embed.add("author", author); + + JsonArray fields = new JsonArray(); + + JsonObject targetField = new JsonObject(); + targetField.addProperty("name", "\uD83D\uDC64 Нарушитель"); + targetField.addProperty("value", report.targetName()); + targetField.addProperty("inline", true); + fields.add(targetField); + + JsonObject reporterField = new JsonObject(); + reporterField.addProperty("name", "\uD83D\uDCDD Отправитель"); + reporterField.addProperty("value", report.reporterName()); + reporterField.addProperty("inline", true); + fields.add(reporterField); + + JsonObject reasonField = new JsonObject(); + reasonField.addProperty("name", "\uD83D\uDCCB Причина"); + reasonField.addProperty("value", report.reason()); + reasonField.addProperty("inline", false); + fields.add(reasonField); + + JsonObject statusField = new JsonObject(); + statusField.addProperty("name", "\uD83C\uDFF7 Статус"); + statusField.addProperty("value", getStatusDisplay(report.status())); + statusField.addProperty("inline", true); + fields.add(statusField); + + JsonObject timeField = new JsonObject(); + timeField.addProperty("name", "\uD83D\uDD50 Время"); + timeField.addProperty("value", FORMATTER.format(report.createdAt())); + timeField.addProperty("inline", true); + fields.add(timeField); + + embed.add("fields", fields); + + if (report.imageUrl() != null && !report.imageUrl().isEmpty()) { + JsonObject image = new JsonObject(); + image.addProperty("url", report.imageUrl()); + embed.add("image", image); + } + + JsonObject footer = new JsonObject(); + footer.addProperty("text", "LoReport \u2022 " + FORMATTER.format(report.createdAt())); + embed.add("footer", footer); + + embed.addProperty("timestamp", report.createdAt().toString()); + + JsonObject root = new JsonObject(); + JsonArray embeds = new JsonArray(); + embeds.add(embed); + root.add("embeds", embeds); + + return new GsonBuilder().create().toJson(root); + } + + private String buildStatusPayload(Report report) { + JsonObject embed = new JsonObject(); + embed.addProperty("title", "\uD83D\uDD04 Статус репорта #" + report.id() + " изменён"); + embed.addProperty("color", getColorForStatus(report.status())); + + JsonObject thumbnail = new JsonObject(); + thumbnail.addProperty("url", "https://mc-heads.net/avatar/" + report.targetUuid()); + embed.add("thumbnail", thumbnail); + + JsonArray fields = new JsonArray(); + + JsonObject targetField = new JsonObject(); + targetField.addProperty("name", "\uD83D\uDC64 Нарушитель"); + targetField.addProperty("value", report.targetName()); + targetField.addProperty("inline", true); + fields.add(targetField); + + JsonObject statusField = new JsonObject(); + statusField.addProperty("name", "\uD83C\uDFF7 Новый статус"); + statusField.addProperty("value", getStatusDisplay(report.status())); + statusField.addProperty("inline", false); + fields.add(statusField); + + embed.add("fields", fields); + + JsonObject footer = new JsonObject(); + footer.addProperty("text", "LoReport \u2022 " + FORMATTER.format(report.createdAt())); + embed.add("footer", footer); + + embed.addProperty("timestamp", report.createdAt().toString()); + + JsonObject root = new JsonObject(); + JsonArray embeds = new JsonArray(); + embeds.add(embed); + root.add("embeds", embeds); + + return new GsonBuilder().create().toJson(root); + } + + private int getColorForStatus(String status) { + return switch (status != null ? status.toUpperCase() : "PENDING") { + case "PENDING" -> 0xFF0000; + case "REVIEWING" -> 0xFFAA00; + case "RESOLVED" -> 0x00FF00; + case "DISMISSED" -> 0x808080; + default -> 0xFF0000; + }; + } + + private String getStatusDisplay(String status) { + return switch (status != null ? status.toUpperCase() : "PENDING") { + case "PENDING" -> "\u23F3 Ожидает"; + case "REVIEWING" -> "\uD83D\uDD0D На рассмотрении"; + case "RESOLVED" -> "\u2705 Решён"; + case "DISMISSED" -> "\u274C Отклонён"; + default -> "\u2753 Неизвестно"; + }; + } + private String escapeJson(String text) { if (text == null) { return ""; diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml index 32e65d9..2c4508a 100644 --- a/src/main/resources/config.yml +++ b/src/main/resources/config.yml @@ -1,16 +1,16 @@ -# Discord Webhook URL для уведомлений +# Discord webhook URL for report notifications webhook-url: "" -# Настройки базы данных +# Database configuration database: - # Тип БД: sqlite или postgresql + # Database type: "sqlite" or "postgresql" type: "sqlite" - # Настройки SQLite + # SQLite settings sqlite: file: "reports.db" - # Настройки PostgreSQL + # PostgreSQL settings postgresql: host: "localhost" port: 5432 @@ -19,48 +19,145 @@ database: password: "password" pool-size: 10 -# Настройки GUI +# GUI settings gui: - # Количество репортов на странице + # Reports per page in GUI reports-per-page: 45 - # Автообновление GUI (в тиках, 20 тиков = 1 секунда) + # Auto-refresh interval in ticks (20 ticks = 1 second) auto-refresh: 100 - # Показывать головы игроков в GUI + # Show player heads in GUI show-player-heads: true + # GUI item definitions — customize materials, textures, and positions + # Set custom-model-data to your resource pack model ID (-1 to disable) + # Use slots to make an item span multiple slots (empty = default position) + items: + prev-page: + material: ARROW + custom-model-data: -1 + name: "&a← Previous page" + lore: [] + slots: [45] + glowing: false + next-page: + material: ARROW + custom-model-data: -1 + name: "&aNext page →" + lore: [] + slots: [53] + glowing: false + page-info: + material: PAPER + custom-model-data: -1 + name: "&ePage %page% of %total%" + lore: + - "&7Total reports: %total_reports%" + slots: [49] + glowing: false + close: + material: BARRIER + custom-model-data: -1 + name: "&cClose" + lore: [] + slots: [47] + glowing: false + report-entry: + material: PLAYER_HEAD + custom-model-data: -1 + name: "&c%target_name%" + lore: + - "&7" + - "&eBy: &f%reporter_name%" + - "&eReason: &f%reason%" + - "&eTime: &f%time%" + - "&eStatus: &f%status%" + - "&7" + - "&eReports: &c%report_count%" + slots: [] + glowing: false + status-pending: + material: RED_STAINED_GLASS_PANE + custom-model-data: -1 + name: "&cPending" + lore: [] + slots: [] + glowing: false + status-reviewing: + material: YELLOW_STAINED_GLASS_PANE + custom-model-data: -1 + name: "&eReviewing" + lore: [] + slots: [] + glowing: true + status-resolved: + material: GREEN_STAINED_GLASS_PANE + custom-model-data: -1 + name: "&aResolved" + lore: [] + slots: [] + glowing: false + status-dismissed: + material: GRAY_STAINED_GLASS_PANE + custom-model-data: -1 + name: "&7Dismissed" + lore: [] + slots: [] + glowing: false + teleport: + material: ENDER_PEARL + custom-model-data: -1 + name: "&bTeleport to player" + lore: [] + slots: [] + glowing: false + screenshot: + material: MAP + custom-model-data: -1 + name: "&aScreenshot" + lore: [] + slots: [] + glowing: false -# Ограничения +# Limits limits: - # Максимальная длина причины репорта + # Maximum reason length in characters max-reason-length: 100 - # Кулдаун между репортами (в секундах) + # Cooldown between reports (seconds) report-cooldown: 30 - # Максимум репортов на одного игрока от одного отправителя + # Max reports per player from the same reporter max-reports-per-player: 3 -# Уведомления +# Notifications notifications: - # Уведомлять администраторов о новых репортах + # Notify admins about new reports notify-admins: true - # Звук уведомления для администраторов + # Admin notification sound admin-notification-sound: "BLOCK_NOTE_BLOCK_PLING" - # Уведомлять игрока о принятии репорта + # Notify reporter when report is handled notify-reporter: true -# Сообщения +# Messages (override defaults from messages.yml) messages: - report-sent: "&aРепорт успешно отправлен!" - already-reported: "&cВы уже отправляли репорт на этого игрока!" - self-report: "&cВы не можете отправить репорт на себя!" - player-not-found: "&cИгрок не найден!" - no-permission: "&cУ вас нет прав на эту команду!" - usage: "&eИспользование: /report <ник> <причина>" - no-reports: "&eУ этого игрока нет репортов." - stats-header: "&6=== Статистика репортов: %player% ===" - stats-count: "&eВсего репортов: &f%count%" - stats-last-online: "&eПоследний онлайн: &f%time%" - stats-report-entry: "&7- %reason% &8(%time% назад)" - gui-title: "Репорты - Страница %page%" - cooldown: "&cПодождите %time% секунд перед следующим репортом!" - reason-too-long: "&cПричина слишком длинная! Максимум %max% символов." - max-reports-reached: "&cВы уже отправили максимальное количество репортов на этого игрока!" - admin-notification: "&6[Loreport] &eНовый репорт от &f%reporter% &eна &f%reported%: &7%reason%" + report-sent: "&aReport sent successfully!" + already-reported: "&cYou have already reported this player!" + self-report: "&cYou cannot report yourself!" + player-not-found: "&cPlayer not found!" + no-permission: "&cYou don't have permission!" + usage: "&eUsage: /report [screenshot_url] | /report gui | /report stats | /report image " + no-reports: "&eThis player has no reports." + stats-header: "&6=== Report stats: %player% ===" + stats-count: "&eTotal reports: &f%count%" + stats-last-online: "&eLast online: &f%time%" + stats-report-entry: "&7- %reason% &8(%time% ago)" + gui-title: "Reports - Page %page%" + cooldown: "&cPlease wait %time% seconds before your next report!" + reason-too-long: "&cReason too long! Max %max% characters." + max-reports-reached: "&cYou've reached the max reports for this player!" + admin-notification: "&6[LoReport] &eNew report from &f%reporter% &eon &f%reported%: &7%reason%" + report-status-changed: "&aReport #%id% status changed to: %status%" + report-image-added: "&aScreenshot added to report #%id%" + report-image-usage: "&eUsage: /report [screenshot_url]" + report-image-url-usage: "&eUsage: /report image " + status-pending: "&cPending" + status-reviewing: "&eReviewing" + status-resolved: "&aResolved" + status-dismissed: "&7Dismissed" diff --git a/src/main/resources/messages.yml b/src/main/resources/messages.yml new file mode 100644 index 0000000..1d3d456 --- /dev/null +++ b/src/main/resources/messages.yml @@ -0,0 +1,50 @@ +# LoReport messages +# You can edit these to change plugin messages without recompiling +# Color codes: &0-9, &a-f, &k-o, &r + +# General +console-only: "&cThis command is for players only!" +no-permission: "&cYou don't have permission to use this command!" +usage: "&eUsage: /report [screenshot_url] | /report gui | /report stats | /report image " + +# Reporting +report-sent: "&aReport sent successfully!" +already-reported: "&cYou have already reported this player!" +self-report: "&cYou cannot report yourself!" +player-not-found: "&cPlayer not found!" +reason-too-long: "&cReason is too long! Maximum %max% characters." +max-reports-reached: "&cYou have reached the maximum number of reports for this player!" +cooldown: "&cPlease wait %time% seconds before your next report!" +invalid-report-id: "&cInvalid report ID." +report-not-found: "&cReport with this ID not found." +report-image-updated: "&aImage for report #%id% updated." + +# Admin notifications +admin-notification: "&6[LoReport] &eNew report from &f%reporter% &eon &f%reported%: &7%reason%" + +# Report status +report-status-changed: "&aReport #%id% status changed to: %status%" +report-image-added: "&aScreenshot added to report #%id%" +report-image-usage: "&eUsage: /report [screenshot_url]" +report-image-url-usage: "&eUsage: /report image " +status-pending: "&cPending" +status-reviewing: "&eReviewing" +status-resolved: "&aResolved" +status-dismissed: "&7Dismissed" + +# GUI +gui-title: "Reports - Page %page%" +gui-title-detail: "&8Report #%id%" +teleported-to: "&aTeleported to %player%" +player-offline: "&cPlayer %player% is offline" +screenshot-link: "&aScreenshot: &e%url%" + +# Stats +online-status: "&aOnline now" +no-reports: "&eThis player has no reports." +stats-header: "&6=== Report stats: %player% ===" +stats-count: "&eTotal reports: &f%count%" +stats-last-online: "&eLast online: &f%time%" +stats-report-entry: "&7- %reason% &8(%time% ago)" +stats-recent-header: "&7Recent reports:" +stats-more-reports: "&7... and %count% more reports" diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index 85cd693..0932beb 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -9,7 +9,7 @@ website: https://codeberg.org/loki5512344/Loreport commands: report: description: Report a player or manage reports - usage: /report | /report gui | /report stats + usage: /report [image_url] | /report gui | /report stats | /report image permission: lorep.report aliases: [rep, reports] diff --git a/src/test/java/dev/loki/lorep/database/DatabaseManagerTest.java b/src/test/java/dev/loki/lorep/database/DatabaseManagerTest.java index 11ed531..f637dfa 100644 --- a/src/test/java/dev/loki/lorep/database/DatabaseManagerTest.java +++ b/src/test/java/dev/loki/lorep/database/DatabaseManagerTest.java @@ -65,7 +65,7 @@ public class DatabaseManagerTest { Assume.that(!reporterUuid.equals(targetUuid)); Report report = new Report( - 0, reporterUuid, "Reporter", targetUuid, "Target", reason, Instant.now() + reporterUuid, "Reporter", targetUuid, "Target", reason ); dbManager.saveReport(report); @@ -95,7 +95,7 @@ public class DatabaseManagerTest { "Should not have reported before creating report"); Report report = new Report( - 0, reporterUuid, "Reporter", targetUuid, "Target", "Test reason", Instant.now() + reporterUuid, "Reporter", targetUuid, "Target", "Test reason" ); dbManager.saveReport(report); @@ -121,7 +121,7 @@ public class DatabaseManagerTest { String targetName = "Target_" + targetUuid.toString().substring(0, 8); Instant createdAt = Instant.now(); - Report original = new Report(0, reporterUuid, reporterName, targetUuid, targetName, reason, createdAt); + Report original = new Report(0, reporterUuid, reporterName, targetUuid, targetName, reason, null, "PENDING", createdAt); dbManager.saveReport(original); // Close and reopen database @@ -151,9 +151,10 @@ public class DatabaseManagerTest { UUID targetUuid = UUID.randomUUID(); for (int i = 0; i < count; i++) { + Instant reportTime = Instant.now().minusSeconds(count - i); Report report = new Report( 0, UUID.randomUUID(), "Reporter" + i, targetUuid, "Target", - "Reason " + i, Instant.now().minusSeconds(count - i) + "Reason " + i, null, "PENDING", reportTime ); dbManager.saveReport(report); } @@ -180,8 +181,8 @@ public class DatabaseManagerTest { for (int i = 0; i < count; i++) { Report report = new Report( - 0, UUID.randomUUID(), "Reporter" + i, targetUuid, "Target", - "Reason " + i, Instant.now() + UUID.randomUUID(), "Reporter" + i, targetUuid, "Target", + "Reason " + i ); dbManager.saveReport(report); } diff --git a/src/test/java/dev/loki/lorep/webhook/DiscordWebhookTest.java b/src/test/java/dev/loki/lorep/webhook/DiscordWebhookTest.java index dc5d0d6..e787ab1 100644 --- a/src/test/java/dev/loki/lorep/webhook/DiscordWebhookTest.java +++ b/src/test/java/dev/loki/lorep/webhook/DiscordWebhookTest.java @@ -33,13 +33,11 @@ public class DiscordWebhookTest { DiscordWebhook webhook = new DiscordWebhook("https://example.com/webhook", logger); Report report = new Report( - 1, UUID.randomUUID(), reporterName, UUID.randomUUID(), targetName, - reason, - Instant.now() + reason ); String payload = webhook.buildPayload(report); @@ -95,9 +93,9 @@ public class DiscordWebhookTest { DiscordWebhook webhook = new DiscordWebhook("https://example.com/webhook", logger); Report report = new Report( - 1, UUID.randomUUID(), "TestReporter", + UUID.randomUUID(), "TestReporter", UUID.randomUUID(), "TestTarget", - "Test reason", Instant.now() + "Test reason" ); String payload = webhook.buildPayload(report);