feat: RP support, config-driven GUI, rich Discord embeds, i18n messages.yml

This commit is contained in:
loki5512344 2026-07-12 13:17:42 +02:00
parent 987e69a976
commit c9f75fbcf2
Signed by: boba
GPG key ID: 253067914055423B
17 changed files with 1106 additions and 235 deletions

View file

@ -22,6 +22,7 @@ dependencies {
// Paper API для 1.19.4+ // Paper API для 1.19.4+
compileOnly("io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT") 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.squareup.okhttp3:okhttp:4.12.0")
implementation("com.zaxxer:HikariCP:6.2.1") implementation("com.zaxxer:HikariCP:6.2.1")
implementation("org.postgresql:postgresql:42.7.4") implementation("org.postgresql:postgresql:42.7.4")

View file

@ -26,7 +26,7 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
@Override @Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
if (!(sender instanceof Player player)) { if (!(sender instanceof Player player)) {
sender.sendMessage("§cЭта команда только для игроков!"); sender.sendMessage(plugin.getConfigManager().getMessage("console-only"));
return true; return true;
} }
@ -35,7 +35,6 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
return true; return true;
} }
// Handle subcommands for admins
if (args[0].equalsIgnoreCase("gui")) { if (args[0].equalsIgnoreCase("gui")) {
return handleGui(player); return handleGui(player);
} }
@ -44,7 +43,10 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
return handleStats(player, args[1]); 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")) { if (!player.hasPermission("lorep.report")) {
player.sendMessage(plugin.getConfigManager().getMessage("no-permission")); player.sendMessage(plugin.getConfigManager().getMessage("no-permission"));
return true; return true;
@ -56,24 +58,34 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
} }
String targetName = args[0]; String targetName = args[0];
String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length)); 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));
}
// Validate reason
if (reason.trim().isEmpty()) { if (reason.trim().isEmpty()) {
player.sendMessage(plugin.getConfigManager().getMessage("usage")); player.sendMessage(plugin.getConfigManager().getMessage("usage"));
return true; return true;
} }
if (reason.length() > 500) { if (reason.length() > 500) {
player.sendMessage("§cПричина слишком длинная! Максимум 500 символов."); player.sendMessage(plugin.getConfigManager().getMessage("reason-too-long", "%max%", "500"));
return true; return true;
} }
return handleReport(player, targetName, reason); return handleReport(player, targetName, reason, imageUrl);
} }
private boolean handleReport(Player reporter, String targetName, String reason) { 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") @SuppressWarnings("deprecation")
OfflinePlayer target = Bukkit.getOfflinePlayer(targetName); OfflinePlayer target = Bukkit.getOfflinePlayer(targetName);
@ -82,30 +94,27 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
return true; return true;
} }
// Check self-report
if (target.getUniqueId().equals(reporter.getUniqueId())) { if (target.getUniqueId().equals(reporter.getUniqueId())) {
reporter.sendMessage(plugin.getConfigManager().getMessage("self-report")); reporter.sendMessage(plugin.getConfigManager().getMessage("self-report"));
return true; return true;
} }
// Check duplicate report
if (plugin.getDatabaseManager().hasReported(reporter.getUniqueId(), target.getUniqueId())) { if (plugin.getDatabaseManager().hasReported(reporter.getUniqueId(), target.getUniqueId())) {
reporter.sendMessage(plugin.getConfigManager().getMessage("already-reported")); reporter.sendMessage(plugin.getConfigManager().getMessage("already-reported"));
return true; return true;
} }
// Create and save report
Report report = new Report( Report report = new Report(
reporter.getUniqueId(), reporter.getUniqueId(),
reporter.getName(), reporter.getName(),
target.getUniqueId(), target.getUniqueId(),
target.getName() != null ? target.getName() : targetName, target.getName() != null ? target.getName() : targetName,
reason reason,
imageUrl
); );
plugin.getDatabaseManager().saveReport(report); plugin.getDatabaseManager().saveReport(report);
// Send to Discord webhook
if (plugin.getDiscordWebhook() != null) { if (plugin.getDiscordWebhook() != null) {
plugin.getDiscordWebhook().sendReport(report); plugin.getDiscordWebhook().sendReport(report);
} }
@ -114,6 +123,48 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
return true; 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) { private boolean handleGui(Player player) {
if (!player.hasPermission("lorep.admin")) { if (!player.hasPermission("lorep.admin")) {
player.sendMessage(plugin.getConfigManager().getMessage("no-permission")); player.sendMessage(plugin.getConfigManager().getMessage("no-permission"));
@ -134,7 +185,6 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
List<String> completions = new ArrayList<>(); List<String> completions = new ArrayList<>();
if (args.length == 1) { if (args.length == 1) {
// First argument: player names or subcommands
String input = args[0].toLowerCase(); String input = args[0].toLowerCase();
if (sender.hasPermission("lorep.admin")) { if (sender.hasPermission("lorep.admin")) {
@ -144,9 +194,11 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
if ("stats".startsWith(input)) { if ("stats".startsWith(input)) {
completions.add("stats"); completions.add("stats");
} }
if ("image".startsWith(input)) {
completions.add("image");
}
} }
// Add online player names
completions.addAll( completions.addAll(
Bukkit.getOnlinePlayers().stream() Bukkit.getOnlinePlayers().stream()
.map(Player::getName) .map(Player::getName)
@ -154,7 +206,6 @@ public class ReportCommand implements CommandExecutor, TabCompleter {
.collect(Collectors.toList()) .collect(Collectors.toList())
); );
} else if (args.length == 2 && args[0].equalsIgnoreCase("stats")) { } else if (args.length == 2 && args[0].equalsIgnoreCase("stats")) {
// Second argument for stats: player names
String input = args[1].toLowerCase(); String input = args[1].toLowerCase();
completions.addAll( completions.addAll(
Bukkit.getOnlinePlayers().stream() Bukkit.getOnlinePlayers().stream()

View file

@ -49,14 +49,13 @@ public class ReportStatsCommand {
player.sendMessage(plugin.getConfigManager().getMessage("stats-count", player.sendMessage(plugin.getConfigManager().getMessage("stats-count",
"%count%", String.valueOf(reportCount))); "%count%", String.valueOf(reportCount)));
// Last online String lastOnline = target.isOnline()
String lastOnline = target.isOnline() ? "Сейчас онлайн" : ? plugin.getConfigManager().getMessage("online-status")
TimeUtil.formatTimeAgo(target.getLastPlayed()); : TimeUtil.formatTimeAgo(target.getLastPlayed());
player.sendMessage(plugin.getConfigManager().getMessage("stats-last-online", player.sendMessage(plugin.getConfigManager().getMessage("stats-last-online",
"%time%", lastOnline)); "%time%", lastOnline));
// Recent reports (max 5) player.sendMessage(plugin.getConfigManager().getMessage("stats-recent-header"));
player.sendMessage("§7Последние репорты:");
int shown = Math.min(MAX_RECENT_REPORTS, reports.size()); int shown = Math.min(MAX_RECENT_REPORTS, reports.size());
for (int i = 0; i < shown; i++) { for (int i = 0; i < shown; i++) {
Report report = reports.get(i); Report report = reports.get(i);
@ -67,7 +66,8 @@ public class ReportStatsCommand {
} }
if (reports.size() > MAX_RECENT_REPORTS) { 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)));
} }
} }
} }

View file

@ -2,12 +2,21 @@ package dev.loki.lorep.config;
import dev.loki.lorep.LorepPlugin; import dev.loki.lorep.LorepPlugin;
import org.bukkit.ChatColor; import org.bukkit.ChatColor;
import org.bukkit.Material;
import org.bukkit.configuration.ConfigurationSection;
import org.bukkit.configuration.file.FileConfiguration; 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 { public class ConfigManager {
private final LorepPlugin plugin; private final LorepPlugin plugin;
private FileConfiguration config; private FileConfiguration config;
private FileConfiguration messages;
public ConfigManager(LorepPlugin plugin) { public ConfigManager(LorepPlugin plugin) {
this.plugin = plugin; this.plugin = plugin;
@ -18,6 +27,15 @@ public class ConfigManager {
plugin.saveDefaultConfig(); plugin.saveDefaultConfig();
plugin.reloadConfig(); plugin.reloadConfig();
this.config = plugin.getConfig(); 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() { public String getWebhookUrl() {
@ -57,10 +75,52 @@ public class ConfigManager {
} }
public String getMessage(String key) { 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); 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<String> lore = section.contains("lore")
? section.getStringList("lore").stream()
.map(line -> ChatColor.translateAlternateColorCodes('&', line))
.collect(Collectors.toList())
: Collections.emptyList();
List<Integer> 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<Integer> 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) { public String getMessage(String key, String... replacements) {
String message = getMessage(key); String message = getMessage(key);
if (replacements == null || replacements.length == 0) { if (replacements == null || replacements.length == 0) {

View file

@ -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<String> lore,
List<Integer> 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;
}
}

View file

@ -22,4 +22,10 @@ public interface DatabaseManager {
List<Report> getReportsPaginated(int page, int pageSize); List<Report> getReportsPaginated(int page, int pageSize);
int getTotalReportCount(); int getTotalReportCount();
void updateReportStatus(int reportId, String status);
void updateReportImage(int reportId, String imageUrl);
Report getReportById(int id);
} }

View file

@ -73,6 +73,8 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
target_uuid TEXT NOT NULL, target_uuid TEXT NOT NULL,
target_name TEXT NOT NULL, target_name TEXT NOT NULL,
reason TEXT NOT NULL, reason TEXT NOT NULL,
image_url TEXT,
status TEXT NOT NULL DEFAULT 'PENDING',
created_at BIGINT NOT NULL created_at BIGINT NOT NULL
) )
"""; """;
@ -98,7 +100,8 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
@Override @Override
public void saveReport(Report report) { 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(); try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) { PreparedStatement stmt = conn.prepareStatement(sql)) {
@ -107,7 +110,13 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
stmt.setString(3, report.targetUuid().toString()); stmt.setString(3, report.targetUuid().toString());
stmt.setString(4, report.targetName()); stmt.setString(4, report.targetName());
stmt.setString(5, report.reason()); 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(); stmt.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to save report", 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 { private Report mapReport(ResultSet rs) throws SQLException {
String imageUrl = rs.getString("image_url");
if (rs.wasNull()) {
imageUrl = null;
}
return new Report( return new Report(
rs.getInt("id"), rs.getInt("id"),
UUID.fromString(rs.getString("reporter_uuid")), UUID.fromString(rs.getString("reporter_uuid")),
@ -237,7 +250,60 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
UUID.fromString(rs.getString("target_uuid")), UUID.fromString(rs.getString("target_uuid")),
rs.getString("target_name"), rs.getString("target_name"),
rs.getString("reason"), rs.getString("reason"),
imageUrl,
rs.getString("status"),
Instant.ofEpochMilli(rs.getLong("created_at")) 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;
}
} }

View file

@ -10,9 +10,15 @@ public record Report(
UUID targetUuid, UUID targetUuid,
String targetName, String targetName,
String reason, String reason,
String imageUrl,
String status,
Instant createdAt Instant createdAt
) { ) {
public Report(UUID reporterUuid, String reporterName, UUID targetUuid, String targetName, String reason) { 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());
} }
} }

View file

@ -54,6 +54,8 @@ public class SQLiteDatabaseManager implements DatabaseManager {
target_uuid TEXT NOT NULL, target_uuid TEXT NOT NULL,
target_name TEXT NOT NULL, target_name TEXT NOT NULL,
reason TEXT NOT NULL, reason TEXT NOT NULL,
image_url TEXT,
status TEXT NOT NULL DEFAULT 'PENDING',
created_at INTEGER NOT NULL created_at INTEGER NOT NULL
) )
"""; """;
@ -82,7 +84,8 @@ public class SQLiteDatabaseManager implements DatabaseManager {
@Override @Override
public void saveReport(Report report) { 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)) { try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, report.reporterUuid().toString()); stmt.setString(1, report.reporterUuid().toString());
@ -90,7 +93,13 @@ public class SQLiteDatabaseManager implements DatabaseManager {
stmt.setString(3, report.targetUuid().toString()); stmt.setString(3, report.targetUuid().toString());
stmt.setString(4, report.targetName()); stmt.setString(4, report.targetName());
stmt.setString(5, report.reason()); 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(); stmt.executeUpdate();
} catch (SQLException e) { } catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to save report", 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 { private Report mapReport(ResultSet rs) throws SQLException {
String imageUrl = rs.getString("image_url");
if (rs.wasNull()) {
imageUrl = null;
}
return new Report( return new Report(
rs.getInt("id"), rs.getInt("id"),
UUID.fromString(rs.getString("reporter_uuid")), UUID.fromString(rs.getString("reporter_uuid")),
@ -214,7 +227,57 @@ public class SQLiteDatabaseManager implements DatabaseManager {
UUID.fromString(rs.getString("target_uuid")), UUID.fromString(rs.getString("target_uuid")),
rs.getString("target_name"), rs.getString("target_name"),
rs.getString("reason"), rs.getString("reason"),
imageUrl,
rs.getString("status"),
Instant.ofEpochMilli(rs.getLong("created_at")) 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;
}
} }

View file

@ -1,7 +1,9 @@
package dev.loki.lorep.gui; package dev.loki.lorep.gui;
import dev.loki.lorep.LorepPlugin; import dev.loki.lorep.LorepPlugin;
import dev.loki.lorep.database.Report;
import dev.loki.lorep.util.PaginationUtil; import dev.loki.lorep.util.PaginationUtil;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
@ -28,22 +30,98 @@ public class GuiClickListener implements Listener {
return; return;
} }
int slot = event.getRawSlot(); String action = gui.slotActions.get(event.getRawSlot());
// Navigation buttons are in bottom row if (action == null) {
if (slot == ReportGui.SLOT_PREV_PAGE) { return;
if (gui.getPage() > 0) {
new ReportGui(plugin, gui.getPage() - 1).open(player);
} }
} else if (slot == ReportGui.SLOT_CLOSE) {
player.closeInventory(); switch (action) {
} else if (slot == ReportGui.SLOT_NEXT_PAGE) { 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 totalReports = plugin.getDatabaseManager().getTotalReportCount();
int totalPages = PaginationUtil.calculateTotalPages(totalReports, ReportGui.PAGE_SIZE); int pageSize = plugin.getConfig().getInt("gui.reports-per-page", 45);
if (gui.getPage() < totalPages - 1) { int totalPages = PaginationUtil.calculateTotalPages(totalReports, pageSize);
new ReportGui(plugin, gui.getPage() + 1).open(player); 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 @EventHandler

View file

@ -1,140 +1,356 @@
package dev.loki.lorep.gui; package dev.loki.lorep.gui;
import dev.loki.lorep.LorepPlugin; import dev.loki.lorep.LorepPlugin;
import dev.loki.lorep.config.GuiItemConfig;
import dev.loki.lorep.database.Report; import dev.loki.lorep.database.Report;
import dev.loki.lorep.util.PaginationUtil;
import dev.loki.lorep.util.TimeUtil; import dev.loki.lorep.util.TimeUtil;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.Material; 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.Inventory;
import org.bukkit.inventory.InventoryHolder; import org.bukkit.inventory.InventoryHolder;
import org.bukkit.inventory.ItemFlag;
import org.bukkit.inventory.ItemStack; import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.ItemMeta; import org.bukkit.inventory.meta.ItemMeta;
import org.bukkit.inventory.meta.SkullMeta; import org.bukkit.inventory.meta.SkullMeta;
import org.bukkit.entity.Player;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
public class ReportGui implements InventoryHolder { public class ReportGui implements InventoryHolder {
public static final int PAGE_SIZE = 45; // 5 rows for reports public static final int INVENTORY_SIZE = 54;
public static final int INVENTORY_SIZE = 54; // 6 rows total
public static final int SLOT_PREV_PAGE = 45; final int page;
public static final int SLOT_CLOSE = 47; boolean detailView;
public static final int SLOT_PAGE_INFO = 49; Report currentReport;
public static final int SLOT_NEXT_PAGE = 53; final Map<Integer, String> slotActions = new HashMap<>();
private final LorepPlugin plugin; private final LorepPlugin plugin;
private final int page;
private Inventory inventory; private Inventory inventory;
public ReportGui(LorepPlugin plugin, int page) { public ReportGui(LorepPlugin plugin, int page) {
this.plugin = plugin; this.plugin = plugin;
this.page = page; this.page = page;
this.detailView = false;
this.currentReport = null;
} }
public void open(Player player) { 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); this.inventory = Bukkit.createInventory(this, INVENTORY_SIZE, title);
List<Report> reports = plugin.getDatabaseManager().getReportsPaginated(page, PAGE_SIZE); int pageSize = plugin.getConfig().getInt("gui.reports-per-page", 45);
List<Report> reports = plugin.getDatabaseManager().getReportsPaginated(page, pageSize);
int totalReports = plugin.getDatabaseManager().getTotalReportCount(); int totalReports = plugin.getDatabaseManager().getTotalReportCount();
int totalPages = PaginationUtil.calculateTotalPages(totalReports, pageSize);
GuiItemConfig entryConfig = plugin.getConfigManager().getGuiItem("report-entry");
// Add report items
for (int i = 0; i < reports.size(); i++) { for (int i = 0; i < reports.size(); i++) {
Report report = reports.get(i); Report report = reports.get(i);
int targetReportCount = plugin.getDatabaseManager().getReportCount(report.targetUuid()); 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<String> lore = meta.getLore();
List<String> newLore = new ArrayList<>();
for (String line : lore) {
newLore.add(replacePlaceholders(line, report, targetReportCount));
}
meta.setLore(newLore);
}
item.setItemMeta(meta);
}
} else {
item = createFallbackReportItem(report, targetReportCount);
} }
// Add navigation buttons List<Integer> slots = entryConfig != null ? entryConfig.slots() : List.of();
addNavigationButtons(totalReports); 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());
}
}
addNavigation(totalReports, totalPages);
player.openInventory(inventory);
}
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<String> lore = meta.getLore();
List<String> 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<Integer> 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); player.openInventory(inventory);
} }
private ItemStack createReportItem(Report report, int totalReports) { private void placeDetailButton(String configPath, String action) {
GuiItemConfig config = plugin.getConfigManager().getGuiItem(configPath);
if (config == null) {
return;
}
List<Integer> 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<Integer> 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<Integer> 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<Integer> 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<String> lore = meta.getLore();
List<String> 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<Integer> 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<Integer> 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); ItemStack head = new ItemStack(Material.PLAYER_HEAD);
SkullMeta meta = (SkullMeta) head.getItemMeta(); SkullMeta meta = (SkullMeta) head.getItemMeta();
if (meta != null) { if (meta != null) {
// Set skull owner for skin
meta.setOwningPlayer(Bukkit.getOfflinePlayer(report.targetUuid())); meta.setOwningPlayer(Bukkit.getOfflinePlayer(report.targetUuid()));
// Display name
meta.setDisplayName("§c" + report.targetName()); meta.setDisplayName("§c" + report.targetName());
// Lore with report info
List<String> lore = new ArrayList<>(); List<String> lore = new ArrayList<>();
lore.add("§7"); lore.add("§7");
lore.add("§eОтправитель: §f" + report.reporterName()); lore.add("§eBy: §f" + report.reporterName());
lore.add("§ePричина: §f" + report.reason()); lore.add("§eReason: §f" + report.reason());
lore.add("§eВремя: §f" + TimeUtil.formatTimeAgo(report.createdAt().toEpochMilli())); lore.add("§eTime: §f" + TimeUtil.formatTimeAgo(report.createdAt()));
lore.add("§7"); lore.add("§7");
lore.add("§eВсего репортов: §c" + totalReports); lore.add("§eReports: §c" + totalReports);
meta.setLore(lore); meta.setLore(lore);
head.setItemMeta(meta); head.setItemMeta(meta);
} }
return head; 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<String> 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() { public int getPage() {
return page; return page;
} }
public boolean isDetailView() {
return detailView;
}
public Report getCurrentReport() {
return currentReport;
}
@Override @Override
public Inventory getInventory() { public Inventory getInventory() {
return inventory; return inventory;
} }
} }

View file

@ -1,5 +1,8 @@
package dev.loki.lorep.webhook; 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 dev.loki.lorep.database.Report;
import okhttp3.Call; import okhttp3.Call;
import okhttp3.Callback; import okhttp3.Callback;
@ -62,28 +65,161 @@ public class DiscordWebhook {
}); });
} }
public String buildPayload(Report report) { public void sendStatusUpdate(Report report) {
String timestamp = FORMATTER.format(report.createdAt()); if (webhookUrl == null || webhookUrl.isEmpty()) {
logger.warning("Discord webhook URL is not configured");
return String.format(""" return;
{
"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}
]
}]
} }
""",
escapeJson(report.targetName()), String json = buildStatusPayload(report);
escapeJson(report.reporterName()),
escapeJson(report.reason()), RequestBody body = RequestBody.create(json, JSON);
timestamp 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);
}
@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) { private String escapeJson(String text) {

View file

@ -1,16 +1,16 @@
# Discord Webhook URL для уведомлений # Discord webhook URL for report notifications
webhook-url: "" webhook-url: ""
# Настройки базы данных # Database configuration
database: database:
# Тип БД: sqlite или postgresql # Database type: "sqlite" or "postgresql"
type: "sqlite" type: "sqlite"
# Настройки SQLite # SQLite settings
sqlite: sqlite:
file: "reports.db" file: "reports.db"
# Настройки PostgreSQL # PostgreSQL settings
postgresql: postgresql:
host: "localhost" host: "localhost"
port: 5432 port: 5432
@ -19,48 +19,145 @@ database:
password: "password" password: "password"
pool-size: 10 pool-size: 10
# Настройки GUI # GUI settings
gui: gui:
# Количество репортов на странице # Reports per page in GUI
reports-per-page: 45 reports-per-page: 45
# Автообновление GUI (в тиках, 20 тиков = 1 секунда) # Auto-refresh interval in ticks (20 ticks = 1 second)
auto-refresh: 100 auto-refresh: 100
# Показывать головы игроков в GUI # Show player heads in GUI
show-player-heads: true 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: limits:
# Максимальная длина причины репорта # Maximum reason length in characters
max-reason-length: 100 max-reason-length: 100
# Кулдаун между репортами (в секундах) # Cooldown between reports (seconds)
report-cooldown: 30 report-cooldown: 30
# Максимум репортов на одного игрока от одного отправителя # Max reports per player from the same reporter
max-reports-per-player: 3 max-reports-per-player: 3
# Уведомления # Notifications
notifications: notifications:
# Уведомлять администраторов о новых репортах # Notify admins about new reports
notify-admins: true notify-admins: true
# Звук уведомления для администраторов # Admin notification sound
admin-notification-sound: "BLOCK_NOTE_BLOCK_PLING" admin-notification-sound: "BLOCK_NOTE_BLOCK_PLING"
# Уведомлять игрока о принятии репорта # Notify reporter when report is handled
notify-reporter: true notify-reporter: true
# Сообщения # Messages (override defaults from messages.yml)
messages: messages:
report-sent: "&aРепорт успешно отправлен!" report-sent: "&aReport sent successfully!"
already-reported: "&cВы уже отправляли репорт на этого игрока!" already-reported: "&cYou have already reported this player!"
self-report: "&cВы не можете отправить репорт на себя!" self-report: "&cYou cannot report yourself!"
player-not-found: "&cИгрок не найден!" player-not-found: "&cPlayer not found!"
no-permission: "&cУ вас нет прав на эту команду!" no-permission: "&cYou don't have permission!"
usage: "&eИспользование: /report <ник> <причина>" usage: "&eUsage: /report <player> <reason> [screenshot_url] | /report gui | /report stats <player> | /report image <id> <url>"
no-reports: "&eУ этого игрока нет репортов." no-reports: "&eThis player has no reports."
stats-header: "&6=== Статистика репортов: %player% ===" stats-header: "&6=== Report stats: %player% ==="
stats-count: "&eВсего репортов: &f%count%" stats-count: "&eTotal reports: &f%count%"
stats-last-online: "&eПоследний онлайн: &f%time%" stats-last-online: "&eLast online: &f%time%"
stats-report-entry: "&7- %reason% &8(%time% назад)" stats-report-entry: "&7- %reason% &8(%time% ago)"
gui-title: "Репорты - Страница %page%" gui-title: "Reports - Page %page%"
cooldown: "&cПодождите %time% секунд перед следующим репортом!" cooldown: "&cPlease wait %time% seconds before your next report!"
reason-too-long: "&cПричина слишком длинная! Максимум %max% символов." reason-too-long: "&cReason too long! Max %max% characters."
max-reports-reached: "&cВы уже отправили максимальное количество репортов на этого игрока!" max-reports-reached: "&cYou've reached the max reports for this player!"
admin-notification: "&6[Loreport] &eНовый репорт от &f%reporter% &eна &f%reported%: &7%reason%" 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 <player> <reason> [screenshot_url]"
report-image-url-usage: "&eUsage: /report image <id> <url>"
status-pending: "&cPending"
status-reviewing: "&eReviewing"
status-resolved: "&aResolved"
status-dismissed: "&7Dismissed"

View file

@ -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 <player> <reason> [screenshot_url] | /report gui | /report stats <player> | /report image <id> <url>"
# 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 <player> <reason> [screenshot_url]"
report-image-url-usage: "&eUsage: /report image <id> <url>"
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"

View file

@ -9,7 +9,7 @@ website: https://codeberg.org/loki5512344/Loreport
commands: commands:
report: report:
description: Report a player or manage reports description: Report a player or manage reports
usage: /report <player> <reason> | /report gui | /report stats <player> usage: /report <player> <reason> [image_url] | /report gui | /report stats <player> | /report image <id> <url>
permission: lorep.report permission: lorep.report
aliases: [rep, reports] aliases: [rep, reports]

View file

@ -65,7 +65,7 @@ public class DatabaseManagerTest {
Assume.that(!reporterUuid.equals(targetUuid)); Assume.that(!reporterUuid.equals(targetUuid));
Report report = new Report( Report report = new Report(
0, reporterUuid, "Reporter", targetUuid, "Target", reason, Instant.now() reporterUuid, "Reporter", targetUuid, "Target", reason
); );
dbManager.saveReport(report); dbManager.saveReport(report);
@ -95,7 +95,7 @@ public class DatabaseManagerTest {
"Should not have reported before creating report"); "Should not have reported before creating report");
Report report = new Report( Report report = new Report(
0, reporterUuid, "Reporter", targetUuid, "Target", "Test reason", Instant.now() reporterUuid, "Reporter", targetUuid, "Target", "Test reason"
); );
dbManager.saveReport(report); dbManager.saveReport(report);
@ -121,7 +121,7 @@ public class DatabaseManagerTest {
String targetName = "Target_" + targetUuid.toString().substring(0, 8); String targetName = "Target_" + targetUuid.toString().substring(0, 8);
Instant createdAt = Instant.now(); 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); dbManager.saveReport(original);
// Close and reopen database // Close and reopen database
@ -151,9 +151,10 @@ public class DatabaseManagerTest {
UUID targetUuid = UUID.randomUUID(); UUID targetUuid = UUID.randomUUID();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Instant reportTime = Instant.now().minusSeconds(count - i);
Report report = new Report( Report report = new Report(
0, UUID.randomUUID(), "Reporter" + i, targetUuid, "Target", 0, UUID.randomUUID(), "Reporter" + i, targetUuid, "Target",
"Reason " + i, Instant.now().minusSeconds(count - i) "Reason " + i, null, "PENDING", reportTime
); );
dbManager.saveReport(report); dbManager.saveReport(report);
} }
@ -180,8 +181,8 @@ public class DatabaseManagerTest {
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Report report = new Report( Report report = new Report(
0, UUID.randomUUID(), "Reporter" + i, targetUuid, "Target", UUID.randomUUID(), "Reporter" + i, targetUuid, "Target",
"Reason " + i, Instant.now() "Reason " + i
); );
dbManager.saveReport(report); dbManager.saveReport(report);
} }

View file

@ -33,13 +33,11 @@ public class DiscordWebhookTest {
DiscordWebhook webhook = new DiscordWebhook("https://example.com/webhook", logger); DiscordWebhook webhook = new DiscordWebhook("https://example.com/webhook", logger);
Report report = new Report( Report report = new Report(
1,
UUID.randomUUID(), UUID.randomUUID(),
reporterName, reporterName,
UUID.randomUUID(), UUID.randomUUID(),
targetName, targetName,
reason, reason
Instant.now()
); );
String payload = webhook.buildPayload(report); String payload = webhook.buildPayload(report);
@ -95,9 +93,9 @@ public class DiscordWebhookTest {
DiscordWebhook webhook = new DiscordWebhook("https://example.com/webhook", logger); DiscordWebhook webhook = new DiscordWebhook("https://example.com/webhook", logger);
Report report = new Report( Report report = new Report(
1, UUID.randomUUID(), "TestReporter", UUID.randomUUID(), "TestReporter",
UUID.randomUUID(), "TestTarget", UUID.randomUUID(), "TestTarget",
"Test reason", Instant.now() "Test reason"
); );
String payload = webhook.buildPayload(report); String payload = webhook.buildPayload(report);