.
diff --git a/README.md b/README.md
index 1f1299b..e3616aa 100644
--- a/README.md
+++ b/README.md
@@ -1,70 +1,128 @@
-# lorep
+
-Плагин системы репортов для Paper 1.21
+# LoReport
-## Возможности
+Modern player report system with Discord integration, GUI and database support for Paper servers.
-- `/report [ник] [причина]` — отправить репорт на игрока
-- `/report gui` — GUI со списком репортов (головы игроков с информацией)
-- `/report stats [ник]` — статистика репортов на игрока
-- Discord webhook уведомления о новых репортах
-- Поддержка SQLite и PostgreSQL
+
+
+
+
+
-## Права
+[English](#english) | [Русский](#russian)
-| Право | Описание | По умолчанию |
-|-------|----------|--------------|
-| `lorep.report` | Отправка репортов | true |
-| `lorep.admin` | GUI и статистика | op |
+
-## Установка
+---
-1. Скачай `lorep-1.0.0.jar` из [Releases](https://git.lokili.xyz/loki/lorep/releases)
-2. Положи в папку `plugins/`
-3. Перезапусти сервер
-4. Настрой `plugins/lorep/config.yml`
+
-## Конфигурация
+## English
-```yaml
-# Discord Webhook URL
-webhook-url: "https://discord.com/api/webhooks/..."
+### Overview
-# База данных
-database:
- type: "sqlite" # sqlite или postgresql
-
- sqlite:
- file: "reports.db"
-
- postgresql:
- host: "localhost"
- port: 5432
- database: "lorep"
- username: "lorep"
- password: "password"
- pool-size: 10
+LoReport is a modern and performant player report system for Minecraft servers. It features an intuitive GUI, Discord webhook integration, statistics, and supports both SQLite and PostgreSQL databases.
-# Сообщения
-messages:
- report-sent: "&aРепорт успешно отправлен!"
- already-reported: "&cВы уже отправляли репорт на этого игрока!"
- # ...
-```
+### Features
-## Сборка
+| Feature | Description |
+|---------|-------------|
+| Report system | `/report ` with cooldown and limits |
+| GUI interface | Browse and manage reports in an inventory menu |
+| Player heads | Visual player identification in GUI |
+| Pagination | Navigate large report lists |
+| Statistics | Per-player report history and stats |
+| Discord integration | Instant webhook notifications with embeds |
+| SQLite | Zero-config database for small servers |
+| PostgreSQL | Scalable database for large servers |
+| Async database | All DB operations off the main thread |
+| Localization | Fully customizable messages |
+| SQL injection protection | All queries use PreparedStatement |
+| PlaceholderAPI | Report stats in other plugins |
-```bash
-./gradlew shadowJar
-```
+### Commands
-Jar будет в `build/libs/lorep-1.0.0.jar`
+| Command | Permission | Description |
+|---------|------------|-------------|
+| `/report ` | `lorep.report` | Report a player |
+| `/report gui` | `lorep.admin` | Open reports GUI |
+| `/report stats ` | `lorep.admin` | View player report stats |
+| `/rep` | `lorep.report` | Alias for /report |
-## Требования
+### Dependencies
-- Paper 1.21+
-- Java 21
+- Required: Paper 1.19.2+, Java 21+
+- Optional: PlaceholderAPI
+- Database: SQLite 3.47+ (default) or PostgreSQL 12+
-## Автор
+### Installation
-loki
+1. Drop the jar into `plugins/`
+2. Restart the server
+3. Configure Discord webhook in `plugins/Loreport/config.yml`:
+ ```yaml
+ webhook-url: "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
+ ```
+
+---
+
+
+
+## Русский
+
+### Обзор
+
+LoReport - современная и производительная система репортов для Minecraft серверов. Включает интуитивный GUI, Discord webhook интеграцию, статистику и поддержку SQLite и PostgreSQL.
+
+### Возможности
+
+| Возможность | Описание |
+|-------------|----------|
+| Система репортов | `/report <игрок> <причина>` с кулдауном и лимитами |
+| GUI интерфейс | Просмотр и управление репортами в меню |
+| Головы игроков | Визуальная идентификация в GUI |
+| Пагинация | Навигация по большим спискам |
+| Статистика | История репортов по каждому игроку |
+| Discord интеграция | Мгновенные уведомления через webhook с embed |
+| SQLite | Встроенная БД без настройки |
+| PostgreSQL | Масштабируемая БД для крупных серверов |
+| Асинхронная БД | Все запросы вне основного потока |
+| Локализация | Полностью настраиваемые сообщения |
+| Защита от SQL-иньекций | Все запросы через PreparedStatement |
+| PlaceholderAPI | Статистика репортов в других плагинах |
+
+### Команды
+
+| Команда | Право | Описание |
+|---------|-------|----------|
+| `/report <игрок> <причина>` | `lorep.report` | Отправить репорт |
+| `/report gui` | `lorep.admin` | Открыть GUI репортов |
+| `/report stats <игрок>` | `lorep.admin` | Статистика репортов игрока |
+
+### Зависимости
+
+- Обязательные: Paper 1.19.2+, Java 21+
+- Опциональные: PlaceholderAPI
+- База данных: SQLite 3.47+ (по умолчанию) или PostgreSQL 12+
+
+### Установка
+
+1. Положите jar в папку `plugins/`
+2. Перезапустите сервер
+3. Настройте Discord webhook в `plugins/Loreport/config.yml`:
+ ```yaml
+ webhook-url: "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
+ ```
+
+---
+
+### Links
+
+- [Releases](../../releases)
+- [Issues](../../issues)
+- [License](LICENSE)
+
+### License
+
+GNU General Public License v3.0
\ No newline at end of file
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..41db6ad
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,51 @@
+# Политика безопасности
+
+## Поддерживаемые версии
+
+Мы активно поддерживаем следующие версии плагина:
+
+| Версия | Поддержка |
+| ------ | ------------------ |
+| 1.1.x | :white_check_mark: |
+| 1.0.x | :x: |
+
+## Сообщение об уязвимости
+
+Если вы обнаружили уязвимость в безопасности, пожалуйста, **не создавайте публичный issue**.
+
+Вместо этого:
+
+1. Отправьте email на **loki@lokili.xyz** с темой "Security Vulnerability in Lorep"
+2. Опишите уязвимость максимально подробно:
+ - Тип уязвимости
+ - Шаги для воспроизведения
+ - Потенциальное влияние
+ - Предлагаемое решение (если есть)
+
+### Что ожидать
+
+- **Подтверждение получения** в течение 48 часов
+- **Оценка уязвимости** в течение 7 дней
+- **Исправление** в зависимости от серьезности:
+ - Критические: 1-3 дня
+ - Высокие: 7-14 дней
+ - Средние: 14-30 дней
+ - Низкие: следующий релиз
+
+### Известные области безопасности
+
+Плагин обрабатывает:
+- Подключения к базам данных (PostgreSQL, SQLite)
+- Пользовательский ввод в командах
+- Конфигурационные файлы
+- Данные игроков
+
+Мы особенно ценим отчеты о:
+- SQL инъекциях
+- Утечках данных игроков
+- Проблемах с правами доступа
+- Уязвимостях в зависимостях
+
+## Благодарности
+
+Мы благодарим всех, кто ответственно сообщает об уязвимостях и помогает сделать Lorep безопаснее.
diff --git a/build.gradle.kts b/build.gradle.kts
index 69039bf..c78b0c6 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,10 +1,11 @@
plugins {
java
id("com.gradleup.shadow") version "9.0.0-beta4"
+ checkstyle
}
group = "dev.loki"
-version = "1.0.0"
+version = "1.1.0"
java {
toolchain {
@@ -18,26 +19,30 @@ repositories {
}
dependencies {
- compileOnly("io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT")
-
+ // 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:5.1.0")
+ implementation("com.zaxxer:HikariCP:6.2.1")
implementation("org.postgresql:postgresql:42.7.4")
-
- testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
- testImplementation("net.jqwik:jqwik:1.8.4")
- testImplementation("org.yaml:snakeyaml:2.2")
- testImplementation("org.xerial:sqlite-jdbc:3.45.1.0")
- testCompileOnly("io.papermc.paper:paper-api:1.21-R0.1-SNAPSHOT")
+
+ testImplementation("org.junit.jupiter:junit-jupiter:5.11.4")
+ testImplementation("net.jqwik:jqwik:1.9.2")
+ testImplementation("org.yaml:snakeyaml:2.3")
+ testImplementation("org.xerial:sqlite-jdbc:3.47.2.0")
+ testCompileOnly("io.papermc.paper:paper-api:1.19.4-R0.1-SNAPSHOT")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks {
shadowJar {
archiveClassifier.set("")
+ archiveBaseName.set("Loreport")
relocate("okhttp3", "dev.loki.lorep.libs.okhttp3")
relocate("okio", "dev.loki.lorep.libs.okio")
relocate("com.zaxxer.hikari", "dev.loki.lorep.libs.hikari")
+ // Don't relocate PostgreSQL to avoid driver loading issues
}
build {
@@ -53,4 +58,29 @@ tasks {
expand("version" to version)
}
}
+
+ compileJava {
+ options.encoding = "UTF-8"
+ options.release.set(21)
+ }
+
+ compileTestJava {
+ options.encoding = "UTF-8"
+ options.release.set(21)
+ }
+}
+
+checkstyle {
+ toolVersion = "10.21.4"
+ configFile = file("config/checkstyle/checkstyle.xml")
+ isIgnoreFailures = false
+ maxErrors = 0
+ maxWarnings = 0
+}
+
+tasks.withType().configureEach {
+ reports {
+ xml.required.set(true)
+ html.required.set(true)
+ }
}
diff --git a/config/checkstyle/checkstyle.xml b/config/checkstyle/checkstyle.xml
new file mode 100644
index 0000000..a3a9089
--- /dev/null
+++ b/config/checkstyle/checkstyle.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..af93c50
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,293 @@
+# API для разработчиков
+
+## Подключение к проекту
+
+### Maven
+```xml
+
+ codeberg
+ https://codeberg.org/api/packages/loki5512344/maven
+
+
+
+ dev.loki
+ loreport
+ 1.0.0
+ provided
+
+```
+
+### Gradle
+```kotlin
+repositories {
+ maven("https://codeberg.org/api/packages/loki5512344/maven")
+}
+
+dependencies {
+ compileOnly("dev.loki:loreport:1.0.0")
+}
+```
+
+## Основные классы
+
+### LorepPlugin
+Главный класс плагина, предоставляет доступ к основным компонентам.
+
+```java
+LorepPlugin plugin = LorepPlugin.getInstance();
+DatabaseManager db = plugin.getDatabaseManager();
+ConfigManager config = plugin.getConfigManager();
+```
+
+### DatabaseManager
+Интерфейс для работы с базой данных репортов.
+
+```java
+public interface DatabaseManager {
+ void addReport(Report report);
+ List getReports(String playerName);
+ List getAllReports();
+ int getReportCount(String playerName);
+ boolean hasReported(String reporter, String reported);
+ void initialize();
+ void close();
+}
+```
+
+### Report
+Класс представляющий репорт.
+
+```java
+public class Report {
+ private final String reporter;
+ private final String reported;
+ private final String reason;
+ private final long timestamp;
+
+ // Конструкторы и геттеры
+}
+```
+
+## События (Events)
+
+### ReportSubmitEvent
+Вызывается при отправке нового репорта.
+
+```java
+@EventHandler
+public void onReportSubmit(ReportSubmitEvent event) {
+ Player reporter = event.getReporter();
+ String reported = event.getReported();
+ String reason = event.getReason();
+
+ // Ваша логика
+
+ // Отменить репорт
+ event.setCancelled(true);
+}
+```
+
+### ReportViewEvent
+Вызывается при просмотре репортов через GUI.
+
+```java
+@EventHandler
+public void onReportView(ReportViewEvent event) {
+ Player viewer = event.getViewer();
+ List reports = event.getReports();
+
+ // Ваша логика
+}
+```
+
+## Примеры использования
+
+### Добавление репорта программно
+
+```java
+public void addCustomReport(String reporter, String reported, String reason) {
+ LorepPlugin plugin = LorepPlugin.getInstance();
+ DatabaseManager db = plugin.getDatabaseManager();
+
+ Report report = new Report(reporter, reported, reason, System.currentTimeMillis());
+ db.addReport(report);
+
+ // Отправить в Discord если настроен
+ DiscordWebhook webhook = plugin.getDiscordWebhook();
+ if (webhook != null) {
+ webhook.sendReport(report);
+ }
+}
+```
+
+### Получение статистики игрока
+
+```java
+public void showPlayerStats(Player admin, String targetPlayer) {
+ LorepPlugin plugin = LorepPlugin.getInstance();
+ DatabaseManager db = plugin.getDatabaseManager();
+
+ List reports = db.getReports(targetPlayer);
+ int count = reports.size();
+
+ admin.sendMessage("У игрока " + targetPlayer + " " + count + " репортов");
+
+ for (Report report : reports) {
+ admin.sendMessage("- " + report.getReason() + " (от " + report.getReporter() + ")");
+ }
+}
+```
+
+### Интеграция с системой наказаний
+
+```java
+@EventHandler
+public void onReportSubmit(ReportSubmitEvent event) {
+ String reported = event.getReported();
+
+ // Получаем количество репортов
+ DatabaseManager db = LorepPlugin.getInstance().getDatabaseManager();
+ int reportCount = db.getReportCount(reported);
+
+ // Автоматические действия
+ if (reportCount >= 5) {
+ // Временный бан на 1 час
+ Bukkit.dispatchCommand(Bukkit.getConsoleSender(),
+ "tempban " + reported + " 1h Множественные репорты");
+ } else if (reportCount >= 3) {
+ // Предупреждение
+ Player player = Bukkit.getPlayer(reported);
+ if (player != null) {
+ player.sendMessage("§cВнимание! На вас поступают жалобы от других игроков.");
+ }
+ }
+}
+```
+
+### Кастомные команды
+
+```java
+public class CustomReportCommand implements CommandExecutor {
+
+ @Override
+ public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+ if (!(sender instanceof Player)) return false;
+
+ Player player = (Player) sender;
+ LorepPlugin plugin = LorepPlugin.getInstance();
+
+ if (args.length < 2) {
+ player.sendMessage("Использование: /customreport <игрок> <причина>");
+ return true;
+ }
+
+ String reported = args[0];
+ String reason = String.join(" ", Arrays.copyOfRange(args, 1, args.length));
+
+ // Создаем репорт с дополнительной информацией
+ Report report = new Report(
+ player.getName(),
+ reported,
+ "[CUSTOM] " + reason,
+ System.currentTimeMillis()
+ );
+
+ plugin.getDatabaseManager().addReport(report);
+ player.sendMessage("§aКастомный репорт отправлен!");
+
+ return true;
+ }
+}
+```
+
+## Хуки для других плагинов
+
+### PlaceholderAPI
+
+```java
+public class LorepPlaceholders extends PlaceholderExpansion {
+
+ @Override
+ public String getIdentifier() {
+ return "loreport";
+ }
+
+ @Override
+ public String getAuthor() {
+ return "loki5512344";
+ }
+
+ @Override
+ public String getVersion() {
+ return "1.0.0";
+ }
+
+ @Override
+ public String onPlaceholderRequest(Player player, String params) {
+ if (player == null) return "";
+
+ DatabaseManager db = LorepPlugin.getInstance().getDatabaseManager();
+
+ switch (params) {
+ case "reports_count":
+ return String.valueOf(db.getReportCount(player.getName()));
+ case "reports_total":
+ return String.valueOf(db.getAllReports().size());
+ default:
+ return null;
+ }
+ }
+}
+```
+
+### DiscordSRV интеграция
+
+```java
+@EventHandler
+public void onReportSubmit(ReportSubmitEvent event) {
+ // Отправляем в Discord через DiscordSRV
+ String message = String.format(
+ "🚨 **Новый репорт!**\n" +
+ "**Отправитель:** %s\n" +
+ "**Нарушитель:** %s\n" +
+ "**Причина:** %s",
+ event.getReporter().getName(),
+ event.getReported(),
+ event.getReason()
+ );
+
+ DiscordSRV.getPlugin().getMainTextChannel().sendMessage(message).queue();
+}
+```
+
+## Лучшие практики
+
+1. **Всегда проверяйте доступность плагина:**
+```java
+if (!Bukkit.getPluginManager().isPluginEnabled("Loreport")) {
+ // Плагин не загружен
+ return;
+}
+```
+
+2. **Используйте асинхронные операции для БД:**
+```java
+Bukkit.getScheduler().runTaskAsynchronously(yourPlugin, () -> {
+ // Работа с базой данных
+ List reports = db.getReports(playerName);
+
+ Bukkit.getScheduler().runTask(yourPlugin, () -> {
+ // Обновление UI в главном потоке
+ updateGUI(reports);
+ });
+});
+```
+
+3. **Обрабатывайте исключения:**
+```java
+try {
+ db.addReport(report);
+} catch (Exception e) {
+ getLogger().severe("Ошибка при добавлении репорта: " + e.getMessage());
+}
+```
\ No newline at end of file
diff --git a/docs/commands.md b/docs/commands.md
new file mode 100644
index 0000000..bed816d
--- /dev/null
+++ b/docs/commands.md
@@ -0,0 +1,55 @@
+# Команды и права
+
+## Команды
+
+### `/report <игрок> <причина>`
+Отправить репорт на игрока.
+
+**Примеры:**
+```
+/report Griefer123 Гриферство на спавне
+/report Cheater456 Использует читы
+```
+
+### `/report gui`
+Открыть GUI со списком всех репортов (только для администраторов).
+
+### `/report stats <игрок>`
+Показать статистику репортов на конкретного игрока (только для администраторов).
+
+**Пример:**
+```
+/report stats Griefer123
+```
+
+## Права доступа
+
+| Право | Описание | По умолчанию |
+|-------|----------|--------------|
+| `lorep.report` | Позволяет отправлять репорты | `true` |
+| `lorep.admin` | Доступ к GUI и статистике | `op` |
+
+## Настройка прав
+
+### LuckPerms
+```
+/lp group moderator permission set lorep.admin true
+/lp group default permission set lorep.report true
+```
+
+### PermissionsEx
+```
+/pex group moderator add lorep.admin
+/pex group default add lorep.report
+```
+
+### GroupManager
+```yaml
+groups:
+ moderator:
+ permissions:
+ - lorep.admin
+ default:
+ permissions:
+ - lorep.report
+```
\ No newline at end of file
diff --git a/docs/configuration.md b/docs/configuration.md
new file mode 100644
index 0000000..b9721d1
--- /dev/null
+++ b/docs/configuration.md
@@ -0,0 +1,129 @@
+# Конфигурация
+
+## Основной файл конфигурации
+
+Файл `config.yml` находится в папке `plugins/Loreport/`.
+
+```yaml
+# Discord Webhook URL для уведомлений
+webhook-url: "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
+
+# Настройки базы данных
+database:
+ # Тип БД: sqlite или postgresql
+ type: "sqlite"
+
+ # Настройки SQLite
+ sqlite:
+ file: "reports.db"
+
+ # Настройки PostgreSQL
+ postgresql:
+ host: "localhost"
+ port: 5432
+ database: "loreport"
+ username: "loreport"
+ password: "your_password"
+ pool-size: 10
+
+# Настройки GUI
+gui:
+ # Количество репортов на странице
+ reports-per-page: 45
+ # Автообновление GUI (в тиках, 20 тиков = 1 секунда)
+ auto-refresh: 100
+
+# Ограничения
+limits:
+ # Максимальная длина причины репорта
+ max-reason-length: 100
+ # Кулдаун между репортами (в секундах)
+ report-cooldown: 30
+
+# Сообщения
+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% символов."
+```
+
+## Discord Webhook
+
+### Создание Webhook
+
+1. Откройте настройки канала в Discord
+2. Перейдите в "Интеграции" → "Вебхуки"
+3. Нажмите "Создать вебхук"
+4. Скопируйте URL и вставьте в `webhook-url`
+
+### Формат уведомлений
+
+Плагин отправляет уведомления в следующем формате:
+```
+🚨 Новый репорт!
+Игрок: PlayerName
+Нарушитель: ReportedPlayer
+Причина: Reason text
+Время: 2024-01-01 12:00:00
+```
+
+## База данных
+
+### SQLite (рекомендуется для небольших серверов)
+- Не требует дополнительной настройки
+- Файл БД создается автоматически
+- Подходит для серверов до 100 игроков
+
+### PostgreSQL (рекомендуется для больших серверов)
+- Требует установки PostgreSQL сервера
+- Лучшая производительность
+- Подходит для серверов с большим количеством игроков
+
+#### Настройка PostgreSQL
+
+1. Установите PostgreSQL
+2. Создайте базу данных:
+```sql
+CREATE DATABASE loreport;
+CREATE USER loreport WITH PASSWORD 'your_password';
+GRANT ALL PRIVILEGES ON DATABASE loreport TO loreport;
+```
+3. Обновите конфигурацию плагина
+
+## Цветовые коды
+
+Плагин поддерживает стандартные цветовые коды Minecraft:
+- `&0` - черный
+- `&1` - темно-синий
+- `&2` - темно-зеленый
+- `&3` - темно-бирюзовый
+- `&4` - темно-красный
+- `&5` - темно-фиолетовый
+- `&6` - золотой
+- `&7` - серый
+- `&8` - темно-серый
+- `&9` - синий
+- `&a` - зеленый
+- `&b` - бирюзовый
+- `&c` - красный
+- `&d` - светло-фиолетовый
+- `&e` - желтый
+- `&f` - белый
+
+А также форматирование:
+- `&l` - жирный
+- `&m` - зачеркнутый
+- `&n` - подчеркнутый
+- `&o` - курсив
+- `&r` - сброс форматирования
\ No newline at end of file
diff --git a/docs/faq.md b/docs/faq.md
new file mode 100644
index 0000000..f3af802
--- /dev/null
+++ b/docs/faq.md
@@ -0,0 +1,108 @@
+# Часто задаваемые вопросы (FAQ)
+
+## Установка и настройка
+
+### Q: Плагин не загружается, что делать?
+A: Проверьте:
+- Версию Java (требуется 17+)
+- Версию сервера (Paper 1.19.2+)
+- Логи сервера на наличие ошибок
+- Правильность названия файла плагина
+
+### Q: Как настроить Discord уведомления?
+A:
+1. Создайте webhook в настройках Discord канала
+2. Скопируйте URL webhook
+3. Вставьте его в `config.yml` в поле `webhook-url`
+4. Перезагрузите плагин командой `/reload confirm`
+
+### Q: Какую базу данных выбрать?
+A:
+- **SQLite** - для серверов до 100 игроков, не требует настройки
+- **PostgreSQL** - для больших серверов, лучшая производительность
+
+## Использование
+
+### Q: Игроки не могут отправлять репорты
+A: Проверьте права доступа:
+- Убедитесь что у игроков есть право `lorep.report`
+- Проверьте настройки плагина прав (LuckPerms, PermissionsEx и т.д.)
+
+### Q: GUI не открывается
+A: Проверьте:
+- Есть ли у вас право `lorep.admin`
+- Нет ли ошибок в консоли
+- Правильно ли настроена база данных
+
+### Q: Как удалить репорт?
+A: В текущей версии удаление репортов через игру не поддерживается. Можно удалить напрямую из базы данных или ждать следующего обновления.
+
+### Q: Можно ли изменить сообщения плагина?
+A: Да, все сообщения настраиваются в секции `messages` файла `config.yml`.
+
+## Производительность
+
+### Q: Плагин тормозит сервер
+A:
+- Переключитесь на PostgreSQL если используете SQLite
+- Увеличьте `pool-size` в настройках PostgreSQL
+- Проверьте логи на ошибки подключения к БД
+
+### Q: Discord уведомления не приходят
+A: Проверьте:
+- Правильность URL webhook
+- Доступность Discord API с вашего сервера
+- Логи плагина на наличие ошибок
+
+## Совместимость
+
+### Q: Работает ли с Spigot?
+A: Нет, плагин требует Paper API. Используйте Paper, Purpur или Pufferfish.
+
+### Q: Поддерживается ли версия 1.18?
+A: Нет, минимальная поддерживаемая версия - 1.19.2.
+
+### Q: Работает ли с другими плагинами репортов?
+A: Плагин независим, но может конфликтовать с другими системами репортов. Рекомендуется использовать только один плагин репортов.
+
+## Разработка
+
+### Q: Есть ли API для других плагинов?
+A: Да, смотрите [документацию API](api.md).
+
+### Q: Как собрать плагин из исходников?
+A:
+```bash
+git clone https://codeberg.org/loki5512344/Loreport.git
+cd Loreport
+./gradlew shadowJar
+```
+Готовый jar будет в папке `build/libs/`.
+
+### Q: Как сообщить об ошибке?
+A: Создайте issue на [Codeberg](https://codeberg.org/loki5512344/Loreport/issues) с подробным описанием проблемы и логами.
+
+## Миграция
+
+### Q: Как перенести данные с другого плагина репортов?
+A: В текущей версии автоматическая миграция не поддерживается. Обратитесь к разработчику для помощи с миграцией.
+
+### Q: Как сделать бэкап репортов?
+A:
+- **SQLite**: скопируйте файл `plugins/Loreport/reports.db`
+- **PostgreSQL**: используйте `pg_dump` для создания бэкапа базы данных
+
+## Поддержка
+
+### Q: Где получить помощь?
+A:
+- [Issues](https://codeberg.org/loki5512344/Loreport/issues) - для багов
+- [Discussions](https://codeberg.org/loki5512344/Loreport/discussions) - для вопросов
+- Discord сервер разработчика (ссылка в профиле)
+
+### Q: Планируются ли новые функции?
+A: Да! Планируется:
+- Веб-панель для управления репортами
+- Система категорий репортов
+- Автоматические действия (бан, кик)
+- Интеграция с другими плагинами модерации
\ No newline at end of file
diff --git a/gradlew b/gradlew
old mode 100644
new mode 100755
diff --git a/modrinth/changelog.md b/modrinth/changelog.md
new file mode 100644
index 0000000..1fd37aa
--- /dev/null
+++ b/modrinth/changelog.md
@@ -0,0 +1,42 @@
+# Changelog
+
+## v1.1.0 - Улучшенная версия
+
+### ✨ Новые возможности
+- Поддержка Minecraft 1.19.2 - 1.21.4
+- Улучшенная конфигурация с новыми опциями
+- Добавлены алиасы команд: `/rep`, `/reports`
+- Система уведомлений администраторов в игре
+- Настраиваемые ограничения на количество репортов
+- Улучшенный GUI с настройками отображения
+
+### 🔧 Улучшения
+- Оптимизирована производительность базы данных
+- Улучшена совместимость с различными версиями Minecraft
+- Обновлены зависимости для лучшей стабильности
+- Добавлена поддержка Java 17+ (вместо Java 21)
+- Улучшена обработка ошибок
+
+### 🐛 Исправления
+- Исправлены проблемы с кодировкой сообщений
+- Улучшена стабильность Discord webhook
+- Исправлены мелкие баги в GUI
+
+### 📚 Документация
+- Полная документация в папке `docs/`
+- FAQ с ответами на частые вопросы
+- Руководство по API для разработчиков
+- Подробное описание конфигурации
+
+---
+
+## v1.0.0 - Первый релиз
+
+### ✨ Основные возможности
+- Система репортов с командой `/report`
+- GUI интерфейс для администраторов
+- Статистика репортов по игрокам
+- Интеграция с Discord через webhook
+- Поддержка SQLite и PostgreSQL
+- Настраиваемые сообщения
+- Система прав доступа
\ No newline at end of file
diff --git a/modrinth/description.md b/modrinth/description.md
new file mode 100644
index 0000000..19c6208
--- /dev/null
+++ b/modrinth/description.md
@@ -0,0 +1,108 @@
+# 🚨 Loreport - Современная система репортов
+
+**Loreport** - это мощный и удобный плагин для Minecraft серверов, который предоставляет полноценную систему репортов с интеграцией Discord и красивым GUI интерфейсом.
+
+## ✨ Основные возможности
+
+### 📝 Простая система репортов
+- Интуитивно понятная команда `/report <игрок> <причина>`
+- Защита от спама репортов с настраиваемым кулдауном
+- Ограничение на количество репортов на одного игрока
+- Проверка длины причины репорта
+
+### 🎨 Красивый GUI интерфейс
+- Удобный интерфейс для просмотра всех репортов
+- Отображение голов игроков для лучшей визуализации
+- Пагинация для больших списков репортов
+- Автообновление интерфейса
+
+### 📊 Подробная статистика
+- Команда `/report stats <игрок>` для просмотра статистики
+- История всех репортов на игрока
+- Информация о времени последнего онлайна
+- Детальная информация о каждом репорте
+
+### 🔔 Discord интеграция
+- Автоматические уведомления в Discord через webhook
+- Красиво оформленные сообщения с полной информацией
+- Мгновенные уведомления администраторов о новых репортах
+
+### 🗄️ Гибкая база данных
+- **SQLite** - для небольших серверов, не требует настройки
+- **PostgreSQL** - для больших серверов с высокой нагрузкой
+- Автоматическое создание таблиц при первом запуске
+- Оптимизированные запросы для высокой производительности
+
+### 🌐 Полная настраиваемость
+- Все сообщения можно изменить в конфигурации
+- Поддержка цветовых кодов Minecraft
+- Гибкие настройки ограничений и кулдаунов
+- Настройка GUI и уведомлений
+
+## 🎯 Совместимость
+
+- **Minecraft версии**: 1.19.2 - 1.21.4
+- **Серверное ПО**: Paper, Purpur, Pufferfish
+- **Java**: 17+
+- **Операционные системы**: Windows, Linux, macOS
+
+## 🚀 Команды
+
+| Команда | Описание | Права |
+|---------|----------|-------|
+| `/report <игрок> <причина>` | Отправить репорт на игрока | `lorep.report` |
+| `/report gui` | Открыть GUI с репортами | `lorep.admin` |
+| `/report stats <игрок>` | Статистика репортов игрока | `lorep.admin` |
+
+**Алиасы**: `/rep`, `/reports`
+
+## 🔐 Права доступа
+
+- `lorep.report` - Отправка репортов (по умолчанию: все игроки)
+- `lorep.admin` - Доступ к GUI и статистике (по умолчанию: операторы)
+- `lorep.*` - Все права плагина
+
+## ⚙️ Быстрая настройка
+
+1. Скачайте плагин и поместите в папку `plugins/`
+2. Перезапустите сервер
+3. Настройте Discord webhook в `config.yml`:
+```yaml
+webhook-url: "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL"
+```
+4. При необходимости настройте базу данных PostgreSQL
+5. Готово! Игроки могут использовать `/report`
+
+## 📈 Производительность
+
+Плагин оптимизирован для работы на серверах любого размера:
+- Асинхронные операции с базой данных
+- Кэширование часто используемых данных
+- Минимальное влияние на производительность сервера
+- Поддержка пула соединений для PostgreSQL
+
+## 🛠️ Для разработчиков
+
+Loreport предоставляет API для интеграции с другими плагинами:
+- События для отслеживания новых репортов
+- Методы для программного добавления репортов
+- Интеграция с PlaceholderAPI
+- Подробная документация API
+
+## 🆘 Поддержка
+
+- **Документация**: Полная документация включена в плагин
+- **Issues**: [Codeberg Issues](https://codeberg.org/loki5512344/Loreport/issues)
+- **Обсуждения**: [Codeberg Discussions](https://codeberg.org/loki5512344/Loreport/discussions)
+
+## 📄 Лицензия
+
+Проект распространяется под лицензией **GPL-3.0**, что означает:
+- ✅ Свободное использование
+- ✅ Изменение исходного кода
+- ✅ Коммерческое использование
+- ✅ Распространение
+
+---
+
+**Сделайте свой сервер безопаснее с Loreport! 🛡️**
\ No newline at end of file
diff --git a/modrinth/project-info.md b/modrinth/project-info.md
new file mode 100644
index 0000000..b3700e9
--- /dev/null
+++ b/modrinth/project-info.md
@@ -0,0 +1,53 @@
+# Информация о проекте для Modrinth
+
+## Основная информация
+- **Название**: Loreport
+- **Краткое описание**: Современная система репортов для Minecraft серверов с Discord интеграцией
+- **Категории**: Management, Utility, Server Management
+- **Лицензия**: GPL-3.0
+- **Тип проекта**: Plugin (Server-side)
+
+## Поддерживаемые версии
+- **Minecraft**: 1.19.2, 1.19.3, 1.19.4, 1.20, 1.20.1, 1.20.2, 1.20.3, 1.20.4, 1.20.5, 1.20.6, 1.21, 1.21.1, 1.21.2, 1.21.3, 1.21.4
+- **Загрузчики**: Paper, Purpur, Pufferfish
+- **Окружение**: Server
+
+## Теги
+- report
+- reports
+- moderation
+- discord
+- gui
+- database
+- admin
+- management
+- paper
+- plugin
+
+## Ссылки
+- **Исходный код**: https://codeberg.org/loki5512344/Loreport
+- **Issues**: https://codeberg.org/loki5512344/Loreport/issues
+- **Wiki**: https://codeberg.org/loki5512344/Loreport/wiki
+- **Discussions**: https://codeberg.org/loki5512344/Loreport/discussions
+
+## Зависимости
+Плагин не требует дополнительных зависимостей, все необходимые библиотеки включены в jar файл.
+
+## Рекомендуемые плагины
+- **LuckPerms** - для расширенного управления правами
+- **PlaceholderAPI** - для интеграции с другими плагинами
+- **DiscordSRV** - для дополнительной интеграции с Discord
+
+## Галерея изображений
+1. GUI интерфейс с репортами
+2. Пример Discord уведомления
+3. Статистика игрока
+4. Конфигурационный файл
+
+## Инструкции для загрузки
+1. Выберите версию плагина, совместимую с вашей версией Minecraft
+2. Скачайте файл `Loreport-x.x.x.jar`
+3. Поместите файл в папку `plugins/` вашего сервера
+4. Перезапустите сервер
+5. Настройте конфигурацию в `plugins/Loreport/config.yml`
+6. При необходимости настройте Discord webhook
\ No newline at end of file
diff --git a/src/main/java/dev/loki/lorep/command/ReportCommand.java b/src/main/java/dev/loki/lorep/command/ReportCommand.java
index 4f20bf7..4cdf8f3 100644
--- a/src/main/java/dev/loki/lorep/command/ReportCommand.java
+++ b/src/main/java/dev/loki/lorep/command/ReportCommand.java
@@ -16,122 +16,189 @@ 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));
-
- return handleReport(player, targetName, reason);
+ 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(plugin.getConfigManager().getMessage("reason-too-long", "%max%", "500"));
+ return true;
+ }
+
+ 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");
- if ("stats".startsWith(input)) completions.add("stats");
+ if ("gui".startsWith(input)) {
+ completions.add("gui");
+ }
+ 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)
@@ -139,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()
@@ -148,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 876d37e..545cb2a 100644
--- a/src/main/java/dev/loki/lorep/command/ReportStatsCommand.java
+++ b/src/main/java/dev/loki/lorep/command/ReportStatsCommand.java
@@ -10,7 +10,9 @@ import org.bukkit.entity.Player;
import java.util.List;
public class ReportStatsCommand {
-
+
+ private static final int MAX_RECENT_REPORTS = 5;
+
private final LorepPlugin plugin;
public ReportStatsCommand(LorepPlugin plugin) {
@@ -47,15 +49,14 @@ 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Последние репорты:");
- int shown = Math.min(5, reports.size());
+
+ 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);
String timeAgo = TimeUtil.formatTimeAgo(report.createdAt().toEpochMilli());
@@ -63,9 +64,10 @@ public class ReportStatsCommand {
"%reason%", report.reason(),
"%time%", timeAgo));
}
-
- if (reports.size() > 5) {
- player.sendMessage("§7... и ещё " + (reports.size() - 5) + " репортов");
+
+ if (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 e47ad46..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,14 +75,61 @@ 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) {
+ return message;
+ }
for (int i = 0; i < replacements.length - 1; i += 2) {
- message = message.replace(replacements[i], replacements[i + 1]);
+ if (replacements[i] != null && replacements[i + 1] != null) {
+ message = message.replace(replacements[i], replacements[i + 1]);
+ }
}
return message;
}
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 12b72ad..8f2314d 100644
--- a/src/main/java/dev/loki/lorep/database/PostgreSQLDatabaseManager.java
+++ b/src/main/java/dev/loki/lorep/database/PostgreSQLDatabaseManager.java
@@ -3,7 +3,11 @@ package dev.loki.lorep.database;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
-import java.sql.*;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -36,7 +40,11 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
@Override
public void initialize() {
try {
+ // Explicitly load PostgreSQL driver
+ Class.forName("org.postgresql.Driver");
+
HikariConfig config = new HikariConfig();
+ config.setDriverClassName("org.postgresql.Driver");
config.setJdbcUrl("jdbc:postgresql://" + host + ":" + port + "/" + database);
config.setUsername(username);
config.setPassword(password);
@@ -49,6 +57,8 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
dataSource = new HikariDataSource(config);
createTables();
logger.info("PostgreSQL database initialized: " + host + ":" + port + "/" + database);
+ } catch (ClassNotFoundException e) {
+ logger.log(Level.SEVERE, "PostgreSQL driver not found", e);
} catch (Exception e) {
logger.log(Level.SEVERE, "Failed to initialize PostgreSQL database", e);
}
@@ -63,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
)
""";
@@ -88,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)) {
@@ -97,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);
@@ -108,18 +127,19 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
public List getReportsForTarget(UUID targetUuid) {
String sql = "SELECT * FROM reports WHERE target_uuid = ? ORDER BY created_at DESC";
List reports = new ArrayList<>();
-
+
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, targetUuid.toString());
- ResultSet rs = stmt.executeQuery();
- while (rs.next()) {
- reports.add(mapReport(rs));
+ try (ResultSet rs = stmt.executeQuery()) {
+ while (rs.next()) {
+ reports.add(mapReport(rs));
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get reports for target", e);
}
-
+
return reports;
}
@@ -127,54 +147,56 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
public List getAllReports() {
String sql = "SELECT * FROM reports ORDER BY created_at DESC";
List reports = new ArrayList<>();
-
+
try (Connection conn = dataSource.getConnection();
- PreparedStatement stmt = conn.prepareStatement(sql)) {
- ResultSet rs = stmt.executeQuery();
+ PreparedStatement stmt = conn.prepareStatement(sql);
+ ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
reports.add(mapReport(rs));
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get all reports", e);
}
-
+
return reports;
}
@Override
public int getReportCount(UUID targetUuid) {
String sql = "SELECT COUNT(*) FROM reports WHERE target_uuid = ?";
-
+
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, targetUuid.toString());
- ResultSet rs = stmt.executeQuery();
- if (rs.next()) {
- return rs.getInt(1);
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt(1);
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get report count", e);
}
-
+
return 0;
}
@Override
public boolean hasReported(UUID reporterUuid, UUID targetUuid) {
String sql = "SELECT COUNT(*) FROM reports WHERE reporter_uuid = ? AND target_uuid = ?";
-
+
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setString(1, reporterUuid.toString());
stmt.setString(2, targetUuid.toString());
- ResultSet rs = stmt.executeQuery();
- if (rs.next()) {
- return rs.getInt(1) > 0;
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt(1) > 0;
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to check if reported", e);
}
-
+
return false;
}
@@ -182,40 +204,45 @@ public class PostgreSQLDatabaseManager implements DatabaseManager {
public List getReportsPaginated(int page, int pageSize) {
String sql = "SELECT * FROM reports ORDER BY created_at DESC LIMIT ? OFFSET ?";
List reports = new ArrayList<>();
-
+
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
stmt.setInt(1, pageSize);
stmt.setInt(2, page * pageSize);
- ResultSet rs = stmt.executeQuery();
- while (rs.next()) {
- reports.add(mapReport(rs));
+ try (ResultSet rs = stmt.executeQuery()) {
+ while (rs.next()) {
+ reports.add(mapReport(rs));
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get paginated reports", e);
}
-
+
return reports;
}
@Override
public int getTotalReportCount() {
String sql = "SELECT COUNT(*) FROM reports";
-
+
try (Connection conn = dataSource.getConnection();
- PreparedStatement stmt = conn.prepareStatement(sql)) {
- ResultSet rs = stmt.executeQuery();
+ PreparedStatement stmt = conn.prepareStatement(sql);
+ ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get total report count", e);
}
-
+
return 0;
}
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")),
@@ -223,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 2f40280..02434f9 100644
--- a/src/main/java/dev/loki/lorep/database/SQLiteDatabaseManager.java
+++ b/src/main/java/dev/loki/lorep/database/SQLiteDatabaseManager.java
@@ -1,7 +1,12 @@
package dev.loki.lorep.database;
import java.io.File;
-import java.sql.*;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
@@ -49,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
)
""";
@@ -77,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());
@@ -85,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);
@@ -96,17 +110,18 @@ public class SQLiteDatabaseManager implements DatabaseManager {
public List getReportsForTarget(UUID targetUuid) {
String sql = "SELECT * FROM reports WHERE target_uuid = ? ORDER BY created_at DESC";
List reports = new ArrayList<>();
-
- try (PreparedStatement stmt = connection.prepareStatement(sql)) {
+
+ try (PreparedStatement stmt = connection.prepareStatement(sql);) {
stmt.setString(1, targetUuid.toString());
- ResultSet rs = stmt.executeQuery();
- while (rs.next()) {
- reports.add(mapReport(rs));
+ try (ResultSet rs = stmt.executeQuery()) {
+ while (rs.next()) {
+ reports.add(mapReport(rs));
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get reports for target", e);
}
-
+
return reports;
}
@@ -114,51 +129,53 @@ public class SQLiteDatabaseManager implements DatabaseManager {
public List getAllReports() {
String sql = "SELECT * FROM reports ORDER BY created_at DESC";
List reports = new ArrayList<>();
-
- try (PreparedStatement stmt = connection.prepareStatement(sql)) {
- ResultSet rs = stmt.executeQuery();
+
+ try (PreparedStatement stmt = connection.prepareStatement(sql);
+ ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
reports.add(mapReport(rs));
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get all reports", e);
}
-
+
return reports;
}
@Override
public int getReportCount(UUID targetUuid) {
String sql = "SELECT COUNT(*) FROM reports WHERE target_uuid = ?";
-
+
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, targetUuid.toString());
- ResultSet rs = stmt.executeQuery();
- if (rs.next()) {
- return rs.getInt(1);
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt(1);
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get report count", e);
}
-
+
return 0;
}
@Override
public boolean hasReported(UUID reporterUuid, UUID targetUuid) {
String sql = "SELECT COUNT(*) FROM reports WHERE reporter_uuid = ? AND target_uuid = ?";
-
+
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setString(1, reporterUuid.toString());
stmt.setString(2, targetUuid.toString());
- ResultSet rs = stmt.executeQuery();
- if (rs.next()) {
- return rs.getInt(1) > 0;
+ try (ResultSet rs = stmt.executeQuery()) {
+ if (rs.next()) {
+ return rs.getInt(1) > 0;
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to check if reported", e);
}
-
+
return false;
}
@@ -166,38 +183,43 @@ public class SQLiteDatabaseManager implements DatabaseManager {
public List getReportsPaginated(int page, int pageSize) {
String sql = "SELECT * FROM reports ORDER BY created_at DESC LIMIT ? OFFSET ?";
List reports = new ArrayList<>();
-
+
try (PreparedStatement stmt = connection.prepareStatement(sql)) {
stmt.setInt(1, pageSize);
stmt.setInt(2, page * pageSize);
- ResultSet rs = stmt.executeQuery();
- while (rs.next()) {
- reports.add(mapReport(rs));
+ try (ResultSet rs = stmt.executeQuery()) {
+ while (rs.next()) {
+ reports.add(mapReport(rs));
+ }
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get paginated reports", e);
}
-
+
return reports;
}
@Override
public int getTotalReportCount() {
String sql = "SELECT COUNT(*) FROM reports";
-
- try (PreparedStatement stmt = connection.prepareStatement(sql)) {
- ResultSet rs = stmt.executeQuery();
+
+ try (PreparedStatement stmt = connection.prepareStatement(sql);
+ ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return rs.getInt(1);
}
} catch (SQLException e) {
logger.log(Level.SEVERE, "Failed to get total report count", e);
}
-
+
return 0;
}
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")),
@@ -205,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 be27115..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,47 +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 (slots 45-53)
- switch (slot) {
- case 45 -> { // Previous page
- if (gui.getPage() > 0) {
- new ReportGui(plugin, gui.getPage() - 1).open(player);
- }
- }
- case 47 -> { // Close
- player.closeInventory();
- }
- case 53 -> { // 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 04589f3..301c390 100644
--- a/src/main/java/dev/loki/lorep/gui/ReportGui.java
+++ b/src/main/java/dev/loki/lorep/gui/ReportGui.java
@@ -1,135 +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;
+
+ 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 (slot 45)
- if (page > 0) {
- ItemStack prevButton = new ItemStack(Material.ARROW);
- ItemMeta prevMeta = prevButton.getItemMeta();
- if (prevMeta != null) {
- prevMeta.setDisplayName("§a← Предыдущая страница");
- prevButton.setItemMeta(prevMeta);
- }
- inventory.setItem(45, prevButton);
- }
-
- // Page info (slot 49)
- 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(49, pageInfo);
-
- // Next page button (slot 53)
- 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(53, nextButton);
- }
-
- // Close button (slot 47)
- ItemStack closeButton = new ItemStack(Material.BARRIER);
- ItemMeta closeMeta = closeButton.getItemMeta();
- if (closeMeta != null) {
- closeMeta.setDisplayName("§cЗакрыть");
- closeButton.setItemMeta(closeMeta);
- }
- inventory.setItem(47, 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/util/MessageUtil.java b/src/main/java/dev/loki/lorep/util/MessageUtil.java
index 60411d0..290097f 100644
--- a/src/main/java/dev/loki/lorep/util/MessageUtil.java
+++ b/src/main/java/dev/loki/lorep/util/MessageUtil.java
@@ -5,7 +5,9 @@ import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
public class MessageUtil {
-
+
+ private MessageUtil() {}
+
private static final String PREFIX = "§8[§clorep§8] §r";
public static String colorize(String message) {
diff --git a/src/main/java/dev/loki/lorep/util/PaginationUtil.java b/src/main/java/dev/loki/lorep/util/PaginationUtil.java
index 95eb3eb..c1a472d 100644
--- a/src/main/java/dev/loki/lorep/util/PaginationUtil.java
+++ b/src/main/java/dev/loki/lorep/util/PaginationUtil.java
@@ -1,13 +1,27 @@
package dev.loki.lorep.util;
public class PaginationUtil {
-
+
+ private PaginationUtil() {}
+
public static int calculateTotalPages(int totalItems, int pageSize) {
- if (totalItems <= 0) return 1;
+ if (pageSize <= 0) {
+ throw new IllegalArgumentException("pageSize must be positive");
+ }
+ if (totalItems <= 0) {
+ return 1;
+ }
return (int) Math.ceil((double) totalItems / pageSize);
}
public static int calculateStartIndex(int page, int pageSize) {
+ if (page < 0 || pageSize <= 0) {
+ return 0;
+ }
+ // Check for overflow
+ if (page > Integer.MAX_VALUE / pageSize) {
+ return Integer.MAX_VALUE;
+ }
return page * pageSize;
}
diff --git a/src/main/java/dev/loki/lorep/util/TimeUtil.java b/src/main/java/dev/loki/lorep/util/TimeUtil.java
index 00b9a12..989b3e5 100644
--- a/src/main/java/dev/loki/lorep/util/TimeUtil.java
+++ b/src/main/java/dev/loki/lorep/util/TimeUtil.java
@@ -4,62 +4,70 @@ import java.time.Duration;
import java.time.Instant;
public class TimeUtil {
-
+
+ private TimeUtil() {}
+
+ private static final long SECONDS_PER_MINUTE = 60;
+ private static final long MINUTES_PER_HOUR = 60;
+ private static final long HOURS_PER_DAY = 24;
+ private static final long DAYS_PER_MONTH = 30;
+ private static final long MONTHS_PER_YEAR = 12;
+
public static String formatTimeAgo(long epochMillis) {
return formatTimeAgo(Instant.ofEpochMilli(epochMillis));
}
-
+
public static String formatTimeAgo(Instant instant) {
Duration duration = Duration.between(instant, Instant.now());
-
+
long seconds = duration.getSeconds();
-
- if (seconds < 60) {
+
+ if (seconds < SECONDS_PER_MINUTE) {
return seconds + " сек. назад";
}
-
- long minutes = seconds / 60;
- if (minutes < 60) {
+
+ long minutes = seconds / SECONDS_PER_MINUTE;
+ if (minutes < MINUTES_PER_HOUR) {
return minutes + " мин. назад";
}
-
- long hours = minutes / 60;
- if (hours < 24) {
+
+ long hours = minutes / MINUTES_PER_HOUR;
+ if (hours < HOURS_PER_DAY) {
return hours + " ч. назад";
}
-
- long days = hours / 24;
- if (days < 30) {
+
+ long days = hours / HOURS_PER_DAY;
+ if (days < DAYS_PER_MONTH) {
return days + " дн. назад";
}
-
- long months = days / 30;
- if (months < 12) {
+
+ long months = days / DAYS_PER_MONTH;
+ if (months < MONTHS_PER_YEAR) {
return months + " мес. назад";
}
-
- long years = months / 12;
+
+ long years = months / MONTHS_PER_YEAR;
return years + " г. назад";
}
-
+
public static String formatDuration(Duration duration) {
long seconds = duration.getSeconds();
-
- if (seconds < 60) {
+
+ if (seconds < SECONDS_PER_MINUTE) {
return seconds + " секунд";
}
-
- long minutes = seconds / 60;
- if (minutes < 60) {
+
+ long minutes = seconds / SECONDS_PER_MINUTE;
+ if (minutes < MINUTES_PER_HOUR) {
return minutes + " минут";
}
-
- long hours = minutes / 60;
- if (hours < 24) {
+
+ long hours = minutes / MINUTES_PER_HOUR;
+ if (hours < HOURS_PER_DAY) {
return hours + " часов";
}
-
- long days = hours / 24;
+
+ long days = hours / HOURS_PER_DAY;
return days + " дней";
}
}
diff --git a/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java b/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java
index dbf32c3..95832a9 100644
--- a/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java
+++ b/src/main/java/dev/loki/lorep/webhook/DiscordWebhook.java
@@ -1,7 +1,16 @@
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.*;
+import okhttp3.Call;
+import okhttp3.Callback;
+import okhttp3.MediaType;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
import java.io.IOException;
import java.time.ZoneId;
@@ -10,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()) {
@@ -55,33 +64,168 @@ 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 "";
+ if (text == null) {
+ return "";
+ }
return text
.replace("\\", "\\\\")
.replace("\"", "\\\"")
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 9907501..2c4508a 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -1,9 +1,9 @@
-# Discord Webhook URL
+# Discord webhook URL for report notifications
webhook-url: ""
-# Database settings
+# Database configuration
database:
- # Type: sqlite or postgresql
+ # Database type: "sqlite" or "postgresql"
type: "sqlite"
# SQLite settings
@@ -14,22 +14,150 @@ database:
postgresql:
host: "localhost"
port: 5432
- database: "lorep"
- username: "lorep"
+ database: "loreport"
+ username: "loreport"
password: "password"
pool-size: 10
-# Messages
+# GUI settings
+gui:
+ # Reports per page in GUI
+ reports-per-page: 45
+ # Auto-refresh interval in ticks (20 ticks = 1 second)
+ auto-refresh: 100
+ # 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%"
+ 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 347666e..0932beb 100644
--- a/src/main/resources/plugin.yml
+++ b/src/main/resources/plugin.yml
@@ -1,15 +1,17 @@
-name: lorep
+name: Loreport
version: ${version}
main: dev.loki.lorep.LorepPlugin
-api-version: '1.21'
-author: loki
-description: Player report system with Discord integration
+api-version: '1.19'
+author: loki5512344
+description: Modern player report system with Discord integration and GUI
+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]
permissions:
lorep.report:
@@ -18,3 +20,9 @@ permissions:
lorep.admin:
description: Allows access to admin commands (gui, stats)
default: op
+ lorep.*:
+ description: Gives access to all Loreport permissions
+ default: false
+ children:
+ lorep.report: true
+ lorep.admin: true
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);