add fiscord webhook
This commit is contained in:
parent
0fea0f4537
commit
5ece2192a2
8 changed files with 757 additions and 2 deletions
|
|
@ -8,9 +8,11 @@ import com.loki.lochat.config.ConfigManager;
|
||||||
import com.loki.lochat.config.MessageConfig;
|
import com.loki.lochat.config.MessageConfig;
|
||||||
import com.loki.lochat.core.registry.ServiceRegistry;
|
import com.loki.lochat.core.registry.ServiceRegistry;
|
||||||
import com.loki.lochat.gradient.GradientModule;
|
import com.loki.lochat.gradient.GradientModule;
|
||||||
|
import com.loki.lochat.integrations.DiscordIntegration;
|
||||||
import com.loki.lochat.integrations.LibertyBansHook;
|
import com.loki.lochat.integrations.LibertyBansHook;
|
||||||
import com.loki.lochat.integrations.PlaceholderAPIHook;
|
import com.loki.lochat.integrations.PlaceholderAPIHook;
|
||||||
import com.loki.lochat.listener.ChatEventListener;
|
import com.loki.lochat.listener.ChatEventListener;
|
||||||
|
import com.loki.lochat.listener.DiscordEventListener;
|
||||||
import com.loki.lochat.listener.PlayerEventListener;
|
import com.loki.lochat.listener.PlayerEventListener;
|
||||||
import com.loki.lochat.managers.AutoMessageManager;
|
import com.loki.lochat.managers.AutoMessageManager;
|
||||||
import com.loki.lochat.managers.CustomCommandManager;
|
import com.loki.lochat.managers.CustomCommandManager;
|
||||||
|
|
@ -30,6 +32,7 @@ public final class LoChat extends JavaPlugin {
|
||||||
private GradientModule gradientModule;
|
private GradientModule gradientModule;
|
||||||
private AutoMessageManager autoMessageManager;
|
private AutoMessageManager autoMessageManager;
|
||||||
private CustomCommandManager customCommandManager;
|
private CustomCommandManager customCommandManager;
|
||||||
|
private DiscordIntegration discordIntegration;
|
||||||
|
|
||||||
// Геттеры
|
// Геттеры
|
||||||
public static LoChat getInstance() {
|
public static LoChat getInstance() {
|
||||||
|
|
@ -86,6 +89,9 @@ public final class LoChat extends JavaPlugin {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initIntegrations() {
|
private void initIntegrations() {
|
||||||
|
// Discord интеграция
|
||||||
|
discordIntegration = new DiscordIntegration(this);
|
||||||
|
|
||||||
// Gradient модуль
|
// Gradient модуль
|
||||||
gradientModule = new GradientModule(this);
|
gradientModule = new GradientModule(this);
|
||||||
if (gradientModule.init()) {
|
if (gradientModule.init()) {
|
||||||
|
|
@ -144,11 +150,23 @@ public final class LoChat extends JavaPlugin {
|
||||||
ClearChatConfigCommand clearCfgCmd = new ClearChatConfigCommand(this);
|
ClearChatConfigCommand clearCfgCmd = new ClearChatConfigCommand(this);
|
||||||
getCommand("clearchatconfig").setExecutor(clearCfgCmd);
|
getCommand("clearchatconfig").setExecutor(clearCfgCmd);
|
||||||
getCommand("clearchatconfig").setTabCompleter(clearCfgCmd);
|
getCommand("clearchatconfig").setTabCompleter(clearCfgCmd);
|
||||||
|
|
||||||
|
// Discord команда
|
||||||
|
DiscordCommand discordCmd = new DiscordCommand(this, discordIntegration);
|
||||||
|
getCommand("discord").setExecutor(discordCmd);
|
||||||
|
getCommand("discord").setTabCompleter(discordCmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerListeners() {
|
private void registerListeners() {
|
||||||
getServer().getPluginManager().registerEvents(new ChatEventListener(this, serviceRegistry), this);
|
getServer().getPluginManager().registerEvents(new ChatEventListener(this, serviceRegistry), this);
|
||||||
getServer().getPluginManager().registerEvents(new PlayerEventListener(serviceRegistry), this);
|
getServer().getPluginManager().registerEvents(new PlayerEventListener(serviceRegistry), this);
|
||||||
|
|
||||||
|
// Discord listener
|
||||||
|
if (discordIntegration.isEnabled()) {
|
||||||
|
getServer().getPluginManager().registerEvents(new DiscordEventListener(discordIntegration), this);
|
||||||
|
getLogger().info("Discord event listener registered");
|
||||||
|
}
|
||||||
|
|
||||||
getLogger().info("Слушатели зарегистрированы (новая архитектура)");
|
getLogger().info("Слушатели зарегистрированы (новая архитектура)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,6 +181,7 @@ public final class LoChat extends JavaPlugin {
|
||||||
autoMessageManager.reload();
|
autoMessageManager.reload();
|
||||||
if (gradientModule != null) gradientModule.reload();
|
if (gradientModule != null) gradientModule.reload();
|
||||||
if (customCommandManager != null) customCommandManager.reload();
|
if (customCommandManager != null) customCommandManager.reload();
|
||||||
|
if (discordIntegration != null) discordIntegration.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServiceRegistry getServiceRegistry() {
|
public ServiceRegistry getServiceRegistry() {
|
||||||
|
|
@ -184,4 +203,8 @@ public final class LoChat extends JavaPlugin {
|
||||||
public CustomCommandManager getCustomCommandManager() {
|
public CustomCommandManager getCustomCommandManager() {
|
||||||
return customCommandManager;
|
return customCommandManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DiscordIntegration getDiscordIntegration() {
|
||||||
|
return discordIntegration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
120
src/main/java/com/loki/lochat/commands/DiscordCommand.java
Normal file
120
src/main/java/com/loki/lochat/commands/DiscordCommand.java
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
package com.loki.lochat.commands;
|
||||||
|
|
||||||
|
import com.loki.lochat.LoChat;
|
||||||
|
import com.loki.lochat.integrations.DiscordIntegration;
|
||||||
|
import com.loki.lochat.utils.ChatFormatter;
|
||||||
|
import org.bukkit.command.Command;
|
||||||
|
import org.bukkit.command.CommandExecutor;
|
||||||
|
import org.bukkit.command.CommandSender;
|
||||||
|
import org.bukkit.command.TabCompleter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Команда для управления Discord интеграцией
|
||||||
|
*/
|
||||||
|
public class DiscordCommand implements CommandExecutor, TabCompleter {
|
||||||
|
private final LoChat plugin;
|
||||||
|
private final DiscordIntegration discord;
|
||||||
|
|
||||||
|
public DiscordCommand(LoChat plugin, DiscordIntegration discord) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.discord = discord;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) {
|
||||||
|
if (!sender.hasPermission("lochat.discord.admin")) {
|
||||||
|
sender.sendMessage(ChatFormatter.parse(plugin.getMessageConfig().get("errors.no-permission")));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length == 0) {
|
||||||
|
sendHelp(sender);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (args[0].toLowerCase()) {
|
||||||
|
case "status":
|
||||||
|
handleStatus(sender);
|
||||||
|
break;
|
||||||
|
case "reload":
|
||||||
|
handleReload(sender);
|
||||||
|
break;
|
||||||
|
case "test":
|
||||||
|
handleTest(sender, args);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
sendHelp(sender);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHelp(CommandSender sender) {
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&6&l=== Discord Integration ==="));
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&e/discord status &7- Показать статус интеграции"));
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&e/discord reload &7- Перезагрузить конфигурацию"));
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&e/discord test [message] &7- Отправить тестовое сообщение"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleStatus(CommandSender sender) {
|
||||||
|
if (discord.isEnabled()) {
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&a✓ Discord интеграция включена"));
|
||||||
|
} else {
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&c✗ Discord интеграция отключена"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleReload(CommandSender sender) {
|
||||||
|
try {
|
||||||
|
discord.reload();
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&a✓ Discord конфигурация перезагружена"));
|
||||||
|
} catch (Exception e) {
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&c✗ Ошибка при перезагрузке: " + e.getMessage()));
|
||||||
|
plugin.getLogger().severe("Error reloading Discord config: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleTest(CommandSender sender, String[] args) {
|
||||||
|
if (!discord.isEnabled()) {
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&c✗ Discord интеграция отключена"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String message = args.length > 1 ? String.join(" ", Arrays.copyOfRange(args, 1, args.length)) : "Тестовое сообщение от " + sender.getName();
|
||||||
|
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&e⏳ Отправка тестового сообщения..."));
|
||||||
|
|
||||||
|
// Отправляем тестовое сообщение
|
||||||
|
plugin.getServer().getScheduler().runTaskAsynchronously(plugin, () -> {
|
||||||
|
try {
|
||||||
|
discord.sendTestMessage(message, sender.getName());
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () ->
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&a✓ Тестовое сообщение отправлено в Discord"))
|
||||||
|
);
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getServer().getScheduler().runTask(plugin, () ->
|
||||||
|
sender.sendMessage(ChatFormatter.parse("&c✗ Ошибка отправки: " + e.getMessage()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, @NotNull String[] args) {
|
||||||
|
if (!sender.hasPermission("lochat.discord.admin")) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.length == 1) {
|
||||||
|
return Arrays.asList("status", "reload", "test");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,244 @@
|
||||||
|
package com.loki.lochat.integrations;
|
||||||
|
|
||||||
|
import com.loki.lochat.util.FoliaUtil;
|
||||||
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||||
|
import org.bukkit.configuration.file.YamlConfiguration;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Интеграция с Discord через вебхуки
|
||||||
|
*/
|
||||||
|
public class DiscordIntegration {
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private YamlConfiguration config;
|
||||||
|
private DiscordWebhook webhook;
|
||||||
|
private boolean enabled = false;
|
||||||
|
|
||||||
|
// Паттерны для очистки сообщений
|
||||||
|
private static final Pattern MENTION_PATTERN = Pattern.compile("@(everyone|here)", Pattern.CASE_INSENSITIVE);
|
||||||
|
private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
public DiscordIntegration(JavaPlugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
loadConfig();
|
||||||
|
initWebhook();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Загрузить конфигурацию
|
||||||
|
*/
|
||||||
|
private void loadConfig() {
|
||||||
|
File configFile = new File(plugin.getDataFolder(), "config/discord.yml");
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
plugin.saveResource("config/discord.yml", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
config = YamlConfiguration.loadConfiguration(configFile);
|
||||||
|
enabled = config.getBoolean("enabled", false);
|
||||||
|
|
||||||
|
if (!enabled) {
|
||||||
|
plugin.getLogger().info("Discord integration disabled in config");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Инициализировать вебхук
|
||||||
|
*/
|
||||||
|
private void initWebhook() {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
String webhookUrl = config.getString("webhook.url", "");
|
||||||
|
String username = config.getString("webhook.username", "Minecraft Server");
|
||||||
|
String avatarUrl = config.getString("webhook.avatar_url", "https://mc-heads.net/avatar/minecraft/64");
|
||||||
|
int timeout = config.getInt("performance.timeout", 10);
|
||||||
|
int retryAttempts = config.getInt("performance.retry_attempts", 3);
|
||||||
|
long retryDelay = config.getLong("performance.retry_delay", 1000);
|
||||||
|
|
||||||
|
webhook = new DiscordWebhook(plugin, webhookUrl, username, avatarUrl, timeout, retryAttempts, retryDelay);
|
||||||
|
|
||||||
|
if (!webhook.isValid()) {
|
||||||
|
plugin.getLogger().severe("Invalid Discord webhook URL! Please check config/discord.yml");
|
||||||
|
enabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().info("Discord integration initialized successfully");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить сообщение чата в Discord
|
||||||
|
*/
|
||||||
|
public void sendChatMessage(Player player, String message, boolean isGlobal) {
|
||||||
|
if (!enabled || !config.getBoolean("chat.enabled", true)) return;
|
||||||
|
|
||||||
|
// Проверяем фильтр глобального чата
|
||||||
|
if (config.getBoolean("chat.global_only", true) && !isGlobal) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Фильтрация сообщения
|
||||||
|
String cleanMessage = sanitizeMessage(message);
|
||||||
|
if (cleanMessage.length() < config.getInt("filter.min_message_length", 1)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрезаем если слишком длинное
|
||||||
|
int maxLength = config.getInt("filter.max_message_length", 2000);
|
||||||
|
if (cleanMessage.length() > maxLength) {
|
||||||
|
cleanMessage = cleanMessage.substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
String playerName = player.getName();
|
||||||
|
String format = config.getString("chat.format", "**{player}**: {message}");
|
||||||
|
String formattedMessage = format
|
||||||
|
.replace("{player}", playerName)
|
||||||
|
.replace("{message}", cleanMessage);
|
||||||
|
|
||||||
|
// Отправляем асинхронно
|
||||||
|
if (config.getBoolean("performance.async", true)) {
|
||||||
|
FoliaUtil.runAsync(plugin, () -> sendChatMessageInternal(formattedMessage, playerName, isGlobal));
|
||||||
|
} else {
|
||||||
|
sendChatMessageInternal(formattedMessage, playerName, isGlobal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Внутренний метод отправки сообщения чата
|
||||||
|
*/
|
||||||
|
private void sendChatMessageInternal(String message, String playerName, boolean isGlobal) {
|
||||||
|
if (config.getBoolean("chat.use_embed", true)) {
|
||||||
|
String title = isGlobal ? "🌍 Глобальный чат" : "📍 Локальный чат";
|
||||||
|
String color = config.getString("chat.embed_color", "5865F2");
|
||||||
|
String thumbnail = "https://mc-heads.net/avatar/" + playerName + "/64";
|
||||||
|
|
||||||
|
webhook.sendEmbed(title, message, color, thumbnail);
|
||||||
|
} else {
|
||||||
|
webhook.sendMessage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить событие входа игрока
|
||||||
|
*/
|
||||||
|
public void sendPlayerJoin(Player player) {
|
||||||
|
if (!enabled || !config.getBoolean("events.enabled", true) ||
|
||||||
|
!config.getBoolean("events.join.enabled", true)) return;
|
||||||
|
|
||||||
|
String playerName = player.getName();
|
||||||
|
String title = config.getString("events.join.title", "🟢 Игрок зашел на сервер");
|
||||||
|
String description = config.getString("events.join.description", "**{player}** присоединился к серверу")
|
||||||
|
.replace("{player}", playerName);
|
||||||
|
String color = config.getString("events.join.color", "00FF00");
|
||||||
|
String thumbnail = config.getString("events.join.thumbnail", "https://mc-heads.net/avatar/{player}/64")
|
||||||
|
.replace("{player}", playerName);
|
||||||
|
|
||||||
|
if (config.getBoolean("performance.async", true)) {
|
||||||
|
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
|
||||||
|
} else {
|
||||||
|
webhook.sendEmbed(title, description, color, thumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить событие выхода игрока
|
||||||
|
*/
|
||||||
|
public void sendPlayerQuit(Player player) {
|
||||||
|
if (!enabled || !config.getBoolean("events.enabled", true) ||
|
||||||
|
!config.getBoolean("events.quit.enabled", true)) return;
|
||||||
|
|
||||||
|
String playerName = player.getName();
|
||||||
|
String title = config.getString("events.quit.title", "🔴 Игрок покинул сервер");
|
||||||
|
String description = config.getString("events.quit.description", "**{player}** покинул сервер")
|
||||||
|
.replace("{player}", playerName);
|
||||||
|
String color = config.getString("events.quit.color", "FF0000");
|
||||||
|
String thumbnail = config.getString("events.quit.thumbnail", "https://mc-heads.net/avatar/{player}/64")
|
||||||
|
.replace("{player}", playerName);
|
||||||
|
|
||||||
|
if (config.getBoolean("performance.async", true)) {
|
||||||
|
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
|
||||||
|
} else {
|
||||||
|
webhook.sendEmbed(title, description, color, thumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить событие смерти игрока
|
||||||
|
*/
|
||||||
|
public void sendPlayerDeath(Player player, String deathMessage) {
|
||||||
|
if (!enabled || !config.getBoolean("events.enabled", true) ||
|
||||||
|
!config.getBoolean("events.death.enabled", true)) return;
|
||||||
|
|
||||||
|
String playerName = player.getName();
|
||||||
|
String title = config.getString("events.death.title", "💀 Смерть игрока");
|
||||||
|
String description = config.getString("events.death.description", "**{player}** {death_message}")
|
||||||
|
.replace("{player}", playerName)
|
||||||
|
.replace("{death_message}", sanitizeMessage(deathMessage));
|
||||||
|
String color = config.getString("events.death.color", "800080");
|
||||||
|
String thumbnail = config.getString("events.death.thumbnail", "https://mc-heads.net/avatar/{player}/64")
|
||||||
|
.replace("{player}", playerName);
|
||||||
|
|
||||||
|
if (config.getBoolean("performance.async", true)) {
|
||||||
|
FoliaUtil.runAsync(plugin, () -> webhook.sendEmbed(title, description, color, thumbnail));
|
||||||
|
} else {
|
||||||
|
webhook.sendEmbed(title, description, color, thumbnail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Очистить сообщение от нежелательных элементов
|
||||||
|
*/
|
||||||
|
private String sanitizeMessage(String message) {
|
||||||
|
if (message == null) return "";
|
||||||
|
|
||||||
|
// Удаляем цветовые коды Minecraft
|
||||||
|
message = COLOR_CODE_PATTERN.matcher(message).replaceAll("");
|
||||||
|
|
||||||
|
// Заменяем опасные упоминания
|
||||||
|
if (config.getBoolean("filter.sanitize_mentions", true)) {
|
||||||
|
message = MENTION_PATTERN.matcher(message).replaceAll("@\u200B$1"); // Добавляем невидимый символ
|
||||||
|
}
|
||||||
|
|
||||||
|
// Экранируем Discord markdown
|
||||||
|
message = message.replace("*", "\\*")
|
||||||
|
.replace("_", "\\_")
|
||||||
|
.replace("`", "\\`")
|
||||||
|
.replace("~", "\\~");
|
||||||
|
|
||||||
|
return message.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Перезагрузить конфигурацию
|
||||||
|
*/
|
||||||
|
public void reload() {
|
||||||
|
loadConfig();
|
||||||
|
initWebhook();
|
||||||
|
plugin.getLogger().info("Discord integration reloaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить тестовое сообщение
|
||||||
|
*/
|
||||||
|
public void sendTestMessage(String message, String senderName) {
|
||||||
|
if (!enabled) return;
|
||||||
|
|
||||||
|
String title = "🧪 Тестовое сообщение";
|
||||||
|
String description = "**Отправитель:** " + senderName + "\n**Сообщение:** " + message;
|
||||||
|
String color = "FFD700"; // золотой цвет
|
||||||
|
String thumbnail = "https://mc-heads.net/avatar/" + senderName + "/64";
|
||||||
|
|
||||||
|
webhook.sendEmbed(title, description, color, thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить включена ли интеграция
|
||||||
|
*/
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
186
src/main/java/com/loki/lochat/integrations/DiscordWebhook.java
Normal file
186
src/main/java/com/loki/lochat/integrations/DiscordWebhook.java
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
package com.loki.lochat.integrations;
|
||||||
|
|
||||||
|
import com.google.gson.JsonArray;
|
||||||
|
import com.google.gson.JsonObject;
|
||||||
|
import com.loki.lochat.util.FoliaUtil;
|
||||||
|
import org.bukkit.plugin.java.JavaPlugin;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.net.HttpURLConnection;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Класс для отправки сообщений в Discord через вебхуки
|
||||||
|
*/
|
||||||
|
public class DiscordWebhook {
|
||||||
|
private final JavaPlugin plugin;
|
||||||
|
private final String webhookUrl;
|
||||||
|
private final String username;
|
||||||
|
private final String avatarUrl;
|
||||||
|
private final int timeout;
|
||||||
|
private final int retryAttempts;
|
||||||
|
private final long retryDelay;
|
||||||
|
|
||||||
|
public DiscordWebhook(JavaPlugin plugin, String webhookUrl, String username, String avatarUrl,
|
||||||
|
int timeout, int retryAttempts, long retryDelay) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.webhookUrl = webhookUrl;
|
||||||
|
this.username = username;
|
||||||
|
this.avatarUrl = avatarUrl;
|
||||||
|
this.timeout = timeout * 1000; // конвертируем в миллисекунды
|
||||||
|
this.retryAttempts = retryAttempts;
|
||||||
|
this.retryDelay = retryDelay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить простое текстовое сообщение
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> sendMessage(String content) {
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.addProperty("content", content);
|
||||||
|
json.addProperty("username", username);
|
||||||
|
json.addProperty("avatar_url", avatarUrl);
|
||||||
|
|
||||||
|
return sendWebhook(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить embed сообщение
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> sendEmbed(String title, String description, String color, String thumbnailUrl) {
|
||||||
|
JsonObject embed = new JsonObject();
|
||||||
|
if (title != null) embed.addProperty("title", title);
|
||||||
|
if (description != null) embed.addProperty("description", description);
|
||||||
|
if (color != null) embed.addProperty("color", Integer.parseInt(color, 16));
|
||||||
|
|
||||||
|
if (thumbnailUrl != null) {
|
||||||
|
JsonObject thumbnail = new JsonObject();
|
||||||
|
thumbnail.addProperty("url", thumbnailUrl);
|
||||||
|
embed.add("thumbnail", thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем timestamp
|
||||||
|
embed.addProperty("timestamp", java.time.Instant.now().toString());
|
||||||
|
|
||||||
|
JsonArray embeds = new JsonArray();
|
||||||
|
embeds.add(embed);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("embeds", embeds);
|
||||||
|
json.addProperty("username", username);
|
||||||
|
json.addProperty("avatar_url", avatarUrl);
|
||||||
|
|
||||||
|
return sendWebhook(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить embed с кастомными полями
|
||||||
|
*/
|
||||||
|
public CompletableFuture<Boolean> sendEmbedWithFields(String title, String description, String color,
|
||||||
|
String thumbnailUrl, JsonArray fields) {
|
||||||
|
JsonObject embed = new JsonObject();
|
||||||
|
if (title != null) embed.addProperty("title", title);
|
||||||
|
if (description != null) embed.addProperty("description", description);
|
||||||
|
if (color != null) embed.addProperty("color", Integer.parseInt(color, 16));
|
||||||
|
if (fields != null) embed.add("fields", fields);
|
||||||
|
|
||||||
|
if (thumbnailUrl != null) {
|
||||||
|
JsonObject thumbnail = new JsonObject();
|
||||||
|
thumbnail.addProperty("url", thumbnailUrl);
|
||||||
|
embed.add("thumbnail", thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
embed.addProperty("timestamp", java.time.Instant.now().toString());
|
||||||
|
|
||||||
|
JsonArray embeds = new JsonArray();
|
||||||
|
embeds.add(embed);
|
||||||
|
|
||||||
|
JsonObject json = new JsonObject();
|
||||||
|
json.add("embeds", embeds);
|
||||||
|
json.addProperty("username", username);
|
||||||
|
json.addProperty("avatar_url", avatarUrl);
|
||||||
|
|
||||||
|
return sendWebhook(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Отправить JSON в вебхук с повторными попытками
|
||||||
|
*/
|
||||||
|
private CompletableFuture<Boolean> sendWebhook(JsonObject json) {
|
||||||
|
return CompletableFuture.supplyAsync(() -> {
|
||||||
|
for (int attempt = 1; attempt <= retryAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
boolean success = sendWebhookSync(json.toString());
|
||||||
|
if (success) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
plugin.getLogger().warning("Discord webhook attempt " + attempt + " failed: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ждем перед следующей попыткой (кроме последней)
|
||||||
|
if (attempt < retryAttempts) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(retryDelay);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugin.getLogger().severe("Failed to send Discord webhook after " + retryAttempts + " attempts");
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Синхронная отправка в вебхук
|
||||||
|
*/
|
||||||
|
private boolean sendWebhookSync(String jsonPayload) throws IOException {
|
||||||
|
URL url = new URL(webhookUrl);
|
||||||
|
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||||
|
|
||||||
|
try {
|
||||||
|
connection.setRequestMethod("POST");
|
||||||
|
connection.setRequestProperty("Content-Type", "application/json");
|
||||||
|
connection.setRequestProperty("User-Agent", "LoChat-Discord-Integration/1.0");
|
||||||
|
connection.setDoOutput(true);
|
||||||
|
connection.setConnectTimeout(timeout);
|
||||||
|
connection.setReadTimeout(timeout);
|
||||||
|
|
||||||
|
// Отправляем JSON
|
||||||
|
try (OutputStream os = connection.getOutputStream()) {
|
||||||
|
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
|
||||||
|
os.write(input, 0, input.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
int responseCode = connection.getResponseCode();
|
||||||
|
|
||||||
|
if (responseCode == 204) {
|
||||||
|
return true; // Успешная отправка
|
||||||
|
} else if (responseCode == 429) {
|
||||||
|
plugin.getLogger().warning("Discord webhook rate limited (429)");
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
plugin.getLogger().warning("Discord webhook returned code: " + responseCode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Проверить валидность вебхука
|
||||||
|
*/
|
||||||
|
public boolean isValid() {
|
||||||
|
return webhookUrl != null &&
|
||||||
|
webhookUrl.startsWith("https://discord.com/api/webhooks/") &&
|
||||||
|
webhookUrl.length() > 50; // Минимальная длина валидного URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -49,6 +49,11 @@ public class ChatEventListener implements Listener {
|
||||||
|
|
||||||
String filteredMessage = filterResult.filteredMessage();
|
String filteredMessage = filterResult.filteredMessage();
|
||||||
|
|
||||||
|
// Убеждаемся что ! удален из отображаемого сообщения
|
||||||
|
if (isGlobal && filteredMessage.startsWith("!")) {
|
||||||
|
filteredMessage = filteredMessage.substring(1).stripLeading();
|
||||||
|
}
|
||||||
|
|
||||||
event.message(com.loki.lochat.utils.ChatFormatter.parse(filteredMessage));
|
event.message(com.loki.lochat.utils.ChatFormatter.parse(filteredMessage));
|
||||||
|
|
||||||
// Фильтруем получателей для локального чата
|
// Фильтруем получателей для локального чата
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,81 @@
|
||||||
|
package com.loki.lochat.listener;
|
||||||
|
|
||||||
|
import com.loki.lochat.integrations.DiscordIntegration;
|
||||||
|
import io.papermc.paper.event.player.AsyncChatEvent;
|
||||||
|
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||||
|
import org.bukkit.entity.Player;
|
||||||
|
import org.bukkit.event.EventHandler;
|
||||||
|
import org.bukkit.event.EventPriority;
|
||||||
|
import org.bukkit.event.Listener;
|
||||||
|
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||||
|
import org.bukkit.event.player.PlayerJoinEvent;
|
||||||
|
import org.bukkit.event.player.PlayerQuitEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Слушатель событий для Discord интеграции
|
||||||
|
*/
|
||||||
|
public class DiscordEventListener implements Listener {
|
||||||
|
private final DiscordIntegration discord;
|
||||||
|
|
||||||
|
public DiscordEventListener(DiscordIntegration discord) {
|
||||||
|
this.discord = discord;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка сообщений чата для Discord
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR, ignoreCancelled = true)
|
||||||
|
public void onChat(AsyncChatEvent event) {
|
||||||
|
if (!discord.isEnabled()) return;
|
||||||
|
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
String message = PlainTextComponentSerializer.plainText().serialize(event.message());
|
||||||
|
|
||||||
|
// Определяем тип чата (глобальный если начинается с !)
|
||||||
|
boolean isGlobal = message.startsWith("!");
|
||||||
|
|
||||||
|
// Очищаем сообщение от ! если это глобальный чат
|
||||||
|
if (isGlobal) {
|
||||||
|
message = message.substring(1).stripLeading();
|
||||||
|
}
|
||||||
|
|
||||||
|
discord.sendChatMessage(player, message, isGlobal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка входа игрока
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerJoin(PlayerJoinEvent event) {
|
||||||
|
if (!discord.isEnabled()) return;
|
||||||
|
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
discord.sendPlayerJoin(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка выхода игрока
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerQuit(PlayerQuitEvent event) {
|
||||||
|
if (!discord.isEnabled()) return;
|
||||||
|
|
||||||
|
Player player = event.getPlayer();
|
||||||
|
discord.sendPlayerQuit(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработка смерти игрока
|
||||||
|
*/
|
||||||
|
@EventHandler(priority = EventPriority.MONITOR)
|
||||||
|
public void onPlayerDeath(PlayerDeathEvent event) {
|
||||||
|
if (!discord.isEnabled()) return;
|
||||||
|
|
||||||
|
Player player = event.getEntity();
|
||||||
|
String deathMessage = event.getDeathMessage() != null ?
|
||||||
|
PlainTextComponentSerializer.plainText().serialize(event.getDeathMessage()) :
|
||||||
|
"умер";
|
||||||
|
|
||||||
|
discord.sendPlayerDeath(player, deathMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/main/resources/config/discord.yml
Normal file
92
src/main/resources/config/discord.yml
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# ╔══════════════════════════════════════════════════════════════════════════════╗
|
||||||
|
# ║ Discord Integration ║
|
||||||
|
# ║ Интеграция с Discord ║
|
||||||
|
# ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
# Включить интеграцию с Discord
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Настройки вебхука
|
||||||
|
webhook:
|
||||||
|
# URL вебхука Discord (получить в настройках канала)
|
||||||
|
url: "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN"
|
||||||
|
|
||||||
|
# Имя бота в Discord
|
||||||
|
username: "Minecraft Server"
|
||||||
|
|
||||||
|
# Аватар бота (URL изображения)
|
||||||
|
avatar_url: "https://mc-heads.net/avatar/minecraft/64"
|
||||||
|
|
||||||
|
# Настройки чата
|
||||||
|
chat:
|
||||||
|
# Отправлять сообщения чата в Discord
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Отправлять только глобальный чат (true) или весь чат (false)
|
||||||
|
global_only: true
|
||||||
|
|
||||||
|
# Формат сообщения чата
|
||||||
|
format: "**{player}**: {message}"
|
||||||
|
|
||||||
|
# Цвет embed для сообщений чата (hex без #)
|
||||||
|
embed_color: "5865F2"
|
||||||
|
|
||||||
|
# Использовать embed или обычное сообщение
|
||||||
|
use_embed: true
|
||||||
|
|
||||||
|
# Настройки событий игроков
|
||||||
|
events:
|
||||||
|
# Отправлять события входа/выхода
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# Настройки входа на сервер
|
||||||
|
join:
|
||||||
|
enabled: true
|
||||||
|
title: "🟢 Игрок зашел на сервер"
|
||||||
|
description: "**{player}** присоединился к серверу"
|
||||||
|
color: "00FF00"
|
||||||
|
thumbnail: "https://mc-heads.net/avatar/{player}/64"
|
||||||
|
|
||||||
|
# Настройки выхода с сервера
|
||||||
|
quit:
|
||||||
|
enabled: true
|
||||||
|
title: "🔴 Игрок покинул сервер"
|
||||||
|
description: "**{player}** покинул сервер"
|
||||||
|
color: "FF0000"
|
||||||
|
thumbnail: "https://mc-heads.net/avatar/{player}/64"
|
||||||
|
|
||||||
|
# Настройки смерти игрока
|
||||||
|
death:
|
||||||
|
enabled: true
|
||||||
|
title: "💀 Смерть игрока"
|
||||||
|
description: "**{player}** {death_message}"
|
||||||
|
color: "800080"
|
||||||
|
thumbnail: "https://mc-heads.net/avatar/{player}/64"
|
||||||
|
|
||||||
|
# Настройки фильтрации
|
||||||
|
filter:
|
||||||
|
# Игнорировать сообщения от ботов
|
||||||
|
ignore_bots: true
|
||||||
|
|
||||||
|
# Минимальная длина сообщения для отправки
|
||||||
|
min_message_length: 1
|
||||||
|
|
||||||
|
# Максимальная длина сообщения (обрезать если больше)
|
||||||
|
max_message_length: 2000
|
||||||
|
|
||||||
|
# Заменять @everyone и @here на безопасные варианты
|
||||||
|
sanitize_mentions: true
|
||||||
|
|
||||||
|
# Настройки производительности
|
||||||
|
performance:
|
||||||
|
# Асинхронная отправка (рекомендуется)
|
||||||
|
async: true
|
||||||
|
|
||||||
|
# Таймаут подключения (секунды)
|
||||||
|
timeout: 10
|
||||||
|
|
||||||
|
# Повторные попытки при ошибке
|
||||||
|
retry_attempts: 3
|
||||||
|
|
||||||
|
# Задержка между попытками (миллисекунды)
|
||||||
|
retry_delay: 1000
|
||||||
|
|
@ -166,8 +166,9 @@ commands:
|
||||||
aliases: [ правила, rule ]
|
aliases: [ правила, rule ]
|
||||||
|
|
||||||
discord:
|
discord:
|
||||||
description: Show Discord invite
|
description: Discord integration management
|
||||||
usage: /discord
|
usage: /discord [status|reload|test]
|
||||||
|
permission: lochat.discord.admin
|
||||||
aliases: [ дискорд, ds ]
|
aliases: [ дискорд, ds ]
|
||||||
|
|
||||||
website:
|
website:
|
||||||
|
|
@ -442,3 +443,6 @@ permissions:
|
||||||
lochat.ping.others:
|
lochat.ping.others:
|
||||||
description: Can view other players' ping
|
description: Can view other players' ping
|
||||||
default: op
|
default: op
|
||||||
|
lochat.discord.admin:
|
||||||
|
description: Can manage Discord integration
|
||||||
|
default: op
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue