feat: economy system with minigames, bank, rob, jobs, fishing, blackjack, slots

+ economy domain (EconomyAccount, Job, SlotCombination, EconomyTransaction)
+ EconomyRepository + SqlEconomyRepository + DB schema
+ EconomyService (transfer, daily, deposit/withdraw, stat upgrades)
+ FishingService (fish grid with difficulty levels, speed bonus)
+ BlackjackService (dealer stands on 16, ties lose)
+ SlotService (weighted combos up to x50)
+ JobService (6 jobs with level reqs and efficiency stat)
+ 11 commands: balance, baltop, pay, daily, bank, rob,
  fish, blackjack, slot, job, work
+ Tests for SqlEconomyRepository
+ EconomyModule + EconomyCommandRegistrar wired into BotModule
This commit is contained in:
loki5512344 2026-06-20 00:23:15 +02:00
parent fcb5cd7207
commit fe121c8546
270 changed files with 18632 additions and 11656 deletions

View file

@ -1,50 +1,50 @@
package creator.loki;
import com.google.inject.Guice;
import com.google.inject.Injector;
import creator.loki.core.BotInitializer;
import creator.loki.di.BotModule;
import net.dv8tion.jda.api.JDA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
private static volatile boolean shutdownRequested = false;
public static void main(String[] args) {
// Регистрируем shutdown hook ДО создания бота
Thread shutdownHook = new Thread(() -> {
if (!shutdownRequested) {
shutdownRequested = true;
logger.info("========================================");
logger.info("Получен сигнал завершения");
logger.info("Выполняется graceful shutdown...");
logger.info("========================================");
}
}, "ShutdownHook");
Runtime.getRuntime().addShutdownHook(shutdownHook);
try {
logger.info("Запуск Discord бота");
// Создаём Guice Injector с модулем конфигурации
Injector injector = Guice.createInjector(new BotModule());
// Получаем BotInitializer с автоматически внедрёнными зависимостями
BotInitializer initializer = injector.getInstance(BotInitializer.class);
// Запускаем бота
JDA jda = initializer.start();
// Ждём полной загрузки
jda.awaitReady();
logger.info("Бот полностью готов к работе!");
} catch (Exception e) {
logger.error("Критическая ошибка при запуске бота", e);
System.exit(1);
}
}
}
package creator.loki.bootstrap;
import com.google.inject.Guice;
import com.google.inject.Injector;
import creator.loki.core.BotInitializer;
import creator.loki.di.BotModule;
import net.dv8tion.jda.api.JDA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
private static volatile boolean shutdownRequested = false;
public static void main(String[] args) {
// Регистрируем shutdown hook ДО создания бота
Thread shutdownHook = new Thread(() -> {
if (!shutdownRequested) {
shutdownRequested = true;
logger.info("========================================");
logger.info("Получен сигнал завершения");
logger.info("Выполняется graceful shutdown...");
logger.info("========================================");
}
}, "ShutdownHook");
Runtime.getRuntime().addShutdownHook(shutdownHook);
try {
logger.info("Запуск Discord бота");
// Создаём Guice Injector с модулем конфигурации
Injector injector = Guice.createInjector(new BotModule());
// Получаем BotInitializer с автоматически внедрёнными зависимостями
BotInitializer initializer = injector.getInstance(BotInitializer.class);
// Запускаем бота
JDA jda = initializer.start();
// Ждём полной загрузки
jda.awaitReady();
logger.info("Бот полностью готов к работе!");
} catch (Exception e) {
logger.error("Критическая ошибка при запуске бота", e);
System.exit(1);
}
}
}

View file

@ -1,16 +1,18 @@
package creator.loki.listener;
import net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ReadyListener extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(ReadyListener.class);
@Override
public void onReady(ReadyEvent event) {
logger.info("Бот {} успешно запущен!", event.getJDA().getSelfUser().getName());
logger.info("Подключен к {} серверам", event.getGuildTotalCount());
}
}
package creator.loki.bootstrap;
import com.google.inject.Singleton;
import net.dv8tion.jda.api.events.session.ReadyEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class ReadyListener extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(ReadyListener.class);
@Override
public void onReady(ReadyEvent event) {
logger.info("Бот {} успешно запущен!", event.getJDA().getSelfUser().getName());
logger.info("Подключен к {} серверам", event.getGuildTotalCount());
}
}

View file

@ -1,16 +1,16 @@
package creator.loki.command;
public interface Command {
String getName();
String getDescription();
default String getUsage() {
return getName();
}
default String getCategory() {
return "Общие";
}
void execute(CommandContext context);
}
package creator.loki.command;
public interface Command {
String getName();
String getDescription();
default String getUsage() {
return getName();
}
default String getCategory() {
return "Общие";
}
void execute(CommandContext context);
}

View file

@ -0,0 +1,12 @@
package creator.loki.command;
/**
* Имена категорий для {@link Command#getCategory()} (справка, селектор help).
*/
public final class CommandCategories {
/** Команды с GIF (nekos.life): hug, slap, poke и т.п. */
public static final String GIF_INTERACTIVE = "GIF-интерактив";
private CommandCategories() {}
}

View file

@ -1,44 +1,44 @@
package creator.loki.command;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import java.util.List;
public class CommandContext {
private final Message message;
private final List<String> args;
private final MessageChannel channel;
private final Member author;
private final Guild guild;
public CommandContext(Message message, List<String> args, MessageChannel channel, Member author, Guild guild) {
this.message = message;
this.args = args;
this.channel = channel;
this.author = author;
this.guild = guild;
}
public Message getMessage() {
return message;
}
public List<String> getArgs() {
return args;
}
public MessageChannel getChannel() {
return channel;
}
public Member getAuthor() {
return author;
}
public Guild getGuild() {
return guild;
}
}
package creator.loki.command;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.Message;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import java.util.List;
public class CommandContext {
private final Message message;
private final List<String> args;
private final MessageChannel channel;
private final Member author;
private final Guild guild;
public CommandContext(Message message, List<String> args, MessageChannel channel, Member author, Guild guild) {
this.message = message;
this.args = args;
this.channel = channel;
this.author = author;
this.guild = guild;
}
public Message getMessage() {
return message;
}
public List<String> getArgs() {
return args;
}
public MessageChannel getChannel() {
return channel;
}
public Member getAuthor() {
return author;
}
public Guild getGuild() {
return guild;
}
}

View file

@ -1,29 +0,0 @@
package creator.loki.command;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
/**
* Интерфейс для slash команд
*/
public interface SlashCommand {
/**
* Выполняет slash команду
*/
void execute(SlashCommandInteractionEvent event);
/**
* Возвращает данные команды для регистрации в Discord
*/
CommandData getCommandData();
/**
* Название команды
*/
String getName();
/**
* Описание команды
*/
String getDescription();
}

View file

@ -1,78 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,68 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,78 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,78 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,78 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,67 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,78 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,61 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,92 +0,0 @@
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<Member> 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();
}
}

View file

@ -1,91 +0,0 @@
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<Member> 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();
}
}
}

View file

@ -1,126 +0,0 @@
package creator.loki.command.impl.moderation;
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<Member> 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().getAsTag() + "\n" +
"**Причина:** " + reason + "\n" +
"**Модератор:** " + context.getAuthor().getAsMention()
)
).queue();
},
error -> {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось забанить: " + error.getMessage())
).queue();
}
);
}
}

View file

@ -1,108 +0,0 @@
package creator.loki.command.impl.moderation;
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)) {
return;
}
TextChannel channel = (TextChannel) context.getChannel();
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 "Модерация";
}
}

View file

@ -1,118 +0,0 @@
package creator.loki.command.impl.moderation;
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<Member> 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().getAsTag() + "\n" +
"**Причина:** " + reason + "\n" +
"**Модератор:** " + context.getAuthor().getAsMention()
)
).queue();
},
error -> {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось кикнуть: " + error.getMessage())
).queue();
}
);
}
}

View file

@ -1,122 +0,0 @@
package creator.loki.command.impl.setup;
import creator.loki.database.ConfigRepository;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
/**
* Команда настройки логирования
*/
public class LogSetupCommand extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(LogSetupCommand.class);
private final ConfigRepository configRepo;
public LogSetupCommand(ConfigRepository configRepo) {
this.configRepo = configRepo;
}
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
if (!"log-setup".equals(event.getName())) {
return;
}
Member member = event.getMember();
if (member == null || !member.hasPermission(Permission.ADMINISTRATOR)) {
event.reply("❌ У вас нет прав для использования этой команды!").setEphemeral(true).queue();
return;
}
String subcommand = event.getSubcommandName();
if (subcommand == null) {
return;
}
switch (subcommand) {
case "moderation" -> handleModeration(event);
case "levelup" -> handleLevelUp(event);
case "info" -> handleInfo(event);
}
}
private void handleModeration(SlashCommandInteractionEvent event) {
TextChannel channel = event.getOption("channel").getAsChannel().asTextChannel();
configRepo.set("moderation.log_channel_id", channel.getId());
EmbedBuilder embed = new EmbedBuilder()
.setTitle("✅ Канал логов модерации установлен")
.setDescription("Логи модерации будут отправляться в " + channel.getAsMention())
.addField("Что логируется",
"• Удаление сообщений\n" +
"• Баны/разбаны\n" +
"• Кики\n" +
"• Муты/размуты\n" +
"• Отключение микрофона/звука", false)
.setColor(Color.GREEN);
event.replyEmbeds(embed.build()).queue();
logger.info("Установлен канал логов модерации: {} ({})", channel.getName(), channel.getId());
}
private void handleLevelUp(SlashCommandInteractionEvent event) {
TextChannel channel = event.getOption("channel").getAsChannel().asTextChannel();
configRepo.set("levelup.notification_channel_id", channel.getId());
EmbedBuilder embed = new EmbedBuilder()
.setTitle("✅ Канал уведомлений о level up установлен")
.setDescription("Уведомления о повышении уровня будут отправляться в " + channel.getAsMention())
.setColor(Color.GREEN);
event.replyEmbeds(embed.build()).queue();
logger.info("Установлен канал уведомлений level up: {} ({})", channel.getName(), channel.getId());
}
private void handleInfo(SlashCommandInteractionEvent event) {
String moderationChannelId = configRepo.get("moderation.log_channel_id");
String levelupChannelId = configRepo.get("levelup.notification_channel_id");
EmbedBuilder embed = new EmbedBuilder()
.setTitle("📋 Конфигурация логирования")
.setColor(Color.BLUE);
if (moderationChannelId != null) {
embed.addField("Логи модерации", "<#" + moderationChannelId + ">", false);
} else {
embed.addField("Логи модерации", "❌ Не установлен", false);
}
if (levelupChannelId != null) {
embed.addField("Уведомления level up", "<#" + levelupChannelId + ">", false);
} else {
embed.addField("Уведомления level up", "❌ Не установлен", false);
}
event.replyEmbeds(embed.build()).setEphemeral(true).queue();
}
public static void register(net.dv8tion.jda.api.JDA jda) {
jda.updateCommands().addCommands(
Commands.slash("log-setup", "Настройка логирования")
.addSubcommands(
new SubcommandData("moderation", "Установить канал для логов модерации")
.addOption(OptionType.CHANNEL, "channel", "Текстовый канал", true),
new SubcommandData("levelup", "Установить канал для уведомлений о level up")
.addOption(OptionType.CHANNEL, "channel", "Текстовый канал", true),
new SubcommandData("info", "Показать текущую конфигурацию")
)
).queue();
}
}

View file

@ -1,89 +0,0 @@
package creator.loki.command.impl.stats;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.feature.level.domain.LevelProgress;
import creator.loki.feature.level.domain.UserStats;
import creator.loki.level.LevelSystem;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
/**
* Команда для просмотра уровня и статистики
*/
public class RankCommand implements Command {
private final LevelSystem levelSystem;
public RankCommand(LevelSystem levelSystem) {
this.levelSystem = levelSystem;
}
@Override
public String getName() {
return "rank";
}
@Override
public String getDescription() {
return "Посмотреть свой уровень и статистику";
}
@Override
public String getUsage() {
return "rank [@user]";
}
@Override
public String getCategory() {
return "Статистика";
}
@Override
public void execute(CommandContext context) {
Member target;
if (!context.getMessage().getMentions().getMembers().isEmpty()) {
target = context.getMessage().getMentions().getMembers().get(0);
} else {
target = context.getAuthor();
}
if (target == null) {
context.getChannel().sendMessage("❌ Пользователь не найден!").queue();
return;
}
String userId = target.getId();
UserStats stats = levelSystem.getUserStats(userId);
LevelProgress progress = levelSystem.getProgress(userId);
// Прогресс-бар
int barLength = 20;
int filled = (int) (progress.getPercentage() / 100.0 * barLength);
StringBuilder progressBar = new StringBuilder();
for (int i = 0; i < barLength; i++) {
progressBar.append(i < filled ? "█" : "░");
}
long hours = stats.getVoiceMinutes() / 60;
long minutes = stats.getVoiceMinutes() % 60;
var embed = EmbedFactory.create()
.setTitle("📊 Статистика " + target.getEffectiveName())
.setThumbnail(target.getEffectiveAvatarUrl())
.addField("🎯 Уровень", "**" + stats.getLevel() + "**", true)
.addField("⭐ Всего XP", "**" + stats.getTotalXP() + "**", true)
.addField("📈 Прогресс",
progressBar.toString() + " **" + String.format("%.1f", progress.getPercentage()) + "%**\n" +
progress.getCurrentLevelXP() + " / " + progress.getXpForNextLevel() + " XP до следующего уровня",
false)
.addField("💬 Сообщений", "**" + stats.getMessageCount() + "**", true)
.addField("🎤 Время в войсе", "**" + hours + "ч " + minutes + "м**", true)
.setColor(new Color(46, 204, 113))
.build();
context.getChannel().sendMessageEmbeds(embed).queue();
}
}

View file

@ -1,75 +0,0 @@
package creator.loki.command.impl.stats;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.OnlineStatus;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
public class StatsCommand implements Command {
@Override
public String getName() {
return "stats";
}
@Override
public String getDescription() {
return "Статистика сервера";
}
@Override
public String getUsage() {
return "stats";
}
@Override
public String getCategory() {
return "Статистика";
}
@Override
public void execute(CommandContext context) {
if (context.getGuild() == null) {
context.getChannel().sendMessage("Команда доступна только на сервере").queue();
return;
}
Guild guild = context.getGuild();
// Подсчёт статистики
int totalMembers = guild.getMemberCount();
long onlineMembers = guild.getMembers().stream()
.filter(m -> m.getOnlineStatus() == OnlineStatus.ONLINE)
.count();
long inVoice = guild.getMembers().stream()
.filter(m -> m.getVoiceState() != null && m.getVoiceState().inAudioChannel())
.count();
int textChannels = guild.getTextChannels().size();
int voiceChannels = guild.getVoiceChannels().size();
int roles = guild.getRoles().size();
int bots = (int) guild.getMembers().stream()
.filter(m -> m.getUser().isBot())
.count();
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("📊 Статистика сервера")
.setThumbnail(guild.getIconUrl())
.addField("👥 Участников", String.valueOf(totalMembers), true)
.addField("🟢 Онлайн", String.valueOf(onlineMembers), true)
.addField("🔊 В войсе", String.valueOf(inVoice), true)
.addField("💬 Текстовых каналов", String.valueOf(textChannels), true)
.addField("🎤 Голосовых каналов", String.valueOf(voiceChannels), true)
.addField("🎭 Ролей", String.valueOf(roles), true)
.addField("🤖 Ботов", String.valueOf(bots), true)
.addField("👤 Людей", String.valueOf(totalMembers - bots), true)
.setColor(new Color(52, 152, 219))
.setFooter("Сервер: " + guild.getName())
.build()
).queue();
}
}

View file

@ -1,159 +0,0 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.interactions.components.selections.StringSelectMenu;
import java.awt.Color;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class HelpCommand implements Command {
private final CommandRegistry registry;
private final BotConfig config;
private static final Map<String, String> CATEGORY_EMOJIS = new HashMap<>();
private static final Map<String, Color> CATEGORY_COLORS = new HashMap<>();
private static final Map<String, String> CATEGORY_DESCRIPTIONS = new HashMap<>();
static {
CATEGORY_EMOJIS.put("Общие", "📋");
CATEGORY_EMOJIS.put("Роли", "🎭");
CATEGORY_EMOJIS.put("Развлечения", "🎮");
CATEGORY_EMOJIS.put("Модерация", "🛡️");
CATEGORY_EMOJIS.put("Статистика", "📊");
CATEGORY_EMOJIS.put("Утилиты", "🔧");
CATEGORY_EMOJIS.put("Настройка", "⚙️");
CATEGORY_COLORS.put("Общие", new Color(52, 152, 219));
CATEGORY_COLORS.put("Роли", new Color(155, 89, 182));
CATEGORY_COLORS.put("Развлечения", new Color(241, 196, 15));
CATEGORY_COLORS.put("Модерация", new Color(231, 76, 60));
CATEGORY_COLORS.put("Статистика", new Color(46, 204, 113));
CATEGORY_COLORS.put("Утилиты", new Color(52, 152, 219));
CATEGORY_COLORS.put("Настройка", new Color(149, 165, 166));
CATEGORY_DESCRIPTIONS.put("Общие", "Основные команды бота");
CATEGORY_DESCRIPTIONS.put("Роли", "Управление кастомными ролями");
CATEGORY_DESCRIPTIONS.put("Развлечения", "Игры и взаимодействия");
CATEGORY_DESCRIPTIONS.put("Модерация", "Команды модерации сервера");
CATEGORY_DESCRIPTIONS.put("Статистика", "Уровни, репутация и статистика");
CATEGORY_DESCRIPTIONS.put("Утилиты", "Полезные инструменты");
CATEGORY_DESCRIPTIONS.put("Настройка", "Настройка систем бота (только админы)");
}
public HelpCommand(CommandRegistry registry, BotConfig config) {
this.registry = registry;
this.config = config;
}
@Override
public String getName() {
return "help";
}
@Override
public String getDescription() {
return "Показывает список всех команд";
}
@Override
public void execute(CommandContext context) {
var commands = registry.getAllCommands();
Map<String, List<Command>> commandsByCategory = commands.stream()
.collect(Collectors.groupingBy(Command::getCategory));
// Главная страница
EmbedFactory embed = createMainPage(context, commandsByCategory);
// Создаём селектор категорий
StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("help:category")
.setPlaceholder("📂 Выберите категорию");
boolean isAdmin = context.getAuthor() != null &&
context.getAuthor().hasPermission(Permission.ADMINISTRATOR);
commandsByCategory.keySet().stream()
.sorted()
.forEach(category -> {
// Скрываем категорию "Настройка" для не-админов
if (category.equals("Настройка") && !isAdmin) {
return;
}
String emoji = CATEGORY_EMOJIS.getOrDefault(category, "📌");
String description = CATEGORY_DESCRIPTIONS.getOrDefault(category, "Команды категории");
int count = commandsByCategory.get(category).size();
menuBuilder.addOption(
category,
"category:" + category,
description + " (" + count + " команд)",
net.dv8tion.jda.api.entities.emoji.Emoji.fromUnicode(emoji)
);
});
context.getChannel()
.sendMessageEmbeds(embed.build())
.addActionRow(menuBuilder.build())
.queue();
}
private EmbedFactory createMainPage(CommandContext context, Map<String, List<Command>> commandsByCategory) {
int totalCommands = commandsByCategory.values().stream()
.mapToInt(List::size)
.sum();
boolean isAdmin = context.getAuthor() != null &&
context.getAuthor().hasPermission(Permission.ADMINISTRATOR);
StringBuilder categoriesText = new StringBuilder();
commandsByCategory.keySet().stream()
.sorted()
.forEach(category -> {
// Скрываем категорию "Настройка" для не-админов
if (category.equals("Настройка") && !isAdmin) {
return;
}
String emoji = CATEGORY_EMOJIS.getOrDefault(category, "📌");
int count = commandsByCategory.get(category).size();
categoriesText.append(emoji).append(" **").append(category)
.append("** — ").append(count).append(" команд\n");
});
return EmbedFactory.create()
.setTitle("📚 Справка по командам Loki Bot")
.setDescription(
"Привет! Я многофункциональный Discord бот с системой уровней, " +
"личными голосовыми каналами и множеством развлекательных команд.\n\n" +
"**Префикс команд:** `" + config.getPrefix() + "`\n" +
"**Всего команд:** " + totalCommands + "\n\n" +
"**📂 Категории команд:**\n" + categoriesText.toString() + "\n" +
"Используй селектор ниже для просмотра команд категории."
)
.setColor(new Color(88, 101, 242))
.setThumbnail(context.getGuild() != null ? context.getGuild().getIconUrl() : null)
.addField("💡 Как использовать",
"Большинство команд используют префикс `!`\n" +
"Пример: `!rank`, `!ship @user1 @user2`\n\n" +
"Некоторые важные команды используют `/`\n" +
"Пример: `/voice-setup`, `/counter-setup`",
false)
.setFooter(
"Запросил: " + context.getAuthor().getUser().getName(),
context.getAuthor().getUser().getAvatarUrl()
);
}
@Override
public String getCategory() {
return "Утилиты";
}
}

View file

@ -1,88 +0,0 @@
package creator.loki.command.impl.utility;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.data.JsonDataStore;
import creator.loki.profile.ProfileData;
import creator.loki.profile.ProfileGenerator;
import creator.loki.profile.ProfileService;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.utils.FileUpload;
import java.util.List;
/**
* Команда для отображения профиля пользователя
*/
public class ProfileCommand implements Command {
private final ProfileService profileService;
private final ProfileGenerator profileGenerator;
public ProfileCommand(JsonDataStore dataStore) {
this.profileService = new ProfileService(dataStore);
this.profileGenerator = new ProfileGenerator();
}
@Override
public String getName() {
return "profile";
}
@Override
public String getDescription() {
return "Показать профиль пользователя";
}
@Override
public String getUsage() {
return "profile [@user]";
}
@Override
public String getCategory() {
return "Общие";
}
@Override
public void execute(CommandContext context) {
if (context.getGuild() == null) {
context.getChannel().sendMessage("Команда доступна только на сервере").queue();
return;
}
// Определяем целевого пользователя
Member target;
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
target = context.getAuthor();
} else {
target = mentioned.get(0);
}
if (target == null) {
context.getChannel().sendMessage("Пользователь не найден").queue();
return;
}
// Отправляем сообщение о генерации
context.getChannel().sendMessage("🎨 Генерирую профиль...").queue(loadingMsg -> {
try {
// Получаем данные профиля
ProfileData profileData = profileService.getProfileData(target, context.getGuild());
// Генерируем изображение
byte[] imageBytes = profileGenerator.generateProfile(profileData);
// Отправляем изображение
loadingMsg.delete().queue();
context.getChannel().sendFiles(
FileUpload.fromData(imageBytes, "profile.png")
).queue();
} catch (Exception e) {
loadingMsg.editMessage("❌ Ошибка генерации профиля: " + e.getMessage()).queue();
e.printStackTrace();
}
});
}
}

View file

@ -1,63 +0,0 @@
package creator.loki.command.impl.utility.system;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.metrics.MetricsCollector;
import net.dv8tion.jda.api.Permission;
import java.awt.Color;
public class MetricsCommand implements Command {
private final MetricsCollector metrics;
public MetricsCommand(MetricsCollector metrics) {
this.metrics = metrics;
}
@Override
public void execute(CommandContext ctx) {
// Только администраторы могут видеть метрики
if (ctx.getAuthor() == null || !ctx.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("❌ Недостаточно прав")
.setDescription("Эта команда доступна только администраторам.")
.setColor(Color.RED)
.build()
).queue();
return;
}
String stats = metrics.getFullStats();
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("📊 Метрики бота")
.setDescription("```\n" + stats + "\n```")
.setColor(new Color(88, 101, 242))
.setFooter("Используй !metrics reset для сброса")
.build()
).queue();
}
@Override
public String getName() {
return "metrics";
}
@Override
public String getDescription() {
return "Показывает метрики и статистику бота (только для администраторов)";
}
@Override
public String getUsage() {
return "metrics";
}
@Override
public String getCategory() {
return "Система";
}
}

View file

@ -1,25 +1,26 @@
package creator.loki.config;
public class BotConfig {
private final String token;
private final String prefix;
private final String activity;
public BotConfig(String token, String prefix, String activity) {
this.token = token;
this.prefix = prefix;
this.activity = activity;
}
public String getToken() {
return token;
}
public String getPrefix() {
return prefix;
}
public String getActivity() {
return activity;
}
}
package creator.loki.config;
public class BotConfig {
private final String token;
private final String prefix;
private final String activity;
private final int healthHttpPort;
private final String superAdminId;
private final String dbPath;
public BotConfig(String token, String prefix, String activity, int healthHttpPort, String superAdminId, String dbPath) {
this.token = token;
this.prefix = prefix;
this.activity = activity;
this.healthHttpPort = healthHttpPort;
this.superAdminId = superAdminId;
this.dbPath = dbPath;
}
public String getToken() { return token; }
public String getPrefix() { return prefix; }
public String getActivity() { return activity; }
public int getHealthHttpPort() { return healthHttpPort; }
public String getSuperAdminId() { return superAdminId; }
public String getDbPath() { return dbPath; }
}

View file

@ -1,39 +1,67 @@
package creator.loki.config;
import io.github.cdimascio.dotenv.Dotenv;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ConfigLoader {
private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
private ConfigLoader() {
// Запрет создания экземпляров
}
public static BotConfig load() {
Dotenv dotenv = Dotenv.configure()
.ignoreIfMissing()
.load();
String token = dotenv.get("DISCORD_BOT_TOKEN");
if (token == null || token.isBlank()) {
throw new IllegalStateException(
"Токен бота не найден! Установите переменную окружения DISCORD_BOT_TOKEN или создайте .env файл"
);
}
String prefix = dotenv.get("BOT_PREFIX");
if (prefix == null || prefix.isBlank()) {
prefix = "!";
}
String activity = dotenv.get("BOT_ACTIVITY");
if (activity == null || activity.isBlank()) {
activity = "Используй !help";
}
logger.info("Конфигурация загружена успешно");
return new BotConfig(token, prefix, activity);
}
}
package creator.loki.config;
import io.github.cdimascio.dotenv.Dotenv;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public final class ConfigLoader {
private static final Logger logger = LoggerFactory.getLogger(ConfigLoader.class);
private ConfigLoader() {
}
private static int parsePort(String raw, String key) {
if (raw == null || raw.isBlank()) return 0;
try {
int port = Integer.parseInt(raw.trim());
if (port < 0 || port > 65535) {
throw new IllegalArgumentException("порт вне диапазона 0..65535");
}
return port;
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Некорректный " + key + ": \"" + raw + "\". Ожидается число 0..65535.",
e
);
}
}
public static BotConfig load() {
Dotenv dotenv = Dotenv.configure()
.ignoreIfMissing()
.load();
String token = dotenv.get("DISCORD_BOT_TOKEN");
if (token == null || token.isBlank()) {
throw new IllegalStateException(
"Токен бота не найден! Установите переменную окружения DISCORD_BOT_TOKEN или создайте .env файл"
);
}
String prefix = dotenv.get("BOT_PREFIX");
if (prefix == null || prefix.isBlank()) {
prefix = "!";
}
String activity = dotenv.get("BOT_ACTIVITY");
if (activity == null || activity.isBlank()) {
activity = "Используй !help";
}
int healthHttpPort = parsePort(dotenv.get("HEALTH_HTTP_PORT"), "HEALTH_HTTP_PORT");
String superAdminId = dotenv.get("SUPER_ADMIN_ID");
if (superAdminId == null || superAdminId.isBlank()) {
superAdminId = "1414975197269987471";
logger.warn("SUPER_ADMIN_ID не задан, используется значение по умолчанию");
}
String dbPath = dotenv.get("DB_PATH");
if (dbPath == null || dbPath.isBlank()) {
dbPath = "data/bot.db";
}
logger.info("Конфигурация загружена успешно");
return new BotConfig(token, prefix, activity, healthHttpPort, superAdminId, dbPath);
}
}

View file

@ -1,37 +1,11 @@
package creator.loki.core;
import com.google.inject.Inject;
import creator.loki.command.impl.setup.LogSetupCommand;
import creator.loki.command.impl.stats.leaderboard.LeaderboardButtonListener;
import creator.loki.config.BotConfig;
import creator.loki.core.EventDispatcher;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.registry.PrefixCommandRegistrar;
import creator.loki.core.registry.SlashCommandRegistrar;
import creator.loki.core.registry.SlashCommandRegistry;
import creator.loki.feature.counter.CounterManager;
import creator.loki.data.JsonDataStore;
import creator.loki.database.ConfigRepository;
import creator.loki.database.Database;
import creator.loki.feature.counter.CounterSetupCommand;
import creator.loki.feature.counter.MemberCounterListener;
import creator.loki.feature.level.LevelUpNotifier;
import creator.loki.feature.level.service.LevelService;
import creator.loki.feature.voice.VoiceButtonHandler;
import creator.loki.feature.voice.VoiceChannelListener;
import creator.loki.feature.voice.VoiceChannelManager;
import creator.loki.feature.voice.VoiceSetupCommand;
import creator.loki.feature.level.VoiceTracker;
import creator.loki.level.LevelSystem;
import creator.loki.listener.*;
import creator.loki.listener.interaction.SlashCommandListener;
import creator.loki.metrics.MetricsCollector;
import creator.loki.profile.ProfileService;
import creator.loki.feature.role.CustomRoleManager;
import creator.loki.core.security.CooldownManager;
import creator.loki.core.security.RateLimiter;
import creator.loki.feature.voice.VoiceChannelManager;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.metrics.HealthHttpServer;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.OnlineStatus;
@ -44,118 +18,52 @@ public class BotInitializer {
private static final Logger logger = LoggerFactory.getLogger(BotInitializer.class);
private final BotConfig config;
private final CommandRegistry registry;
private final SlashCommandRegistry slashRegistry;
private final EventDispatcher dispatcher;
private final JsonDataStore dataStore;
private final CustomRoleManager roleManager;
private final CooldownManager cooldownManager;
private final RateLimiter rateLimiter;
private final MetricsCollector metrics;
private final LevelSystem levelSystem;
private final LevelService levelService;
private final VoiceTracker voiceTracker;
private final ProfileService profileService;
private final creator.loki.feature.profile.ProfileService newProfileService;
private final VoiceChannelManager voiceChannelManager;
private final CounterManager counterManager;
private final VoiceChannelListener voiceChannelListener;
private final PrefixCommandRegistrar registrar;
private final ListenerAggregator listenerAggregator;
private final MuteScheduler muteScheduler;
private final MemberCounterListener memberCounterListener;
private final Database database;
private final ConfigRepository configRepo;
private final VoiceSetupCommand voiceSetupCommand;
private final CounterSetupCommand counterSetupCommand;
private final LogSetupCommand logSetupCommand;
private final VoiceButtonHandler voiceButtonHandler;
private final ModerationLogger moderationLogger;
private final LevelUpNotifier levelUpNotifier;
private final HelpMenuHandler helpMenuHandler;
private final HealthHttpServer healthHttpServer;
private final ShutdownManager shutdownManager;
@Inject
public BotInitializer(
BotConfig config,
CommandRegistry registry,
SlashCommandRegistry slashRegistry,
EventDispatcher dispatcher,
JsonDataStore dataStore,
CustomRoleManager roleManager,
CooldownManager cooldownManager,
RateLimiter rateLimiter,
MetricsCollector metrics,
LevelSystem levelSystem,
LevelService levelService,
VoiceTracker voiceTracker,
ProfileService profileService,
creator.loki.feature.profile.ProfileService newProfileService,
VoiceChannelManager voiceChannelManager,
CounterManager counterManager,
Database database,
ConfigRepository configRepo,
LevelUpNotifier levelUpNotifier
PrefixCommandRegistrar registrar,
ListenerAggregator listenerAggregator,
MuteScheduler muteScheduler,
MemberCounterListener memberCounterListener,
HealthHttpServer healthHttpServer,
ShutdownManager shutdownManager
) {
this.config = config;
this.registry = registry;
this.slashRegistry = slashRegistry;
this.dispatcher = dispatcher;
this.dataStore = dataStore;
this.roleManager = roleManager;
this.cooldownManager = cooldownManager;
this.rateLimiter = rateLimiter;
this.metrics = metrics;
this.levelSystem = levelSystem;
this.levelService = levelService;
this.voiceTracker = voiceTracker;
this.profileService = profileService;
this.newProfileService = newProfileService;
this.voiceChannelManager = voiceChannelManager;
this.counterManager = counterManager;
this.database = database;
this.configRepo = configRepo;
this.levelUpNotifier = levelUpNotifier;
this.voiceChannelListener = new VoiceChannelListener(voiceChannelManager);
this.memberCounterListener = new MemberCounterListener(counterManager);
this.voiceSetupCommand = new VoiceSetupCommand(configRepo);
this.counterSetupCommand = new CounterSetupCommand(configRepo);
this.logSetupCommand = new LogSetupCommand(configRepo);
this.voiceButtonHandler = new VoiceButtonHandler(voiceChannelManager);
this.moderationLogger = new ModerationLogger(configRepo);
this.helpMenuHandler = new HelpMenuHandler(registry, config);
this.registrar = registrar;
this.listenerAggregator = listenerAggregator;
this.muteScheduler = muteScheduler;
this.memberCounterListener = memberCounterListener;
this.healthHttpServer = healthHttpServer;
this.shutdownManager = shutdownManager;
}
public JDA start() {
logger.info("Инициализация бота...");
// Регистрируем команды через специализированные регистраторы
registerCommands();
registerSlashCommands();
// Создаём JDA
registrar.registerAll();
JDA jda = createJDA();
// Регистрируем slash команды в Discord
registerSlashCommandsInDiscord(jda);
// Настраиваем graceful shutdown
setupShutdownHook(jda);
try {
jda.awaitReady();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Бот прерван во время ожидания готовности", e);
}
jda.updateCommands().queue();
muteScheduler.restorePending(jda);
for (var guild : jda.getGuilds()) {
memberCounterListener.startScheduler(guild);
}
healthHttpServer.start();
registerShutdownHook(jda);
return jda;
}
private void registerCommands() {
PrefixCommandRegistrar registrar = new PrefixCommandRegistrar(
registry, config, dataStore, roleManager, cooldownManager,
levelSystem, levelService, newProfileService, metrics
);
registrar.registerAll();
}
private void registerSlashCommands() {
SlashCommandRegistrar registrar = new SlashCommandRegistrar(
slashRegistry, dataStore, metrics
);
registrar.registerAll();
}
private JDA createJDA() {
return JDABuilder.createDefault(config.getToken())
.setStatus(OnlineStatus.ONLINE)
@ -166,85 +74,12 @@ public class BotInitializer {
GatewayIntent.GUILD_MEMBERS,
GatewayIntent.GUILD_VOICE_STATES
)
.addEventListeners(
new ReadyListener(),
new MessageListener(dispatcher, dataStore, levelSystem),
new SlashCommandListener(slashRegistry, rateLimiter, metrics),
voiceTracker,
voiceChannelListener,
memberCounterListener,
voiceSetupCommand,
counterSetupCommand,
logSetupCommand,
voiceButtonHandler,
moderationLogger,
helpMenuHandler,
new LeaderboardButtonListener(levelSystem)
)
.addEventListeners(listenerAggregator.getAll())
.build();
}
private void registerSlashCommandsInDiscord(JDA jda) {
try {
jda.awaitReady();
slashRegistry.registerCommandsInDiscord(jda);
// Регистрируем setup команды
VoiceSetupCommand.register(jda);
CounterSetupCommand.register(jda);
LogSetupCommand.register(jda);
// Запускаем планировщик счетчика участников
if (!jda.getGuilds().isEmpty()) {
memberCounterListener.startScheduler(jda.getGuilds().get(0));
}
} catch (InterruptedException e) {
logger.error("Прервано ожидание готовности JDA", e);
Thread.currentThread().interrupt();
}
}
private void setupShutdownHook(JDA jda) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
logger.info("========================================");
logger.info("Получен сигнал завершения работы бота...");
logger.info("========================================");
shutdownComponents(jda);
logger.info("========================================");
logger.info("Бот успешно завершён!");
logger.info("========================================");
}, "ShutdownHook"));
}
private void shutdownComponents(JDA jda) {
try {
logger.info("Сохранение данных...");
dataStore.flush();
logger.info("Остановка менеджеров...");
cooldownManager.shutdown();
rateLimiter.shutdown();
voiceTracker.shutdown();
memberCounterListener.shutdown();
dispatcher.shutdown();
logger.info("Завершение JsonDataStore...");
dataStore.shutdown();
logger.info("Закрытие базы данных...");
database.close();
logger.info("Отключение JDA...");
jda.shutdown();
if (!jda.awaitShutdown(java.time.Duration.ofSeconds(10))) {
logger.warn("JDA не завершился за 10 секунд, принудительная остановка");
jda.shutdownNow();
}
} catch (Exception e) {
logger.error("Ошибка при завершении работы бота", e);
}
private void registerShutdownHook(JDA jda) {
Runtime.getRuntime().addShutdownHook(new Thread(
() -> shutdownManager.shutdown(jda), "ShutdownHook"));
}
}

View file

@ -1,130 +1,130 @@
package creator.loki.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedTemplates;
import creator.loki.exception.CommandExceptionHandler;
import creator.loki.metrics.MetricsCollector;
import creator.loki.core.security.RateLimiter;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class EventDispatcher {
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
private final BotConfig config;
private final CommandRegistry registry;
private final ExecutorService executorService;
private final RateLimiter rateLimiter;
private final MetricsCollector metrics;
public EventDispatcher(BotConfig config, CommandRegistry registry, RateLimiter rateLimiter, MetricsCollector metrics) {
this.config = config;
this.registry = registry;
this.executorService = Executors.newCachedThreadPool();
this.rateLimiter = rateLimiter;
this.metrics = metrics;
}
public void handleMessage(MessageReceivedEvent event) {
// Игнорируем ботов
if (event.getAuthor().isBot()) {
return;
}
String content = event.getMessage().getContentRaw();
if (!content.startsWith(config.getPrefix())) {
return;
}
// Парсим команду и аргументы
String[] parts = content.substring(config.getPrefix().length()).trim().split("\\s+");
if (parts.length == 0) {
return;
}
String commandName = parts[0].toLowerCase();
List<String> args = parts.length > 1
? Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length))
: List.of();
Command command = registry.getCommand(commandName);
if (command == null) {
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Команда не найдена",
"Команда `" + commandName + "` не существует. Используй `" + config.getPrefix() + "help`"
)
).queue();
return;
}
// Проверяем глобальный rate limit
String userId = event.getAuthor().getId();
String guildId = event.getGuild() != null ? event.getGuild().getId() : "DM";
if (!rateLimiter.allow(userId, guildId)) {
long waitTime = rateLimiter.getUserWaitTime(userId);
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"⏱️ Слишком быстро!",
"Ты отправляешь команды слишком часто. Подожди " + waitTime + " секунд."
)
).queue();
logger.warn("Rate limit для пользователя {} в гильдии {}", userId, guildId);
return;
}
// Выполняем команду асинхронно
executorService.submit(() -> {
long startTime = System.currentTimeMillis();
try {
CommandContext context = new CommandContext(
event.getMessage(),
args,
event.getChannel(),
event.getMember(),
event.getGuild()
);
command.execute(context);
// Записываем метрики
long executionTime = System.currentTimeMillis() - startTime;
metrics.recordCommand(commandName, executionTime);
logger.info("Выполнена команда: {} от {} за {} мс",
commandName, event.getAuthor().getName(), executionTime);
} catch (Exception e) {
// Записываем ошибку в метрики
metrics.recordError(commandName);
// Используем централизованный обработчик ошибок
CommandExceptionHandler.handle(e, event.getChannel(), commandName);
}
});
}
public void shutdown() {
logger.info("Завершение работы EventDispatcher...");
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
logger.warn("ExecutorService не завершился за 10 секунд, принудительная остановка");
executorService.shutdownNow();
}
} catch (InterruptedException e) {
logger.error("Прервано ожидание завершения ExecutorService", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("EventDispatcher завершён");
}
}
package creator.loki.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.embed.EmbedTemplates;
import creator.loki.core.exception.CommandExceptionHandler;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.core.security.RateLimiter;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class EventDispatcher {
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
private final BotConfig config;
private final CommandRegistry registry;
private final ExecutorService executorService;
private final RateLimiter rateLimiter;
private final MetricsCollector metrics;
public EventDispatcher(BotConfig config, CommandRegistry registry, RateLimiter rateLimiter, MetricsCollector metrics) {
this.config = config;
this.registry = registry;
this.executorService = Executors.newVirtualThreadPerTaskExecutor();
this.rateLimiter = rateLimiter;
this.metrics = metrics;
}
public void handleMessage(MessageReceivedEvent event) {
// Игнорируем ботов
if (event.getAuthor().isBot()) {
return;
}
String content = event.getMessage().getContentRaw();
if (!content.startsWith(config.getPrefix())) {
return;
}
// Парсим команду и аргументы
String[] parts = content.substring(config.getPrefix().length()).trim().split("\\s+");
if (parts.length == 0) {
return;
}
String commandName = parts[0].toLowerCase();
List<String> args = parts.length > 1
? Arrays.asList(Arrays.copyOfRange(parts, 1, parts.length))
: List.of();
Command command = registry.getCommand(commandName);
if (command == null) {
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"Команда не найдена",
"Команда `" + commandName + "` не существует. Используй `" + config.getPrefix() + "help`"
)
).queue();
return;
}
// Проверяем глобальный rate limit
String userId = event.getAuthor().getId();
String guildId = event.getGuild() != null ? event.getGuild().getId() : "DM";
if (!rateLimiter.allow(userId, guildId)) {
long waitTime = rateLimiter.getUserWaitTime(userId);
event.getChannel().sendMessageEmbeds(
EmbedTemplates.error(
"⏱️ Слишком быстро!",
"Ты отправляешь команды слишком часто. Подожди " + waitTime + " секунд."
)
).queue();
logger.warn("Rate limit для пользователя {} в гильдии {}", userId, guildId);
return;
}
// Выполняем команду асинхронно
executorService.submit(() -> {
long startTime = System.currentTimeMillis();
try {
CommandContext context = new CommandContext(
event.getMessage(),
args,
event.getChannel(),
event.getMember(),
event.getGuild()
);
command.execute(context);
// Записываем метрики
long executionTime = System.currentTimeMillis() - startTime;
metrics.recordCommand(commandName, executionTime);
logger.info("Выполнена команда: {} от {} за {} мс",
commandName, event.getAuthor().getName(), executionTime);
} catch (Exception e) {
// Записываем ошибку в метрики
metrics.recordError(commandName);
// Используем централизованный обработчик ошибок
CommandExceptionHandler.handle(e, event.getChannel(), commandName);
}
});
}
public void shutdown() {
logger.info("Завершение работы EventDispatcher...");
executorService.shutdown();
try {
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
logger.warn("ExecutorService не завершился за 10 секунд, принудительная остановка");
executorService.shutdownNow();
}
} catch (InterruptedException e) {
logger.error("Прервано ожидание завершения ExecutorService", e);
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("EventDispatcher завершён");
}
}

View file

@ -0,0 +1,54 @@
package creator.loki.core;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.feature.level.MessageListener;
import creator.loki.feature.moderation.ModerationLogger;
import creator.loki.bootstrap.ReadyListener;
import creator.loki.feature.help.HelpMenuHandler;
import creator.loki.feature.counter.MemberCounterListener;
import creator.loki.feature.leaderboard.LeaderboardButtonListener;
import creator.loki.feature.level.LevelUpNotifier;
import creator.loki.feature.level.ResetLevelsButtonHandler;
import creator.loki.feature.level.VoiceTracker;
import creator.loki.feature.voice.VoiceButtonHandler;
import creator.loki.feature.voice.VoiceChannelListener;
import creator.loki.feature.voice.VoiceModalHandler;
import creator.loki.feature.voice.VoiceSyncListener;
import creator.loki.feature.welcome.WelcomeListener;
@Singleton
public class ListenerAggregator {
private final Object[] listeners;
@Inject
public ListenerAggregator(
ReadyListener readyListener,
MessageListener messageListener,
VoiceTracker voiceTracker,
VoiceChannelListener voiceChannelListener,
MemberCounterListener memberCounterListener,
VoiceButtonHandler voiceButtonHandler,
VoiceModalHandler voiceModalHandler,
VoiceSyncListener voiceSyncListener,
WelcomeListener welcomeListener,
ModerationLogger moderationLogger,
HelpMenuHandler helpMenuHandler,
ResetLevelsButtonHandler resetLevelsButtonHandler,
LeaderboardButtonListener leaderboardButtonListener,
MessageCache messageCache
) {
this.listeners = new Object[]{
readyListener, messageListener, voiceTracker,
voiceChannelListener, memberCounterListener,
voiceButtonHandler, voiceModalHandler, voiceSyncListener,
welcomeListener, moderationLogger, helpMenuHandler,
resetLevelsButtonHandler, leaderboardButtonListener,
messageCache
};
}
public Object[] getAll() {
return listeners;
}
}

View file

@ -0,0 +1,32 @@
package creator.loki.core;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import java.util.concurrent.TimeUnit;
public class MessageCache extends ListenerAdapter {
private final Cache<String, String> cache;
public MessageCache() {
this.cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
}
@Override
public void onMessageReceived(MessageReceivedEvent event) {
if (event.getAuthor().isBot()) return;
String content = event.getMessage().getContentRaw();
if (!content.isBlank()) {
cache.put(event.getMessageId(), content);
}
}
public String getContent(String messageId) {
return cache.getIfPresent(messageId);
}
}

View file

@ -0,0 +1,85 @@
package creator.loki.core;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import creator.loki.database.Database;
import creator.loki.feature.counter.MemberCounterListener;
import creator.loki.feature.level.VoiceTracker;
import creator.loki.feature.moderation.MuteScheduler;
import creator.loki.feature.metrics.HealthHttpServer;
import creator.loki.core.security.CooldownManager;
import creator.loki.core.security.RateLimiter;
import net.dv8tion.jda.api.JDA;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
@Singleton
public class ShutdownManager {
private static final Logger logger = LoggerFactory.getLogger(ShutdownManager.class);
private final HealthHttpServer healthHttpServer;
private final CooldownManager cooldownManager;
private final RateLimiter rateLimiter;
private final VoiceTracker voiceTracker;
private final MemberCounterListener memberCounterListener;
private final MuteScheduler muteScheduler;
private final EventDispatcher dispatcher;
private final Database database;
@Inject
public ShutdownManager(
HealthHttpServer healthHttpServer,
CooldownManager cooldownManager,
RateLimiter rateLimiter,
VoiceTracker voiceTracker,
MemberCounterListener memberCounterListener,
MuteScheduler muteScheduler,
EventDispatcher dispatcher,
Database database
) {
this.healthHttpServer = healthHttpServer;
this.cooldownManager = cooldownManager;
this.rateLimiter = rateLimiter;
this.voiceTracker = voiceTracker;
this.memberCounterListener = memberCounterListener;
this.muteScheduler = muteScheduler;
this.dispatcher = dispatcher;
this.database = database;
}
public void shutdown(JDA jda) {
logger.info("========================================");
logger.info("Получен сигнал завершения работы бота...");
logger.info("========================================");
try {
healthHttpServer.stop();
logger.info("Остановка менеджеров...");
cooldownManager.shutdown();
rateLimiter.shutdown();
voiceTracker.shutdown();
memberCounterListener.shutdown();
muteScheduler.shutdown();
dispatcher.shutdown();
logger.info("Закрытие базы данных...");
database.close();
logger.info("Отключение JDA...");
jda.shutdown();
if (!jda.awaitShutdown(Duration.ofSeconds(10))) {
logger.warn("JDA не завершился за 10 секунд, принудительная остановка");
jda.shutdownNow();
}
} catch (Exception e) {
logger.error("Ошибка при завершении работы бота", e);
}
logger.info("========================================");
logger.info("Бот успешно завершён!");
logger.info("========================================");
}
}

View file

@ -1,22 +1,23 @@
package creator.loki.exception;
/**
* Исключение для ошибок выполнения команд
*/
public class CommandException extends Exception {
private final String userMessage;
public CommandException(String userMessage) {
super(userMessage);
this.userMessage = userMessage;
}
public CommandException(String userMessage, Throwable cause) {
super(userMessage, cause);
this.userMessage = userMessage;
}
public String getUserMessage() {
return userMessage;
}
}
package creator.loki.core.exception;
/**
* Исключение для ошибок выполнения команд
*/
public class CommandException extends Exception {
private static final long serialVersionUID = 1L;
private final String userMessage;
public CommandException(String userMessage) {
super(userMessage);
this.userMessage = userMessage;
}
public CommandException(String userMessage, Throwable cause) {
super(userMessage, cause);
this.userMessage = userMessage;
}
public String getUserMessage() {
return userMessage;
}
}

View file

@ -1,63 +1,63 @@
package creator.loki.exception;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
/**
* Обработчик исключений для команд
*/
public class CommandExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CommandExceptionHandler.class);
/**
* Обрабатывает исключение и отправляет сообщение пользователю
*/
public static void handle(Throwable throwable, MessageChannel channel, String commandName) {
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
String userMessage;
if (throwable instanceof CommandException) {
userMessage = ((CommandException) throwable).getUserMessage();
} else {
userMessage = "Произошла непредвиденная ошибка при выполнении команды.";
}
MessageEmbed errorEmbed = EmbedFactory.create()
.setTitle("❌ Ошибка")
.setDescription(userMessage)
.setColor(Color.RED)
.addField("Команда", commandName, true)
.setFooter("Если ошибка повторяется, обратитесь к администратору")
.build();
channel.sendMessageEmbeds(errorEmbed).queue(
success -> {},
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
);
}
/**
* Обрабатывает исключение с кастомным сообщением
*/
public static void handle(Throwable throwable, MessageChannel channel, String commandName, String customMessage) {
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
MessageEmbed errorEmbed = EmbedFactory.create()
.setTitle("❌ Ошибка")
.setDescription(customMessage)
.setColor(Color.RED)
.addField("Команда", commandName, true)
.setFooter("Если ошибка повторяется, обратитесь к администратору")
.build();
channel.sendMessageEmbeds(errorEmbed).queue(
success -> {},
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
);
}
}
package creator.loki.core.exception;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.MessageEmbed;
import net.dv8tion.jda.api.entities.channel.middleman.MessageChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
/**
* Обработчик исключений для команд
*/
public class CommandExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(CommandExceptionHandler.class);
/**
* Обрабатывает исключение и отправляет сообщение пользователю
*/
public static void handle(Throwable throwable, MessageChannel channel, String commandName) {
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
String userMessage;
if (throwable instanceof CommandException ce) {
userMessage = ce.getUserMessage();
} else {
userMessage = "Не удалось выполнить команду. Попробуйте позже или обратитесь к администратору.";
}
MessageEmbed errorEmbed = EmbedFactory.create()
.withTitle("❌ Ошибка")
.withDescription(userMessage)
.withColor(Color.RED)
.addField("Команда", commandName, true)
.withFooter("Если ошибка повторяется, обратитесь к администратору")
.build();
channel.sendMessageEmbeds(errorEmbed).queue(
success -> {},
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
);
}
/**
* Обрабатывает исключение с кастомным сообщением
*/
public static void handle(Throwable throwable, MessageChannel channel, String commandName, String customMessage) {
logger.error("Ошибка при выполнении команды: {}", commandName, throwable);
MessageEmbed errorEmbed = EmbedFactory.create()
.withTitle("❌ Ошибка")
.withDescription(customMessage)
.withColor(Color.RED)
.addField("Команда", commandName, true)
.withFooter("Если ошибка повторяется, обратитесь к администратору")
.build();
channel.sendMessageEmbeds(errorEmbed).queue(
success -> {},
error -> logger.error("Не удалось отправить сообщение об ошибке", error)
);
}
}

View file

@ -0,0 +1,6 @@
package creator.loki.core.registry;
@FunctionalInterface
public interface CommandRegistrar {
void register(CommandRegistry registry);
}

View file

@ -1,32 +1,38 @@
package creator.loki.core.registry;
import creator.loki.command.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class CommandRegistry {
private static final Logger logger = LoggerFactory.getLogger(CommandRegistry.class);
private final Map<String, Command> commands = new HashMap<>();
public void register(Command command) {
commands.put(command.getName().toLowerCase(), command);
logger.debug("Зарегистрирована команда: {}", command.getName());
}
public Command getCommand(String name) {
return commands.get(name.toLowerCase());
}
public Collection<Command> getAllCommands() {
return Collections.unmodifiableCollection(commands.values());
}
public int getCommandCount() {
return commands.size();
}
}
package creator.loki.core.registry;
import creator.loki.command.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class CommandRegistry {
private static final Logger logger = LoggerFactory.getLogger(CommandRegistry.class);
private final Map<String, Command> commands = new HashMap<>();
public void register(Command command) {
String key = command.getName().toLowerCase();
Command previous = commands.put(key, command);
if (previous != null) {
logger.warn("Имя команды «{}» уже занято: {} заменена на {}",
key, previous.getClass().getSimpleName(), command.getClass().getSimpleName());
} else {
logger.debug("Зарегистрирована команда: {}", command.getName());
}
}
public Command getCommand(String name) {
return commands.get(name.toLowerCase());
}
public Collection<Command> getAllCommands() {
return Collections.unmodifiableCollection(commands.values());
}
public int getCommandCount() {
return commands.size();
}
}

View file

@ -1,156 +1,29 @@
package creator.loki.core.registry;
import creator.loki.command.impl.entertainment.games.*;
import creator.loki.command.impl.entertainment.games.random.CoinCommand;
import creator.loki.command.impl.entertainment.games.random.DiceCommand;
import creator.loki.command.impl.entertainment.games.random.GlazbobaCommand;
import creator.loki.command.impl.entertainment.games.random.MemeCommand;
import creator.loki.command.impl.entertainment.games.random.RateCommand;
import creator.loki.command.impl.entertainment.games.rating.BoykisserCommand;
import creator.loki.command.impl.entertainment.games.rating.FatCommand;
import creator.loki.command.impl.entertainment.games.rating.GayCommand;
import creator.loki.command.impl.entertainment.games.rating.IqCommand;
import creator.loki.command.impl.entertainment.games.rating.MustacheCommand;
import creator.loki.command.impl.entertainment.games.rating.PpCommand;
import creator.loki.command.impl.entertainment.interaction.*;
import creator.loki.command.impl.entertainment.interaction.negative.PokeCommand;
import creator.loki.command.impl.entertainment.interaction.negative.SlapCommand;
import creator.loki.command.impl.entertainment.interaction.negative.TickleCommand;
import creator.loki.command.impl.entertainment.interaction.positive.CuddleCommand;
import creator.loki.command.impl.entertainment.interaction.positive.FeedCommand;
import creator.loki.command.impl.entertainment.interaction.positive.HugCommand;
import creator.loki.command.impl.entertainment.interaction.positive.KissCommand;
import creator.loki.command.impl.entertainment.interaction.positive.PatCommand;
import creator.loki.command.impl.moderation.*;
import creator.loki.feature.role.*;
import creator.loki.command.impl.stats.*;
import creator.loki.command.impl.utility.*;
import creator.loki.command.impl.utility.info.*;
import creator.loki.command.impl.utility.system.*;
import creator.loki.config.BotConfig;
import creator.loki.data.JsonDataStore;
import creator.loki.feature.level.service.LevelService;
import creator.loki.feature.role.CreateRoleCommand;
import creator.loki.feature.role.DeleteRoleCommand;
import creator.loki.feature.role.EditRoleCommand;
import creator.loki.feature.role.MyRolesCommand;
import creator.loki.level.LevelSystem;
import creator.loki.metrics.MetricsCollector;
import creator.loki.profile.ProfileService;
import creator.loki.feature.role.CustomRoleManager;
import creator.loki.core.security.CooldownManager;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Регистратор префиксных команд (!) - Single Responsibility
*/
import java.util.Set;
@Singleton
public class PrefixCommandRegistrar {
private static final Logger logger = LoggerFactory.getLogger(PrefixCommandRegistrar.class);
private final CommandRegistry registry;
private final BotConfig config;
private final JsonDataStore dataStore;
private final CustomRoleManager roleManager;
private final CooldownManager cooldownManager;
private final LevelSystem levelSystem;
private final LevelService levelService;
private final creator.loki.feature.profile.ProfileService newProfileService;
private final MetricsCollector metrics;
private final Set<CommandRegistrar> registrars;
public PrefixCommandRegistrar(
CommandRegistry registry,
BotConfig config,
JsonDataStore dataStore,
CustomRoleManager roleManager,
CooldownManager cooldownManager,
LevelSystem levelSystem,
LevelService levelService,
creator.loki.feature.profile.ProfileService newProfileService,
MetricsCollector metrics
) {
@Inject
public PrefixCommandRegistrar(CommandRegistry registry, Set<CommandRegistrar> registrars) {
this.registry = registry;
this.config = config;
this.dataStore = dataStore;
this.roleManager = roleManager;
this.cooldownManager = cooldownManager;
this.levelSystem = levelSystem;
this.levelService = levelService;
this.newProfileService = newProfileService;
this.metrics = metrics;
this.registrars = registrars;
}
public void registerAll() {
registerUtilityCommands();
registerRoleCommands();
registerInteractionCommands();
registerGameCommands();
registerStatsCommands();
registerModerationCommands();
for (CommandRegistrar registrar : registrars) {
registrar.register(registry);
}
logger.info("Зарегистрировано префиксных команд: {}", registry.getCommandCount());
}
private void registerUtilityCommands() {
registry.register(new PingCommand());
registry.register(new HelpCommand(registry, config));
registry.register(new InfoCommand());
registry.register(new ClearCommand());
registry.register(new ProfileCommand(dataStore));
registry.register(new creator.loki.feature.profile.ProfileCommand(newProfileService));
registry.register(new MetricsCommand(metrics));
registry.register(new AvatarCommand());
registry.register(new PollCommand());
registry.register(new SwapCommand());
}
private void registerRoleCommands() {
registry.register(new CreateRoleCommand(roleManager));
registry.register(new DeleteRoleCommand(roleManager));
registry.register(new MyRolesCommand(roleManager));
registry.register(new EditRoleCommand(roleManager));
}
private void registerInteractionCommands() {
registry.register(new PatCommand(cooldownManager));
registry.register(new HugCommand());
registry.register(new KissCommand(cooldownManager));
registry.register(new SlapCommand(cooldownManager));
registry.register(new TickleCommand(cooldownManager));
registry.register(new PokeCommand(cooldownManager));
registry.register(new CuddleCommand(cooldownManager));
registry.register(new FeedCommand(cooldownManager));
}
private void registerGameCommands() {
registry.register(new EightBallCommand());
registry.register(new CoinCommand());
registry.register(new DiceCommand());
registry.register(new MemeCommand());
registry.register(new ShipCommand(dataStore));
registry.register(new RoastCommand());
registry.register(new ComplimentCommand());
registry.register(new GlazbobaCommand());
registry.register(new IqCommand(cooldownManager));
registry.register(new GayCommand(cooldownManager));
registry.register(new PpCommand(cooldownManager));
registry.register(new RateCommand(cooldownManager));
registry.register(new BoykisserCommand(cooldownManager));
registry.register(new MustacheCommand(cooldownManager));
registry.register(new FatCommand(cooldownManager));
}
private void registerStatsCommands() {
registry.register(new creator.loki.feature.level.RankCommand(levelService));
registry.register(new creator.loki.command.impl.stats.leaderboard.NewLeaderboardCommand(levelSystem));
registry.register(new RepCommand(dataStore, cooldownManager));
registry.register(new MinusRepCommand(dataStore, cooldownManager));
registry.register(new RepTopCommand(dataStore));
registry.register(new creator.loki.feature.level.StatsCommand());
}
private void registerModerationCommands() {
registry.register(new BanCommand());
registry.register(new KickCommand());
}
}

View file

@ -1,34 +0,0 @@
package creator.loki.core.registry;
import creator.loki.data.JsonDataStore;
import creator.loki.metrics.MetricsCollector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Регистратор slash команд (/) - Single Responsibility
*/
public class SlashCommandRegistrar {
private static final Logger logger = LoggerFactory.getLogger(SlashCommandRegistrar.class);
private final SlashCommandRegistry registry;
private final JsonDataStore dataStore;
private final MetricsCollector metrics;
public SlashCommandRegistrar(
SlashCommandRegistry registry,
JsonDataStore dataStore,
MetricsCollector metrics
) {
this.registry = registry;
this.dataStore = dataStore;
this.metrics = metrics;
}
public void registerAll() {
// Все slash команды удалены согласно рефакторингу
// Используются только prefix команды (!)
logger.info("Зарегистрировано slash команд: {}", registry.getCommandCount());
}
}

View file

@ -1,67 +0,0 @@
package creator.loki.core.registry;
import creator.loki.command.SlashCommand;
import net.dv8tion.jda.api.JDA;
import net.dv8tion.jda.api.interactions.commands.build.CommandData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Реестр для slash команд
*/
public class SlashCommandRegistry {
private static final Logger logger = LoggerFactory.getLogger(SlashCommandRegistry.class);
private final Map<String, SlashCommand> commands = new HashMap<>();
/**
* Регистрирует slash команду
*/
public void register(SlashCommand command) {
commands.put(command.getName().toLowerCase(), command);
logger.debug("Зарегистрирована slash команда: {}", command.getName());
}
/**
* Получает slash команду по имени
*/
public SlashCommand getCommand(String name) {
return commands.get(name.toLowerCase());
}
/**
* Возвращает все slash команды
*/
public Collection<SlashCommand> getAllCommands() {
return Collections.unmodifiableCollection(commands.values());
}
/**
* Возвращает количество slash команд
*/
public int getCommandCount() {
return commands.size();
}
/**
* Регистрирует все slash команды в Discord
*/
public void registerCommandsInDiscord(JDA jda) {
List<CommandData> commandDataList = new ArrayList<>();
for (SlashCommand command : commands.values()) {
commandDataList.add(command.getCommandData());
}
jda.updateCommands().addCommands(commandDataList).queue(
success -> logger.info("Зарегистрировано {} slash команд в Discord", commandDataList.size()),
error -> logger.error("Ошибка при регистрации slash команд", error)
);
}
}

View file

@ -1,83 +1,83 @@
package creator.loki.core.security;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CooldownManager {
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
public CooldownManager() {
// Очистка истёкших cooldown'ов каждые 5 минут
cleanupExecutor.scheduleAtFixedRate(this::cleanup, 5, 5, TimeUnit.MINUTES);
}
/**
* Проверяет, находится ли пользователь на cooldown для команды
*/
public boolean isOnCooldown(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
if (cooldownEnd == null) {
return false;
}
if (Instant.now().isAfter(cooldownEnd)) {
cooldowns.remove(key);
return false;
}
return true;
}
/**
* Возвращает оставшееся время cooldown в секундах
*/
public long getRemainingSeconds(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
if (cooldownEnd == null) {
return 0;
}
long remaining = cooldownEnd.getEpochSecond() - Instant.now().getEpochSecond();
return Math.max(0, remaining);
}
/**
* Применяет cooldown для пользователя на команду
*/
public void applyCooldown(String userId, String commandName, int seconds) {
String key = userId + ":" + commandName;
Instant cooldownEnd = Instant.now().plusSeconds(seconds);
cooldowns.put(key, cooldownEnd);
}
/**
* Очищает истёкшие cooldown'ы
*/
public void cleanup() {
Instant now = Instant.now();
cooldowns.entrySet().removeIf(entry -> now.isAfter(entry.getValue()));
}
/**
* Завершает работу менеджера
*/
public void shutdown() {
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
package creator.loki.core.security;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class CooldownManager {
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
@SuppressWarnings("this-escape")
public CooldownManager() {
cleanupExecutor.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
}
/**
* Проверяет, находится ли пользователь на cooldown для команды
*/
public boolean isOnCooldown(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
if (cooldownEnd == null) {
return false;
}
if (Instant.now().isAfter(cooldownEnd)) {
cooldowns.remove(key);
return false;
}
return true;
}
/**
* Возвращает оставшееся время cooldown в секундах
*/
public long getRemainingSeconds(String userId, String commandName) {
String key = userId + ":" + commandName;
Instant cooldownEnd = cooldowns.get(key);
if (cooldownEnd == null) {
return 0;
}
long remaining = cooldownEnd.getEpochSecond() - Instant.now().getEpochSecond();
return Math.max(0, remaining);
}
/**
* Применяет cooldown для пользователя на команду
*/
public void applyCooldown(String userId, String commandName, int seconds) {
String key = userId + ":" + commandName;
Instant cooldownEnd = Instant.now().plusSeconds(seconds);
cooldowns.put(key, cooldownEnd);
}
/**
* Очищает истёкшие cooldown'ы
*/
public void cleanup() {
Instant now = Instant.now();
cooldowns.entrySet().removeIf(entry -> now.isAfter(entry.getValue()));
}
/**
* Завершает работу менеджера
*/
public void shutdown() {
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

View file

@ -1,157 +1,163 @@
package creator.loki.core.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Rate limiter для защиты от спама
* Использует алгоритм Token Bucket
*/
public class RateLimiter {
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
// Лимиты по умолчанию
private static final int DEFAULT_USER_CAPACITY = 10; // Максимум токенов на пользователя
private static final int DEFAULT_USER_REFILL_RATE = 2; // Токенов в секунду
private static final int DEFAULT_GUILD_CAPACITY = 50; // Максимум токенов на гильдию
private static final int DEFAULT_GUILD_REFILL_RATE = 10; // Токенов в секунду
private final ConcurrentHashMap<String, TokenBucket> userBuckets = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TokenBucket> guildBuckets = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
public RateLimiter() {
// Очистка старых bucket'ов каждые 5 минут
cleanupExecutor.scheduleAtFixedRate(this::cleanup, 5, 5, TimeUnit.MINUTES);
logger.info("RateLimiter инициализирован");
}
/**
* Проверяет, может ли пользователь выполнить действие
*/
public boolean allowUser(String userId) {
TokenBucket bucket = userBuckets.computeIfAbsent(
userId,
k -> new TokenBucket(DEFAULT_USER_CAPACITY, DEFAULT_USER_REFILL_RATE)
);
return bucket.tryConsume();
}
/**
* Проверяет, может ли гильдия выполнить действие
*/
public boolean allowGuild(String guildId) {
TokenBucket bucket = guildBuckets.computeIfAbsent(
guildId,
k -> new TokenBucket(DEFAULT_GUILD_CAPACITY, DEFAULT_GUILD_REFILL_RATE)
);
return bucket.tryConsume();
}
/**
* Проверяет оба лимита: пользователя и гильдии
*/
public boolean allow(String userId, String guildId) {
return allowUser(userId) && allowGuild(guildId);
}
/**
* Возвращает оставшееся время до восстановления для пользователя (в секундах)
*/
public long getUserWaitTime(String userId) {
TokenBucket bucket = userBuckets.get(userId);
if (bucket == null) {
return 0;
}
return bucket.getWaitTime();
}
/**
* Очищает неактивные bucket'ы
*/
private void cleanup() {
long now = System.currentTimeMillis();
long threshold = now - TimeUnit.MINUTES.toMillis(10);
userBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
guildBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
logger.debug("Очистка rate limiter: осталось {} user buckets, {} guild buckets",
userBuckets.size(), guildBuckets.size());
}
/**
* Завершает работу rate limiter
*/
public void shutdown() {
logger.info("Завершение работы RateLimiter...");
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("RateLimiter завершён");
}
/**
* Token Bucket для rate limiting
*/
private static class TokenBucket {
private final int capacity;
private final double refillRate; // токенов в секунду
private double tokens;
private long lastRefillTime;
private long lastAccessTime;
public TokenBucket(int capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
this.lastAccessTime = System.currentTimeMillis();
}
public synchronized boolean tryConsume() {
refill();
lastAccessTime = System.currentTimeMillis();
if (tokens >= 1.0) {
tokens -= 1.0;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long timePassed = now - lastRefillTime;
double tokensToAdd = (timePassed / 1000.0) * refillRate;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
public synchronized long getWaitTime() {
refill();
if (tokens >= 1.0) {
return 0;
}
double tokensNeeded = 1.0 - tokens;
return (long) Math.ceil(tokensNeeded / refillRate);
}
public long getLastAccessTime() {
return lastAccessTime;
}
}
}
package creator.loki.core.security;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Rate limiter для защиты от спама
* Использует алгоритм Token Bucket
*/
public class RateLimiter {
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
// Лимиты по умолчанию
private static final int DEFAULT_USER_CAPACITY = 10; // Максимум токенов на пользователя
private static final int DEFAULT_USER_REFILL_RATE = 2; // Токенов в секунду
private static final int DEFAULT_GUILD_CAPACITY = 50; // Максимум токенов на гильдию
private static final int DEFAULT_GUILD_REFILL_RATE = 10; // Токенов в секунду
private final ConcurrentHashMap<String, TokenBucket> userBuckets = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, TokenBucket> guildBuckets = new ConcurrentHashMap<>();
private final ScheduledExecutorService cleanupExecutor = Executors.newSingleThreadScheduledExecutor();
public RateLimiter() {
// Очистка старых bucket'ов каждые 5 минут
cleanupExecutor.scheduleAtFixedRate(() -> Thread.startVirtualThread(this::cleanup), 5, 5, TimeUnit.MINUTES);
logger.info("RateLimiter инициализирован");
}
/**
* Проверяет, может ли пользователь выполнить действие
*/
public boolean allowUser(String userId) {
TokenBucket bucket = userBuckets.computeIfAbsent(
userId,
k -> new TokenBucket(DEFAULT_USER_CAPACITY, DEFAULT_USER_REFILL_RATE)
);
return bucket.tryConsume();
}
/**
* Проверяет, может ли гильдия выполнить действие
*/
public boolean allowGuild(String guildId) {
TokenBucket bucket = guildBuckets.computeIfAbsent(
guildId,
k -> new TokenBucket(DEFAULT_GUILD_CAPACITY, DEFAULT_GUILD_REFILL_RATE)
);
return bucket.tryConsume();
}
/**
* Проверяет оба лимита: пользователя и гильдии
*/
public boolean allow(String userId, String guildId) {
return allowUser(userId) && allowGuild(guildId);
}
/**
* Возвращает оставшееся время до восстановления для пользователя (в секундах)
*/
public long getUserWaitTime(String userId) {
TokenBucket bucket = userBuckets.get(userId);
if (bucket == null) {
return 0;
}
return bucket.getWaitTime();
}
/**
* Очищает неактивные bucket'ы
*/
private void cleanup() {
long now = System.currentTimeMillis();
long threshold = now - TimeUnit.MINUTES.toMillis(10);
int usersBefore = userBuckets.size();
int guildsBefore = guildBuckets.size();
userBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
guildBuckets.entrySet().removeIf(entry -> entry.getValue().getLastAccessTime() < threshold);
int removedUsers = usersBefore - userBuckets.size();
int removedGuilds = guildsBefore - guildBuckets.size();
if (removedUsers > 0 || removedGuilds > 0) {
logger.debug("Очистка rate limiter: удалено {} user / {} guild buckets, осталось {} / {}",
removedUsers, removedGuilds, userBuckets.size(), guildBuckets.size());
}
}
/**
* Завершает работу rate limiter
*/
public void shutdown() {
logger.info("Завершение работы RateLimiter...");
cleanupExecutor.shutdown();
try {
if (!cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
cleanupExecutor.shutdownNow();
}
} catch (InterruptedException e) {
cleanupExecutor.shutdownNow();
Thread.currentThread().interrupt();
}
logger.info("RateLimiter завершён");
}
/**
* Token Bucket для rate limiting
*/
private static class TokenBucket {
private final int capacity;
private final double refillRate; // токенов в секунду
private double tokens;
private long lastRefillTime;
private long lastAccessTime;
public TokenBucket(int capacity, double refillRate) {
this.capacity = capacity;
this.refillRate = refillRate;
this.tokens = capacity;
this.lastRefillTime = System.currentTimeMillis();
this.lastAccessTime = System.currentTimeMillis();
}
public synchronized boolean tryConsume() {
refill();
lastAccessTime = System.currentTimeMillis();
if (tokens >= 1.0) {
tokens -= 1.0;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long timePassed = now - lastRefillTime;
double tokensToAdd = (timePassed / 1000.0) * refillRate;
tokens = Math.min(capacity, tokens + tokensToAdd);
lastRefillTime = now;
}
public synchronized long getWaitTime() {
refill();
if (tokens >= 1.0) {
return 0;
}
double tokensNeeded = 1.0 - tokens;
return (long) Math.ceil(tokensNeeded / refillRate);
}
public long getLastAccessTime() {
return lastAccessTime;
}
}
}

View file

@ -1,62 +1,55 @@
package creator.loki.database;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
/**
* Менеджер SQLite базы данных
*/
public class Database {
private static final Logger logger = LoggerFactory.getLogger(Database.class);
private final String dbPath;
private Connection connection;
private HikariDataSource dataSource;
public Database(String dbPath) {
this.dbPath = dbPath;
}
/**
* Подключиться к базе данных
*/
public void connect() {
try {
String url = "jdbc:sqlite:" + dbPath;
connection = DriverManager.getConnection(url);
logger.info("Подключение к базе данных: {}", dbPath);
// Включаем foreign keys
try (Statement stmt = connection.createStatement()) {
stmt.execute("PRAGMA foreign_keys = ON");
}
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:sqlite:" + dbPath);
config.setMaximumPoolSize(5);
config.setConnectionTimeout(10000);
config.setIdleTimeout(300000);
config.setMaxLifetime(600000);
config.setPoolName("LokiDB");
dataSource = new HikariDataSource(config);
logger.info("Подключение к базе данных: {} (pool size: {})", dbPath, config.getMaximumPoolSize());
try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement()) {
stmt.execute("PRAGMA foreign_keys = ON");
} catch (SQLException e) {
logger.error("Ошибка подключения к базе данных", e);
logger.error("Ошибка включения foreign keys", e);
}
}
public Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
logger.error("Ошибка получения соединения из пула", e);
throw new RuntimeException(e);
}
}
/**
* Получить соединение
*/
public Connection getConnection() {
try {
if (connection == null || connection.isClosed()) {
connect();
}
return connection;
} catch (SQLException e) {
logger.error("Ошибка проверки соединения", e);
connect();
return connection;
}
}
/**
* Выполнить SQL запрос
*/
public void execute(String sql) {
try (Statement stmt = getConnection().createStatement()) {
try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) {
stmt.execute(sql);
} catch (SQLException e) {
logger.error("Ошибка выполнения SQL: {}", sql, e);
@ -64,29 +57,34 @@ public class Database {
}
}
/**
* Выполнить SQL запрос с параметрами
*/
public PreparedStatement prepare(String sql) {
try {
return getConnection().prepareStatement(sql);
Connection conn = getConnection();
PreparedStatement stmt = conn.prepareStatement(sql);
return (PreparedStatement) Proxy.newProxyInstance(
PreparedStatement.class.getClassLoader(),
new Class<?>[]{PreparedStatement.class},
(proxy, method, args) -> {
if (method.getName().equals("close")) {
try {
return method.invoke(stmt, args);
} finally {
conn.close();
}
}
return method.invoke(stmt, args);
}
);
} catch (SQLException e) {
logger.error("Ошибка подготовки SQL: {}", sql, e);
throw new RuntimeException(e);
}
}
/**
* Закрыть соединение
*/
public void close() {
try {
if (connection != null && !connection.isClosed()) {
connection.close();
logger.info("Соединение с базой данных закрыто");
}
} catch (SQLException e) {
logger.error("Ошибка закрытия соединения", e);
if (dataSource != null && !dataSource.isClosed()) {
dataSource.close();
logger.info("Пул соединений закрыт");
}
}
}

View file

@ -1,191 +1,190 @@
package creator.loki.database;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
/**
* Миграция данных из JSON в SQLite
*/
public class DatabaseMigration {
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
private final Database database;
private final Gson gson = new Gson();
public DatabaseMigration(Database database) {
this.database = database;
}
/**
* Мигрировать данные из JSON
*/
public void migrateFromJson(String jsonPath) {
logger.info("Начало миграции данных из JSON: {}", jsonPath);
try (FileReader reader = new FileReader(jsonPath)) {
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> data = gson.fromJson(reader, type);
if (data == null) {
logger.warn("JSON файл пустой или не существует");
return;
}
if (isMigrated()) {
logger.info("Данные уже мигрированы, пропускаем");
return;
}
migrateUserStats(data);
migrateReputation(data);
migrateShipPairs(data);
migrateCustomRoles(data);
markMigrated();
logger.info("Миграция завершена успешно");
} catch (Exception e) {
logger.error("Ошибка миграции данных", e);
}
}
private boolean isMigrated() {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = 'migrated'")) {
ResultSet rs = stmt.executeQuery();
return rs.next() && "true".equals(rs.getString("value"));
} catch (SQLException e) {
return false;
}
}
private void markMigrated() {
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')"
)) {
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка сохранения статуса миграции", e);
}
}
@SuppressWarnings("unchecked")
private void migrateUserStats(Map<String, Object> data) {
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
if (userStats == null) return;
logger.info("Миграция userStats: {} записей", userStats.size());
String sql = """
INSERT OR REPLACE INTO user_stats
(user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
try (PreparedStatement stmt = database.prepare(sql)) {
for (Map.Entry<String, Object> entry : userStats.entrySet()) {
Map<String, Object> stats = (Map<String, Object>) entry.getValue();
stmt.setString(1, (String) stats.get("userId"));
stmt.setInt(2, ((Number) stats.get("level")).intValue());
stmt.setInt(3, ((Number) stats.get("totalXP")).intValue());
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue());
stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue());
stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue());
stmt.executeUpdate();
}
logger.info("userStats мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции userStats", e);
}
}
@SuppressWarnings("unchecked")
private void migrateReputation(Map<String, Object> data) {
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
if (reputation == null) return;
logger.info("Миграция reputation: {} записей", reputation.size());
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)"
)) {
for (Map.Entry<String, Object> entry : reputation.entrySet()) {
stmt.setString(1, entry.getKey());
stmt.setDouble(2, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
logger.info("reputation мигрирована");
} catch (SQLException e) {
logger.error("Ошибка миграции reputation", e);
}
}
@SuppressWarnings("unchecked")
private void migrateShipPairs(Map<String, Object> data) {
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
if (shipPairs == null) return;
logger.info("Миграция shipPairs: {} записей", shipPairs.size());
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> entry : shipPairs.entrySet()) {
String[] users = entry.getKey().split("-");
if (users.length == 2) {
stmt.setString(1, users[0]);
stmt.setString(2, users[1]);
stmt.setDouble(3, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
}
logger.info("shipPairs мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции shipPairs", e);
}
}
@SuppressWarnings("unchecked")
private void migrateCustomRoles(Map<String, Object> data) {
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
if (customRoles == null) return;
logger.info("Миграция custom_roles");
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> guildEntry : customRoles.entrySet()) {
String guildId = guildEntry.getKey();
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
for (Map.Entry<String, Object> userEntry : users.entrySet()) {
String userId = userEntry.getKey();
Object rolesObj = userEntry.getValue();
if (rolesObj instanceof java.util.List) {
for (Object roleId : (java.util.List<?>) rolesObj) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setString(3, roleId.toString());
stmt.executeUpdate();
}
}
}
}
logger.info("custom_roles мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции custom_roles", e);
}
}
}
package creator.loki.database;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileReader;
import java.lang.reflect.Type;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
/**
* Миграция данных из JSON в SQLite
*/
public class DatabaseMigration {
private static final Logger logger = LoggerFactory.getLogger(DatabaseMigration.class);
private final Database database;
private final Gson gson = new Gson();
public DatabaseMigration(Database database) {
this.database = database;
}
/**
* Мигрировать данные из JSON
*/
public void migrateFromJson(String jsonPath) {
logger.info("Начало миграции данных из JSON: {}", jsonPath);
try (FileReader reader = new FileReader(jsonPath)) {
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> data = gson.fromJson(reader, type);
if (data == null) {
logger.warn("JSON файл пустой или не существует");
return;
}
if (isMigrated()) {
logger.info("Данные уже мигрированы, пропускаем");
return;
}
migrateUserStats(data);
migrateReputation(data);
migrateShipPairs(data);
migrateCustomRoles(data);
markMigrated();
logger.info("Миграция завершена успешно");
} catch (Exception e) {
logger.error("Ошибка миграции данных", e);
}
}
private boolean isMigrated() {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = 'migrated'")) {
ResultSet rs = stmt.executeQuery();
return rs.next() && "true".equals(rs.getString("value"));
} catch (SQLException e) {
return false;
}
}
private void markMigrated() {
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO config (key, value) VALUES ('migrated', 'true')"
)) {
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка сохранения статуса миграции", e);
}
}
@SuppressWarnings("unchecked")
private void migrateUserStats(Map<String, Object> data) {
Map<String, Object> userStats = (Map<String, Object>) data.get("userStats");
if (userStats == null) return;
logger.info("Миграция userStats: {} записей", userStats.size());
String sql = """
INSERT OR REPLACE INTO user_stats
(user_id, level, total_xp, message_count, voice_minutes, last_message_time, last_voice_join)
VALUES (?, ?, ?, ?, ?, ?, ?)""";
try (PreparedStatement stmt = database.prepare(sql)) {
for (Map.Entry<String, Object> entry : userStats.entrySet()) {
Map<String, Object> stats = (Map<String, Object>) entry.getValue();
stmt.setString(1, (String) stats.get("userId"));
stmt.setInt(2, ((Number) stats.get("level")).intValue());
stmt.setInt(3, ((Number) stats.get("totalXP")).intValue());
stmt.setInt(4, ((Number) stats.get("messageCount")).intValue());
stmt.setLong(5, ((Number) stats.get("voiceMinutes")).longValue());
stmt.setLong(6, ((Number) stats.get("lastMessageTime")).longValue());
stmt.setLong(7, ((Number) stats.get("lastVoiceJoin")).longValue());
stmt.executeUpdate();
}
logger.info("userStats мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции userStats", e);
}
}
@SuppressWarnings("unchecked")
private void migrateReputation(Map<String, Object> data) {
Map<String, Object> reputation = (Map<String, Object>) data.get("reputation");
if (reputation == null) return;
logger.info("Миграция reputation: {} записей", reputation.size());
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO reputation (user_id, reputation) VALUES (?, ?)"
)) {
for (Map.Entry<String, Object> entry : reputation.entrySet()) {
stmt.setString(1, entry.getKey());
stmt.setDouble(2, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
logger.info("reputation мигрирована");
} catch (SQLException e) {
logger.error("Ошибка миграции reputation", e);
}
}
@SuppressWarnings("unchecked")
private void migrateShipPairs(Map<String, Object> data) {
Map<String, Object> shipPairs = (Map<String, Object>) data.get("shipPairs");
if (shipPairs == null) return;
logger.info("Миграция shipPairs: {} записей", shipPairs.size());
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO ship_pairs (user1_id, user2_id, compatibility) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> entry : shipPairs.entrySet()) {
String[] users = entry.getKey().split("-");
if (users.length == 2) {
stmt.setString(1, users[0]);
stmt.setString(2, users[1]);
stmt.setDouble(3, ((Number) entry.getValue()).doubleValue());
stmt.executeUpdate();
}
}
logger.info("shipPairs мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции shipPairs", e);
}
}
@SuppressWarnings("unchecked")
private void migrateCustomRoles(Map<String, Object> data) {
Map<String, Object> customRoles = (Map<String, Object>) data.get("custom_roles");
if (customRoles == null) return;
logger.info("Миграция custom_roles");
try (PreparedStatement stmt = database.prepare(
"INSERT OR REPLACE INTO custom_roles (guild_id, user_id, role_id) VALUES (?, ?, ?)"
)) {
for (Map.Entry<String, Object> guildEntry : customRoles.entrySet()) {
String guildId = guildEntry.getKey();
Map<String, Object> users = (Map<String, Object>) guildEntry.getValue();
for (Map.Entry<String, Object> userEntry : users.entrySet()) {
String userId = userEntry.getKey();
Object rolesObj = userEntry.getValue();
if (rolesObj instanceof java.util.List<?> rolesList) {
for (Object roleId : rolesList) {
stmt.setString(1, guildId);
stmt.setString(2, userId);
stmt.setString(3, roleId.toString());
stmt.executeUpdate();
}
}
}
}
logger.info("custom_roles мигрированы");
} catch (SQLException e) {
logger.error("Ошибка миграции custom_roles", e);
}
}
}

View file

@ -1,112 +1,249 @@
package creator.loki.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Схема базы данных - создание таблиц
*/
public class DatabaseSchema {
private static final Logger logger = LoggerFactory.getLogger(DatabaseSchema.class);
private final Database database;
public DatabaseSchema(Database database) {
this.database = database;
}
/**
* Создать все таблицы
*/
public void createTables() {
logger.info("Создание таблиц базы данных...");
createConfigTable();
createUserStatsTable();
createReputationTable();
createShipPairsTable();
createCustomRolesTable();
createVoiceChannelsTable();
createCounterStatsTable();
logger.info("Таблицы созданы успешно");
}
private void createConfigTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""");
}
private void createUserStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS user_stats (
user_id TEXT PRIMARY KEY,
level INTEGER DEFAULT 0,
total_xp INTEGER DEFAULT 0,
message_count INTEGER DEFAULT 0,
voice_minutes INTEGER DEFAULT 0,
last_message_time INTEGER DEFAULT 0,
last_voice_join INTEGER DEFAULT 0
)
""");
}
private void createReputationTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS reputation (
user_id TEXT PRIMARY KEY,
reputation REAL DEFAULT 0.0
)
""");
}
private void createShipPairsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS ship_pairs (
user1_id TEXT NOT NULL,
user2_id TEXT NOT NULL,
compatibility REAL NOT NULL,
PRIMARY KEY (user1_id, user2_id)
)
""");
}
private void createCustomRolesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS custom_roles (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
role_id TEXT NOT NULL,
PRIMARY KEY (guild_id, user_id, role_id)
)
""");
}
private void createVoiceChannelsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS voice_channels (
channel_id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
locked INTEGER DEFAULT 0
)
""");
}
private void createCounterStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS counter_stats (
date TEXT PRIMARY KEY,
joined INTEGER DEFAULT 0,
left_count INTEGER DEFAULT 0,
difference INTEGER DEFAULT 0
)
""");
}
}
package creator.loki.database;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Схема базы данных - создание таблиц
*/
public class DatabaseSchema {
private static final Logger logger = LoggerFactory.getLogger(DatabaseSchema.class);
private final Database database;
public DatabaseSchema(Database database) {
this.database = database;
}
/**
* Создать все таблицы
*/
public void createTables() {
logger.info("Создание таблиц базы данных...");
createConfigTable();
createUserStatsTable();
createReputationTable();
createShipPairsTable();
createCustomRolesTable();
createVoiceChannelsTable();
createVoicePresetsTable();
createCounterStatsTable();
createRatingsTable();
createWarningsTable();
createModerationCasesTable();
createActiveMutesTable();
createEconomyTable();
createIndexes();
logger.info("Таблицы созданы успешно");
}
private void createConfigTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS config (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
)
""");
}
private void createUserStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS user_stats (
user_id TEXT PRIMARY KEY,
level INTEGER DEFAULT 0,
total_xp INTEGER DEFAULT 0,
message_count INTEGER DEFAULT 0,
voice_minutes INTEGER DEFAULT 0,
last_message_time INTEGER DEFAULT 0,
last_voice_join INTEGER DEFAULT 0
)
""");
}
private void createReputationTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS reputation (
user_id TEXT PRIMARY KEY,
reputation REAL DEFAULT 0.0
)
""");
}
private void createShipPairsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS ship_pairs (
user1_id TEXT NOT NULL,
user2_id TEXT NOT NULL,
compatibility REAL NOT NULL,
PRIMARY KEY (user1_id, user2_id)
)
""");
}
private void createCustomRolesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS custom_roles (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
role_id TEXT NOT NULL,
PRIMARY KEY (guild_id, user_id, role_id)
)
""");
}
private void createVoiceChannelsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS voice_channels (
channel_id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
locked INTEGER DEFAULT 0,
text_channel_id TEXT
)
""");
}
private void createVoicePresetsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS voice_presets (
user_id TEXT PRIMARY KEY,
name TEXT,
user_limit INTEGER DEFAULT 0,
bitrate INTEGER DEFAULT 64000,
region TEXT DEFAULT 'auto',
locked INTEGER DEFAULT 0,
allowed_role_id TEXT,
denied_role_id TEXT
)
""");
}
private void createCounterStatsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS counter_stats (
date TEXT PRIMARY KEY,
joined INTEGER DEFAULT 0,
left_count INTEGER DEFAULT 0,
difference INTEGER DEFAULT 0
)
""");
}
private void createRatingsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS ratings (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
rating_type TEXT NOT NULL,
value INTEGER NOT NULL,
PRIMARY KEY (guild_id, user_id, rating_type)
)
""");
}
private void createWarningsTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS warnings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
moderator_id TEXT NOT NULL,
reason TEXT NOT NULL,
created_at INTEGER NOT NULL
)
""");
}
private void createModerationCasesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS moderation_cases (
id INTEGER PRIMARY KEY AUTOINCREMENT,
guild_id TEXT NOT NULL,
action TEXT NOT NULL,
target_id TEXT NOT NULL,
moderator_id TEXT NOT NULL,
reason TEXT NOT NULL,
created_at INTEGER NOT NULL,
duration_sec INTEGER,
until_ms INTEGER
)
""");
}
private void createActiveMutesTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS active_mutes (
guild_id TEXT NOT NULL,
user_id TEXT NOT NULL,
ends_at_ms INTEGER NOT NULL,
case_id INTEGER NOT NULL,
PRIMARY KEY (guild_id, user_id)
)
""");
}
private void createEconomyTable() {
database.execute("""
CREATE TABLE IF NOT EXISTS economy (
user_id TEXT PRIMARY KEY,
cash INTEGER NOT NULL DEFAULT 0,
bank INTEGER NOT NULL DEFAULT 0,
total_earned INTEGER NOT NULL DEFAULT 0,
total_spent INTEGER NOT NULL DEFAULT 0,
last_daily TEXT,
daily_streak INTEGER DEFAULT 0,
fishing_luck INTEGER DEFAULT 0,
efficiency INTEGER DEFAULT 0,
last_fish TEXT,
job TEXT,
last_work TEXT
)
""");
}
/**
* Создать индексы для ускорения запросов
*/
private void createIndexes() {
logger.info("Создание индексов...");
// Индексы для user_stats
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_level ON user_stats(level DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_total_xp ON user_stats(total_xp DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_message_count ON user_stats(message_count DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_user_stats_voice_minutes ON user_stats(voice_minutes DESC)");
// Индексы для reputation
database.execute("CREATE INDEX IF NOT EXISTS idx_reputation_value ON reputation(reputation DESC)");
// Индексы для ratings
database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_guild_type ON ratings(guild_id, rating_type)");
database.execute("CREATE INDEX IF NOT EXISTS idx_ratings_value ON ratings(value DESC)");
// Индексы для voice_channels
database.execute("CREATE INDEX IF NOT EXISTS idx_voice_channels_owner ON voice_channels(owner_id)");
database.execute("CREATE INDEX IF NOT EXISTS idx_voice_channels_created ON voice_channels(created_at)");
// Индексы для custom_roles
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_guild ON custom_roles(guild_id)");
database.execute("CREATE INDEX IF NOT EXISTS idx_custom_roles_user ON custom_roles(user_id)");
// Индексы для counter_stats
database.execute("CREATE INDEX IF NOT EXISTS idx_counter_stats_date ON counter_stats(date DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_warnings_guild_user ON warnings(guild_id, user_id)");
database.execute("CREATE INDEX IF NOT EXISTS idx_warnings_guild_time ON warnings(guild_id, created_at DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_mod_cases_guild_id ON moderation_cases(guild_id, id DESC)");
database.execute("CREATE INDEX IF NOT EXISTS idx_active_mutes_ends ON active_mutes(ends_at_ms)");
database.execute("CREATE INDEX IF NOT EXISTS idx_economy_balance ON economy((cash + bank) DESC)");
logger.info("Индексы созданы успешно");
}
}

View file

@ -3,46 +3,78 @@ package creator.loki.di;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.multibindings.Multibinder;
import creator.loki.config.BotConfig;
import creator.loki.config.ConfigLoader;
import creator.loki.core.EventDispatcher;
import creator.loki.core.ListenerAggregator;
import creator.loki.core.MessageCache;
import creator.loki.core.ShutdownManager;
import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.registry.SlashCommandRegistry;
import creator.loki.core.registry.PrefixCommandRegistrar;
import creator.loki.core.security.CooldownManager;
import creator.loki.core.security.RateLimiter;
import creator.loki.feature.counter.CounterManager;
import creator.loki.data.JsonDataStore;
import creator.loki.database.ConfigRepository;
import creator.loki.database.Database;
import creator.loki.database.DatabaseMigration;
import creator.loki.database.DatabaseSchema;
import creator.loki.feature.level.domain.XpFormula;
import creator.loki.feature.level.LevelUpNotifier;
import creator.loki.feature.level.service.LeaderboardService;
import creator.loki.feature.level.service.LevelService;
import creator.loki.feature.level.service.MessageCooldownService;
import creator.loki.feature.level.VoiceTracker;
import creator.loki.level.LevelSystem;
import creator.loki.metrics.MetricsCollector;
import creator.loki.profile.ProfileService;
import creator.loki.repository.UserStatsRepository;
import creator.loki.repository.impl.JsonUserStatsRepository;
import creator.loki.feature.role.CustomRoleManager;
import creator.loki.feature.voice.VoiceChannelManager;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.config.repository.PrefixedGuildConfigRepository;
import creator.loki.feature.rating.repository.RatingsRepository;
import creator.loki.feature.reputation.repository.ReputationRepository;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.counter.CounterModule;
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
import creator.loki.feature.level.LevelCommandRegistrar;
import creator.loki.feature.level.LevelModule;
import creator.loki.feature.metrics.MetricsCollector;
import creator.loki.feature.metrics.HealthHttpServer;
import creator.loki.feature.metrics.MetricsModule;
import creator.loki.feature.moderation.ModerationCommandRegistrar;
import creator.loki.feature.moderation.ModerationModule;
import creator.loki.feature.profile.ProfileModule;
import creator.loki.feature.role.RoleModule;
import creator.loki.feature.utility.UtilityCommandRegistrar;
import creator.loki.feature.music.MusicModule;
import creator.loki.feature.music.MusicCommandRegistrar;
import creator.loki.feature.voice.VoiceModule;
import creator.loki.feature.welcome.WelcomeModule;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.economy.EconomyCommandRegistrar;
import creator.loki.feature.economy.EconomyModule;
/**
* Главный модуль Guice для конфигурации Dependency Injection
*/
public class BotModule extends AbstractModule {
@Override
protected void configure() {
// Все основные компоненты - Singleton
bind(CommandRegistry.class).in(Singleton.class);
bind(SlashCommandRegistry.class).in(Singleton.class);
bind(CooldownManager.class).in(Singleton.class);
bind(RateLimiter.class).in(Singleton.class);
bind(MetricsCollector.class).in(Singleton.class);
bind(HealthHttpServer.class).in(Singleton.class);
bind(ListenerAggregator.class).in(Singleton.class);
bind(MessageCache.class).in(Singleton.class);
bind(ShutdownManager.class).in(Singleton.class);
bind(PrefixCommandRegistrar.class).in(Singleton.class);
install(new VoiceModule());
install(new MusicModule());
install(new LevelModule());
install(new ModerationModule());
install(new RoleModule());
install(new CounterModule());
install(new ProfileModule());
install(new MetricsModule());
install(new WelcomeModule());
install(new EconomyModule());
Multibinder<CommandRegistrar> commandBinder = Multibinder.newSetBinder(binder(), CommandRegistrar.class);
commandBinder.addBinding().to(LevelCommandRegistrar.class);
commandBinder.addBinding().to(ModerationCommandRegistrar.class);
commandBinder.addBinding().to(EntertainmentCommandRegistrar.class);
commandBinder.addBinding().to(UtilityCommandRegistrar.class);
commandBinder.addBinding().to(MusicCommandRegistrar.class);
commandBinder.addBinding().to(EconomyCommandRegistrar.class);
}
@Provides
@ -51,38 +83,6 @@ public class BotModule extends AbstractModule {
return ConfigLoader.load();
}
@Provides
@Singleton
JsonDataStore provideJsonDataStore() {
JsonDataStore dataStore = new JsonDataStore("data/bot_data.json");
dataStore.load();
return dataStore;
}
@Provides
@Singleton
CustomRoleManager provideCustomRoleManager(JsonDataStore dataStore) {
return new CustomRoleManager(dataStore);
}
@Provides
@Singleton
LevelSystem provideLevelSystem(JsonDataStore dataStore) {
return new LevelSystem(dataStore);
}
@Provides
@Singleton
VoiceTracker provideVoiceTracker(LevelService levelService) {
return new VoiceTracker(levelService);
}
@Provides
@Singleton
ProfileService provideProfileService(JsonDataStore dataStore) {
return new ProfileService(dataStore);
}
@Provides
@Singleton
EventDispatcher provideEventDispatcher(
@ -96,18 +96,13 @@ public class BotModule extends AbstractModule {
@Provides
@Singleton
Database provideDatabase() {
Database database = new Database("data/bot.db");
Database provideDatabase(BotConfig config) {
Database database = new Database(config.getDbPath());
database.connect();
// Создаем таблицы
DatabaseSchema schema = new DatabaseSchema(database);
schema.createTables();
// Мигрируем данные из JSON если нужно
DatabaseMigration migration = new DatabaseMigration(database);
migration.migrateFromJson("data/bot_data.json");
return database;
}
@ -119,69 +114,25 @@ public class BotModule extends AbstractModule {
@Provides
@Singleton
LevelUpNotifier provideLevelUpNotifier(ConfigRepository configRepo, LevelService levelService) {
return new LevelUpNotifier(configRepo, levelService);
GuildConfigRepository provideGuildConfigRepository(ConfigRepository configRepository) {
return new PrefixedGuildConfigRepository(configRepository);
}
@Provides
@Singleton
VoiceChannelManager provideVoiceChannelManager(JsonDataStore dataStore, ConfigRepository configRepo) {
return new VoiceChannelManager(dataStore, configRepo);
RatingsRepository provideRatingsRepository(Database database) {
return new RatingsRepository(database);
}
@Provides
@Singleton
UserStatsRepository provideUserStatsRepository(JsonDataStore dataStore) {
return new JsonUserStatsRepository(dataStore);
ReputationRepository provideReputationRepository(Database database) {
return new ReputationRepository(database);
}
@Provides
@Singleton
CounterManager provideCounterManager(JsonDataStore dataStore, ConfigRepository configRepo) {
return new CounterManager(dataStore, configRepo);
}
@Provides
@Singleton
XpFormula provideXpFormula() {
return new XpFormula();
}
@Provides
@Singleton
MessageCooldownService provideMessageCooldownService(UserStatsRepository repository) {
return new MessageCooldownService(repository);
}
@Provides
@Singleton
LeaderboardService provideLeaderboardService(UserStatsRepository repository) {
return new LeaderboardService(repository);
}
@Provides
@Singleton
LevelService provideLevelService(
UserStatsRepository repository,
XpFormula xpFormula,
MessageCooldownService cooldownService
) {
return new LevelService(repository, xpFormula, cooldownService);
}
@Provides
@Singleton
ProfileService provideOldProfileService(JsonDataStore dataStore) {
return new ProfileService(dataStore);
}
@Provides
@Singleton
creator.loki.feature.profile.ProfileService provideNewProfileService(
UserStatsRepository repository,
LevelService levelService,
LeaderboardService leaderboardService
) {
return new creator.loki.feature.profile.ProfileService(repository, levelService, leaderboardService);
ShipPairRepository provideShipPairRepository(Database database) {
return new ShipPairRepository(database);
}
}

View file

@ -1,79 +1,79 @@
package creator.loki.embed;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.time.Instant;
public class EmbedFactory {
private final EmbedBuilder builder;
private EmbedFactory() {
this.builder = new EmbedBuilder();
}
public static EmbedFactory create() {
return new EmbedFactory();
}
public EmbedFactory setTitle(String title) {
builder.setTitle(title);
return this;
}
public EmbedFactory setDescription(String description) {
builder.setDescription(description);
return this;
}
public EmbedFactory setColor(Color color) {
builder.setColor(color);
return this;
}
public EmbedFactory addField(String name, String value, boolean inline) {
builder.addField(name, value, inline);
return this;
}
public EmbedFactory setFooter(String text) {
builder.setFooter(text);
return this;
}
public EmbedFactory setFooter(String text, String iconUrl) {
builder.setFooter(text, iconUrl);
return this;
}
public EmbedFactory setThumbnail(String url) {
builder.setThumbnail(url);
return this;
}
public EmbedFactory setImage(String url) {
builder.setImage(url);
return this;
}
public EmbedFactory setAuthor(String name) {
builder.setAuthor(name);
return this;
}
public EmbedFactory setAuthor(String name, String url) {
builder.setAuthor(name, url);
return this;
}
public EmbedFactory setAuthor(String name, String url, String iconUrl) {
builder.setAuthor(name, url, iconUrl);
return this;
}
public MessageEmbed build() {
builder.setTimestamp(Instant.now());
return builder.build();
}
}
package creator.loki.embed;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.time.Instant;
public class EmbedFactory {
private final EmbedBuilder builder;
private EmbedFactory() {
this.builder = new EmbedBuilder();
}
public static EmbedFactory create() {
return new EmbedFactory();
}
public EmbedFactory withTitle(String title) {
builder.setTitle(title);
return this;
}
public EmbedFactory withDescription(String description) {
builder.setDescription(description);
return this;
}
public EmbedFactory withColor(Color color) {
builder.setColor(color);
return this;
}
public EmbedFactory addField(String name, String value, boolean inline) {
builder.addField(name, value, inline);
return this;
}
public EmbedFactory withFooter(String text) {
builder.setFooter(text);
return this;
}
public EmbedFactory withFooter(String text, String iconUrl) {
builder.setFooter(text, iconUrl);
return this;
}
public EmbedFactory withThumbnail(String url) {
builder.setThumbnail(url);
return this;
}
public EmbedFactory withImage(String url) {
builder.setImage(url);
return this;
}
public EmbedFactory withAuthor(String name) {
builder.setAuthor(name);
return this;
}
public EmbedFactory withAuthor(String name, String url) {
builder.setAuthor(name, url);
return this;
}
public EmbedFactory withAuthor(String name, String url, String iconUrl) {
builder.setAuthor(name, url, iconUrl);
return this;
}
public MessageEmbed build() {
builder.setTimestamp(Instant.now());
return builder.build();
}
}

View file

@ -1,50 +1,50 @@
package creator.loki.embed;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.time.Instant;
public final class EmbedTemplates {
private EmbedTemplates() {
// Запрет создания экземпляров
}
public static MessageEmbed success(String title, String description) {
return new EmbedBuilder()
.setColor(Color.GREEN)
.setTitle("✅ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed error(String title, String description) {
return new EmbedBuilder()
.setColor(Color.RED)
.setTitle("❌ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed info(String title, String description) {
return new EmbedBuilder()
.setColor(Color.CYAN)
.setTitle("ℹ️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed warning(String title, String description) {
return new EmbedBuilder()
.setColor(Color.ORANGE)
.setTitle("⚠️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
}
package creator.loki.embed;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.time.Instant;
public final class EmbedTemplates {
private EmbedTemplates() {
// Запрет создания экземпляров
}
public static MessageEmbed success(String title, String description) {
return new EmbedBuilder()
.setColor(Color.GREEN)
.setTitle("✅ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed error(String title, String description) {
return new EmbedBuilder()
.setColor(Color.RED)
.setTitle("❌ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed info(String title, String description) {
return new EmbedBuilder()
.setColor(Color.CYAN)
.setTitle("ℹ️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
public static MessageEmbed warning(String title, String description) {
return new EmbedBuilder()
.setColor(Color.ORANGE)
.setTitle("⚠️ " + title)
.setDescription(description)
.setTimestamp(Instant.now())
.build();
}
}

View file

@ -0,0 +1,75 @@
package creator.loki.embed;
import creator.loki.command.CommandCategories;
import java.awt.Color;
import java.util.Map;
public final class HelpCategories {
private static final Map<String, String> EMOJIS;
private static final Map<String, Color> COLORS;
private static final Map<String, String> DESCRIPTIONS;
static {
EMOJIS = Map.ofEntries(
Map.entry("Общие", "📋"),
Map.entry("Роли", "🎭"),
Map.entry("Развлечения", "🎮"),
Map.entry(CommandCategories.GIF_INTERACTIVE, "💞"),
Map.entry("Модерация", "🛡️"),
Map.entry("Статистика", "📊"),
Map.entry("Утилиты", "🔧"),
Map.entry("Система", "💠"),
Map.entry("Настройка", "⚙️"),
Map.entry("Супер-Админ", "🔐")
);
COLORS = Map.ofEntries(
Map.entry("Общие", new Color(52, 152, 219)),
Map.entry("Роли", new Color(155, 89, 182)),
Map.entry("Развлечения", new Color(241, 196, 15)),
Map.entry(CommandCategories.GIF_INTERACTIVE, new Color(233, 30, 99)),
Map.entry("Модерация", new Color(231, 76, 60)),
Map.entry("Статистика", new Color(46, 204, 113)),
Map.entry("Утилиты", new Color(52, 152, 219)),
Map.entry("Система", new Color(52, 73, 94)),
Map.entry("Настройка", new Color(149, 165, 166)),
Map.entry("Супер-Админ", new Color(113, 54, 138))
);
DESCRIPTIONS = Map.ofEntries(
Map.entry("Общие", "Основные команды бота"),
Map.entry("Роли", "Управление кастомными ролями"),
Map.entry("Развлечения", "Игры, мемы, рейтинги, текст"),
Map.entry(CommandCategories.GIF_INTERACTIVE, "Действия с участником в GIF (nekos.life)"),
Map.entry("Модерация", "Команды модерации сервера"),
Map.entry("Статистика", "Уровни, репутация и статистика"),
Map.entry("Утилиты", "Полезные инструменты"),
Map.entry("Система", "Метрики и служебная информация для админов"),
Map.entry("Настройка", "Настройка систем бота (только админы)"),
Map.entry("Супер-Админ", "Сервисные команды владельца бота")
);
}
private HelpCategories() {
}
public static String getEmoji(String category) {
return EMOJIS.getOrDefault(category, "📌");
}
public static Color getColor(String category) {
return COLORS.getOrDefault(category, new Color(88, 101, 242));
}
public static String getDescription(String category) {
return DESCRIPTIONS.getOrDefault(category, "Команды категории");
}
public static boolean isCategoryVisible(String category, boolean isAdmin, boolean isSuper) {
if ("Настройка".equals(category) && !isAdmin) {
return false;
}
return !"Супер-Админ".equals(category) || isSuper;
}
}

View file

@ -0,0 +1,158 @@
package creator.loki.feature.admin.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.feature.rating.repository.RatingsRepository;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
import java.util.Random;
/**
* Команда для генерации всех rating значений для всех участников сервера
* Доступна только супер-админу
*/
public class GenerateRatingsCommand implements Command {
private static final Logger logger = LoggerFactory.getLogger(GenerateRatingsCommand.class);
private final RatingsRepository ratingsRepository;
private final BotConfig config;
private final Random random = new Random();
public GenerateRatingsCommand(RatingsRepository ratingsRepository, BotConfig config) {
this.ratingsRepository = ratingsRepository;
this.config = config;
}
@Override
public String getName() {
return "generateratings";
}
@Override
public String getDescription() {
return "Генерирует все rating значения для всех участников сервера (только для супер-админа)";
}
@Override
public String getUsage() {
return "generateratings";
}
@Override
public String getCategory() {
return "Супер-Админ";
}
@Override
public void execute(CommandContext context) {
if (!config.getSuperAdminId().equals(context.getAuthor().getUser().getId())) {
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("❌ Недостаточно прав")
.withDescription("Эта команда доступна только супер-администратору бота.")
.withColor(Color.RED)
.build()
).queue();
return;
}
if (context.getGuild() == null) {
context.getChannel().sendMessage("❌ Команда работает только на сервере.").queue();
return;
}
var initial = context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("⏳ Генерация рейтингов...")
.withDescription("Начинаю генерацию всех rating значений для участников сервера...")
.withColor(Color.YELLOW)
.build()
).complete();
try {
String guildId = context.getGuild().getId();
int memberCount = 0;
int generatedCount = 0;
for (Member member : context.getGuild().getMembers()) {
if (member.getUser().isBot()) {
continue;
}
memberCount++;
String userId = member.getUser().getId();
generateRatingForUser(guildId, userId, "ship");
generateRatingForUser(guildId, userId, "gay");
generateRatingForUser(guildId, userId, "iq");
generateRatingForUser(guildId, userId, "pp");
generateRatingForUser(guildId, userId, "mustache");
generateRatingForUser(guildId, userId, "fat");
generateRatingForUser(guildId, userId, "boykisser");
generatedCount++;
}
initial.editMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Генерация завершена")
.withDescription(
"Успешно сгенерированы все rating значения!\n\n" +
"**Статистика:**\n" +
"• Обработано участников: **" + memberCount + "**\n" +
"• Сгенерировано значений: **" + (generatedCount * 7) + "**\n" +
"• Типы рейтингов: ship, gay, iq, pp, mustache, fat, boykisser"
)
.withColor(Color.GREEN)
.withFooter("Инициатор: " + context.getAuthor().getUser().getName(), context.getAuthor().getAvatarUrl())
.build()
).queue();
logger.info("Супер-админ {} сгенерировал рейтинги для {} участников",
context.getAuthor().getUser().getName(), memberCount);
} catch (Exception e) {
logger.error("Ошибка при генерации рейтингов", e);
initial.editMessageEmbeds(
EmbedFactory.create()
.withTitle("❌ Ошибка")
.withDescription("Произошла ошибка при генерации рейтингов: " + e.getMessage())
.withColor(Color.RED)
.build()
).queue();
}
}
/**
* Генерирует rating значение для пользователя
*/
private void generateRatingForUser(String guildId, String userId, String ratingType) {
// Проверяем существует ли уже значение
if (ratingsRepository.hasRating(guildId, userId, ratingType)) {
return; // Не перезаписываем существующие значения
}
// Генерируем значение в зависимости от типа
int value = switch (ratingType) {
case "iq" -> random.nextInt(201); // 0-200
case "pp" -> random.nextInt(31); // 0-30 см
case "mustache" -> {
// Редкий шанс на легендарные усы
if (random.nextInt(100) < 1) {
yield 1000 + random.nextInt(500); // 1000-1500
}
yield random.nextInt(101); // 0-100
}
case "fat" -> random.nextInt(101); // 0-100%
case "boykisser" -> random.nextInt(101); // 0-100%
default -> random.nextInt(101); // ship, gay: 0-100%
};
// Сохраняем значение в БД
ratingsRepository.setRating(guildId, userId, ratingType, value);
}
}

View file

@ -1,5 +1,7 @@
package creator.loki.database;
package creator.loki.feature.config.repository;
import creator.loki.database.Database;
import creator.loki.feature.config.repository.StringConfigStore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -10,7 +12,7 @@ import java.sql.SQLException;
/**
* Репозиторий для работы с конфигурацией
*/
public class ConfigRepository {
public class ConfigRepository implements StringConfigStore {
private static final Logger logger = LoggerFactory.getLogger(ConfigRepository.class);
private final Database database;
@ -25,7 +27,7 @@ public class ConfigRepository {
try (PreparedStatement stmt = database.prepare("SELECT value FROM config WHERE key = ?")) {
stmt.setString(1, key);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
return rs.getString("value");
}

View file

@ -0,0 +1,16 @@
package creator.loki.feature.config.repository;
/**
* Ключи конфигурации с привязкой к гильдии (хранение: {@code guildId + ":" + key} в общей таблице config).
*/
public interface GuildConfigRepository {
/**
* @return значение или {@code null}, если нет записи
*/
String get(String guildId, String key);
void set(String guildId, String key, String value);
void delete(String guildId, String key);
}

View file

@ -0,0 +1,35 @@
package creator.loki.feature.config.repository;
import creator.loki.feature.config.repository.GuildConfigRepository;
import creator.loki.feature.config.repository.StringConfigStore;
/**
* Реализация {@link GuildConfigRepository} поверх {@link StringConfigStore} с префиксом {@code guildId:}.
*/
public final class PrefixedGuildConfigRepository implements GuildConfigRepository {
private final StringConfigStore store;
public PrefixedGuildConfigRepository(StringConfigStore store) {
this.store = store;
}
private static String storageKey(String guildId, String key) {
return guildId + ":" + key;
}
@Override
public String get(String guildId, String key) {
return store.get(storageKey(guildId, key));
}
@Override
public void set(String guildId, String key, String value) {
store.set(storageKey(guildId, key), value);
}
@Override
public void delete(String guildId, String key) {
store.delete(storageKey(guildId, key));
}
}

View file

@ -0,0 +1,16 @@
package creator.loki.feature.config.repository;
/**
* Плоское хранилище строковых ключей (глобальная таблица config и т.п.).
*/
public interface StringConfigStore {
/**
* @return значение или {@code null}
*/
String get(String key);
void set(String key, String value);
void delete(String key);
}

View file

@ -1,226 +1,110 @@
package creator.loki.feature.counter;
import com.google.gson.reflect.TypeToken;
import creator.loki.data.JsonDataStore;
import creator.loki.database.ConfigRepository;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Type;
import java.time.LocalDate;
import java.util.*;
/**
* Менеджер счетчика участников
*/
public class CounterManager {
private static final Logger logger = LoggerFactory.getLogger(CounterManager.class);
private final JsonDataStore dataStore;
private final ConfigRepository configRepo;
private String counterChannelId;
private String format = "【👥】Участники: {count}";
public CounterManager(JsonDataStore dataStore, ConfigRepository configRepo) {
this.dataStore = dataStore;
this.configRepo = configRepo;
loadConfig();
}
/**
* Загрузить конфигурацию
*/
private void loadConfig() {
this.counterChannelId = configRepo.get("counter.channel_id");
String savedFormat = configRepo.get("counter.format");
if (savedFormat != null) {
this.format = savedFormat;
}
}
/**
* Получить данные счетчика
*/
@SuppressWarnings("unchecked")
private Map<String, Object> getCounterData() {
Type type = new TypeToken<Map<String, Object>>(){}.getType();
Map<String, Object> data = dataStore.getData("counter_data", type);
return data != null ? data : new HashMap<>();
}
/**
* Сохранить данные счетчика
*/
private void saveCounterData(Map<String, Object> data) {
dataStore.setData("counter_data", data);
}
/**
* Установить канал счетчика
*/
public void setCounterChannel(String channelId) {
configRepo.set("counter.channel_id", channelId);
this.counterChannelId = channelId;
logger.info("Установлен канал счетчика: {}", channelId);
}
/**
* Установить формат отображения
*/
public void setFormat(String format) {
configRepo.set("counter.format", format);
this.format = format;
logger.info("Установлен формат: {}", format);
}
/**
* Получить текущую дневную статистику
*/
@SuppressWarnings("unchecked")
public DailyStats getDailyStats() {
Map<String, Object> data = getCounterData();
Map<String, Object> statsMap = (Map<String, Object>) data.get("daily_stats");
if (statsMap == null) {
return new DailyStats();
}
DailyStats stats = new DailyStats();
stats.setDate((String) statsMap.getOrDefault("date", LocalDate.now().toString()));
stats.setJoined(((Number) statsMap.getOrDefault("joined", 0)).intValue());
stats.setLeft(((Number) statsMap.getOrDefault("left", 0)).intValue());
// Если статистика не сегодняшняя, создаем новую
if (!stats.isToday()) {
// Сохраняем старую в историю
saveToHistory(stats);
stats = new DailyStats();
}
return stats;
}
/**
* Сохранить статистику
*/
private void saveDailyStats(DailyStats stats) {
Map<String, Object> data = getCounterData();
Map<String, Object> statsMap = new HashMap<>();
statsMap.put("date", stats.getDate());
statsMap.put("joined", stats.getJoined());
statsMap.put("left", stats.getLeft());
statsMap.put("difference", stats.getDifference());
data.put("daily_stats", statsMap);
saveCounterData(data);
}
/**
* Сохранить статистику в историю
*/
@SuppressWarnings("unchecked")
private void saveToHistory(DailyStats stats) {
Map<String, Object> data = getCounterData();
List<Map<String, Object>> history = (List<Map<String, Object>>) data.get("history");
if (history == null) {
history = new ArrayList<>();
}
Map<String, Object> statsMap = new HashMap<>();
statsMap.put("date", stats.getDate());
statsMap.put("joined", stats.getJoined());
statsMap.put("left", stats.getLeft());
statsMap.put("difference", stats.getDifference());
history.add(statsMap);
// Храним только последние 30 дней
if (history.size() > 30) {
history.remove(0);
}
data.put("history", history);
saveCounterData(data);
}
/**
* Зарегистрировать присоединение участника
*/
public void recordJoin() {
DailyStats stats = getDailyStats();
stats.incrementJoined();
saveDailyStats(stats);
logger.debug("Зарегистрировано присоединение. Всего за день: {}", stats.getJoined());
}
/**
* Зарегистрировать выход участника
*/
public void recordLeave() {
DailyStats stats = getDailyStats();
stats.incrementLeft();
saveDailyStats(stats);
logger.debug("Зарегистрирован выход. Всего за день: {}", stats.getLeft());
}
/**
* Обновить счетчик участников
*/
public void updateCounter(Guild guild) {
if (counterChannelId == null || counterChannelId.isEmpty()) {
return;
}
VoiceChannel channel = guild.getVoiceChannelById(counterChannelId);
if (channel == null) {
logger.warn("Канал счетчика не найден: {}", counterChannelId);
return;
}
int memberCount = guild.getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
// Обновляем название канала только если оно изменилось
if (!channel.getName().equals(newName)) {
channel.getManager().setName(newName).queue(
success -> logger.debug("Обновлен счетчик участников: {}", memberCount),
error -> logger.error("Ошибка при обновлении счетчика", error)
);
}
}
/**
* Получить историю статистики
*/
@SuppressWarnings("unchecked")
public List<DailyStats> getHistory(int days) {
Map<String, Object> data = getCounterData();
List<Map<String, Object>> historyMaps = (List<Map<String, Object>>) data.get("history");
if (historyMaps == null) {
return new ArrayList<>();
}
List<DailyStats> history = new ArrayList<>();
int start = Math.max(0, historyMaps.size() - days);
for (int i = start; i < historyMaps.size(); i++) {
Map<String, Object> statsMap = historyMaps.get(i);
DailyStats stats = new DailyStats();
stats.setDate((String) statsMap.get("date"));
stats.setJoined(((Number) statsMap.get("joined")).intValue());
stats.setLeft(((Number) statsMap.get("left")).intValue());
history.add(stats);
}
return history;
}
// Getters
public String getCounterChannelId() { return counterChannelId; }
public String getFormat() { return format; }
}
package creator.loki.feature.counter;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.counter.repository.CounterStatsRepository;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.*;
/**
* Менеджер счетчика участников
*/
public class CounterManager {
private static final Logger logger = LoggerFactory.getLogger(CounterManager.class);
private final CounterStatsRepository statsRepo;
private final ConfigRepository configRepo;
private String counterChannelId;
private String format = "【👥】Участники: {count}";
public CounterManager(CounterStatsRepository statsRepo, ConfigRepository configRepo) {
this.statsRepo = statsRepo;
this.configRepo = configRepo;
loadConfig();
}
/**
* Загрузить конфигурацию
*/
private void loadConfig() {
this.counterChannelId = configRepo.get("counter.channel_id");
String savedFormat = configRepo.get("counter.format");
if (savedFormat != null) {
this.format = savedFormat;
}
}
/**
* Получить текущую дневную статистику
*/
public DailyStats getDailyStats() {
return statsRepo.getTodayStats();
}
/**
* Сохранить статистику
*/
private void saveDailyStats(DailyStats stats) {
statsRepo.saveStats(stats);
}
/**
* Зарегистрировать присоединение участника
*/
public void recordJoin() {
DailyStats stats = getDailyStats();
stats.incrementJoined();
saveDailyStats(stats);
logger.debug("Зарегистрировано присоединение. Всего за день: {}", stats.getJoined());
}
/**
* Зарегистрировать выход участника
*/
public void recordLeave() {
DailyStats stats = getDailyStats();
stats.incrementLeft();
saveDailyStats(stats);
logger.debug("Зарегистрирован выход. Всего за день: {}", stats.getLeft());
}
/**
* Обновить счетчик участников
*/
public void updateCounter(Guild guild) {
if (counterChannelId == null || counterChannelId.isEmpty()) {
return;
}
VoiceChannel channel = guild.getVoiceChannelById(counterChannelId);
if (channel == null) {
logger.warn("Канал счетчика не найден: {}", counterChannelId);
return;
}
int memberCount = guild.getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
// Обновляем название канала только если оно изменилось
if (!channel.getName().equals(newName)) {
channel.getManager().setName(newName).queue(
success -> logger.debug("Обновлен счетчик участников: {}", memberCount),
error -> logger.error("Ошибка при обновлении счетчика", error)
);
}
}
/**
* Получить историю статистики
*/
public List<DailyStats> getHistory(int days) {
return statsRepo.getHistory(days);
}
// Getters
public String getCounterChannelId() { return counterChannelId; }
public String getFormat() { return format; }
}

View file

@ -0,0 +1,27 @@
package creator.loki.feature.counter;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import creator.loki.database.Database;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.feature.counter.repository.CounterStatsRepository;
public class CounterModule extends AbstractModule {
@Override
protected void configure() {
}
@Provides
@Singleton
CounterStatsRepository provideCounterStatsRepository(Database database) {
return new CounterStatsRepository(database);
}
@Provides
@Singleton
CounterManager provideCounterManager(CounterStatsRepository statsRepo, ConfigRepository configRepo) {
return new CounterManager(statsRepo, configRepo);
}
}

View file

@ -1,159 +0,0 @@
package creator.loki.feature.counter;
import creator.loki.database.ConfigRepository;
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.Member;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import net.dv8tion.jda.api.interactions.commands.OptionType;
import net.dv8tion.jda.api.interactions.commands.build.Commands;
import net.dv8tion.jda.api.interactions.commands.build.SubcommandData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.Color;
/**
* Команда настройки счетчика участников
*/
public class CounterSetupCommand extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(CounterSetupCommand.class);
private final ConfigRepository configRepo;
public CounterSetupCommand(ConfigRepository configRepo) {
this.configRepo = configRepo;
}
@Override
public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
if (!"counter-setup".equals(event.getName())) {
return;
}
// Проверка прав администратора
Member member = event.getMember();
if (member == null || !member.hasPermission(Permission.ADMINISTRATOR)) {
event.reply("❌ У вас нет прав для использования этой команды!").setEphemeral(true).queue();
return;
}
String subcommand = event.getSubcommandName();
if (subcommand == null) {
return;
}
switch (subcommand) {
case "channel" -> handleChannel(event);
case "format" -> handleFormat(event);
case "info" -> handleInfo(event);
}
}
/**
* Установить канал счетчика
*/
private void handleChannel(SlashCommandInteractionEvent event) {
VoiceChannel channel = event.getOption("channel").getAsChannel().asVoiceChannel();
configRepo.set("counter.channel_id", channel.getId());
// Сразу обновляем название канала
int memberCount = event.getGuild().getMemberCount();
String format = configRepo.get("counter.format");
if (format == null) {
format = "【👥】Участники: {count}";
configRepo.set("counter.format", format);
}
String newName = format.replace("{count}", String.valueOf(memberCount));
channel.getManager().setName(newName).queue();
EmbedBuilder embed = new EmbedBuilder()
.setTitle("✅ Канал счетчика установлен")
.setDescription("Канал " + channel.getAsMention() + " теперь отображает количество участников")
.addField("Текущее количество", String.valueOf(memberCount), false)
.setColor(Color.GREEN);
event.replyEmbeds(embed.build()).queue();
logger.info("Установлен канал счетчика: {} ({})", channel.getName(), channel.getId());
}
/**
* Установить формат отображения
*/
private void handleFormat(SlashCommandInteractionEvent event) {
String format = event.getOption("format").getAsString();
if (!format.contains("{count}")) {
event.reply("❌ Формат должен содержать `{count}`!").setEphemeral(true).queue();
return;
}
configRepo.set("counter.format", format);
// Обновляем канал если он установлен
String channelId = configRepo.get("counter.channel_id");
if (channelId != null) {
VoiceChannel channel = event.getGuild().getVoiceChannelById(channelId);
if (channel != null) {
int memberCount = event.getGuild().getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
channel.getManager().setName(newName).queue();
}
}
EmbedBuilder embed = new EmbedBuilder()
.setTitle("✅ Формат обновлен")
.setDescription("Новый формат: `" + format + "`")
.addField("Пример", format.replace("{count}", "123"), false)
.setColor(Color.GREEN);
event.replyEmbeds(embed.build()).queue();
logger.info("Установлен формат счетчика: {}", format);
}
/**
* Показать текущую конфигурацию
*/
private void handleInfo(SlashCommandInteractionEvent event) {
String channelId = configRepo.get("counter.channel_id");
String format = configRepo.get("counter.format");
EmbedBuilder embed = new EmbedBuilder()
.setTitle("📊 Конфигурация счетчика участников")
.setColor(Color.BLUE);
if (channelId != null) {
embed.addField("Канал", "<#" + channelId + ">", false);
} else {
embed.addField("Канал", "❌ Не установлен", false);
}
if (format != null) {
embed.addField("Формат", "`" + format + "`", false);
embed.addField("Пример", format.replace("{count}", String.valueOf(event.getGuild().getMemberCount())), false);
} else {
embed.addField("Формат", "❌ Не установлен", false);
}
event.replyEmbeds(embed.build()).setEphemeral(true).queue();
}
/**
* Зарегистрировать команду в Discord
*/
public static void register(net.dv8tion.jda.api.JDA jda) {
jda.updateCommands().addCommands(
Commands.slash("counter-setup", "Настройка счетчика участников")
.addSubcommands(
new SubcommandData("channel", "Установить канал для счетчика")
.addOption(OptionType.CHANNEL, "channel", "Голосовой канал", true),
new SubcommandData("format", "Установить формат отображения")
.addOption(OptionType.STRING, "format", "Формат (используйте {count})", true),
new SubcommandData("info", "Показать текущую конфигурацию")
)
).queue();
}
}

View file

@ -0,0 +1,186 @@
package creator.loki.feature.counter;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.config.repository.ConfigRepository;
import creator.loki.embed.EmbedFactory;
import creator.loki.util.GuildChannelResolve;
import net.dv8tion.jda.api.Permission;
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
import java.awt.Color;
import java.util.List;
/**
* Prefix команда настройки счетчика участников
*/
public class CounterSetupPrefixCommand implements Command {
private final CounterManager counterManager;
private final ConfigRepository configRepo;
public CounterSetupPrefixCommand(CounterManager counterManager, ConfigRepository configRepo) {
this.counterManager = counterManager;
this.configRepo = configRepo;
}
@Override
public String getName() {
return "counter-setup";
}
@Override
public String getDescription() {
return "Настройка счетчика участников";
}
@Override
public String getUsage() {
return "counter-setup <channel|format|info> [значение]";
}
@Override
public String getCategory() {
return "Настройка";
}
@Override
public void execute(CommandContext context) {
// Проверка админ прав
if (!context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
context.getChannel().sendMessage("❌ У вас нет прав администратора!").queue();
return;
}
List<String> args = context.getArgs();
if (args.isEmpty()) {
sendHelp(context);
return;
}
String action = args.get(0).toLowerCase();
switch (action) {
case "channel" -> handleChannel(context, args);
case "format" -> handleFormat(context, args);
case "info" -> handleInfo(context);
default -> sendHelp(context);
}
}
private void handleChannel(CommandContext context, List<String> args) {
if (args.size() < 2) {
context.getChannel().sendMessage(
"❌ Укажите голосовой канал: `#канал`, `<#id>` или числовой id. Пример: `!counter-setup channel 123456789012345678`"
).queue();
return;
}
VoiceChannel channel = GuildChannelResolve.voice(context.getGuild(), context.getMessage(), args, 1);
if (channel == null) {
context.getChannel().sendMessage(
"❌ Не найден голосовой канал. Укажите `#войс`, `<#id>` или id из 17-20 цифр."
).queue();
return;
}
configRepo.set("counter.channel_id", channel.getId());
// Сразу обновляем название канала
int memberCount = context.getGuild().getMemberCount();
String format = configRepo.get("counter.format");
if (format == null) {
format = "【👥】Участники: {count}";
configRepo.set("counter.format", format);
}
String newName = format.replace("{count}", String.valueOf(memberCount));
channel.getManager().setName(newName).queue();
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Канал счетчика установлен")
.withDescription("Канал " + channel.getAsMention() + " теперь отображает количество участников")
.addField("Текущее количество", String.valueOf(memberCount), false)
.withColor(Color.GREEN)
.build()
).queue();
}
private void handleFormat(CommandContext context, List<String> args) {
if (args.size() < 2) {
context.getChannel().sendMessage("❌ Укажите формат! Пример: `!counter-setup format 【👥】Участники: {count}`").queue();
return;
}
// Объединяем все аргументы в одну строку
String format = String.join(" ", args.subList(1, args.size()));
if (!format.contains("{count}")) {
context.getChannel().sendMessage("❌ Формат должен содержать `{count}`!").queue();
return;
}
configRepo.set("counter.format", format);
// Обновляем канал если он установлен
String channelId = configRepo.get("counter.channel_id");
if (channelId != null) {
VoiceChannel channel = context.getGuild().getVoiceChannelById(channelId);
if (channel != null) {
int memberCount = context.getGuild().getMemberCount();
String newName = format.replace("{count}", String.valueOf(memberCount));
channel.getManager().setName(newName).queue();
}
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("✅ Формат обновлен")
.withDescription("Новый формат: `" + format + "`")
.addField("Пример", format.replace("{count}", "123"), false)
.withColor(Color.GREEN)
.build()
).queue();
}
private void handleInfo(CommandContext context) {
String channelId = configRepo.get("counter.channel_id");
String format = configRepo.get("counter.format");
var embed = EmbedFactory.create()
.withTitle("📊 Конфигурация счетчика участников")
.withColor(Color.BLUE);
if (channelId != null) {
embed.addField("Канал", "<#" + channelId + ">", false);
} else {
embed.addField("Канал", "❌ Не установлен", false);
}
if (format != null) {
embed.addField("Формат", "`" + format + "`", false);
embed.addField("Пример", format.replace("{count}", String.valueOf(context.getGuild().getMemberCount())), false);
} else {
embed.addField("Формат", "❌ Не установлен", false);
}
context.getChannel().sendMessageEmbeds(embed.build()).queue();
}
private void sendHelp(CommandContext context) {
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("📊 Настройка счетчика участников")
.withDescription("""
**Доступные команды:**
• `!counter-setup channel #войс` или `id` - канал счётчика
• `!counter-setup format 【👥】Участники: {count}` - Установить формат
• `!counter-setup info` - Показать текущую конфигурацию
В формате используйте `{count}` для отображения количества.
Только для администраторов!
""")
.withColor(Color.BLUE)
.build()
).queue();
}
}

View file

@ -1,66 +1,66 @@
package creator.loki.feature.counter;
import java.time.LocalDate;
/**
* Дневная статистика участников
*/
public class DailyStats {
private String date;
private int joined;
private int left;
private int difference;
public DailyStats() {
this.date = LocalDate.now().toString();
this.joined = 0;
this.left = 0;
this.difference = 0;
}
public DailyStats(String date, int joined, int left) {
this.date = date;
this.joined = joined;
this.left = left;
this.difference = joined - left;
}
// Getters
public String getDate() { return date; }
public int getJoined() { return joined; }
public int getLeft() { return left; }
public int getDifference() { return difference; }
// Setters
public void setDate(String date) { this.date = date; }
public void setJoined(int joined) {
this.joined = joined;
updateDifference();
}
public void setLeft(int left) {
this.left = left;
updateDifference();
}
// Методы
public void incrementJoined() {
this.joined++;
updateDifference();
}
public void incrementLeft() {
this.left++;
updateDifference();
}
private void updateDifference() {
this.difference = this.joined - this.left;
}
/**
* Проверить является ли статистика сегодняшней
*/
public boolean isToday() {
return this.date.equals(LocalDate.now().toString());
}
}
package creator.loki.feature.counter;
import java.time.LocalDate;
/**
* Дневная статистика участников
*/
public class DailyStats {
private String date;
private int joined;
private int left;
private int difference;
public DailyStats() {
this.date = LocalDate.now().toString();
this.joined = 0;
this.left = 0;
this.difference = 0;
}
public DailyStats(String date, int joined, int left) {
this.date = date;
this.joined = joined;
this.left = left;
this.difference = joined - left;
}
// Getters
public String getDate() { return date; }
public int getJoined() { return joined; }
public int getLeft() { return left; }
public int getDifference() { return difference; }
// Setters
public void setDate(String date) { this.date = date; }
public void setJoined(int joined) {
this.joined = joined;
updateDifference();
}
public void setLeft(int left) {
this.left = left;
updateDifference();
}
// Методы
public void incrementJoined() {
this.joined++;
updateDifference();
}
public void incrementLeft() {
this.left++;
updateDifference();
}
private void updateDifference() {
this.difference = this.joined - this.left;
}
/**
* Проверить является ли статистика сегодняшней
*/
public boolean isToday() {
return this.date.equals(LocalDate.now().toString());
}
}

View file

@ -1,86 +1,86 @@
package creator.loki.feature.counter;
import creator.loki.feature.counter.CounterManager;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Listener для счетчика участников
*/
public class MemberCounterListener extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
private final CounterManager counterManager;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Guild guild;
public MemberCounterListener(CounterManager counterManager) {
this.counterManager = counterManager;
}
/**
* Запустить периодическое обновление счетчика
*/
public void startScheduler(Guild guild) {
this.guild = guild;
// Обновляем счетчик каждые 5 минут
scheduler.scheduleAtFixedRate(() -> {
try {
counterManager.updateCounter(guild);
} catch (Exception e) {
logger.error("Ошибка при обновлении счетчика", e);
}
}, 0, 5, TimeUnit.MINUTES);
logger.info("Запущен планировщик обновления счетчика участников");
}
@Override
public void onGuildMemberJoin(GuildMemberJoinEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
counterManager.recordJoin();
counterManager.updateCounter(event.getGuild());
logger.info("Участник {} присоединился к серверу", event.getUser().getName());
}
@Override
public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
counterManager.recordLeave();
counterManager.updateCounter(event.getGuild());
logger.info("Участник {} покинул сервер", event.getUser().getName());
}
/**
* Остановить планировщик
*/
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
package creator.loki.feature.counter;
import creator.loki.feature.counter.CounterManager;
import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
import net.dv8tion.jda.api.events.guild.member.GuildMemberRemoveEvent;
import net.dv8tion.jda.api.hooks.ListenerAdapter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
/**
* Listener для счетчика участников
*/
public class MemberCounterListener extends ListenerAdapter {
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
private final CounterManager counterManager;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
private Guild guild;
public MemberCounterListener(CounterManager counterManager) {
this.counterManager = counterManager;
}
/**
* Запустить периодическое обновление счетчика
*/
public void startScheduler(Guild guild) {
this.guild = guild;
// Обновляем счетчик каждые 5 минут
scheduler.scheduleAtFixedRate(() -> Thread.startVirtualThread(() -> {
try {
counterManager.updateCounter(guild);
} catch (Exception e) {
logger.error("Ошибка при обновлении счетчика", e);
}
}), 0, 5, TimeUnit.MINUTES);
logger.info("Запущен планировщик обновления счетчика участников");
}
@Override
public void onGuildMemberJoin(GuildMemberJoinEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
counterManager.recordJoin();
counterManager.updateCounter(event.getGuild());
logger.info("Участник {} присоединился к серверу", event.getUser().getName());
}
@Override
public void onGuildMemberRemove(GuildMemberRemoveEvent event) {
// Игнорируем ботов
if (event.getUser().isBot()) {
return;
}
counterManager.recordLeave();
counterManager.updateCounter(event.getGuild());
logger.info("Участник {} покинул сервер", event.getUser().getName());
}
/**
* Остановить планировщик
*/
public void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

View file

@ -0,0 +1,137 @@
package creator.loki.feature.counter.repository;
import creator.loki.database.Database;
import creator.loki.feature.counter.DailyStats;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
/**
* Репозиторий для работы со статистикой счетчика участников
*/
public class CounterStatsRepository {
private static final Logger logger = LoggerFactory.getLogger(CounterStatsRepository.class);
private final Database database;
public CounterStatsRepository(Database database) {
this.database = database;
}
/**
* Получить статистику за дату
*/
public DailyStats getStats(String date) {
String sql = "SELECT * FROM counter_stats WHERE date = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, date);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
DailyStats stats = new DailyStats();
stats.setDate(rs.getString("date"));
stats.setJoined(rs.getInt("joined"));
stats.setLeft(rs.getInt("left_count"));
return stats;
}
}
} catch (SQLException e) {
logger.error("Ошибка получения статистики за {}", date, e);
}
return null;
}
/**
* Получить статистику за сегодня
*/
public DailyStats getTodayStats() {
String today = LocalDate.now().toString();
DailyStats stats = getStats(today);
if (stats == null) {
stats = new DailyStats();
stats.setDate(today);
}
return stats;
}
/**
* Сохранить статистику
*/
public void saveStats(DailyStats stats) {
String sql = """
INSERT INTO counter_stats (date, joined, left_count, difference)
VALUES (?, ?, ?, ?)
ON CONFLICT(date)
DO UPDATE SET
joined = excluded.joined,
left_count = excluded.left_count,
difference = excluded.difference
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, stats.getDate());
stmt.setInt(2, stats.getJoined());
stmt.setInt(3, stats.getLeft());
stmt.setInt(4, stats.getDifference());
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Ошибка сохранения статистики за {}", stats.getDate(), e);
}
}
/**
* Получить историю за последние N дней
*/
public List<DailyStats> getHistory(int days) {
List<DailyStats> history = new ArrayList<>();
String sql = "SELECT * FROM counter_stats ORDER BY date DESC LIMIT ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, days);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
DailyStats stats = new DailyStats();
stats.setDate(rs.getString("date"));
stats.setJoined(rs.getInt("joined"));
stats.setLeft(rs.getInt("left_count"));
history.add(stats);
}
}
} catch (SQLException e) {
logger.error("Ошибка получения истории", e);
}
java.util.Collections.reverse(history);
return history;
}
/**
* Удалить старую статистику (старше N дней)
*/
public void cleanOldStats(int daysToKeep) {
String sql = """
DELETE FROM counter_stats
WHERE date < date('now', '-' || ? || ' days')
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, daysToKeep);
int deleted = stmt.executeUpdate();
if (deleted > 0) {
logger.info("Удалено {} старых записей статистики", deleted);
}
} catch (SQLException e) {
logger.error("Ошибка очистки старой статистики", e);
}
}
}

View file

@ -0,0 +1,63 @@
package creator.loki.feature.economy;
import com.google.inject.Inject;
import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.economy.command.bank.BankCommand;
import creator.loki.feature.economy.command.core.BalanceCommand;
import creator.loki.feature.economy.command.core.BaltopCommand;
import creator.loki.feature.economy.command.core.DailyCommand;
import creator.loki.feature.economy.command.core.PayCommand;
import creator.loki.feature.economy.command.minigame.BlackjackCommand;
import creator.loki.feature.economy.command.minigame.FishCommand;
import creator.loki.feature.economy.command.minigame.JobCommand;
import creator.loki.feature.economy.command.minigame.SlotCommand;
import creator.loki.feature.economy.command.minigame.WorkCommand;
import creator.loki.feature.economy.command.social.RobCommand;
import creator.loki.feature.economy.service.BlackjackService;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.economy.service.FishingService;
import creator.loki.feature.economy.service.JobService;
import creator.loki.feature.economy.service.SlotService;
public class EconomyCommandRegistrar implements CommandRegistrar {
private final EconomyService economyService;
private final FishingService fishingService;
private final BlackjackService blackjackService;
private final SlotService slotService;
private final JobService jobService;
private final CooldownManager cooldownManager;
@Inject
public EconomyCommandRegistrar(
EconomyService economyService,
FishingService fishingService,
BlackjackService blackjackService,
SlotService slotService,
JobService jobService,
CooldownManager cooldownManager
) {
this.economyService = economyService;
this.fishingService = fishingService;
this.blackjackService = blackjackService;
this.slotService = slotService;
this.jobService = jobService;
this.cooldownManager = cooldownManager;
}
@Override
public void register(CommandRegistry registry) {
registry.register(new BalanceCommand(economyService));
registry.register(new BaltopCommand(economyService));
registry.register(new PayCommand(economyService));
registry.register(new DailyCommand(economyService));
registry.register(new BankCommand(economyService));
registry.register(new RobCommand(economyService, cooldownManager));
registry.register(new FishCommand(fishingService));
registry.register(new BlackjackCommand(blackjackService, economyService));
registry.register(new SlotCommand(slotService, economyService));
registry.register(new JobCommand(jobService));
registry.register(new WorkCommand(jobService));
}
}

View file

@ -0,0 +1,57 @@
package creator.loki.feature.economy;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import creator.loki.core.security.CooldownManager;
import creator.loki.database.Database;
import creator.loki.feature.economy.repository.EconomyRepository;
import creator.loki.feature.economy.repository.SqlEconomyRepository;
import creator.loki.feature.economy.service.BlackjackService;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.economy.service.FishingService;
import creator.loki.feature.economy.service.JobService;
import creator.loki.feature.economy.service.SlotService;
public class EconomyModule extends AbstractModule {
@Override
protected void configure() {
}
@Provides
@Singleton
EconomyRepository provideEconomyRepository(Database database) {
return new SqlEconomyRepository(database);
}
@Provides
@Singleton
EconomyService provideEconomyService(EconomyRepository repository, CooldownManager cooldownManager) {
return new EconomyService(repository, cooldownManager);
}
@Provides
@Singleton
FishingService provideFishingService(EconomyRepository repository) {
return new FishingService(repository);
}
@Provides
@Singleton
BlackjackService provideBlackjackService(EconomyRepository repository) {
return new BlackjackService(repository);
}
@Provides
@Singleton
SlotService provideSlotService(EconomyRepository repository) {
return new SlotService(repository);
}
@Provides
@Singleton
JobService provideJobService(EconomyRepository repository) {
return new JobService(repository);
}
}

View file

@ -0,0 +1,93 @@
package creator.loki.feature.economy.command.bank;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import java.util.List;
public class BankCommand implements Command {
private final EconomyService economyService;
public BankCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() { return "bank"; }
@Override
public String getDescription() { return "Управление банковским счётом"; }
@Override
public String getUsage() { return "bank [deposit|withdraw|balance] <amount>"; }
@Override
public String getCategory() { return "Экономика"; }
@Override
public void execute(CommandContext ctx) {
List<String> args = ctx.getArgs();
String uid = ctx.getAuthor().getId();
if (args.isEmpty() || args.get(0).equalsIgnoreCase("balance")) {
EconomyAccount acc = economyService.getAccount(uid);
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏦 Банк — " + ctx.getAuthor().getEffectiveName())
.withColor(new Color(52, 152, 219))
.addField("Наличные", "**" + acc.getCash() + "** LC", true)
.addField("Банк", "**" + acc.getBank() + "** LC", true)
.addField("Всего", "**" + acc.getTotalBalance() + "** LC", true)
.build()
).queue();
return;
}
String sub = args.get(0).toLowerCase();
if (!sub.equals("deposit") && !sub.equals("withdraw")) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Неизвестная подкоманда. Используй: " + getUsage())
).queue();
return;
}
if (args.size() < 2) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите сумму: bank " + sub + " <amount>")
).queue();
return;
}
long amount;
try {
amount = Long.parseLong(args.get(1));
} catch (NumberFormatException e) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть числом!")
).queue();
return;
}
if (amount <= 0) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!")
).queue();
return;
}
EconomyAccount acc = economyService.getAccount(uid);
boolean isDep = sub.equals("deposit");
long balance = isDep ? acc.getCash() : acc.getBank();
if (balance < amount) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка",
"Недостаточно " + (isDep ? "наличных" : "в банке") + "! У вас **" + balance + "** LC")
).queue();
return;
}
if (isDep) economyService.deposit(uid, amount);
else economyService.withdraw(uid, amount);
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.success(isDep ? "Депозит" : "Снятие",
"Ты " + (isDep ? "положил" : "снял") + " **" + amount + "** LC " + (isDep ? "в банк!" : "из банка!"))
).queue();
}
}

View file

@ -0,0 +1,57 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
public class BalanceCommand implements Command {
private final EconomyService economyService;
public BalanceCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "balance";
}
@Override
public String getDescription() {
return "Показать баланс пользователя";
}
@Override
public String getUsage() {
return "balance [@user]";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
EconomyAccount account = economyService.getAccount(target.getId());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💰 Баланс — " + target.getEffectiveName())
.withColor(new Color(255, 215, 0))
.addField("Наличные", "**" + account.getCash() + "** LC", true)
.addField("Банк", "**" + account.getBank() + "** LC", true)
.addField("Всего", "**" + account.getTotalBalance() + "** LC", true)
.build()
).queue();
}
}

View file

@ -0,0 +1,57 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.List;
public class BaltopCommand implements Command {
private final EconomyService economyService;
public BaltopCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "baltop";
}
@Override
public String getDescription() {
return "Топ 10 богатейших пользователей";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
List<EconomyAccount> top = economyService.getTopBalances(10);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < top.size(); i++) {
EconomyAccount acc = top.get(i);
String name = context.getGuild().getMemberById(acc.getUserId()) != null
? context.getGuild().getMemberById(acc.getUserId()).getEffectiveName()
: acc.getUserId();
sb.append("**").append(i + 1).append(".** ")
.append(name)
.append(" — **").append(acc.getTotalBalance()).append("** LC\n");
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏆 Топ богатейших")
.withDescription(sb.toString())
.withColor(new Color(255, 215, 0))
.build()
).queue();
}
}

View file

@ -0,0 +1,55 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
public class DailyCommand implements Command {
private final EconomyService economyService;
public DailyCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "daily";
}
@Override
public String getDescription() {
return "Получить ежедневную награду";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
EconomyService.DailyResult result = economyService.claimDaily(context.getAuthor().getId());
if (result == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Ежедневная награда",
"Ты уже получил награду сегодня! Возвращайся завтра.")
).queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎁 Ежедневная награда")
.withDescription("Ты получил **" + result.amount() + "** LC!")
.withColor(new Color(46, 204, 113))
.addField("Стрик", "**" + result.streak() + "** дней", true)
.addField("Баланс", "**" + result.newCash() + "** LC", true)
.build()
).queue();
}
}

View file

@ -0,0 +1,96 @@
package creator.loki.feature.economy.command.core;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.Member;
import java.util.List;
public class PayCommand implements Command {
private final EconomyService economyService;
public PayCommand(EconomyService economyService) {
this.economyService = economyService;
}
@Override
public String getName() {
return "pay";
}
@Override
public String getDescription() {
return "Перевести деньги другому пользователю";
}
@Override
public String getUsage() {
return "pay <@user> <amount>";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage())
).queue();
return;
}
Member target = mentioned.get(0);
if (target.getId().equals(context.getAuthor().getId())) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нельзя переводить деньги самому себе!")
).queue();
return;
}
List<String> args = context.getArgs();
if (args.size() < 2) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите сумму: " + getUsage())
).queue();
return;
}
long amount;
try {
amount = Long.parseLong(args.get(1));
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть числом!")
).queue();
return;
}
if (amount <= 0) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Сумма должна быть положительной!")
).queue();
return;
}
var senderAccount = economyService.getAccount(context.getAuthor().getId());
if (senderAccount.getCash() < amount) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Недостаточно средств! У вас **" + senderAccount.getCash() + "** LC")
).queue();
return;
}
economyService.transfer(context.getAuthor().getId(), target.getId(), amount);
context.getChannel().sendMessageEmbeds(
EmbedTemplates.success("Перевод выполнен",
context.getAuthor().getAsMention() + " перевёл **" + amount + "** LC " + target.getAsMention())
).queue();
}
}

View file

@ -0,0 +1,148 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.BlackjackService;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.MessageEmbed;
import java.awt.Color;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class BlackjackCommand implements Command {
private final BlackjackService blackjackService;
private final EconomyService economyService;
private final Set<String> activeGames = ConcurrentHashMap.newKeySet();
public BlackjackCommand(BlackjackService blackjackService, EconomyService economyService) {
this.blackjackService = blackjackService;
this.economyService = economyService;
}
@Override
public String getName() {
return "blackjack";
}
@Override
public String getDescription() {
return "Блэкджек — играй против дилера";
}
@Override
public String getUsage() {
return "bj <ставка> — начать игру\nbj hit — взять карту\nbj stand — остановиться";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Блэкджек", "Используй: `!bj <ставка>` чтобы начать")
).queue();
return;
}
var action = args.get(0).toLowerCase();
if (action.equals("hit")) {
handleAction(context, userId, true);
} else if (action.equals("stand")) {
handleAction(context, userId, false);
} else {
handleStart(context, userId, action);
}
}
private void handleStart(CommandContext context, String userId, String betStr) {
if (activeGames.contains(userId)) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "У тебя уже есть активная игра! Используй `!bj hit` или `!bj stand`")
).queue();
return;
}
long bet;
try {
bet = Long.parseLong(betStr);
if (bet <= 0) throw new NumberFormatException();
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажи положительное число в качестве ставки")
).queue();
return;
}
var account = economyService.getAccount(userId);
if (account.getCash() < bet) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC")
).queue();
return;
}
BlackjackService.BlackjackState state = blackjackService.startGame(userId, bet);
if (state == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Не удалось начать игру.")
).queue();
return;
}
if (!state.isOver()) activeGames.add(userId);
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
}
private void handleAction(CommandContext context, String userId, boolean isHit) {
if (!activeGames.contains(userId)) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нет активной игры! Начни с `!bj <ставка>`")
).queue();
return;
}
BlackjackService.BlackjackState state = isHit
? blackjackService.hit(userId)
: blackjackService.stand(userId);
if (state.isOver()) activeGames.remove(userId);
context.getChannel().sendMessageEmbeds(buildGameEmbed(state)).queue();
}
private MessageEmbed buildGameEmbed(BlackjackService.BlackjackState state) {
var embed = EmbedFactory.create()
.withTitle("🃏 Блэкджек")
.withColor(new Color(34, 139, 34))
.addField("Твои карты", formatCards(state.playerCards(), state.playerSum()), true);
if (state.isOver()) {
embed.addField("Карты дилера", formatCards(state.dealerCards(), state.dealerSum()), true)
.addField("Результат", formatResult(state), false);
} else {
embed.addField("Карты дилера", formatHiddenDealerCards(state.dealerCards()), true)
.withFooter("!bj hit — взять карту | !bj stand — остановиться");
}
return embed.addField("Ставка", state.bet() + " LC", false).build();
}
private String formatCards(String[] cards, int sum) {
return String.join(" + ", cards) + " = " + sum;
}
private String formatHiddenDealerCards(String[] cards) {
var sb = new StringBuilder("?");
for (int i = 1; i < cards.length; i++) sb.append(" + ").append(cards[i]);
return sb.toString();
}
private String formatResult(BlackjackService.BlackjackState state) {
long payout = state.payout();
if (payout > state.bet()) return "🎉 Блэкджек! +" + payout + " LC";
if (payout > 0) return "✅ Ты выиграл +" + payout + " LC";
if (payout == state.bet()) return "🤝 Ничья. Возврат " + payout + " LC";
return "💔 Ты проиграл " + state.bet() + " LC";
}
}

View file

@ -0,0 +1,106 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.FishingService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
import java.util.concurrent.ConcurrentHashMap;
public class FishCommand implements Command {
private static record FishSession(String difficulty, long startTime) {}
private final FishingService fishingService;
private final ConcurrentHashMap<String, FishSession> sessions = new ConcurrentHashMap<>();
public FishCommand(FishingService fishingService) {
this.fishingService = fishingService;
}
@Override
public String getName() {
return "fish";
}
@Override
public String getDescription() {
return "Рыбалка — угадай количество рыбы в пруду";
}
@Override
public String getUsage() {
return "fish <easy/normal/hard> — начать рыбалку\nfish <число> — ответить";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Рыбалка", "Используй: `!fish easy/normal/hard`")
).queue();
return;
}
var first = args.get(0).toLowerCase();
if (first.equals("easy") || first.equals("normal") || first.equals("hard")) {
FishingService.FishResult result = fishingService.startFishing(userId, first);
sessions.put(userId, new FishSession(first, System.currentTimeMillis()));
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎣 Рыбалка")
.withDescription("```\n" + String.join("\n", result.grid()) + "\n```")
.withColor(new Color(0, 150, 255))
.addField("Вопрос", "Сколько **" + result.targetFish() + "** ты видишь?", false)
.withFooter("Напиши !fish <число>, чтобы ответить")
.build()
).queue();
return;
}
try {
int answer = Integer.parseInt(first);
FishSession session = sessions.remove(userId);
if (session == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нет активной рыбалки! Начни с `!fish easy/normal/hard`")
).queue();
return;
}
FishingService.FishResult result = fishingService.checkAnswer(userId, answer, session.startTime(), session.difficulty());
var embed = EmbedFactory.create()
.withTitle("🎣 Рыбалка — Результат")
.withDescription("```\n" + String.join("\n", result.grid()) + "\n```")
.withColor(new Color(255, 215, 0));
if (result.reward() > 0) {
embed.addField("Твой ответ", String.valueOf(answer), true)
.addField("Правильный ответ", String.valueOf(result.count()), true)
.addField("Награда", "+" + result.reward() + " LC", true)
.addField("Новый баланс", result.cashAfter() + " LC", true);
} else {
embed.withDescription("❌ Неверно! Правильный ответ: **" + result.count() + "**\n\n```\n"
+ String.join("\n", result.grid()) + "\n```");
}
context.getChannel().sendMessageEmbeds(embed.build()).queue();
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Неверный аргумент. Используй `!fish easy/normal/hard` или `!fish <число>`")
).queue();
}
}
}

View file

@ -0,0 +1,146 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.JobService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
public class JobCommand implements Command {
private final JobService jobService;
public JobCommand(JobService jobService) {
this.jobService = jobService;
}
@Override
public String getName() {
return "job";
}
@Override
public String getDescription() {
return "Управление работой — список, присоединиться";
}
@Override
public String getUsage() {
return "job — текущая работа\njob list — список работ\njob join <название> — устроиться";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
handleCurrent(context, userId);
} else if (args.get(0).equalsIgnoreCase("list")) {
handleList(context);
} else if (args.get(0).equalsIgnoreCase("join")) {
if (args.size() < 2) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажи название работы: `!job join <название>`")
).queue();
return;
}
handleJoin(context, userId, args.get(1));
} else {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Работа", "Используй: `!job`, `!job list`, `!job join <название>`")
).queue();
}
}
private void handleCurrent(CommandContext context, String userId) {
String current = jobService.getCurrentJob(userId);
if (current == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.info("Работа", "У тебя нет работы. Используй `!job list` чтобы посмотреть доступные.")
).queue();
return;
}
JobService.JobInfo info = findJob(current);
if (info == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Работа", "Текущая работа: **" + current + "**")
).queue();
return;
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💼 Твоя работа")
.withColor(new Color(100, 200, 255))
.addField("Должность", info.emoji() + " " + info.displayName(), false)
.addField("Зарплата", info.minPayout() + " – " + info.maxPayout() + " LC", true)
.addField("Требуемый уровень", String.valueOf(info.requiredLevel()), true)
.build()
).queue();
}
private void handleList(CommandContext context) {
var jobs = jobService.listJobs();
var desc = new StringBuilder();
for (var job : jobs) {
desc.append(job.emoji()).append(" **").append(job.displayName())
.append("** — ").append(job.minPayout()).append("–").append(job.maxPayout())
.append(" LC (ур. ").append(job.requiredLevel()).append(")\n");
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("📋 Доступные работы")
.withDescription(desc.toString())
.withColor(new Color(100, 200, 255))
.withFooter("!job join <название> — устроиться на работу")
.build()
).queue();
}
private void handleJoin(CommandContext context, String userId, String name) {
var jobs = jobService.listJobs();
JobService.JobInfo target = null;
for (var job : jobs) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
target = job;
break;
}
}
if (target == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Работа **" + name + "** не найдена. Используй `!job list`")
).queue();
return;
}
jobService.setJob(userId, target.name());
context.getChannel().sendMessageEmbeds(
EmbedTemplates.success("Устройство на работу",
"Ты устроился на работу **" + target.emoji() + " " + target.displayName() + "**!\n"
+ "Используй `!work` чтобы начать работать.")
).queue();
}
private JobService.JobInfo findJob(String name) {
for (var job : jobService.listJobs()) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
return job;
}
}
return null;
}
}

View file

@ -0,0 +1,96 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.feature.economy.service.SlotService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
public class SlotCommand implements Command {
private final SlotService slotService;
private final EconomyService economyService;
public SlotCommand(SlotService slotService, EconomyService economyService) {
this.slotService = slotService;
this.economyService = economyService;
}
@Override
public String getName() {
return "slot";
}
@Override
public String getDescription() {
return "Крути слот-машину и выигрывай";
}
@Override
public String getUsage() {
return "slot <ставка>";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var args = context.getArgs();
var userId = context.getAuthor().getId();
if (args.isEmpty()) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Слоты", "Используй: `!slot <ставка>`")
).queue();
return;
}
long bet;
try {
bet = Long.parseLong(args.get(0));
if (bet <= 0) throw new NumberFormatException();
} catch (NumberFormatException e) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Ставка должна быть положительным числом!")
).queue();
return;
}
var account = economyService.getAccount(userId);
if (account.getCash() < bet) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Недостаточно средств! Твой баланс: " + account.getCash() + " LC")
).queue();
return;
}
SlotService.SlotResult result = slotService.spin(userId, bet);
var comboDisplay = result.emoji1() + " | " + result.emoji2() + " | " + result.emoji3();
var desc = new StringBuilder("**" + comboDisplay + "**\n");
if (result.multiplier() > 0) {
desc.append("\n🎰 **").append(result.comboName()).append("!**")
.append(" x").append(result.multiplier())
.append("\n\n+**").append(result.winAmount()).append("** LC");
} else {
desc.append("\n\n😔 Повезёт в следующий раз!");
}
desc.append("\n\nБаланс: **").append(result.cashAfter()).append("** LC");
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎰 Слот-машина")
.withDescription(desc.toString())
.withColor(new Color(255, 215, 0))
.build()
).queue();
}
}

View file

@ -0,0 +1,78 @@
package creator.loki.feature.economy.command.minigame;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.feature.economy.service.JobService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import java.awt.Color;
public class WorkCommand implements Command {
private final JobService jobService;
public WorkCommand(JobService jobService) {
this.jobService = jobService;
}
@Override
public String getName() {
return "work";
}
@Override
public String getDescription() {
return "Работать и зарабатывать деньги";
}
@Override
public String getCategory() {
return "Экономика";
}
@Override
public void execute(CommandContext context) {
var userId = context.getAuthor().getId();
String currentJob = jobService.getCurrentJob(userId);
if (currentJob == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.warning("Работа", "У тебя нет работы! Используй `!job list` чтобы выбрать.")
).queue();
return;
}
JobService.JobInfo info = findJob(currentJob);
JobService.WorkResult result = jobService.work(userId, 0);
if (result == null) {
context.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "⏳ Ты уже работал сегодня! Отдохни и возвращайся завтра.")
).queue();
return;
}
String emoji = info != null ? info.emoji() : "💼";
String jobName = info != null ? info.displayName() : currentJob;
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💼 Работа завершена")
.withColor(new Color(100, 200, 255))
.addField("Должность", emoji + " " + jobName, false)
.addField("Заработано", "+**" + result.amount() + "** LC", true)
.addField("Новый баланс", "**" + result.newCash() + "** LC", true)
.build()
).queue();
}
private JobService.JobInfo findJob(String name) {
for (var job : jobService.listJobs()) {
if (job.name().equalsIgnoreCase(name) || job.displayName().equalsIgnoreCase(name)) {
return job;
}
}
return null;
}
}

View file

@ -0,0 +1,99 @@
package creator.loki.feature.economy.command.social;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.economy.service.EconomyService;
import creator.loki.embed.EmbedFactory;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;
public class RobCommand implements Command {
private final EconomyService economyService;
private final CooldownManager cooldownManager;
private final Random random;
public RobCommand(EconomyService economyService, CooldownManager cooldownManager) {
this.economyService = economyService;
this.cooldownManager = cooldownManager;
this.random = ThreadLocalRandom.current();
}
@Override
public String getName() { return "rob"; }
@Override
public String getDescription() { return "Ограбить пользователя"; }
@Override
public String getUsage() { return "rob <@user>"; }
@Override
public String getCategory() { return "Экономика"; }
@Override
public void execute(CommandContext ctx) {
List<Member> mentioned = ctx.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Укажите пользователя: " + getUsage())
).queue();
return;
}
Member target = mentioned.get(0);
String uid = ctx.getAuthor().getId();
String tid = target.getId();
if (tid.equals(uid)) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "Нельзя ограбить самого себя!")
).queue();
return;
}
if (cooldownManager.isOnCooldown(uid, "rob")) {
long rem = cooldownManager.getRemainingSeconds(uid, "rob");
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.warning("Кдн",
"Подожди ещё **" + (rem / 3600) + "ч " + ((rem % 3600) / 60) + "м**")
).queue();
return;
}
cooldownManager.applyCooldown(uid, "rob", 86400);
var sender = economyService.getAccount(uid);
var victim = economyService.getAccount(tid);
if (sender.getCash() <= 0 || victim.getCash() <= 0) {
ctx.getChannel().sendMessageEmbeds(
EmbedTemplates.error("Ошибка", "У одного из вас нет наличных!")
).queue();
return;
}
if (random.nextDouble() < 0.4) {
long steal = Math.min(Math.min((long) (victim.getCash() * 0.15), 10000L), victim.getCash());
if (steal <= 0) {
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.info("Ограбление", "Слишком мелкая цель...")).queue();
return;
}
economyService.transfer(tid, uid, steal);
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🔫 Ограбление удалось!")
.withDescription("Ты украл **" + steal + "** LC у " + target.getAsMention())
.withColor(new Color(46, 204, 113))
.build()
).queue();
} else {
long penalty = Math.min(Math.min((long) (sender.getCash() * 0.10), 5000L), sender.getCash());
if (penalty > 0) economyService.transfer(uid, tid, penalty);
ctx.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("👮 Ограбление провалено!")
.withDescription("Ты заплатил **" + penalty + "** LC " + target.getAsMention())
.withColor(new Color(231, 76, 60))
.build()
).queue();
}
}
}

View file

@ -0,0 +1,151 @@
package creator.loki.feature.economy.domain;
import java.util.Objects;
public class EconomyAccount {
private final String userId;
private final long cash;
private final long bank;
private final long totalEarned;
private final long totalSpent;
private final String lastDaily;
private final int dailyStreak;
private final int fishingLuck;
private final int efficiency;
private final String lastFish;
private final String job;
private final String lastWork;
public EconomyAccount(String userId) {
this.userId = userId;
this.cash = 0;
this.bank = 0;
this.totalEarned = 0;
this.totalSpent = 0;
this.lastDaily = null;
this.dailyStreak = 0;
this.fishingLuck = 0;
this.efficiency = 0;
this.lastFish = null;
this.job = null;
this.lastWork = null;
}
private EconomyAccount(Builder builder) {
this.userId = builder.userId;
this.cash = builder.cash;
this.bank = builder.bank;
this.totalEarned = builder.totalEarned;
this.totalSpent = builder.totalSpent;
this.lastDaily = builder.lastDaily;
this.dailyStreak = builder.dailyStreak;
this.fishingLuck = builder.fishingLuck;
this.efficiency = builder.efficiency;
this.lastFish = builder.lastFish;
this.job = builder.job;
this.lastWork = builder.lastWork;
}
public String getUserId() { return userId; }
public long getCash() { return cash; }
public long getBank() { return bank; }
public long getTotalEarned() { return totalEarned; }
public long getTotalSpent() { return totalSpent; }
public String getLastDaily() { return lastDaily; }
public int getDailyStreak() { return dailyStreak; }
public int getFishingLuck() { return fishingLuck; }
public int getEfficiency() { return efficiency; }
public String getLastFish() { return lastFish; }
public String getJob() { return job; }
public String getLastWork() { return lastWork; }
public long getTotalBalance() { return cash + bank; }
public EconomyAccount withCash(long cash) {
return new Builder(this).cash(cash).build();
}
public EconomyAccount withBank(long bank) {
return new Builder(this).bank(bank).build();
}
public EconomyAccount withAddedEarned(long amount) {
return new Builder(this).totalEarned(this.totalEarned + amount).build();
}
public EconomyAccount withAddedSpent(long amount) {
return new Builder(this).totalSpent(this.totalSpent + amount).build();
}
public EconomyAccount withFishingLuck(int level) {
return new Builder(this).fishingLuck(level).build();
}
public EconomyAccount withEfficiency(int level) {
return new Builder(this).efficiency(level).build();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EconomyAccount that = (EconomyAccount) o;
return Objects.equals(userId, that.userId);
}
@Override
public int hashCode() { return Objects.hash(userId); }
@Override
public String toString() {
return String.format("EconomyAccount[%s: Cash %d, Bank %d]",
userId, cash, bank);
}
public static class Builder {
private String userId;
private long cash;
private long bank;
private long totalEarned;
private long totalSpent;
private String lastDaily;
private int dailyStreak;
private int fishingLuck;
private int efficiency;
private String lastFish;
private String job;
private String lastWork;
public Builder() {}
public Builder(EconomyAccount existing) {
this.userId = existing.userId;
this.cash = existing.cash;
this.bank = existing.bank;
this.totalEarned = existing.totalEarned;
this.totalSpent = existing.totalSpent;
this.lastDaily = existing.lastDaily;
this.dailyStreak = existing.dailyStreak;
this.fishingLuck = existing.fishingLuck;
this.efficiency = existing.efficiency;
this.lastFish = existing.lastFish;
this.job = existing.job;
this.lastWork = existing.lastWork;
}
public Builder userId(String userId) { this.userId = userId; return this; }
public Builder cash(long cash) { this.cash = cash; return this; }
public Builder bank(long bank) { this.bank = bank; return this; }
public Builder totalEarned(long totalEarned) { this.totalEarned = totalEarned; return this; }
public Builder totalSpent(long totalSpent) { this.totalSpent = totalSpent; return this; }
public Builder lastDaily(String lastDaily) { this.lastDaily = lastDaily; return this; }
public Builder dailyStreak(int dailyStreak) { this.dailyStreak = dailyStreak; return this; }
public Builder fishingLuck(int fishingLuck) { this.fishingLuck = fishingLuck; return this; }
public Builder efficiency(int efficiency) { this.efficiency = efficiency; return this; }
public Builder lastFish(String lastFish) { this.lastFish = lastFish; return this; }
public Builder job(String job) { this.job = job; return this; }
public Builder lastWork(String lastWork) { this.lastWork = lastWork; return this; }
public EconomyAccount build() { return new EconomyAccount(this); }
}
}

View file

@ -0,0 +1,9 @@
package creator.loki.feature.economy.domain;
public record EconomyTransaction(
String fromId,
String toId,
long amount,
String type,
long timestamp
) {}

View file

@ -0,0 +1,39 @@
package creator.loki.feature.economy.domain;
public enum Job {
WOODCUTTER("Лесоруб", 0, 15, 30, "\uD83E\uDE93"),
MINER("Шахтёр", 0, 20, 40, "\u26CF\uFE0F"),
FISHERMAN("Рыбак", 0, 25, 50, "\uD83C\uDFA3"),
COOK("Повар", 15, 30, 60, "\uD83C\uDF73"),
PROGRAMMER("Программист", 20, 40, 80, "\uD83D\uDCBB"),
ASTRONAUT("Космонавт", 35, 50, 100, "\uD83E\uDEB8");
private final String displayName;
private final int requiredLevel;
private final int minPayout;
private final int maxPayout;
private final String emoji;
Job(String displayName, int requiredLevel, int minPayout, int maxPayout, String emoji) {
this.displayName = displayName;
this.requiredLevel = requiredLevel;
this.minPayout = minPayout;
this.maxPayout = maxPayout;
this.emoji = emoji;
}
public String getDisplayName() { return displayName; }
public int getRequiredLevel() { return requiredLevel; }
public int getMinPayout() { return minPayout; }
public int getMaxPayout() { return maxPayout; }
public String getEmoji() { return emoji; }
public static Job fromName(String name) {
for (Job j : values()) {
if (j.name().equalsIgnoreCase(name) || j.getDisplayName().equalsIgnoreCase(name)) {
return j;
}
}
return null;
}
}

View file

@ -0,0 +1,31 @@
package creator.loki.feature.economy.domain;
public enum SlotCombination {
CHERRY(new String[]{"\uD83C\uDF52", "\uD83C\uDF52", "\uD83C\uDF52"}, 3, 10.0),
BELL(new String[]{"\uD83D\uDD14", "\uD83D\uDD14", "\uD83D\uDD14"}, 5, 5.0),
DIAMOND(new String[]{"\uD83D\uDC8E", "\uD83D\uDC8E", "\uD83D\uDC8E"}, 10, 2.0),
CROWN(new String[]{"\uD83D\uDC51", "\uD83D\uDC51", "\uD83D\uDC51"}, 15, 1.0),
JACKPOT(new String[]{"\u26A1", "\u26A1", "\u26A1"}, 50, 0.1);
private final String[] emojis;
private final int multiplier;
private final double chance;
SlotCombination(String[] emojis, int multiplier, double chance) {
this.emojis = emojis;
this.multiplier = multiplier;
this.chance = chance;
}
public String[] getEmojis() { return emojis; }
public int getMultiplier() { return multiplier; }
public double getChance() { return chance; }
public static SlotCombination match(String e1, String e2, String e3) {
if (!e1.equals(e2) || !e2.equals(e3)) return null;
for (SlotCombination sc : values()) {
if (sc.emojis[0].equals(e1)) return sc;
}
return null;
}
}

View file

@ -0,0 +1,17 @@
package creator.loki.feature.economy.repository;
import creator.loki.feature.economy.domain.EconomyAccount;
import java.util.List;
import java.util.Optional;
public interface EconomyRepository {
Optional<EconomyAccount> findById(String userId);
void save(EconomyAccount account);
boolean exists(String userId);
List<EconomyAccount> findTopByBalance(int limit);
default EconomyAccount getAccount(String userId) {
return findById(userId).orElse(new EconomyAccount(userId));
}
}

View file

@ -0,0 +1,165 @@
package creator.loki.feature.economy.repository;
import creator.loki.database.Database;
import creator.loki.feature.economy.domain.EconomyAccount;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
public class SqlEconomyRepository implements EconomyRepository {
private static final Logger logger = LoggerFactory.getLogger(SqlEconomyRepository.class);
private static final int MAX_TOP = 100;
private final Database database;
public SqlEconomyRepository(Database database) {
this.database = database;
}
@Override
public Optional<EconomyAccount> findById(String userId) {
String sql = """
SELECT user_id, cash, bank, total_earned, total_spent,
last_daily, daily_streak, fishing_luck, efficiency,
last_fish, job, last_work
FROM economy
WHERE user_id = ?
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) {
if (rs.next()) {
return Optional.of(mapResultSetToAccount(rs));
}
}
} catch (SQLException e) {
logger.error("Error fetching economy account for {}", userId, e);
}
return Optional.empty();
}
@Override
public void save(EconomyAccount account) {
String sql = """
INSERT INTO economy (user_id, cash, bank, total_earned, total_spent,
last_daily, daily_streak, fishing_luck, efficiency,
last_fish, job, last_work)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(user_id)
DO UPDATE SET
cash = excluded.cash,
bank = excluded.bank,
total_earned = excluded.total_earned,
total_spent = excluded.total_spent,
last_daily = excluded.last_daily,
daily_streak = excluded.daily_streak,
fishing_luck = excluded.fishing_luck,
efficiency = excluded.efficiency,
last_fish = excluded.last_fish,
job = excluded.job,
last_work = excluded.last_work
""";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, account.getUserId());
stmt.setLong(2, account.getCash());
stmt.setLong(3, account.getBank());
stmt.setLong(4, account.getTotalEarned());
stmt.setLong(5, account.getTotalSpent());
if (account.getLastDaily() != null) {
stmt.setString(6, account.getLastDaily());
} else {
stmt.setNull(6, java.sql.Types.VARCHAR);
}
stmt.setInt(7, account.getDailyStreak());
stmt.setInt(8, account.getFishingLuck());
stmt.setInt(9, account.getEfficiency());
if (account.getLastFish() != null) {
stmt.setString(10, account.getLastFish());
} else {
stmt.setNull(10, java.sql.Types.VARCHAR);
}
if (account.getJob() != null) {
stmt.setString(11, account.getJob());
} else {
stmt.setNull(11, java.sql.Types.VARCHAR);
}
if (account.getLastWork() != null) {
stmt.setString(12, account.getLastWork());
} else {
stmt.setNull(12, java.sql.Types.VARCHAR);
}
stmt.executeUpdate();
} catch (SQLException e) {
logger.error("Error saving economy account for {}", account.getUserId(), e);
}
}
@Override
public boolean exists(String userId) {
String sql = "SELECT 1 FROM economy WHERE user_id = ?";
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setString(1, userId);
try (ResultSet rs = stmt.executeQuery()) {
return rs.next();
}
} catch (SQLException e) {
logger.error("Error checking economy existence for {}", userId, e);
}
return false;
}
@Override
public List<EconomyAccount> findTopByBalance(int limit) {
int lim = clampLimit(limit);
String sql = """
SELECT user_id, cash, bank, total_earned, total_spent,
last_daily, daily_streak, fishing_luck, efficiency,
last_fish, job, last_work
FROM economy
ORDER BY (cash + bank) DESC, cash DESC, user_id ASC
LIMIT ?
""";
List<EconomyAccount> list = new ArrayList<>();
try (PreparedStatement stmt = database.prepare(sql)) {
stmt.setInt(1, lim);
try (ResultSet rs = stmt.executeQuery()) {
while (rs.next()) {
list.add(mapResultSetToAccount(rs));
}
}
} catch (SQLException e) {
logger.error("Error fetching top by balance", e);
}
return Collections.unmodifiableList(list);
}
private static int clampLimit(int limit) {
if (limit <= 0) return 1;
return Math.min(limit, MAX_TOP);
}
private EconomyAccount mapResultSetToAccount(ResultSet rs) throws SQLException {
return new EconomyAccount.Builder()
.userId(rs.getString("user_id"))
.cash(rs.getLong("cash"))
.bank(rs.getLong("bank"))
.totalEarned(rs.getLong("total_earned"))
.totalSpent(rs.getLong("total_spent"))
.lastDaily(rs.getString("last_daily"))
.dailyStreak(rs.getInt("daily_streak"))
.fishingLuck(rs.getInt("fishing_luck"))
.efficiency(rs.getInt("efficiency"))
.lastFish(rs.getString("last_fish"))
.job(rs.getString("job"))
.lastWork(rs.getString("last_work"))
.build();
}
}

View file

@ -0,0 +1,139 @@
package creator.loki.feature.economy.service;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class BlackjackService {
private static final Logger logger = LoggerFactory.getLogger(BlackjackService.class);
private static final String[] DECK = {"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"};
private final EconomyRepository repository;
private final ConcurrentHashMap<String, ActiveBlackjack> activeGames = new ConcurrentHashMap<>();
public BlackjackService(EconomyRepository repository) {
this.repository = repository;
}
private record ActiveBlackjack(List<String> playerCards, List<String> dealerCards, long bet, String userId) {}
public BlackjackState startGame(String userId, long bet) {
if (bet <= 0 || activeGames.containsKey(userId)) return null;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < bet) return null;
repository.save(account.withCash(account.getCash() - bet));
List<String> playerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
List<String> dealerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
int playerSum = calculateSum(playerCards);
int dealerSum = calculateSum(dealerCards);
boolean playerBj = playerSum == 21;
boolean dealerBj = dealerSum == 21;
if (playerBj || dealerBj) {
long payout;
if (playerBj && !dealerBj) {
payout = (long) (bet * 2.5);
} else {
payout = 0;
}
repository.save(repository.getAccount(userId).withCash(repository.getAccount(userId).getCash() + payout));
return new BlackjackState(
playerCards.toArray(String[]::new), dealerCards.toArray(String[]::new),
playerSum, dealerSum, bet, true, payout
);
}
ActiveBlackjack game = new ActiveBlackjack(playerCards, dealerCards, bet, userId);
activeGames.put(userId, game);
return new BlackjackState(
playerCards.toArray(String[]::new), dealerCards.toArray(String[]::new),
playerSum, dealerSum, bet, false, 0
);
}
public BlackjackState hit(String userId) {
ActiveBlackjack game = activeGames.get(userId);
if (game == null) return null;
game.playerCards().add(drawCard());
int sum = calculateSum(game.playerCards());
if (sum > 21) {
activeGames.remove(userId);
return new BlackjackState(
game.playerCards().toArray(String[]::new), game.dealerCards().toArray(String[]::new),
sum, calculateSum(game.dealerCards()), game.bet(), true, 0
);
}
return new BlackjackState(
game.playerCards().toArray(String[]::new), game.dealerCards().toArray(String[]::new),
sum, calculateSum(game.dealerCards()), game.bet(), false, 0
);
}
public BlackjackState stand(String userId) {
ActiveBlackjack game = activeGames.remove(userId);
if (game == null) return null;
List<String> dealerCards = new ArrayList<>(game.dealerCards());
while (calculateSum(dealerCards) < 16) {
dealerCards.add(drawCard());
}
int playerSum = calculateSum(game.playerCards());
int dealerSum = calculateSum(dealerCards);
long payout;
if (dealerSum > 21 || playerSum > dealerSum) {
payout = game.bet() * 2;
} else {
payout = 0;
}
EconomyAccount account = repository.getAccount(userId);
repository.save(account.withCash(account.getCash() + payout));
return new BlackjackState(
game.playerCards().toArray(String[]::new), dealerCards.toArray(String[]::new),
playerSum, dealerSum, game.bet(), true, payout
);
}
private String drawCard() {
return DECK[ThreadLocalRandom.current().nextInt(DECK.length)];
}
private int calculateSum(List<String> cards) {
int sum = 0;
int aces = 0;
for (String c : cards) {
switch (c) {
case "J", "Q", "K" -> sum += 10;
case "A" -> { sum += 11; aces++; }
default -> sum += Integer.parseInt(c);
}
}
while (sum > 21 && aces > 0) {
sum -= 10;
aces--;
}
return sum;
}
public record BlackjackState(String[] playerCards, String[] dealerCards, int playerSum, int dealerSum, long bet, boolean isOver, long payout) {}
public record BlackjackResult(boolean playerWon, long payout, String reason) {}
}

View file

@ -0,0 +1,118 @@
package creator.loki.feature.economy.service;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.LocalDate;
import java.util.List;
public class EconomyService {
private static final Logger logger = LoggerFactory.getLogger(EconomyService.class);
private final EconomyRepository repository;
private final CooldownManager cooldownManager;
public EconomyService(EconomyRepository repository, CooldownManager cooldownManager) {
this.repository = repository;
this.cooldownManager = cooldownManager;
}
public EconomyAccount getAccount(String userId) {
return repository.getAccount(userId);
}
public boolean transfer(String fromId, String toId, long amount) {
if (amount <= 0) return false;
EconomyAccount from = repository.getAccount(fromId);
if (from.getCash() < amount) return false;
EconomyAccount to = repository.getAccount(toId);
repository.save(from.withCash(from.getCash() - amount));
repository.save(to.withCash(to.getCash() + amount));
return true;
}
public boolean deposit(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < amount) return false;
repository.save(account.withCash(account.getCash() - amount).withBank(account.getBank() + amount));
return true;
}
public boolean withdraw(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getBank() < amount) return false;
repository.save(account.withBank(account.getBank() - amount).withCash(account.getCash() + amount));
return true;
}
public long earnCash(String userId, long amount) {
EconomyAccount account = repository.getAccount(userId);
EconomyAccount updated = account.withCash(account.getCash() + amount).withAddedEarned(amount);
repository.save(updated);
return updated.getCash();
}
public boolean spendCash(String userId, long amount) {
if (amount <= 0) return false;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < amount) return false;
repository.save(account.withCash(account.getCash() - amount).withAddedSpent(amount));
return true;
}
public DailyResult claimDaily(String userId) {
EconomyAccount account = repository.getAccount(userId);
String today = LocalDate.now().toString();
if (today.equals(account.getLastDaily())) return null;
String yesterday = LocalDate.now().minusDays(1).toString();
int streak;
if (yesterday.equals(account.getLastDaily())) {
streak = account.getDailyStreak() + 1;
} else {
streak = 1;
}
long amount = streak >= 7 ? (long) (50 * 1.5) : 50;
long newCash = account.getCash() + amount;
EconomyAccount updated = new EconomyAccount.Builder(account)
.cash(newCash)
.totalEarned(account.getTotalEarned() + amount)
.lastDaily(today)
.dailyStreak(streak)
.build();
repository.save(updated);
return new DailyResult(amount, streak, newCash);
}
public int upgradeFishingLuck(String userId) {
EconomyAccount account = repository.getAccount(userId);
if (account.getFishingLuck() >= 5) return -1;
long cost = 800L * (account.getFishingLuck() + 1);
if (account.getCash() < cost) return -1;
int newLevel = account.getFishingLuck() + 1;
repository.save(account.withCash(account.getCash() - cost).withFishingLuck(newLevel).withAddedSpent(cost));
return newLevel;
}
public int upgradeEfficiency(String userId) {
EconomyAccount account = repository.getAccount(userId);
if (account.getEfficiency() >= 5) return -1;
long cost = 800L * (account.getEfficiency() + 1);
if (account.getCash() < cost) return -1;
int newLevel = account.getEfficiency() + 1;
repository.save(account.withCash(account.getCash() - cost).withEfficiency(newLevel).withAddedSpent(cost));
return newLevel;
}
public List<EconomyAccount> getTopBalances(int limit) {
return repository.findTopByBalance(limit);
}
public record DailyResult(long amount, int streak, long newCash) {}
}

View file

@ -0,0 +1,120 @@
package creator.loki.feature.economy.service;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
public class FishingService {
private static final Logger logger = LoggerFactory.getLogger(FishingService.class);
private static final String[] FISH = {"\uD83D\uDC1F", "\uD83D\uDC20", "\uD83D\uDC21", "\uD83D\uDC19"};
private static final String WAVE = "\uD83C\uDF0A";
private static final long COOLDOWN_MS = 7200000;
private final EconomyRepository repository;
private final ConcurrentHashMap<String, ActiveFish> activeFishing = new ConcurrentHashMap<>();
public FishingService(EconomyRepository repository) {
this.repository = repository;
}
private record ActiveFish(String targetFish, int correctCount, long startTime, String difficulty, String[] grid) {}
public FishResult startFishing(String userId, String difficulty) {
EconomyAccount account = repository.getAccount(userId);
if (account.getLastFish() != null) {
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastFish());
if (elapsed < COOLDOWN_MS) return null;
}
int size;
int fishTypes;
int baseReward;
long timeLimit;
switch (difficulty.toLowerCase()) {
case "easy" -> { size = 5; fishTypes = 2; baseReward = 15; timeLimit = 30000; }
case "normal" -> { size = 7; fishTypes = 3; baseReward = 30; timeLimit = 45000; }
case "hard" -> { size = 9; fishTypes = 4; baseReward = 50; timeLimit = 60000; }
default -> { return null; }
}
String[] available = Arrays.copyOf(FISH, fishTypes);
String[][] grid = new String[size][size];
int[] counts = new int[fishTypes];
var rng = ThreadLocalRandom.current();
for (int r = 0; r < size; r++) {
for (int c = 0; c < size; c++) {
if (rng.nextInt(100) < 40) {
grid[r][c] = WAVE;
} else {
int fi = rng.nextInt(fishTypes);
grid[r][c] = available[fi];
counts[fi]++;
}
}
}
int targetIdx;
do {
targetIdx = rng.nextInt(fishTypes);
} while (counts[targetIdx] == 0);
String targetFish = available[targetIdx];
int correctCount = counts[targetIdx];
String[] rows = new String[size];
for (int r = 0; r < size; r++) {
rows[r] = String.join(" ", grid[r]);
}
ActiveFish session = new ActiveFish(targetFish, correctCount, System.currentTimeMillis(), difficulty, rows);
activeFishing.put(userId, session);
return new FishResult(rows, targetFish, correctCount, 0, account.getCash());
}
public FishResult checkAnswer(String userId, int answer, long startTime, String difficulty) {
ActiveFish session = activeFishing.remove(userId);
if (session == null) return null;
if (session.startTime() != startTime || !session.difficulty().equals(difficulty)) return null;
EconomyAccount account = repository.getAccount(userId);
long elapsed = System.currentTimeMillis() - session.startTime();
int baseReward;
long timeLimit;
switch (difficulty.toLowerCase()) {
case "easy" -> { baseReward = 15; timeLimit = 30000; }
case "normal" -> { baseReward = 30; timeLimit = 45000; }
case "hard" -> { baseReward = 50; timeLimit = 60000; }
default -> { return null; }
}
long reward = 0;
if (answer == session.correctCount() && elapsed <= timeLimit) {
double ratio = (double) elapsed / timeLimit;
double speedMult = ratio < 0.3 ? 2.0 : ratio < 0.5 ? 1.5 : 1.0;
reward = (long) (baseReward * speedMult * (1 + account.getFishingLuck() * 0.01));
}
long newCash = account.getCash() + reward;
EconomyAccount updated = new EconomyAccount.Builder(account)
.cash(newCash)
.totalEarned(account.getTotalEarned() + reward)
.lastFish(String.valueOf(System.currentTimeMillis()))
.build();
repository.save(updated);
return new FishResult(session.grid(), session.targetFish(), session.correctCount(), reward, newCash);
}
public record FishResult(String[] grid, String targetFish, int count, long reward, long cashAfter) {}
}

View file

@ -0,0 +1,91 @@
package creator.loki.feature.economy.service;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.Job;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
public class JobService {
private static final Logger logger = LoggerFactory.getLogger(JobService.class);
private static final long WORK_COOLDOWN = 21600000;
private final EconomyRepository repository;
public JobService(EconomyRepository repository) {
this.repository = repository;
}
public void setJob(String userId, String jobName) {
Job job = Job.fromName(jobName);
if (job == null) return;
EconomyAccount account = repository.getAccount(userId);
EconomyAccount updated = new EconomyAccount.Builder(account)
.job(job.name())
.build();
repository.save(updated);
}
public WorkResult work(String userId, int userLevel) {
EconomyAccount account = repository.getAccount(userId);
String jobName = account.getJob();
if (jobName == null) return null;
Job job = Job.fromName(jobName);
if (job == null) return null;
if (userLevel < job.getRequiredLevel()) return null;
if (account.getLastWork() != null) {
long elapsed = System.currentTimeMillis() - Long.parseLong(account.getLastWork());
if (elapsed < WORK_COOLDOWN) return null;
}
var rng = ThreadLocalRandom.current();
long basePayout = rng.nextLong(job.getMinPayout(), job.getMaxPayout() + 1);
long amount = (long) (basePayout * (1 + account.getEfficiency() * 0.01));
long newCash = account.getCash() + amount;
EconomyAccount updated = new EconomyAccount.Builder(account)
.cash(newCash)
.totalEarned(account.getTotalEarned() + amount)
.lastWork(String.valueOf(System.currentTimeMillis()))
.build();
repository.save(updated);
return new WorkResult(amount, newCash);
}
public List<JobInfo> listJobs() {
List<JobInfo> infos = new ArrayList<>();
for (Job job : Job.values()) {
infos.add(new JobInfo(
job.name(),
job.getDisplayName(),
job.getRequiredLevel(),
job.getMinPayout(),
job.getMaxPayout(),
job.getEmoji()
));
}
return infos;
}
public String getCurrentJob(String userId) {
EconomyAccount account = repository.getAccount(userId);
String jobName = account.getJob();
if (jobName == null) return null;
Job job = Job.fromName(jobName);
return job != null ? job.getDisplayName() : null;
}
public record WorkResult(long amount, long newCash) {}
public record JobInfo(String name, String displayName, int requiredLevel, int minPayout, int maxPayout, String emoji) {}
}

View file

@ -0,0 +1,60 @@
package creator.loki.feature.economy.service;
import creator.loki.feature.economy.domain.EconomyAccount;
import creator.loki.feature.economy.domain.SlotCombination;
import creator.loki.feature.economy.repository.EconomyRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.ThreadLocalRandom;
public class SlotService {
private static final Logger logger = LoggerFactory.getLogger(SlotService.class);
private static final String[] EMOJIS = {"\uD83C\uDF52", "\uD83D\uDD14", "\uD83D\uDC8E", "\uD83D\uDC51", "\u26A1"};
private static final int[] WEIGHTS = {40, 25, 18, 12, 5};
private static final int TOTAL_WEIGHT = 100;
private final EconomyRepository repository;
public SlotService(EconomyRepository repository) {
this.repository = repository;
}
public SlotResult spin(String userId, long bet) {
if (bet <= 0) return null;
EconomyAccount account = repository.getAccount(userId);
if (account.getCash() < bet) return null;
repository.save(account.withCash(account.getCash() - bet));
String e1 = spinEmoji();
String e2 = spinEmoji();
String e3 = spinEmoji();
SlotCombination combo = SlotCombination.match(e1, e2, e3);
long winAmount = combo != null ? bet * combo.getMultiplier() : 0;
long cashAfter = account.getCash() - bet + winAmount;
repository.save(repository.getAccount(userId).withCash(cashAfter).withAddedEarned(winAmount));
return new SlotResult(e1, e2, e3,
combo != null ? combo.name() : null,
combo != null ? combo.getMultiplier() : 0,
winAmount, cashAfter);
}
private String spinEmoji() {
int roll = ThreadLocalRandom.current().nextInt(TOTAL_WEIGHT);
int cumulative = 0;
for (int i = 0; i < EMOJIS.length; i++) {
cumulative += WEIGHTS[i];
if (roll < cumulative) return EMOJIS[i];
}
return EMOJIS[EMOJIS.length - 1];
}
public record SlotResult(String emoji1, String emoji2, String emoji3, String comboName, int multiplier, long winAmount, long cashAfter) {}
}

View file

@ -0,0 +1,64 @@
package creator.loki.feature.entertainment;
import com.google.inject.Inject;
import creator.loki.feature.social.commands.*;
import creator.loki.feature.social.commands.*;
import creator.loki.feature.entertainment.commands.*;
import creator.loki.feature.entertainment.commands.*;
import creator.loki.config.BotConfig;
import creator.loki.core.registry.CommandRegistrar;
import creator.loki.core.registry.CommandRegistry;
import creator.loki.core.security.CooldownManager;
import creator.loki.feature.ship.repository.ShipPairRepository;
import creator.loki.feature.ship.ShipCommand;
public class EntertainmentCommandRegistrar implements CommandRegistrar {
private final CooldownManager cooldownManager;
private final ShipPairRepository shipPairRepository;
private final BotConfig config;
@Inject
public EntertainmentCommandRegistrar(
CooldownManager cooldownManager,
ShipPairRepository shipPairRepository,
BotConfig config
) {
this.cooldownManager = cooldownManager;
this.shipPairRepository = shipPairRepository;
this.config = config;
}
@Override
public void register(CommandRegistry registry) {
registry.register(new EightBallCommand());
registry.register(new CoinCommand());
registry.register(new DiceCommand());
registry.register(new MemeCommand());
registry.register(new ShipCommand(shipPairRepository));
registry.register(new RoastCommand());
registry.register(new ComplimentCommand());
registry.register(new GlazbobaCommand());
registry.register(new IqCommand(cooldownManager));
registry.register(new GayCommand(cooldownManager));
registry.register(new PpCommand(cooldownManager));
registry.register(new RateCommand(cooldownManager));
registry.register(new BoykisserCommand(cooldownManager));
registry.register(new MustacheCommand(cooldownManager));
registry.register(new FatCommand(cooldownManager));
registry.register(new PatCommand(cooldownManager));
registry.register(new HugCommand());
registry.register(new KissCommand(cooldownManager));
registry.register(new SlapCommand(cooldownManager));
registry.register(new TickleCommand(cooldownManager));
registry.register(new PokeCommand(cooldownManager));
registry.register(new CuddleCommand(cooldownManager));
registry.register(new FeedCommand(cooldownManager));
registry.register(new RandomChoiceCommand(config, "choose", RandomChoiceCommand.Flavor.DEFAULT));
registry.register(new RandomChoiceCommand(config, "pick", RandomChoiceCommand.Flavor.DEFAULT));
registry.register(new RandomChoiceCommand(config, "decide", RandomChoiceCommand.Flavor.ORACLE));
registry.register(new RandomChoiceCommand(config, "wheel", RandomChoiceCommand.Flavor.WHEEL));
}
}

View file

@ -1,99 +1,101 @@
package creator.loki.command.impl.entertainment.games.rating;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Boykisser метр с детерминированным результатом
*/
public class BoykisserCommand implements Command {
private final CooldownManager cooldownManager;
public BoykisserCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "boykisser";
}
@Override
public String getDescription() {
return "На сколько ты boykisser (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "boykisser [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "boykisser")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "boykisser");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5);
// Детерминированный процент
int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100);
// Прогресс-бар с поцелуями
StringBuilder bar = new StringBuilder();
int filled = boykisserLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("💋");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("💋 Boykisser Метр")
.setDescription(target.getAsMention())
.addField("Уровень", bar.toString() + "\n**" + boykisserLevel + "%** - " + level.getComment(), false)
.setThumbnail(target.getUser().getAvatarUrl())
.setColor(level.getColor())
.setFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Boykisser метр с детерминированным результатом
*/
public class BoykisserCommand implements Command {
private final CooldownManager cooldownManager;
public BoykisserCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "boykisser";
}
@Override
public String getDescription() {
return "На сколько ты boykisser (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "boykisser [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "boykisser")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "boykisser");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не целуют мальчиков!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "boykisser", 5);
// Детерминированный процент
int boykisserLevel = getDeterministicValue(target.getId(), 777, 0, 100);
// Прогресс-бар с поцелуями
StringBuilder bar = new StringBuilder();
int filled = boykisserLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("💋");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(boykisserLevel, RatingConfig.getBoykisserLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("💋 Boykisser Метр")
.withDescription(target.getAsMention())
.addField("Уровень", bar.toString() + "\n**" + boykisserLevel + "%** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}

View file

@ -0,0 +1,78 @@
package creator.loki.feature.entertainment.commands;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.regex.Pattern;
/**
* Разбор хвоста сообщения и строки вариантов для {@link RandomChoiceCommand}.
*/
final class ChoiceOptionParser {
private static final Pattern OR_SPLIT = Pattern.compile("(?i)\\s+или\\s+");
private record SplitRule(Predicate<String> when, Function<String, String[]> split) {
}
/** Порядок важен: последнее правило - «слова через пробел». */
private static final List<SplitRule> RULES = List.of(
new SplitRule(s -> s.contains("|"), s -> s.split("\\|", -1)),
new SplitRule(s -> s.contains(","), s -> s.split(",", -1)),
new SplitRule(s -> OR_SPLIT.matcher(s).find(), OR_SPLIT::split),
new SplitRule(s -> s.contains("/"), s -> s.split("/", -1)),
new SplitRule(s -> true, s -> s.split("\\s+", -1))
);
private ChoiceOptionParser() {
}
static String tailAfterCommand(String raw, String prefix, String commandName) {
String t = raw.trim();
if (!t.startsWith(prefix)) {
return "";
}
String rest = t.substring(prefix.length()).trim();
int sp = rest.indexOf(' ');
if (sp < 0) {
return "";
}
String cmd = rest.substring(0, sp);
if (!cmd.equalsIgnoreCase(commandName)) {
return "";
}
return rest.substring(sp).trim();
}
static List<String> parseOptions(String tail) {
String t = tail.strip();
for (SplitRule rule : RULES) {
if (rule.when.test(t)) {
return dedupeIgnoreCase(trimNonEmpty(rule.split.apply(t)));
}
}
return List.of();
}
private static List<String> trimNonEmpty(String[] arr) {
return Arrays.stream(arr).map(String::trim).filter(s -> !s.isEmpty()).toList();
}
private static List<String> dedupeIgnoreCase(List<String> parts) {
Set<String> seen = new HashSet<>();
List<String> out = new ArrayList<>();
for (String p : parts) {
String key = p.toLowerCase(Locale.ROOT);
if (seen.add(key)) {
out.add(p);
}
}
return List.copyOf(out);
}
}

View file

@ -1,47 +1,47 @@
package creator.loki.command.impl.entertainment.games.random;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class CoinCommand implements Command {
private final Random random = new Random();
@Override
public String getName() {
return "coin";
}
@Override
public String getDescription() {
return "Подбросить монетку";
}
@Override
public String getUsage() {
return "coin";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
boolean isHeads = random.nextBoolean();
String result = isHeads ? "Орёл" : "Решка";
String emoji = isHeads ? "🦅" : "🪙";
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle(emoji + " Подбрасываю монетку...")
.setDescription("**Результат:** " + result)
.setColor(isHeads ? new Color(255, 215, 0) : new Color(192, 192, 192))
.build()
).queue();
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class CoinCommand implements Command {
private final Random random = new Random();
@Override
public String getName() {
return "coin";
}
@Override
public String getDescription() {
return "Подбросить монетку";
}
@Override
public String getUsage() {
return "coin";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
boolean isHeads = random.nextBoolean();
String result = isHeads ? "Орёл" : "Решка";
String emoji = isHeads ? "🦅" : "🪙";
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle(emoji + " Подбрасываю монетку...")
.withDescription("**Результат:** " + result)
.withColor(isHeads ? new Color(255, 215, 0) : new Color(192, 192, 192))
.build()
).queue();
}
}

View file

@ -1,60 +1,60 @@
package creator.loki.command.impl.entertainment.games.random;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class DiceCommand implements Command {
private final Random random = new Random();
@Override
public String getName() {
return "dice";
}
@Override
public String getDescription() {
return "Бросить кубик";
}
@Override
public String getUsage() {
return "dice [N]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
int maxValue = 6;
if (!context.getArgs().isEmpty()) {
try {
maxValue = Integer.parseInt(context.getArgs().get(0));
if (maxValue < 2 || maxValue > 100) {
context.getChannel().sendMessage("Число должно быть от 2 до 100").queue();
return;
}
} catch (NumberFormatException e) {
context.getChannel().sendMessage("Укажи корректное число").queue();
return;
}
}
int result = random.nextInt(maxValue) + 1;
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🎲 Бросаю кубик...")
.setDescription("**Результат:** " + result + " (из " + maxValue + ")")
.setColor(new Color(46, 204, 113))
.build()
).queue();
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class DiceCommand implements Command {
private final Random random = new Random();
@Override
public String getName() {
return "dice";
}
@Override
public String getDescription() {
return "Бросить кубик";
}
@Override
public String getUsage() {
return "dice [N]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
int maxValue = 6;
if (!context.getArgs().isEmpty()) {
try {
maxValue = Integer.parseInt(context.getArgs().get(0));
if (maxValue < 2 || maxValue > 100) {
context.getChannel().sendMessage("Число должно быть от 2 до 100").queue();
return;
}
} catch (NumberFormatException e) {
context.getChannel().sendMessage("Укажи корректное число").queue();
return;
}
}
int result = random.nextInt(maxValue) + 1;
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎲 Бросаю кубик...")
.withDescription("**Результат:** " + result + " (из " + maxValue + ")")
.withColor(new Color(46, 204, 113))
.build()
).queue();
}
}

View file

@ -1,74 +1,74 @@
package creator.loki.command.impl.entertainment.games;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class EightBallCommand implements Command {
private final Random random = new Random();
private static final String[] ANSWERS = {
"Да, определённо",
"Бесспорно",
"Без сомнений",
"Можешь быть уверен",
"Мне кажется — да",
"Вероятнее всего",
"Хорошие перспективы",
"Знаки говорят — да",
"Да",
"Пока не ясно, попробуй снова",
"Спроси позже",
"Лучше не рассказывать",
"Сейчас нельзя предсказать",
"Сконцентрируйся и спроси опять",
"Даже не думай",
"Мой ответ — нет",
"По моим данным — нет",
"Перспективы не очень хорошие",
"Весьма сомнительно"
};
@Override
public String getName() {
return "8ball";
}
@Override
public String getDescription() {
return "Магический шар предсказаний";
}
@Override
public String getUsage() {
return "8ball <вопрос>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getArgs().isEmpty()) {
context.getChannel().sendMessage("Задай вопрос!").queue();
return;
}
String question = String.join(" ", context.getArgs());
String answer = ANSWERS[random.nextInt(ANSWERS.length)];
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🎱 Магический шар")
.addField("Вопрос:", question, false)
.addField("Ответ:", answer, false)
.setColor(new Color(52, 73, 94))
.build()
).queue();
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.util.Random;
public class EightBallCommand implements Command {
private final Random random = new Random();
private static final String[] ANSWERS = {
"Да, определённо",
"Бесспорно",
"Без сомнений",
"Можешь быть уверен",
"Мне кажется - да",
"Вероятнее всего",
"Хорошие перспективы",
"Знаки говорят - да",
"Да",
"Пока не ясно, попробуй снова",
"Спроси позже",
"Лучше не рассказывать",
"Сейчас нельзя предсказать",
"Сконцентрируйся и спроси опять",
"Даже не думай",
"Мой ответ - нет",
"По моим данным - нет",
"Перспективы не очень хорошие",
"Весьма сомнительно"
};
@Override
public String getName() {
return "8ball";
}
@Override
public String getDescription() {
return "Магический шар предсказаний";
}
@Override
public String getUsage() {
return "8ball <вопрос>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
if (context.getArgs().isEmpty()) {
context.getChannel().sendMessage("Задай вопрос!").queue();
return;
}
String question = String.join(" ", context.getArgs());
String answer = ANSWERS[random.nextInt(ANSWERS.length)];
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🎱 Магический шар")
.addField("Вопрос:", question, false)
.addField("Ответ:", answer, false)
.withColor(new Color(52, 73, 94))
.build()
).queue();
}
}

View file

@ -1,94 +1,96 @@
package creator.loki.command.impl.entertainment.games.rating;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Fat метр - проверка на жирность
*/
public class FatCommand implements Command {
private final CooldownManager cooldownManager;
public FatCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "fat";
}
@Override
public String getDescription() {
return "Проверка на жирность (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "fat [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "fat")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "fat");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5);
// Детерминированный процент от 0 до 100
int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100);
// Прогресс-бар
StringBuilder bar = new StringBuilder();
int filled = fatLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("🟩");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🐷 Fat Метр")
.setDescription(target.getAsMention())
.addField("Жирность", bar.toString() + "\n**" + fatLevel + "%** - " + level.getComment(), false)
.setThumbnail(target.getUser().getAvatarUrl())
.setColor(level.getColor())
.setFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Fat метр - проверка на жирность
*/
public class FatCommand implements Command {
private final CooldownManager cooldownManager;
public FatCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "fat";
}
@Override
public String getDescription() {
return "Проверка на жирность (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "fat [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "fat")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "fat");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "fat", 5);
// Детерминированный процент от 0 до 100
int fatLevel = getDeterministicValue(target.getId(), 999, 0, 100);
// Прогресс-бар
StringBuilder bar = new StringBuilder();
int filled = fatLevel / 10;
for (int i = 0; i < filled; i++) {
bar.append("🟩");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(fatLevel, RatingConfig.getFatLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🐷 Fat Метр")
.withDescription(target.getAsMention())
.addField("Жирность", bar.toString() + "\n**" + fatLevel + "%** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}

View file

@ -1,97 +1,97 @@
package creator.loki.command.impl.entertainment.games.rating;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Gay-метр с детерминированным результатом
*/
public class GayCommand implements Command {
private final CooldownManager cooldownManager;
public GayCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "gay";
}
@Override
public String getDescription() {
return "Гей-метр (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "gay [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "gay")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "gay");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5);
// Детерминированный процент
int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100);
// Прогресс-бар с радугой
String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"};
StringBuilder bar = new StringBuilder();
int filled = Math.min(gayLevel / 17 + 1, 6);
for (int i = 0; i < filled; i++) {
bar.append(rainbow[i]);
}
for (int i = filled; i < 6; i++) {
bar.append("⬜");
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🏳️‍🌈 Гей-метр")
.setDescription(target.getAsMention())
.addField("Уровень", bar.toString() + "\n**" + gayLevel + "%**", false)
.setThumbnail(target.getUser().getAvatarUrl())
.setColor(new Color(255, 0, 255))
.setFooter("💡 Это просто мем, не воспринимайте серьезно")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Gay-метр с детерминированным результатом
*/
public class GayCommand implements Command {
private final CooldownManager cooldownManager;
public GayCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "gay";
}
@Override
public String getDescription() {
return "Гей-метр (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "gay [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "gay")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "gay");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ Боты не имеют ориентации!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "gay", 5);
// Детерминированный процент
int gayLevel = getDeterministicValue(target.getId(), 456, 0, 100);
// Прогресс-бар с радугой
String[] rainbow = {"🟥", "🟧", "🟨", "🟩", "🟦", "🟪"};
StringBuilder bar = new StringBuilder();
int filled = Math.min(gayLevel / 17 + 1, 6);
for (int i = 0; i < filled; i++) {
bar.append(rainbow[i]);
}
for (int i = filled; i < 6; i++) {
bar.append("⬜");
}
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🏳️‍🌈 Гей-метр")
.withDescription(target.getAsMention())
.addField("Уровень", bar.toString() + "\n**" + gayLevel + "%**", false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(new Color(255, 0, 255))
.withFooter("💡 Это просто мем, не воспринимайте серьезно")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}

View file

@ -1,96 +1,96 @@
package creator.loki.command.impl.entertainment.games.random;
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.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
public class GlazbobaCommand implements Command {
@Override
public String getName() {
return "glazboba";
}
@Override
public String getDescription() {
return "Расширенная информация о пользователе";
}
@Override
public String getUsage() {
return "glazboba <@user>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
// Анимация загрузки
context.getChannel().sendMessage("🔍 Сканирую данные...").queue(loadingMsg -> {
try {
Thread.sleep(1000);
loadingMsg.editMessage("📊 Анализирую профиль...").queue();
Thread.sleep(1000);
loadingMsg.editMessage("🔐 Взламываю Discord...").queue();
Thread.sleep(1000);
// Собираем информацию
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
String createdAt = target.getUser().getTimeCreated().format(formatter);
String joinedAt = target.getTimeJoined().format(formatter);
String status = target.getOnlineStatus().name();
String nickname = target.getNickname() != null ? target.getNickname() : "Нет";
String roles = target.getRoles().stream()
.map(role -> role.getName())
.limit(10)
.collect(Collectors.joining(", "));
if (roles.isEmpty()) roles = "Нет ролей";
String activities = target.getActivities().isEmpty()
? "Нет активности"
: target.getActivities().get(0).getName();
boolean isBot = target.getUser().isBot();
boolean isBoosting = target.isBoosting();
loadingMsg.editMessageEmbeds(
EmbedFactory.create()
.setTitle("👤 Информация о " + target.getEffectiveName())
.setThumbnail(target.getUser().getAvatarUrl())
.addField("🆔 ID", target.getId(), true)
.addField("📛 Никнейм", nickname, true)
.addField("🤖 Бот", isBot ? "Да" : "Нет", true)
.addField("📅 Аккаунт создан", createdAt, false)
.addField("📥 Присоединился", joinedAt, false)
.addField("🟢 Статус", status, true)
.addField("🎮 Активность", activities, true)
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
.addField("🎭 Роли", roles, false)
.setColor(target.getColor() != null ? target.getColor() : Color.BLUE)
.setFooter("Запросил: " + context.getAuthor().getUser().getName())
.build()
).queue();
} catch (InterruptedException e) {
loadingMsg.editMessage("❌ Ошибка загрузки").queue();
}
});
}
}
package creator.loki.feature.entertainment.commands;
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.time.format.DateTimeFormatter;
import java.util.List;
import java.util.stream.Collectors;
public class GlazbobaCommand implements Command {
@Override
public String getName() {
return "glazboba";
}
@Override
public String getDescription() {
return "Расширенная информация о пользователе";
}
@Override
public String getUsage() {
return "glazboba <@user>";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
List<Member> mentioned = context.getMessage().getMentions().getMembers();
if (mentioned.isEmpty()) {
context.getChannel().sendMessage("Укажи пользователя через @").queue();
return;
}
Member target = mentioned.get(0);
// Анимация загрузки
context.getChannel().sendMessage("🔍 Сканирую данные...").queue(loadingMsg -> {
try {
Thread.sleep(1000);
loadingMsg.editMessage("📊 Анализирую профиль...").queue();
Thread.sleep(1000);
loadingMsg.editMessage("🔐 Взламываю Discord...").queue();
Thread.sleep(1000);
// Собираем информацию
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
String createdAt = target.getUser().getTimeCreated().format(formatter);
String joinedAt = target.getTimeJoined().format(formatter);
String status = target.getOnlineStatus().name();
String nickname = target.getNickname() != null ? target.getNickname() : "Нет";
String roles = target.getRoles().stream()
.map(role -> role.getName())
.limit(10)
.collect(Collectors.joining(", "));
if (roles.isEmpty()) roles = "Нет ролей";
String activities = target.getActivities().isEmpty()
? "Нет активности"
: target.getActivities().get(0).getName();
boolean isBot = target.getUser().isBot();
boolean isBoosting = target.isBoosting();
loadingMsg.editMessageEmbeds(
EmbedFactory.create()
.withTitle("👤 Информация о " + target.getEffectiveName())
.withThumbnail(target.getUser().getAvatarUrl())
.addField("🆔 ID", target.getId(), true)
.addField("📛 Никнейм", nickname, true)
.addField("🤖 Бот", isBot ? "Да" : "Нет", true)
.addField("📅 Аккаунт создан", createdAt, false)
.addField("📥 Присоединился", joinedAt, false)
.addField("🟢 Статус", status, true)
.addField("🎮 Активность", activities, true)
.addField("💎 Бустер", isBoosting ? "Да" : "Нет", true)
.addField("🎭 Роли", roles, false)
.withColor(target.getColor() != null ? target.getColor() : Color.BLUE)
.withFooter("Запросил: " + context.getAuthor().getUser().getName())
.build()
).queue();
} catch (InterruptedException e) {
loadingMsg.editMessage("❌ Ошибка загрузки").queue();
}
});
}
}

View file

@ -1,91 +1,93 @@
package creator.loki.command.impl.entertainment.games.rating;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* IQ тест с детерминированным результатом
*/
public class IqCommand implements Command {
private final CooldownManager cooldownManager;
public IqCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "iq";
}
@Override
public String getDescription() {
return "Тест IQ (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "iq [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "iq")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "iq");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5);
int iq;
if (target.getUser().isBot()) {
iq = 9000; // Боты умные!
} else if (target.getId().equals("1414975197269987471")) {
iq = 1000; // Специальное значение
} else {
// Детерминированный IQ от 0 до 140
iq = getDeterministicValue(target.getId(), 123, 0, 140);
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🧠 Тест IQ")
.setDescription("Результат для " + target.getAsMention())
.addField("IQ Уровень", "**" + iq + "** - " + level.getComment(), false)
.setThumbnail(target.getUser().getAvatarUrl())
.setColor(level.getColor())
.setFooter("💡 Научно доказано вашим ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* IQ тест с детерминированным результатом
*/
public class IqCommand implements Command {
private final CooldownManager cooldownManager;
public IqCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "iq";
}
@Override
public String getDescription() {
return "Тест IQ (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "iq [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "iq")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "iq");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "iq", 5);
int iq;
if (target.getUser().isBot()) {
iq = 9000; // Боты умные!
} else if (target.getId().equals("1414975197269987471")) {
iq = 1000; // Специальное значение
} else {
// Детерминированный IQ от 0 до 140
iq = getDeterministicValue(target.getId(), 123, 0, 140);
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🧠 Тест IQ")
.withDescription("Результат для " + target.getAsMention())
.addField("IQ Уровень", "**" + iq + "** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Научно доказано вашим ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}

View file

@ -1,73 +1,73 @@
package creator.loki.command.impl.entertainment.games.random;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
public class MemeCommand implements Command {
@Override
public String getName() {
return "meme";
}
@Override
public String getDescription() {
return "Случайный мем";
}
@Override
public String getUsage() {
return "meme";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
context.getChannel().sendMessage("🔍 Ищу мем...").queue(loadingMsg -> {
try {
URL url = new URL("https://meme-api.com/gimme");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
JsonObject json = JsonParser.parseString(response.toString()).getAsJsonObject();
String memeUrl = json.get("url").getAsString();
String title = json.get("title").getAsString();
String subreddit = json.get("subreddit").getAsString();
loadingMsg.editMessageEmbeds(
EmbedFactory.create()
.setTitle(title)
.setImage(memeUrl)
.setColor(new Color(255, 69, 0))
.setFooter("r/" + subreddit)
.build()
).queue();
} catch (Exception e) {
loadingMsg.editMessage("❌ Не удалось загрузить мем: " + e.getMessage()).queue();
}
});
}
}
package creator.loki.feature.entertainment.commands;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import java.awt.Color;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
public class MemeCommand implements Command {
@Override
public String getName() {
return "meme";
}
@Override
public String getDescription() {
return "Случайный мем";
}
@Override
public String getUsage() {
return "meme";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
context.getChannel().sendMessage("🔍 Ищу мем...").queue(loadingMsg -> {
try {
var url = new URI("https://meme-api.com/gimme").toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
StringBuilder response = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
response.append(line);
}
in.close();
JsonObject json = JsonParser.parseString(response.toString()).getAsJsonObject();
String memeUrl = json.get("url").getAsString();
String title = json.get("title").getAsString();
String subreddit = json.get("subreddit").getAsString();
loadingMsg.editMessageEmbeds(
EmbedFactory.create()
.withTitle(title)
.withImage(memeUrl)
.withColor(new Color(255, 69, 0))
.withFooter("r/" + subreddit)
.build()
).queue();
} catch (Exception e) {
loadingMsg.editMessage("❌ Не удалось загрузить мем: " + e.getMessage()).queue();
}
});
}
}

View file

@ -1,103 +1,105 @@
package creator.loki.command.impl.entertainment.games.rating;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Mustache метр - проверка на усатость
*/
public class MustacheCommand implements Command {
private final CooldownManager cooldownManager;
public MustacheCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "mustache";
}
@Override
public String getDescription() {
return "Проверка на усатость (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "mustache [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "mustache")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "mustache");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5);
int mustacheLevel;
// Специальные значения
if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) {
mustacheLevel = 0; // Боты и специальный ID = 0%
} else if (target.getId().equals("678283217094180915")) {
mustacheLevel = 1000; // Специальный ID = 1000%
} else {
// Детерминированный процент от 0 до 100
mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100);
}
// Прогресс-бар с усами
StringBuilder bar = new StringBuilder();
int filled = Math.min(mustacheLevel / 10, 10);
for (int i = 0; i < filled; i++) {
bar.append("🧔");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🧔 Mustache Метр")
.setDescription(target.getAsMention())
.addField("Усатость", bar.toString() + "\n**" + mustacheLevel + "%** - " + level.getComment(), false)
.setThumbnail(target.getUser().getAvatarUrl())
.setColor(level.getColor())
.setFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* Mustache метр - проверка на усатость
*/
public class MustacheCommand implements Command {
private final CooldownManager cooldownManager;
public MustacheCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "mustache";
}
@Override
public String getDescription() {
return "Проверка на усатость (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "mustache [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "mustache")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "mustache");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "mustache", 5);
int mustacheLevel;
// Специальные значения
if (target.getUser().isBot() || target.getId().equals("1414975197269987471")) {
mustacheLevel = 0; // Боты и специальный ID = 0%
} else if (target.getId().equals("678283217094180915")) {
mustacheLevel = 1000; // Специальный ID = 1000%
} else {
// Детерминированный процент от 0 до 100
mustacheLevel = getDeterministicValue(target.getId(), 888, 0, 100);
}
// Прогресс-бар с усами
StringBuilder bar = new StringBuilder();
int filled = Math.min(mustacheLevel / 10, 10);
for (int i = 0; i < filled; i++) {
bar.append("🧔");
}
for (int i = filled; i < 10; i++) {
bar.append("⬜");
}
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🧔 Mustache Метр")
.withDescription(target.getAsMention())
.addField("Усатость", bar.toString() + "\n**" + mustacheLevel + "%** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}

View file

@ -1,102 +1,104 @@
package creator.loki.command.impl.entertainment.games.rating;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* PP размер с детерминированным результатом
*/
public class PpCommand implements Command {
private final CooldownManager cooldownManager;
public PpCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "pp";
}
@Override
public String getDescription() {
return "Измеритель pp (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "pp [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "pp")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "pp");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ У ботов нет pp!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5);
// Специальное значение для конкретного пользователя
int size;
if (target.getId().equals("1414975197269987471")) {
size = 100;
} else {
// Детерминированный размер от 0 до 30 см
size = getDeterministicValue(target.getId(), 6969, 0, 30);
}
// Визуализация
StringBuilder ppVisual = new StringBuilder("8");
for (int i = 0; i < size; i++) {
ppVisual.append("=");
}
ppVisual.append("D");
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("🍆 PP Размер")
.setDescription(target.getAsMention())
.addField("Размер", "```" + ppVisual.toString() + "```\n**" + size + " см** - " + level.getComment(), false)
.setThumbnail(target.getUser().getAvatarUrl())
.setColor(level.getColor())
.setFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.feature.entertainment.commands.RatingConfig;
import creator.loki.feature.entertainment.commands.RatingLevel;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.List;
import java.util.Random;
/**
* PP размер с детерминированным результатом
*/
public class PpCommand implements Command {
private final CooldownManager cooldownManager;
public PpCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "pp";
}
@Override
public String getDescription() {
return "Измеритель pp (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "pp [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "pp")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "pp");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
if (target.getUser().isBot()) {
context.getChannel().sendMessage("❌ У ботов нет pp!").queue();
return;
}
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "pp", 5);
// Специальное значение для конкретного пользователя
int size;
if (target.getId().equals("1414975197269987471")) {
size = 100;
} else {
// Детерминированный размер от 0 до 30 см
size = getDeterministicValue(target.getId(), 6969, 0, 30);
}
// Визуализация
StringBuilder ppVisual = new StringBuilder("8");
for (int i = 0; i < size; i++) {
ppVisual.append("=");
}
ppVisual.append("D");
// Определяем уровень
RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("🍆 PP Размер")
.withDescription(target.getAsMention())
.addField("Размер", "```" + ppVisual.toString() + "```\n**" + size + " см** - " + level.getComment(), false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(level.getColor())
.withFooter("💡 Детерминировано вашим Discord ID")
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}

View file

@ -0,0 +1,135 @@
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.config.BotConfig;
import creator.loki.embed.EmbedTemplates;
import net.dv8tion.jda.api.EmbedBuilder;
import java.awt.Color;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
/**
* Случайный выбор из вариантов. Разбор строки - {@link ChoiceOptionParser}.
*/
public class RandomChoiceCommand implements Command {
public enum Flavor {
DEFAULT(
"Выбери один вариант - рандом решит за тебя.",
new String[] {
"🎲 Выпало: **%s**",
"🎯 Окей, берём **%s**",
"✨ Вселенная шепчет: **%s**",
"🍀 Фортуна на твоей стороне - **%s**",
"📌 Без лишних слов: **%s**"
}
),
WHEEL(
"Колесо фортуны (тот же рандом, но драматичнее).",
new String[] {
"🎡 Колесо остановилось на **%s**",
"🎪 Барабанная дробь… **%s**!",
"🎠 Карусель выбрала **%s**",
"⚡ Судьба указывает на **%s**"
}
),
ORACLE(
"Оракул в шутливом тоне.",
new String[] {
"🔮 Оракул видит… **%s**",
"🌙 Звёзды сложились: **%s**",
"✴️ Судьба-печенька говорит: **%s**",
"🧿 Сглаз обойдёт - бери **%s** и не оглядывайся"
}
);
private final String blurb;
private final String[] templates;
Flavor(String blurb, String[] templates) {
this.blurb = blurb;
this.templates = templates;
}
String randomLine(String pick) {
var rnd = ThreadLocalRandom.current();
return String.format(templates[rnd.nextInt(templates.length)], pick);
}
}
private final BotConfig config;
private final String commandName;
private final Flavor flavor;
public RandomChoiceCommand(BotConfig config, String commandName, Flavor flavor) {
this.config = config;
this.commandName = commandName;
this.flavor = flavor;
}
@Override
public String getName() {
return commandName;
}
@Override
public String getDescription() {
return flavor.blurb;
}
@Override
public String getUsage() {
return commandName + " вариант1 | вариант2 | …";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
String p = config.getPrefix();
String tail = ChoiceOptionParser.tailAfterCommand(context.getMessage().getContentRaw(), p, commandName);
if (tail.isBlank()) {
reply(context, EmbedTemplates.error("Нужны варианты", usageExamples(p)));
return;
}
List<String> options = ChoiceOptionParser.parseOptions(tail);
if (options.size() < 2) {
reply(context, EmbedTemplates.error(
"Мало вариантов",
"Минимум **2** разных варианта: `|`, запятая, `или`, `/` или слова через пробел."
));
return;
}
var rnd = ThreadLocalRandom.current();
String pick = options.get(rnd.nextInt(options.size()));
String line = flavor.randomLine(pick);
String listed = options.stream().map(s -> "`" + s + "`").reduce((a, b) -> a + " · " + b).orElse("");
var embed = new EmbedBuilder()
.setTitle("🎲 " + commandName)
.setDescription(line + "\n\n*Варианты:* " + listed)
.setColor(new Color(88, 101, 242))
.setFooter("choose · pick · decide · wheel");
reply(context, embed.build());
}
private static void reply(CommandContext ctx, net.dv8tion.jda.api.entities.MessageEmbed embed) {
ctx.getChannel().sendMessageEmbeds(embed).queue();
}
private String usageExamples(String p) {
return "**Примеры:**\n"
+ "`" + p + commandName + " пицца | суши | бургер`\n"
+ "`" + p + commandName + " чай, кофе, сок`\n"
+ "`" + p + commandName + " дома или в клуб`\n"
+ "`" + p + commandName + " да / нет / может`";
}
}

View file

@ -1,107 +1,101 @@
package creator.loki.command.impl.entertainment.games.random;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Оценка пользователя из 10 с детерминированным результатом
*/
public class RateCommand implements Command {
private final CooldownManager cooldownManager;
public RateCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "rate";
}
@Override
public String getDescription() {
return "Оценить пользователя из 10 (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "rate [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5);
// Детерминированная оценка от 1 до 10
int rating = getDeterministicValue(target.getId(), 420, 1, 10);
// Визуализация звездами
StringBuilder stars = new StringBuilder();
for (int i = 0; i < rating; i++) {
stars.append("⭐");
}
for (int i = rating; i < 10; i++) {
stars.append("☆");
}
// Комментарий
Map<Integer, String> comments = new HashMap<>();
comments.put(10, "💯 ИДЕАЛЬНО!");
comments.put(9, "🔥 Почти идеально!");
comments.put(8, "😍 Отлично!");
comments.put(7, "👍 Хорошо!");
comments.put(6, "🙂 Неплохо");
comments.put(5, "😐 Средне");
comments.put(4, "😕 Ниже среднего");
comments.put(3, "😬 Так себе");
comments.put(2, "💀 Плохо");
comments.put(1, "🗿 Ужасно");
String comment = comments.getOrDefault(rating, "🤔");
Color color = rating >= 8 ? new Color(255, 215, 0) : new Color(52, 152, 219);
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.setTitle("⭐ Оценка")
.setDescription(target.getAsMention())
.addField("Рейтинг", stars.toString() + "\n**" + rating + "/10** - " + comment, false)
.setThumbnail(target.getUser().getAvatarUrl())
.setColor(color)
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}
package creator.loki.feature.entertainment.commands;
import creator.loki.command.Command;
import creator.loki.command.CommandContext;
import creator.loki.embed.EmbedFactory;
import creator.loki.core.security.CooldownManager;
import net.dv8tion.jda.api.entities.Member;
import java.awt.Color;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
/**
* Оценка пользователя из 10 с детерминированным результатом
*/
public class RateCommand implements Command {
private final CooldownManager cooldownManager;
public RateCommand(CooldownManager cooldownManager) {
this.cooldownManager = cooldownManager;
}
@Override
public String getName() {
return "rate";
}
@Override
public String getDescription() {
return "Оценить пользователя из 10 (результат всегда одинаковый)";
}
@Override
public String getUsage() {
return "rate [@user]";
}
@Override
public String getCategory() {
return "Развлечения";
}
@Override
public void execute(CommandContext context) {
// Проверка cooldown (5 секунд)
if (cooldownManager.isOnCooldown(context.getAuthor().getId(), "rate")) {
long remaining = cooldownManager.getRemainingSeconds(context.getAuthor().getId(), "rate");
context.getChannel().sendMessage("⏰ Подожди ещё " + remaining + " сек").queue();
return;
}
List<Member> mentioned = context.getMessage().getMentions().getMembers();
Member target = mentioned.isEmpty() ? context.getAuthor() : mentioned.get(0);
if (target == null) return;
// Применяем cooldown
cooldownManager.applyCooldown(context.getAuthor().getId(), "rate", 5);
// Детерминированная оценка от 1 до 10
int rating = getDeterministicValue(target.getId(), 420, 1, 10);
// Визуализация звездами
String stars = "⭐".repeat(rating) + "☆".repeat(10 - rating);
// Комментарий
Map<Integer, String> comments = new HashMap<>();
comments.put(10, "💯 ИДЕАЛЬНО!");
comments.put(9, "🔥 Почти идеально!");
comments.put(8, "😍 Отлично!");
comments.put(7, "👍 Хорошо!");
comments.put(6, "🙂 Неплохо");
comments.put(5, "😐 Средне");
comments.put(4, "😕 Ниже среднего");
comments.put(3, "😬 Так себе");
comments.put(2, "💀 Плохо");
comments.put(1, "🗿 Ужасно");
String comment = comments.getOrDefault(rating, "🤔");
Color color = rating >= 8 ? new Color(255, 215, 0) : new Color(52, 152, 219);
context.getChannel().sendMessageEmbeds(
EmbedFactory.create()
.withTitle("⭐ Оценка")
.withDescription(target.getAsMention())
.addField("Рейтинг", stars + "\n**" + rating + "/10** - " + comment, false)
.withThumbnail(target.getUser().getAvatarUrl())
.withColor(color)
.build()
).queue();
}
private int getDeterministicValue(String userId, int salt, int min, int max) {
long seed = Long.parseLong(userId) + salt;
Random random = new Random(seed);
return random.nextInt(max - min + 1) + min;
}
}

View file

@ -1,4 +1,4 @@
package creator.loki.command.impl.entertainment.games.rating;
package creator.loki.feature.entertainment.commands;
import java.awt.Color;
import java.util.Arrays;

Some files were not shown because too many files have changed in this diff Show more