diff --git a/src/main/java/creator/loki/command/entertainment/social/negative/PokeCommand.java b/src/main/java/creator/loki/command/entertainment/social/negative/PokeCommand.java new file mode 100644 index 0000000..bdf2f4c --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/negative/PokeCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction.negative; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.core.security.CooldownManager; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Тыкнуть пользователя с GIF + */ +public class PokeCommand implements Command { + + private final CooldownManager cooldownManager; + + public PokeCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "poke"; + } + + @Override + public String getDescription() { + return "Тыкнуть пользователя"; + } + + @Override + public String getUsage() { + return "poke <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "poke")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "poke"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "poke", 5); + + String gifUrl = NekosLifeApi.fetchImage("poke"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("👉 " + context.getAuthor().getAsMention() + " тыкает в " + target.getAsMention() + "! 😏") + .setImage(gifUrl) + .setColor(new Color(0, 128, 128)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/negative/RoastCommand.java b/src/main/java/creator/loki/command/entertainment/social/negative/RoastCommand.java new file mode 100644 index 0000000..b15d025 --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/negative/RoastCommand.java @@ -0,0 +1,68 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +public class RoastCommand implements Command { + private final Random random = new Random(); + + private static final String[] ROASTS = { + "Ты как Internet Explorer — медленный и никому не нужный", + "Твой код работает? Это баг, исправь его", + "Ты пишешь код как будто тебе за каждую строку платят", + "Твоя логика как у JavaScript: непредсказуемая", + "Ты как HTML без CSS — работаешь, но выглядишь ужасно", + "Твой IQ как температура в Антарктиде — ниже нуля", + "Ты как баг в продакшене — неожиданный и нежеланный", + "Твои шутки как deprecated код — устарели", + "Ты как NULL pointer — бесполезный и вызываешь ошибки", + "Твой уровень как индекс массива в Python — может быть отрицательным" + }; + + @Override + public String getName() { + return "roast"; + } + + @Override + public String getDescription() { + return "Дружеский рофл над пользователем"; + } + + @Override + public String getUsage() { + return "roast <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + String roast = ROASTS[random.nextInt(ROASTS.length)]; + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("🔥 Roast") + .setDescription(target.getAsMention() + ", " + roast) + .setColor(new Color(231, 76, 60)) + .setFooter("Это шутка, не обижайся!") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/negative/SlapCommand.java b/src/main/java/creator/loki/command/entertainment/social/negative/SlapCommand.java new file mode 100644 index 0000000..bebb731 --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/negative/SlapCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction.negative; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.core.security.CooldownManager; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Шлепнуть пользователя с GIF + */ +public class SlapCommand implements Command { + + private final CooldownManager cooldownManager; + + public SlapCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "slap"; + } + + @Override + public String getDescription() { + return "Шлепнуть пользователя"; + } + + @Override + public String getUsage() { + return "slap <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "slap")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "slap"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "slap", 5); + + String gifUrl = NekosLifeApi.fetchImage("slap"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("👋 " + context.getAuthor().getAsMention() + " шлепает " + target.getAsMention() + "! 😠") + .setImage(gifUrl) + .setColor(new Color(255, 165, 0)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/negative/TickleCommand.java b/src/main/java/creator/loki/command/entertainment/social/negative/TickleCommand.java new file mode 100644 index 0000000..426d28f --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/negative/TickleCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction.negative; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.core.security.CooldownManager; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Пощекотать пользователя с GIF + */ +public class TickleCommand implements Command { + + private final CooldownManager cooldownManager; + + public TickleCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "tickle"; + } + + @Override + public String getDescription() { + return "Пощекотать пользователя"; + } + + @Override + public String getUsage() { + return "tickle <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "tickle")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "tickle"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "tickle", 5); + + String gifUrl = NekosLifeApi.fetchImage("tickle"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🤗 " + context.getAuthor().getAsMention() + " щекочет " + target.getAsMention() + "!") + .setImage(gifUrl) + .setColor(new Color(0, 255, 0)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/positive/ComplimentCommand.java b/src/main/java/creator/loki/command/entertainment/social/positive/ComplimentCommand.java new file mode 100644 index 0000000..4e37cae --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/positive/ComplimentCommand.java @@ -0,0 +1,67 @@ +package creator.loki.command.impl.entertainment.interaction; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; +import java.util.Random; + +public class ComplimentCommand implements Command { + private final Random random = new Random(); + + private static final String[] COMPLIMENTS = { + "Ты как идеальный код — красивый и без багов", + "Твоя улыбка ярче, чем RGB подсветка", + "Ты как хороший коммит — делаешь всё лучше", + "Твой вайб как у Python — простой и приятный", + "Ты как успешный деплой — всё работает отлично", + "Твоя энергия как у SSD — быстрая и эффективная", + "Ты как open source проект — полезный для всех", + "Твоя харизма как у TypeScript — добавляет уверенности", + "Ты как хороший API — понятный и надёжный", + "Твоё присутствие как кофе для программиста — необходимо" + }; + + @Override + public String getName() { + return "compliment"; + } + + @Override + public String getDescription() { + return "Сделать комплимент пользователю"; + } + + @Override + public String getUsage() { + return "compliment <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + String compliment = COMPLIMENTS[random.nextInt(COMPLIMENTS.length)]; + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setTitle("💖 Комплимент") + .setDescription(target.getAsMention() + ", " + compliment) + .setColor(new Color(255, 182, 193)) + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/positive/CuddleCommand.java b/src/main/java/creator/loki/command/entertainment/social/positive/CuddleCommand.java new file mode 100644 index 0000000..a4c070d --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/positive/CuddleCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction.positive; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.core.security.CooldownManager; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Обниматься с пользователем с GIF + */ +public class CuddleCommand implements Command { + + private final CooldownManager cooldownManager; + + public CuddleCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "cuddle"; + } + + @Override + public String getDescription() { + return "Обниматься с пользователем"; + } + + @Override + public String getUsage() { + return "cuddle <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "cuddle")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "cuddle"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "cuddle", 5); + + String gifUrl = NekosLifeApi.fetchImage("cuddle"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🥰 " + context.getAuthor().getAsMention() + " обнимается с " + target.getAsMention() + "! 💕") + .setImage(gifUrl) + .setColor(new Color(255, 0, 255)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/positive/FeedCommand.java b/src/main/java/creator/loki/command/entertainment/social/positive/FeedCommand.java new file mode 100644 index 0000000..1a267a3 --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/positive/FeedCommand.java @@ -0,0 +1,78 @@ +package creator.loki.command.impl.entertainment.interaction.positive; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.core.security.CooldownManager; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Покормить пользователя с GIF + */ +public class FeedCommand implements Command { + + private final CooldownManager cooldownManager; + + public FeedCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "feed"; + } + + @Override + public String getDescription() { + return "Покормить пользователя"; + } + + @Override + public String getUsage() { + return "feed <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "feed")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "feed"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + cooldownManager.applyCooldown(context.getAuthor().getId(), "feed", 5); + + String gifUrl = NekosLifeApi.fetchImage("feed"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🍽️ " + context.getAuthor().getAsMention() + " кормит " + target.getAsMention() + "! 😋") + .setImage(gifUrl) + .setColor(new Color(255, 200, 100)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/positive/HugCommand.java b/src/main/java/creator/loki/command/entertainment/social/positive/HugCommand.java new file mode 100644 index 0000000..4558073 --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/positive/HugCommand.java @@ -0,0 +1,61 @@ +package creator.loki.command.impl.entertainment.interaction.positive; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +public class HugCommand implements Command { + + @Override + public String getName() { + return "hug"; + } + + @Override + public String getDescription() { + return "Обнять пользователя"; + } + + @Override + public String getUsage() { + return "hug <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + // Получаем GIF из nekos.life API + String gifUrl = NekosLifeApi.fetchImage("hug"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("🤗 " + context.getAuthor().getAsMention() + " обнял " + target.getAsMention() + " 💕") + .setImage(gifUrl) + .setColor(new Color(255, 182, 193)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/positive/KissCommand.java b/src/main/java/creator/loki/command/entertainment/social/positive/KissCommand.java new file mode 100644 index 0000000..6a175f5 --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/positive/KissCommand.java @@ -0,0 +1,92 @@ +package creator.loki.command.impl.entertainment.interaction.positive; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedFactory; +import creator.loki.core.security.CooldownManager; +import creator.loki.integration.nekos.NekosLifeApi; +import net.dv8tion.jda.api.entities.Member; + +import java.awt.Color; +import java.util.List; + +/** + * Поцелуй пользователя с GIF + */ +public class KissCommand implements Command { + + private final CooldownManager cooldownManager; + + public KissCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "kiss"; + } + + @Override + public String getDescription() { + return "Поцеловать пользователя"; + } + + @Override + public String getUsage() { + return "kiss <@user>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + // Проверка cooldown (5 секунд) + if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "kiss")) { + long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "kiss"); + context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи кого поцеловать! Использование: `!kiss @user`").queue(); + return; + } + + Member target = mentioned.get(0); + + if (target.getId().equals(context.getAuthor().getId())) { + context.getChannel().sendMessage("❌ Нельзя целовать самого себя!").queue(); + return; + } + + if (target.getUser().isBot()) { + context.getChannel().sendMessage("❌ Боты не умеют целоваться!").queue(); + return; + } + + // Применяем cooldown + cooldownManager.applyCooldown(context.getAuthor().getId(), "kiss", 5); + + // Получаем GIF из nekos.life API + String gifUrl = NekosLifeApi.fetchImage("kiss"); + + if (gifUrl == null) { + context.getChannel().sendMessage("❌ Не удалось получить изображение!").queue(); + return; + } + + context.getChannel().sendMessageEmbeds( + EmbedFactory.create() + .setDescription("💋 " + context.getAuthor().getAsMention() + " целует " + target.getAsMention() + " ❤️") + .setImage(gifUrl) + .setColor(new Color(255, 105, 180)) + .setFooter("Powered by nekos.life") + .build() + ).queue(); + } +} diff --git a/src/main/java/creator/loki/command/entertainment/social/positive/PatCommand.java b/src/main/java/creator/loki/command/entertainment/social/positive/PatCommand.java new file mode 100644 index 0000000..955dd26 --- /dev/null +++ b/src/main/java/creator/loki/command/entertainment/social/positive/PatCommand.java @@ -0,0 +1,91 @@ +package creator.loki.command.impl.entertainment.interaction.positive; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.core.security.CooldownManager; +import net.dv8tion.jda.api.entities.Member; + +import java.util.List; + +public class PatCommand implements Command { + private static final int COOLDOWN_SECONDS = 5; + private final CooldownManager cooldownManager; + + public PatCommand(CooldownManager cooldownManager) { + this.cooldownManager = cooldownManager; + } + + @Override + public String getName() { + return "pat"; + } + + @Override + public String getDescription() { + return "Погладить пользователя"; + } + + @Override + public String getUsage() { + return "pat <@user> <количество 1-7>"; + } + + @Override + public String getCategory() { + return "Развлечения"; + } + + @Override + public void execute(CommandContext context) { + String userId = context.getAuthor().getId(); + + // Проверка cooldown + if (cooldownManager.isOnCooldown(userId, "pat")) { + long remaining = cooldownManager.getRemainingSeconds(userId, "pat"); + context.getChannel().sendMessage( + "⏰ Подожди " + remaining + " сек." + ).queue(); + return; + } + + // Проверка аргументов + if (context.getArgs().size() < 2) { + context.getChannel().sendMessage( + "❌ Используй: `!pat <@user> <количество 1-7>`" + ).queue(); + return; + } + + // Получение упомянутого пользователя + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessage("❌ Укажи пользователя через @").queue(); + return; + } + + Member target = mentioned.get(0); + + // Парсинг количества + int count; + try { + count = Integer.parseInt(context.getArgs().get(1)); + } catch (NumberFormatException e) { + context.getChannel().sendMessage("❌ Укажи число от 1 до 7").queue(); + return; + } + + // Валидация количества + if (count < 1 || count > 7) { + context.getChannel().sendMessage("❌ Количество должно быть от 1 до 7").queue(); + return; + } + + // Применяем cooldown + cooldownManager.applyCooldown(userId, "pat", COOLDOWN_SECONDS); + + // Отправляем каждое "pat-pat" отдельным сообщением + for (int i = 0; i < count; i++) { + context.getChannel().sendMessage("pat-pat " + target.getAsMention()).queue(); + } + } +} diff --git a/src/main/java/creator/loki/feature/metrics/MetricsCollector.java b/src/main/java/creator/loki/feature/metrics/MetricsCollector.java new file mode 100644 index 0000000..9374a81 --- /dev/null +++ b/src/main/java/creator/loki/feature/metrics/MetricsCollector.java @@ -0,0 +1,203 @@ +package creator.loki.feature.metrics; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; + +/** + * Сборщик метрик для мониторинга бота + */ +public class MetricsCollector { + private static final Logger logger = LoggerFactory.getLogger(MetricsCollector.class); + + // Счётчики команд + private final ConcurrentHashMap commandCounts = new ConcurrentHashMap<>(); + private final ConcurrentHashMap commandErrors = new ConcurrentHashMap<>(); + private final ConcurrentHashMap commandTotalTime = new ConcurrentHashMap<>(); + + // Общие метрики + private final LongAdder totalCommands = new LongAdder(); + private final LongAdder totalErrors = new LongAdder(); + private final AtomicLong startTime = new AtomicLong(System.currentTimeMillis()); + + public MetricsCollector() { + logger.info("MetricsCollector инициализирован"); + } + + /** + * Записывает выполнение команды + */ + public void recordCommand(String commandName, long executionTimeMs) { + commandCounts.computeIfAbsent(commandName, k -> new LongAdder()).increment(); + commandTotalTime.computeIfAbsent(commandName, k -> new AtomicLong()).addAndGet(executionTimeMs); + totalCommands.increment(); + } + + /** + * Записывает ошибку команды + */ + public void recordError(String commandName) { + commandErrors.computeIfAbsent(commandName, k -> new LongAdder()).increment(); + totalErrors.increment(); + } + + /** + * Возвращает количество выполнений команды + */ + public long getCommandCount(String commandName) { + LongAdder counter = commandCounts.get(commandName); + return counter != null ? counter.sum() : 0; + } + + /** + * Возвращает количество ошибок команды + */ + public long getCommandErrors(String commandName) { + LongAdder counter = commandErrors.get(commandName); + return counter != null ? counter.sum() : 0; + } + + /** + * Возвращает среднее время выполнения команды + */ + public double getAverageExecutionTime(String commandName) { + long count = getCommandCount(commandName); + if (count == 0) { + return 0.0; + } + AtomicLong totalTime = commandTotalTime.get(commandName); + if (totalTime == null) { + return 0.0; + } + return (double) totalTime.get() / count; + } + + /** + * Возвращает общее количество выполненных команд + */ + public long getTotalCommands() { + return totalCommands.sum(); + } + + /** + * Возвращает общее количество ошибок + */ + public long getTotalErrors() { + return totalErrors.sum(); + } + + /** + * Возвращает процент ошибок + */ + public double getErrorRate() { + long total = getTotalCommands(); + if (total == 0) { + return 0.0; + } + return (double) getTotalErrors() / total * 100; + } + + /** + * Возвращает время работы бота в секундах + */ + public long getUptimeSeconds() { + return (System.currentTimeMillis() - startTime.get()) / 1000; + } + + /** + * JSON для {@code GET /health} (пробы оркестраторов). + */ + public String buildHealthJson() { + String version = Optional.ofNullable(MetricsCollector.class.getPackage().getImplementationVersion()) + .filter(v -> !v.isBlank()) + .orElse("dev"); + return String.format(Locale.ROOT, + "{\"status\":\"ok\",\"version\":%s,\"uptimeSeconds\":%d,\"commandsRun\":%d,\"errors\":%d,\"errorRatePercent\":%.4f}", + jsonQuoted(version), + getUptimeSeconds(), + getTotalCommands(), + getTotalErrors(), + getErrorRate()); + } + + private static String jsonQuoted(String s) { + StringBuilder sb = new StringBuilder(s.length() + 2); + sb.append('"'); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '\\' -> sb.append("\\\\"); + case '"' -> sb.append("\\\""); + case '\n' -> sb.append("\\n"); + case '\r' -> sb.append("\\r"); + case '\t' -> sb.append("\\t"); + default -> sb.append(c < ' ' ? ' ' : c); + } + } + sb.append('"'); + return sb.toString(); + } + + /** + * Возвращает топ команд по использованию + */ + public String getTopCommands(int limit) { + StringBuilder sb = new StringBuilder(); + sb.append("📊 Топ команд:\n"); + + commandCounts.entrySet().stream() + .sorted((e1, e2) -> Long.compare(e2.getValue().sum(), e1.getValue().sum())) + .limit(limit) + .forEach(entry -> { + String cmd = entry.getKey(); + long count = entry.getValue().sum(); + long errors = getCommandErrors(cmd); + double avgTime = getAverageExecutionTime(cmd); + + sb.append(String.format(" %s: %d выполнений, %.1f мс среднее, %d ошибок\n", + cmd, count, avgTime, errors)); + }); + + return sb.toString(); + } + + /** + * Возвращает полную статистику + */ + public String getFullStats() { + long uptime = getUptimeSeconds(); + long hours = uptime / 3600; + long minutes = (uptime % 3600) / 60; + long seconds = uptime % 60; + + StringBuilder sb = new StringBuilder(); + sb.append("📈 Статистика бота\n"); + sb.append("━━━━━━━━━━━━━━━━━━━━\n"); + sb.append(String.format("⏱️ Uptime: %dч %dм %dс\n", hours, minutes, seconds)); + sb.append(String.format("📝 Всего команд: %d\n", getTotalCommands())); + sb.append(String.format("❌ Всего ошибок: %d (%.2f%%)\n", getTotalErrors(), getErrorRate())); + sb.append(String.format("📊 Уникальных команд: %d\n", commandCounts.size())); + sb.append("\n"); + sb.append(getTopCommands(10)); + + return sb.toString(); + } + + /** + * Сбрасывает все метрики + */ + public void reset() { + commandCounts.clear(); + commandErrors.clear(); + commandTotalTime.clear(); + totalCommands.reset(); + totalErrors.reset(); + startTime.set(System.currentTimeMillis()); + logger.info("Метрики сброшены"); + } +} diff --git a/src/main/java/creator/loki/feature/moderation/commands/BanCommand.java b/src/main/java/creator/loki/feature/moderation/commands/BanCommand.java new file mode 100644 index 0000000..848feec --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/commands/BanCommand.java @@ -0,0 +1,129 @@ +package creator.loki.feature.moderation.commands; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; + +import java.util.List; + +public class BanCommand implements Command { + + @Override + public String getName() { + return "ban"; + } + + @Override + public String getDescription() { + return "Банит пользователя на сервере"; + } + + @Override + public String getUsage() { + return "ban <@user> [причина]"; + } + + @Override + public String getCategory() { + return "Модерация"; + } + + @Override + public void execute(CommandContext context) { + if (context.getGuild() == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return; + } + + // Проверка прав модератора + if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.BAN_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У вас нет прав на бан пользователей") + ).queue(); + return; + } + + // Проверка прав бота + if (!context.getGuild().getSelfMember().hasPermission(Permission.BAN_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на бан пользователей") + ).queue(); + return; + } + + // Получение упомянутого пользователя + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Укажите пользователя через @") + ).queue(); + return; + } + + Member target = mentioned.get(0); + + // Нельзя забанить самого себя + if (target.getId().equals(context.getAuthor().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Вы не можете забанить самого себя") + ).queue(); + return; + } + + // Нельзя забанить бота + if (target.getUser().isBot() && target.getId().equals(context.getGuild().getSelfMember().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Я не могу забанить самого себя") + ).queue(); + return; + } + + // Проверка иерархии ролей + if (!context.getAuthor().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Вы не можете забанить этого пользователя (роль выше вашей)") + ).queue(); + return; + } + + if (!context.getGuild().getSelfMember().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Я не могу забанить этого пользователя (роль выше моей)") + ).queue(); + return; + } + + // Причина бана + String reason = context.getArgs().size() > 1 + ? String.join(" ", context.getArgs().subList(1, context.getArgs().size())) + : "Причина не указана"; + + // Бан + context.getGuild().ban(target, 0, java.util.concurrent.TimeUnit.DAYS) + .reason(reason) + .queue( + success -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Пользователь забанен", + "**Пользователь:** " + target.getUser().getGlobalName() + "\n" + + "**Причина:** " + reason + "\n" + + "**Модератор:** " + context.getAuthor().getAsMention() + ) + ).queue(); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Ошибка", + "Не удалось забанить. Проверь права бота, иерархию ролей и что участник на сервере." + ) + ).queue(); + } + ); + } +} diff --git a/src/main/java/creator/loki/feature/moderation/commands/ClearCommand.java b/src/main/java/creator/loki/feature/moderation/commands/ClearCommand.java new file mode 100644 index 0000000..affd9cb --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/commands/ClearCommand.java @@ -0,0 +1,106 @@ +package creator.loki.feature.moderation.commands; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.channel.concrete.TextChannel; + +import java.util.concurrent.TimeUnit; + +public class ClearCommand implements Command { + + @Override + public String getName() { + return "clear"; + } + + @Override + public String getDescription() { + return "Удаляет указанное количество сообщений"; + } + + @Override + public String getUsage() { + return "clear <количество>"; + } + + @Override + public void execute(CommandContext context) { + // Проверка прав + if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.MESSAGE_MANAGE)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Недостаточно прав", + "У вас нет прав на управление сообщениями" + ) + ).queue(); + return; + } + + // Проверка аргументов + if (context.getArgs().isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Неверное количество", + "Укажите число от 1 до 100" + ) + ).queue(); + return; + } + + int amount; + try { + amount = Integer.parseInt(context.getArgs().get(0)); + } catch (NumberFormatException e) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Неверное количество", + "Укажите число от 1 до 100" + ) + ).queue(); + return; + } + + if (amount < 1 || amount > 100) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Неверное количество", + "Укажите число от 1 до 100" + ) + ).queue(); + return; + } + + // Удаление сообщений + if (!(context.getChannel() instanceof TextChannel channel)) { + return; + } + + channel.getHistory().retrievePast(amount + 1).queue(messages -> { + channel.deleteMessages(messages).queue( + success -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Сообщения удалены", + "Удалено сообщений: " + messages.size() + ) + ).queue(msg -> msg.delete().queueAfter(5, TimeUnit.SECONDS)); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Ошибка удаления", + "Не удалось удалить сообщения: " + error.getMessage() + ) + ).queue(); + } + ); + }); + } + + @Override + public String getCategory() { + return "Модерация"; + } +} diff --git a/src/main/java/creator/loki/feature/moderation/commands/KickCommand.java b/src/main/java/creator/loki/feature/moderation/commands/KickCommand.java new file mode 100644 index 0000000..1f35c85 --- /dev/null +++ b/src/main/java/creator/loki/feature/moderation/commands/KickCommand.java @@ -0,0 +1,121 @@ +package creator.loki.feature.moderation.commands; + +import creator.loki.command.Command; +import creator.loki.command.CommandContext; +import creator.loki.embed.EmbedTemplates; +import net.dv8tion.jda.api.Permission; +import net.dv8tion.jda.api.entities.Member; + +import java.util.List; + +public class KickCommand implements Command { + + @Override + public String getName() { + return "kick"; + } + + @Override + public String getDescription() { + return "Кикает пользователя с сервера"; + } + + @Override + public String getUsage() { + return "kick <@user> [причина]"; + } + + @Override + public String getCategory() { + return "Модерация"; + } + + @Override + public void execute(CommandContext context) { + if (context.getGuild() == null) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Команда доступна только на сервере") + ).queue(); + return; + } + + if (context.getAuthor() == null || !context.getAuthor().hasPermission(Permission.KICK_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У вас нет прав на кик пользователей") + ).queue(); + return; + } + + if (!context.getGuild().getSelfMember().hasPermission(Permission.KICK_MEMBERS)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "У бота нет прав на кик пользователей") + ).queue(); + return; + } + + List mentioned = context.getMessage().getMentions().getMembers(); + if (mentioned.isEmpty()) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Укажите пользователя через @") + ).queue(); + return; + } + + Member target = mentioned.get(0); + + if (target.getId().equals(context.getAuthor().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Вы не можете кикнуть самого себя") + ).queue(); + return; + } + + if (target.getUser().isBot() && target.getId().equals(context.getGuild().getSelfMember().getId())) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Ошибка", "Я не могу кикнуть самого себя") + ).queue(); + return; + } + + if (!context.getAuthor().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Вы не можете кикнуть этого пользователя") + ).queue(); + return; + } + + if (!context.getGuild().getSelfMember().canInteract(target)) { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error("Недостаточно прав", "Я не могу кикнуть этого пользователя") + ).queue(); + return; + } + + String reason = context.getArgs().size() > 1 + ? String.join(" ", context.getArgs().subList(1, context.getArgs().size())) + : "Причина не указана"; + + context.getGuild().kick(target) + .reason(reason) + .queue( + success -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.success( + "Пользователь кикнут", + "**Пользователь:** " + target.getUser().getGlobalName() + "\n" + + "**Причина:** " + reason + "\n" + + "**Модератор:** " + context.getAuthor().getAsMention() + ) + ).queue(); + }, + error -> { + context.getChannel().sendMessageEmbeds( + EmbedTemplates.error( + "Ошибка", + "Не удалось кикнуть. Проверь права бота (**Kick Members**), иерархию ролей и что участник не выше бота." + ) + ).queue(); + } + ); + } +}