refactor: split remaining >150-line files (voice, economy, image, serverstats, admin, core)
- VoiceButtonHandler: extract VoiceModals (limit/rename/bitrate) - VoiceChannelManager: extract VoiceChannelActions (preset apply, control panel, save/get) - VoiceSetupPrefixCommand: extract VoiceSetupEmbeds - BlackjackService: extract card logic into BlackjackEngine - ImageService: extract BooruParser + ImageUrls - ServerStatsService: extract StatsChartRenderer - GenerateRatingsCommand: extract RatingsEmbeds - BotInitializer: extract AuxiliaryBotFactory (music/radio JDA) - RateLimiter: extract TokenBucket into own file All main files now <=150 lines (project rule). Build + 438 tests green.
This commit is contained in:
parent
1483c5cee0
commit
e5e727b6c4
19 changed files with 653 additions and 505 deletions
69
src/main/java/creator/loki/core/AuxiliaryBotFactory.java
Normal file
69
src/main/java/creator/loki/core/AuxiliaryBotFactory.java
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.listener.MusicBotListener;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
import creator.loki.feature.radio.listener.RadioBotListener;
|
||||
import creator.loki.feature.radio.service.RadioManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class AuxiliaryBotFactory {
|
||||
private static final Logger logger = LoggerFactory.getLogger(AuxiliaryBotFactory.class);
|
||||
|
||||
private AuxiliaryBotFactory() {}
|
||||
|
||||
static JDA createMusicJDA(
|
||||
final BotConfig config,
|
||||
final QueueRepository queueRepository,
|
||||
final LavalinkManager lavalinkManager) {
|
||||
if (config.getMusicToken() == null || config.getMusicToken().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
LoggerDecorator.startup(logger, "Инициализация music бота...");
|
||||
LavalinkManager ml =
|
||||
LavalinkManagerFactory.create(config, queueRepository, config.getMusicToken());
|
||||
var builder =
|
||||
JdaFactory.createBuilder(
|
||||
config.getMusicToken(),
|
||||
"🎵 m!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
builder.addEventListeners(new MusicBotListener(ml, queueRepository, new PlayerManager(ml)));
|
||||
return buildWithLavalink(builder, ml);
|
||||
}
|
||||
|
||||
static JDA createRadioJDA(
|
||||
final BotConfig config,
|
||||
final LavalinkManager radioLavalink,
|
||||
final RadioManager radioManager) {
|
||||
if (config.getRadioToken() == null || config.getRadioToken().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
LoggerDecorator.startup(logger, "Инициализация radio бота...");
|
||||
var builder =
|
||||
JdaFactory.createBuilder(
|
||||
config.getRadioToken(),
|
||||
"📻 r!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
builder.addEventListeners(new RadioBotListener(radioManager));
|
||||
return buildWithLavalink(builder, radioLavalink);
|
||||
}
|
||||
|
||||
private static JDA buildWithLavalink(final JDABuilder builder, final LavalinkManager ll) {
|
||||
if (ll.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ll.getClient()));
|
||||
}
|
||||
return JdaFactory.awaitJda(builder.build());
|
||||
}
|
||||
}
|
||||
|
|
@ -7,13 +7,9 @@ import creator.loki.feature.counter.service.listener.MemberCounterListener;
|
|||
import creator.loki.feature.metrics.HealthHttpServer;
|
||||
import creator.loki.feature.moderation.MuteScheduler;
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.listener.MusicBotListener;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
import creator.loki.feature.radio.listener.RadioBotListener;
|
||||
import creator.loki.feature.radio.service.RadioManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
|
|
@ -74,13 +70,13 @@ public class BotInitializer {
|
|||
}
|
||||
});
|
||||
|
||||
JDA musicJda = createMusicJDA();
|
||||
JDA musicJda = AuxiliaryBotFactory.createMusicJDA(config, queueRepository, lavalinkManager);
|
||||
JDA radioJda = null;
|
||||
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
||||
sleep(3000);
|
||||
LavalinkManager radioLavalink =
|
||||
LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken());
|
||||
radioJda = createRadioJDA(radioLavalink);
|
||||
radioJda = AuxiliaryBotFactory.createRadioJDA(config, radioLavalink, radioManager);
|
||||
if (radioJda != null) {
|
||||
radioManager.setRadioJda(radioJda);
|
||||
radioManager.setLavalinkManager(radioLavalink);
|
||||
|
|
@ -105,43 +101,10 @@ public class BotInitializer {
|
|||
return buildWithLavalink(builder, lavalinkManager);
|
||||
}
|
||||
|
||||
private JDA createMusicJDA() {
|
||||
if (config.getMusicToken() == null || config.getMusicToken().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
LoggerDecorator.startup(logger, "Инициализация music бота...");
|
||||
LavalinkManager ml =
|
||||
LavalinkManagerFactory.create(config, queueRepository, config.getMusicToken());
|
||||
var builder =
|
||||
JdaFactory.createBuilder(
|
||||
config.getMusicToken(),
|
||||
"🎵 m!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
builder.addEventListeners(new MusicBotListener(ml, queueRepository, new PlayerManager(ml)));
|
||||
return buildWithLavalink(builder, ml);
|
||||
}
|
||||
|
||||
private JDA createRadioJDA(final LavalinkManager radioLavalink) {
|
||||
if (config.getRadioToken() == null || config.getRadioToken().isBlank()) {
|
||||
return null;
|
||||
}
|
||||
LoggerDecorator.startup(logger, "Инициализация radio бота...");
|
||||
var builder =
|
||||
JdaFactory.createBuilder(
|
||||
config.getRadioToken(),
|
||||
"📻 r!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
builder.addEventListeners(new RadioBotListener(radioManager));
|
||||
return buildWithLavalink(builder, radioLavalink);
|
||||
}
|
||||
|
||||
private static JDA buildWithLavalink(final JDABuilder builder, final LavalinkManager ll) {
|
||||
if (ll.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ll.getClient()));
|
||||
builder.setVoiceDispatchInterceptor(
|
||||
new dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener(ll.getClient()));
|
||||
}
|
||||
return JdaFactory.awaitJda(builder.build());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,54 +98,4 @@ public class RateLimiter implements Lifecycle {
|
|||
}
|
||||
LoggerDecorator.success(logger, "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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
51
src/main/java/creator/loki/core/security/TokenBucket.java
Normal file
51
src/main/java/creator/loki/core/security/TokenBucket.java
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
package creator.loki.core.security;
|
||||
|
||||
/** Token Bucket для rate limiting */
|
||||
final class TokenBucket {
|
||||
private final int capacity;
|
||||
private final double refillRate; // токенов в секунду
|
||||
private double tokens;
|
||||
private long lastRefillTime;
|
||||
private long lastAccessTime;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +1,13 @@
|
|||
package creator.loki.feature.admin.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.Random;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -51,7 +48,7 @@ public class GenerateRatingsCommand implements Command {
|
|||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
errorEmbed(
|
||||
RatingsEmbeds.error(
|
||||
"❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота."))
|
||||
.queue();
|
||||
return;
|
||||
|
|
@ -64,7 +61,7 @@ public class GenerateRatingsCommand implements Command {
|
|||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
warnEmbed(
|
||||
RatingsEmbeds.warn(
|
||||
"⏳ Генерация рейтингов...",
|
||||
"Начинаю генерацию всех rating значений для участников сервера..."))
|
||||
.complete();
|
||||
|
|
@ -83,7 +80,9 @@ public class GenerateRatingsCommand implements Command {
|
|||
}
|
||||
generatedCount++;
|
||||
}
|
||||
initial.editMessageEmbeds(successEmbed(context, memberCount, generatedCount)).queue();
|
||||
initial
|
||||
.editMessageEmbeds(RatingsEmbeds.success(context, memberCount, generatedCount))
|
||||
.queue();
|
||||
logger.info(
|
||||
"Супер-админ {} сгенерировал рейтинги для {} участников",
|
||||
context.getAuthor().getUser().getName(),
|
||||
|
|
@ -92,7 +91,8 @@ public class GenerateRatingsCommand implements Command {
|
|||
logger.error("Ошибка при генерации рейтингов", e);
|
||||
initial
|
||||
.editMessageEmbeds(
|
||||
errorEmbed("❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage()))
|
||||
RatingsEmbeds.error(
|
||||
"❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage()))
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -116,41 +116,4 @@ public class GenerateRatingsCommand implements Command {
|
|||
};
|
||||
ratingsRepository.setRating(guildId, userId, ratingType, value);
|
||||
}
|
||||
|
||||
private MessageEmbed errorEmbed(final String title, final String description) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle(title)
|
||||
.withDescription(description)
|
||||
.withColor(Color.RED)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MessageEmbed warnEmbed(final String title, final String description) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle(title)
|
||||
.withDescription(description)
|
||||
.withColor(Color.YELLOW)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MessageEmbed successEmbed(
|
||||
final CommandContext context, final int memberCount, final int generatedCount) {
|
||||
return 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,48 @@
|
|||
package creator.loki.feature.admin.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
final class RatingsEmbeds {
|
||||
private RatingsEmbeds() {}
|
||||
|
||||
static MessageEmbed error(final String title, final String description) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle(title)
|
||||
.withDescription(description)
|
||||
.withColor(Color.RED)
|
||||
.build();
|
||||
}
|
||||
|
||||
static MessageEmbed warn(final String title, final String description) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle(title)
|
||||
.withDescription(description)
|
||||
.withColor(Color.YELLOW)
|
||||
.build();
|
||||
}
|
||||
|
||||
static MessageEmbed success(
|
||||
final CommandContext context, final int memberCount, final int generatedCount) {
|
||||
return 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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package creator.loki.feature.economy.service.game;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
final class BlackjackEngine {
|
||||
private static final String[] DECK = {
|
||||
"2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"
|
||||
};
|
||||
|
||||
private BlackjackEngine() {}
|
||||
|
||||
static String drawCard() {
|
||||
return DECK[ThreadLocalRandom.current().nextInt(DECK.length)];
|
||||
}
|
||||
|
||||
static int calculateSum(final List<String> cards) {
|
||||
var sum = 0;
|
||||
var aces = 0;
|
||||
for (var 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,20 +3,11 @@ package creator.loki.feature.economy.service.game;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
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<>();
|
||||
|
||||
|
|
@ -38,11 +29,13 @@ public class BlackjackService {
|
|||
|
||||
repository.save(account.withCash(account.getCash() - bet));
|
||||
|
||||
var playerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
|
||||
var dealerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
|
||||
var playerCards =
|
||||
new ArrayList<>(List.of(BlackjackEngine.drawCard(), BlackjackEngine.drawCard()));
|
||||
var dealerCards =
|
||||
new ArrayList<>(List.of(BlackjackEngine.drawCard(), BlackjackEngine.drawCard()));
|
||||
|
||||
var playerSum = calculateSum(playerCards);
|
||||
var dealerSum = calculateSum(dealerCards);
|
||||
var playerSum = BlackjackEngine.calculateSum(playerCards);
|
||||
var dealerSum = BlackjackEngine.calculateSum(dealerCards);
|
||||
var playerBj = playerSum == 21;
|
||||
var dealerBj = dealerSum == 21;
|
||||
|
||||
|
|
@ -62,8 +55,8 @@ public class BlackjackService {
|
|||
return null;
|
||||
}
|
||||
|
||||
game.playerCards().add(drawCard());
|
||||
var sum = calculateSum(game.playerCards());
|
||||
game.playerCards().add(BlackjackEngine.drawCard());
|
||||
var sum = BlackjackEngine.calculateSum(game.playerCards());
|
||||
|
||||
var over = sum > 21;
|
||||
if (over) {
|
||||
|
|
@ -74,7 +67,7 @@ public class BlackjackService {
|
|||
game.playerCards(),
|
||||
game.dealerCards(),
|
||||
sum,
|
||||
calculateSum(game.dealerCards()),
|
||||
BlackjackEngine.calculateSum(game.dealerCards()),
|
||||
game.bet(),
|
||||
over,
|
||||
0);
|
||||
|
|
@ -87,12 +80,12 @@ public class BlackjackService {
|
|||
}
|
||||
|
||||
var dealerCards = new ArrayList<>(game.dealerCards());
|
||||
while (calculateSum(dealerCards) < 16) {
|
||||
dealerCards.add(drawCard());
|
||||
while (BlackjackEngine.calculateSum(dealerCards) < 16) {
|
||||
dealerCards.add(BlackjackEngine.drawCard());
|
||||
}
|
||||
|
||||
var playerSum = calculateSum(game.playerCards());
|
||||
var dealerSum = calculateSum(dealerCards);
|
||||
var playerSum = BlackjackEngine.calculateSum(game.playerCards());
|
||||
var dealerSum = BlackjackEngine.calculateSum(dealerCards);
|
||||
var payout = dealerSum > 21 || playerSum > dealerSum ? game.bet() * 2 : 0;
|
||||
|
||||
creditUser(userId, payout);
|
||||
|
|
@ -109,30 +102,6 @@ public class BlackjackService {
|
|||
repository.save(account.withCash(account.getCash() + amount));
|
||||
}
|
||||
|
||||
private String drawCard() {
|
||||
return DECK[ThreadLocalRandom.current().nextInt(DECK.length)];
|
||||
}
|
||||
|
||||
private int calculateSum(final List<String> cards) {
|
||||
var sum = 0;
|
||||
var aces = 0;
|
||||
for (var 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;
|
||||
}
|
||||
|
||||
private BlackjackState buildState(
|
||||
final List<String> playerCards,
|
||||
final List<String> dealerCards,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,65 @@
|
|||
package creator.loki.feature.image.service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
final class BooruParser {
|
||||
private BooruParser() {}
|
||||
|
||||
static List<String> parseE621(final String body, final int limit) {
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
JsonArray posts = json.getAsJsonArray("posts");
|
||||
if (posts == null || posts.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (JsonElement el : posts) {
|
||||
JsonObject post = el.getAsJsonObject();
|
||||
JsonObject file = post.getAsJsonObject("file");
|
||||
if (file != null && file.has("url") && !file.get("url").isJsonNull()) {
|
||||
String imageUrl = file.get("url").getAsString();
|
||||
if (!imageUrl.isEmpty()) {
|
||||
urls.add(imageUrl);
|
||||
}
|
||||
}
|
||||
if (urls.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
static List<String> parseBooru(final String body, final int code, final int limit) {
|
||||
if (body.isBlank() || !body.startsWith("[")) {
|
||||
return List.of();
|
||||
}
|
||||
JsonArray posts = JsonParser.parseString(body).getAsJsonArray();
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (JsonElement el : posts) {
|
||||
JsonObject post = el.getAsJsonObject();
|
||||
String fileUrl = post.get("file_url").getAsString();
|
||||
if (fileUrl != null && !fileUrl.isEmpty()) {
|
||||
if (fileUrl.startsWith("//")) {
|
||||
fileUrl = "https:" + fileUrl;
|
||||
}
|
||||
urls.add(fileUrl);
|
||||
}
|
||||
if (urls.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
static String encodeTags(String tags) {
|
||||
if (tags == null || tags.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return tags.trim().replace(" ", "+").replace("#", "%23");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,9 @@
|
|||
package creator.loki.feature.image.service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.config.BotConfig;
|
||||
|
|
@ -21,7 +16,6 @@ import org.slf4j.LoggerFactory;
|
|||
@Singleton
|
||||
public class ImageService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ImageService.class);
|
||||
private static final String E621_API = "https://e621.net/posts.json";
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
|
|
@ -40,37 +34,13 @@ public class ImageService {
|
|||
|
||||
public List<String> fetchE621(String tags, int limit, int page) {
|
||||
int capped = Math.min(limit, 100);
|
||||
String url = E621_API + "?tags=" + encodeTags(tags) + "&limit=" + capped + "&page=" + page;
|
||||
Request request =
|
||||
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
String url = ImageUrls.e621(BooruParser.encodeTags(tags), capped, page);
|
||||
try (Response response = call(url)) {
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
logger.error("e621 API error: {} {}", response.code(), response.message());
|
||||
return List.of();
|
||||
}
|
||||
String body = response.body().string();
|
||||
JsonObject json = JsonParser.parseString(body).getAsJsonObject();
|
||||
JsonArray posts = json.getAsJsonArray("posts");
|
||||
if (posts == null || posts.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (JsonElement el : posts) {
|
||||
JsonObject post = el.getAsJsonObject();
|
||||
JsonObject file = post.getAsJsonObject("file");
|
||||
if (file != null && file.has("url") && !file.get("url").isJsonNull()) {
|
||||
String imageUrl = file.get("url").getAsString();
|
||||
if (!imageUrl.isEmpty()) {
|
||||
urls.add(imageUrl);
|
||||
}
|
||||
}
|
||||
if (urls.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
return BooruParser.parseE621(response.body().string(), limit);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error fetching from e621", e);
|
||||
return List.of();
|
||||
|
|
@ -83,13 +53,7 @@ public class ImageService {
|
|||
|
||||
public List<String> fetchGelbooru(String tags, int limit, int page) {
|
||||
int capped = Math.min(limit, 100);
|
||||
String url =
|
||||
"https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&tags="
|
||||
+ encodeTags(tags)
|
||||
+ "&limit="
|
||||
+ capped
|
||||
+ "&pid="
|
||||
+ (page - 1);
|
||||
String url = ImageUrls.gelbooru(BooruParser.encodeTags(tags), capped, page);
|
||||
return fetchBooruJson(url, limit);
|
||||
}
|
||||
|
||||
|
|
@ -99,62 +63,26 @@ public class ImageService {
|
|||
|
||||
public List<String> fetchYandere(String tags, int limit, int page) {
|
||||
int capped = Math.min(limit, 100);
|
||||
String url =
|
||||
"https://yande.re/post.json?tags="
|
||||
+ encodeTags(tags)
|
||||
+ "&limit="
|
||||
+ capped
|
||||
+ "&page="
|
||||
+ page;
|
||||
String url = ImageUrls.yandere(BooruParser.encodeTags(tags), capped, page);
|
||||
return fetchBooruJson(url, limit);
|
||||
}
|
||||
|
||||
private List<String> fetchBooruJson(final String url, final int limit) {
|
||||
Request request =
|
||||
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
try (Response response = call(url)) {
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
logger.error("Booru API error: {} {}", response.code(), response.message());
|
||||
return List.of();
|
||||
}
|
||||
return parseBooruResponse(response.body().string(), response.code(), limit);
|
||||
return BooruParser.parseBooru(response.body().string(), response.code(), limit);
|
||||
} catch (IOException e) {
|
||||
logger.error("Error fetching from booru", e);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> parseBooruResponse(final String body, final int code, final int limit) {
|
||||
if (body.isBlank() || !body.startsWith("[")) {
|
||||
logger.warn(
|
||||
"Booru API returned non-JSON response (code {}): {}",
|
||||
code,
|
||||
body.length() > 200 ? body.substring(0, 200) : body);
|
||||
return List.of();
|
||||
}
|
||||
JsonArray posts = JsonParser.parseString(body).getAsJsonArray();
|
||||
List<String> urls = new ArrayList<>();
|
||||
for (JsonElement el : posts) {
|
||||
JsonObject post = el.getAsJsonObject();
|
||||
String fileUrl = post.get("file_url").getAsString();
|
||||
if (fileUrl != null && !fileUrl.isEmpty()) {
|
||||
if (fileUrl.startsWith("//")) {
|
||||
fileUrl = "https:" + fileUrl;
|
||||
}
|
||||
urls.add(fileUrl);
|
||||
}
|
||||
if (urls.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
private String encodeTags(String tags) {
|
||||
if (tags == null || tags.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
return tags.trim().replace(" ", "+").replace("#", "%23");
|
||||
private Response call(final String url) throws IOException {
|
||||
Request request =
|
||||
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
||||
return client.newCall(request).execute();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
package creator.loki.feature.image.service;
|
||||
|
||||
final class ImageUrls {
|
||||
private static final String E621_API = "https://e621.net/posts.json";
|
||||
|
||||
private ImageUrls() {}
|
||||
|
||||
static String e621(String tags, int limit, int page) {
|
||||
return E621_API + "?tags=" + encode(tags) + "&limit=" + limit + "&page=" + page;
|
||||
}
|
||||
|
||||
static String gelbooru(String tags, int limit, int page) {
|
||||
return "https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&tags="
|
||||
+ encode(tags)
|
||||
+ "&limit="
|
||||
+ limit
|
||||
+ "&pid="
|
||||
+ (page - 1);
|
||||
}
|
||||
|
||||
static String yandere(String tags, int limit, int page) {
|
||||
return "https://yande.re/post.json?tags=" + encode(tags) + "&limit=" + limit + "&page=" + page;
|
||||
}
|
||||
|
||||
private static String encode(String tags) {
|
||||
return BooruParser.encodeTags(tags);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +1,17 @@
|
|||
package creator.loki.feature.serverstats;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import creator.loki.feature.counter.repository.CounterStatsRepository;
|
||||
import creator.loki.feature.counter.service.format.DailyStats;
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
|
||||
public class ServerStatsService {
|
||||
|
||||
private static final int CHART_WIDTH = 800;
|
||||
private static final int CHART_HEIGHT = 400;
|
||||
private static final int MARGIN = 60;
|
||||
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("dd.MM");
|
||||
private static final Color COLOR_JOINED = new Color(46, 204, 113);
|
||||
private static final Color COLOR_LEFT = new Color(231, 76, 60);
|
||||
private static final Color COLOR_BG = new Color(44, 47, 51);
|
||||
private static final Color COLOR_TEXT = new Color(220, 220, 220);
|
||||
private static final Color COLOR_GRID = new Color(70, 70, 70);
|
||||
|
||||
private final CounterStatsRepository counterStatsRepository;
|
||||
private final StatsChartRenderer chartRenderer = new StatsChartRenderer();
|
||||
|
||||
public ServerStatsService(CounterStatsRepository counterStatsRepository) {
|
||||
this.counterStatsRepository = counterStatsRepository;
|
||||
|
|
@ -39,80 +22,7 @@ public class ServerStatsService {
|
|||
}
|
||||
|
||||
public byte[] generateChart(List<DailyStats> history) throws Exception {
|
||||
var image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
var g = image.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g.setColor(COLOR_BG);
|
||||
g.fillRect(0, 0, CHART_WIDTH, CHART_HEIGHT);
|
||||
var maxVal =
|
||||
history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(1);
|
||||
var plotW = CHART_WIDTH - 2 * MARGIN;
|
||||
var plotH = CHART_HEIGHT - 2 * MARGIN;
|
||||
var barW = Math.max(4, plotW / (history.size() * 2 + 1));
|
||||
g.setColor(COLOR_GRID);
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
var y = MARGIN + plotH - (plotH * i / 4);
|
||||
g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y);
|
||||
}
|
||||
drawLabels(g, plotH, maxVal);
|
||||
var maxBars = Math.min(history.size(), 30);
|
||||
var displayHistory = history.subList(history.size() - maxBars, history.size());
|
||||
drawData(g, displayHistory, maxVal, plotH, barW);
|
||||
drawLegend(g);
|
||||
g.dispose();
|
||||
var baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private void drawLabels(final Graphics2D g, final int plotH, final int maxVal) {
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.setFont(new Font("Arial", Font.BOLD, 16));
|
||||
g.drawString("Активность сервера (присоединились / вышли)", 160, 30);
|
||||
g.setFont(new Font("Arial", Font.PLAIN, 11));
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
var y = MARGIN + plotH - (plotH * i / 4);
|
||||
g.drawString(String.valueOf(maxVal * i / 4), 5, y + 4);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawData(
|
||||
final Graphics2D g,
|
||||
final List<DailyStats> history,
|
||||
final int maxVal,
|
||||
final int plotH,
|
||||
final int barW) {
|
||||
var step = Math.max(1, history.size() / 15);
|
||||
for (var i = 0; i < history.size(); i++) {
|
||||
var stats = history.get(i);
|
||||
var x = MARGIN + i * (barW * 2 + 2);
|
||||
var joinedH = (int) ((double) stats.getJoined() / maxVal * plotH);
|
||||
var leftH = (int) ((double) stats.getLeft() / maxVal * plotH);
|
||||
g.setColor(COLOR_JOINED);
|
||||
g.fillRect(x, MARGIN + plotH - joinedH, barW, joinedH);
|
||||
g.setColor(COLOR_LEFT);
|
||||
g.fillRect(x + barW + 2, MARGIN + plotH - leftH, barW, leftH);
|
||||
if (i % step == 0) {
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.rotate(-Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15);
|
||||
g.drawString(stats.getDate().substring(5), x + barW, CHART_HEIGHT - MARGIN + 15);
|
||||
g.rotate(Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLegend(final Graphics2D g) {
|
||||
g.setFont(new Font("Arial", Font.PLAIN, 12));
|
||||
g.setColor(COLOR_JOINED);
|
||||
g.fillRect(CHART_WIDTH - 170, 10, 12, 12);
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.drawString("Присоединились", CHART_WIDTH - 152, 21);
|
||||
g.setColor(COLOR_LEFT);
|
||||
g.fillRect(CHART_WIDTH - 60, 10, 12, 12);
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.drawString("Вышли", CHART_WIDTH - 42, 21);
|
||||
return chartRenderer.render(history);
|
||||
}
|
||||
|
||||
public byte[] generateCsv(final Guild guild, final List<DailyStats> history) {
|
||||
|
|
@ -142,7 +52,7 @@ public class ServerStatsService {
|
|||
var header =
|
||||
"server_name,server_id,owner,created_at,total_members,online,bots,text_channels,voice_channels,roles\n";
|
||||
return (header + info + "\ndate,joined,left,difference\n" + dailyRows + totals)
|
||||
.getBytes(java.nio.charset.StandardCharsets.UTF_8);
|
||||
.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private static String escapeCsv(String value) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
package creator.loki.feature.serverstats;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.awt.Font;
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.RenderingHints;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.List;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
import creator.loki.feature.counter.service.format.DailyStats;
|
||||
|
||||
final class StatsChartRenderer {
|
||||
private static final int CHART_WIDTH = 800;
|
||||
private static final int CHART_HEIGHT = 400;
|
||||
private static final int MARGIN = 60;
|
||||
private static final Color COLOR_JOINED = new Color(46, 204, 113);
|
||||
private static final Color COLOR_LEFT = new Color(231, 76, 60);
|
||||
private static final Color COLOR_BG = new Color(44, 47, 51);
|
||||
private static final Color COLOR_TEXT = new Color(220, 220, 220);
|
||||
private static final Color COLOR_GRID = new Color(70, 70, 70);
|
||||
|
||||
byte[] render(final List<DailyStats> history) throws Exception {
|
||||
var image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
||||
var g = image.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(
|
||||
RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
g.setColor(COLOR_BG);
|
||||
g.fillRect(0, 0, CHART_WIDTH, CHART_HEIGHT);
|
||||
var maxVal =
|
||||
history.stream().mapToInt(s -> Math.max(s.getJoined(), s.getLeft())).max().orElse(1);
|
||||
var plotW = CHART_WIDTH - 2 * MARGIN;
|
||||
var plotH = CHART_HEIGHT - 2 * MARGIN;
|
||||
var barW = Math.max(4, plotW / (history.size() * 2 + 1));
|
||||
g.setColor(COLOR_GRID);
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
var y = MARGIN + plotH - (plotH * i / 4);
|
||||
g.drawLine(MARGIN, y, CHART_WIDTH - MARGIN, y);
|
||||
}
|
||||
drawLabels(g, plotH, maxVal);
|
||||
var maxBars = Math.min(history.size(), 30);
|
||||
var displayHistory = history.subList(history.size() - maxBars, history.size());
|
||||
drawData(g, displayHistory, maxVal, plotH, barW);
|
||||
drawLegend(g);
|
||||
g.dispose();
|
||||
var baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "PNG", baos);
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
private void drawLabels(final Graphics2D g, final int plotH, final int maxVal) {
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.setFont(new Font("Arial", Font.BOLD, 16));
|
||||
g.drawString("Активность сервера (присоединились / вышли)", 160, 30);
|
||||
g.setFont(new Font("Arial", Font.PLAIN, 11));
|
||||
for (var i = 0; i <= 4; i++) {
|
||||
var y = MARGIN + plotH - (plotH * i / 4);
|
||||
g.drawString(String.valueOf(maxVal * i / 4), 5, y + 4);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawData(
|
||||
final Graphics2D g,
|
||||
final List<DailyStats> history,
|
||||
final int maxVal,
|
||||
final int plotH,
|
||||
final int barW) {
|
||||
var step = Math.max(1, history.size() / 15);
|
||||
for (var i = 0; i < history.size(); i++) {
|
||||
var stats = history.get(i);
|
||||
var x = MARGIN + i * (barW * 2 + 2);
|
||||
var joinedH = (int) ((double) stats.getJoined() / maxVal * plotH);
|
||||
var leftH = (int) ((double) stats.getLeft() / maxVal * plotH);
|
||||
g.setColor(COLOR_JOINED);
|
||||
g.fillRect(x, MARGIN + plotH - joinedH, barW, joinedH);
|
||||
g.setColor(COLOR_LEFT);
|
||||
g.fillRect(x + barW + 2, MARGIN + plotH - leftH, barW, leftH);
|
||||
if (i % step == 0) {
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.rotate(-Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15);
|
||||
g.drawString(stats.getDate().substring(5), x + barW, CHART_HEIGHT - MARGIN + 15);
|
||||
g.rotate(Math.PI / 4, x + barW, CHART_HEIGHT - MARGIN + 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void drawLegend(final Graphics2D g) {
|
||||
g.setFont(new Font("Arial", Font.PLAIN, 12));
|
||||
g.setColor(COLOR_JOINED);
|
||||
g.fillRect(CHART_WIDTH - 170, 10, 12, 12);
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.drawString("Присоединились", CHART_WIDTH - 152, 21);
|
||||
g.setColor(COLOR_LEFT);
|
||||
g.fillRect(CHART_WIDTH - 60, 10, 12, 12);
|
||||
g.setColor(COLOR_TEXT);
|
||||
g.drawString("Вышли", CHART_WIDTH - 42, 21);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,14 +5,10 @@ import com.google.inject.Singleton;
|
|||
import creator.loki.feature.voice.service.VoiceChannelManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.components.label.Label;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInput;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInputStyle;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import net.dv8tion.jda.api.modals.Modal;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -96,57 +92,15 @@ public class VoiceButtonHandler extends ListenerAdapter {
|
|||
}
|
||||
|
||||
private void handleLimit(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
||||
TextInput limitInput =
|
||||
TextInput.create("limit", TextInputStyle.SHORT)
|
||||
.setPlaceholder("0-99 (0 = без лимита)")
|
||||
.setRequired(true)
|
||||
.setMinLength(1)
|
||||
.setMaxLength(2)
|
||||
.setValue(String.valueOf(channel.getUserLimit()))
|
||||
.build();
|
||||
|
||||
Modal modal =
|
||||
Modal.create("voice_limit:" + channel.getId(), "Установить лимит")
|
||||
.addComponents(Label.of("Лимит пользователей", limitInput))
|
||||
.build();
|
||||
|
||||
event.replyModal(modal).queue();
|
||||
event.replyModal(VoiceModals.limit(channel)).queue();
|
||||
}
|
||||
|
||||
private void handleRename(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
||||
TextInput nameInput =
|
||||
TextInput.create("name", TextInputStyle.SHORT)
|
||||
.setPlaceholder("Введите название канала")
|
||||
.setRequired(true)
|
||||
.setMinLength(1)
|
||||
.setMaxLength(100)
|
||||
.setValue(channel.getName())
|
||||
.build();
|
||||
|
||||
Modal modal =
|
||||
Modal.create("voice_rename:" + channel.getId(), "Переименовать канал")
|
||||
.addComponents(Label.of("Новое название", nameInput))
|
||||
.build();
|
||||
|
||||
event.replyModal(modal).queue();
|
||||
event.replyModal(VoiceModals.rename(channel)).queue();
|
||||
}
|
||||
|
||||
private void handleBitrate(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
||||
TextInput bitrateInput =
|
||||
TextInput.create("bitrate", TextInputStyle.SHORT)
|
||||
.setPlaceholder("8-384")
|
||||
.setRequired(true)
|
||||
.setMinLength(1)
|
||||
.setMaxLength(3)
|
||||
.setValue(String.valueOf(channel.getBitrate() / 1000))
|
||||
.build();
|
||||
|
||||
Modal modal =
|
||||
Modal.create("voice_bitrate:" + channel.getId(), "🎨 Установить битрейт")
|
||||
.addComponents(Label.of("Битрейт (kbps)", bitrateInput))
|
||||
.build();
|
||||
|
||||
event.replyModal(modal).queue();
|
||||
event.replyModal(VoiceModals.bitrate(channel)).queue();
|
||||
}
|
||||
|
||||
private void handleDelete(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package creator.loki.feature.voice.handler;
|
||||
|
||||
import net.dv8tion.jda.api.components.label.Label;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInput;
|
||||
import net.dv8tion.jda.api.components.textinput.TextInputStyle;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
import net.dv8tion.jda.api.modals.Modal;
|
||||
|
||||
final class VoiceModals {
|
||||
private VoiceModals() {}
|
||||
|
||||
static Modal limit(final VoiceChannel channel) {
|
||||
TextInput limitInput =
|
||||
TextInput.create("limit", TextInputStyle.SHORT)
|
||||
.setPlaceholder("0-99 (0 = без лимита)")
|
||||
.setRequired(true)
|
||||
.setMinLength(1)
|
||||
.setMaxLength(2)
|
||||
.setValue(String.valueOf(channel.getUserLimit()))
|
||||
.build();
|
||||
return Modal.create("voice_limit:" + channel.getId(), "Установить лимит")
|
||||
.addComponents(Label.of("Лимит пользователей", limitInput))
|
||||
.build();
|
||||
}
|
||||
|
||||
static Modal rename(final VoiceChannel channel) {
|
||||
TextInput nameInput =
|
||||
TextInput.create("name", TextInputStyle.SHORT)
|
||||
.setPlaceholder("Введите название канала")
|
||||
.setRequired(true)
|
||||
.setMinLength(1)
|
||||
.setMaxLength(100)
|
||||
.setValue(channel.getName())
|
||||
.build();
|
||||
return Modal.create("voice_rename:" + channel.getId(), "Переименовать канал")
|
||||
.addComponents(Label.of("Новое название", nameInput))
|
||||
.build();
|
||||
}
|
||||
|
||||
static Modal bitrate(final VoiceChannel channel) {
|
||||
TextInput bitrateInput =
|
||||
TextInput.create("bitrate", TextInputStyle.SHORT)
|
||||
.setPlaceholder("8-384")
|
||||
.setRequired(true)
|
||||
.setMinLength(1)
|
||||
.setMaxLength(3)
|
||||
.setValue(String.valueOf(channel.getBitrate() / 1000))
|
||||
.build();
|
||||
return Modal.create("voice_bitrate:" + channel.getId(), "🎨 Установить битрейт")
|
||||
.addComponents(Label.of("Битрейт (kbps)", bitrateInput))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package creator.loki.feature.voice.service;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.feature.voice.repository.VoicePresetRepository;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
final class VoiceChannelActions {
|
||||
private static final Logger logger = LoggerFactory.getLogger(VoiceChannelActions.class);
|
||||
private final VoicePresetRepository presetRepo;
|
||||
|
||||
VoiceChannelActions(final VoicePresetRepository presetRepo) {
|
||||
this.presetRepo = presetRepo;
|
||||
}
|
||||
|
||||
void applyUserPreset(final VoiceChannel channel, final String userId) {
|
||||
var preset = getPreset(userId);
|
||||
try {
|
||||
if (preset.getName() != null && !preset.getName().isEmpty()) {
|
||||
channel.getManager().setName(preset.getName()).queue();
|
||||
}
|
||||
if (preset.getLimit() >= 0) {
|
||||
channel.getManager().setUserLimit(preset.getLimit()).queue();
|
||||
}
|
||||
if (preset.getBitrate() > 0) {
|
||||
channel.getManager().setBitrate(preset.getBitrate()).queue();
|
||||
}
|
||||
if (preset.isLocked()) {
|
||||
var publicRole = channel.getGuild().getPublicRole();
|
||||
channel.upsertPermissionOverride(publicRole).deny(Permission.VOICE_CONNECT).queue();
|
||||
}
|
||||
LoggerDecorator.voice(
|
||||
logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId());
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка при применении пресета", e);
|
||||
}
|
||||
}
|
||||
|
||||
void sendControlPanel(final VoiceChannel channel, final Member owner) {
|
||||
var limitText = channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit());
|
||||
var embed =
|
||||
new EmbedBuilder()
|
||||
.setTitle("🎛️ Управление каналом")
|
||||
.setDescription("Ваш личный голосовой канал")
|
||||
.addField("Владелец", owner.getAsMention(), true)
|
||||
.addField("Лимит", limitText, true)
|
||||
.setColor(Color.CYAN)
|
||||
.setFooter("Управляйте каналом с помощью кнопок ниже");
|
||||
var row =
|
||||
ActionRow.of(
|
||||
Button.primary("voice_lock:" + channel.getId(), "🔒 Замок"),
|
||||
Button.secondary("voice_limit:" + channel.getId(), "👥 Лимит"),
|
||||
Button.secondary("voice_rename:" + channel.getId(), "✏️ Название"),
|
||||
Button.secondary("voice_bitrate:" + channel.getId(), "🎵 Битрейт"),
|
||||
Button.danger("voice_delete:" + channel.getId(), "🗑️ Удалить"));
|
||||
channel.sendMessageEmbeds(embed.build()).addComponents(row).queue();
|
||||
}
|
||||
|
||||
void savePreset(final String userId, final VoicePreset preset) {
|
||||
presetRepo.savePreset(userId, preset);
|
||||
LoggerDecorator.voice(logger, "Сохранён пресет для пользователя " + userId);
|
||||
}
|
||||
|
||||
VoicePreset getPreset(final String userId) {
|
||||
return presetRepo.getPreset(userId) != null ? presetRepo.getPreset(userId) : new VoicePreset();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
package creator.loki.feature.voice.service;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.EnumSet;
|
||||
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
|
|
@ -8,10 +7,7 @@ import creator.loki.feature.voice.VoiceChannelData;
|
|||
import creator.loki.feature.voice.repository.VoiceChannelRepository;
|
||||
import creator.loki.feature.voice.repository.VoicePresetRepository;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||
|
|
@ -25,7 +21,7 @@ public class VoiceChannelManager {
|
|||
Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT,
|
||||
Permission.VOICE_MOVE_OTHERS, Permission.VOICE_MUTE_OTHERS);
|
||||
private final VoiceChannelRepository channelRepo;
|
||||
private final VoicePresetRepository presetRepo;
|
||||
private final VoiceChannelActions channelActions;
|
||||
private String triggerChannelId;
|
||||
private String categoryId;
|
||||
|
||||
|
|
@ -34,7 +30,7 @@ public class VoiceChannelManager {
|
|||
VoicePresetRepository presetRepo,
|
||||
final ConfigRepository configRepo) {
|
||||
this.channelRepo = channelRepo;
|
||||
this.presetRepo = presetRepo;
|
||||
this.channelActions = new VoiceChannelActions(presetRepo);
|
||||
this.triggerChannelId = configRepo.get("voice.trigger_channel_id");
|
||||
this.categoryId = configRepo.get("voice.category_id");
|
||||
}
|
||||
|
|
@ -51,7 +47,7 @@ public class VoiceChannelManager {
|
|||
logger,
|
||||
"Создан личный канал %s для пользователя %s"
|
||||
.formatted(channel.getId(), member.getEffectiveName()));
|
||||
applyUserPreset(channel, member.getId());
|
||||
channelActions.applyUserPreset(channel, member.getId());
|
||||
return channel;
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка при создании личного канала", e);
|
||||
|
|
@ -107,55 +103,19 @@ public class VoiceChannelManager {
|
|||
saveUserPreset(userId, preset);
|
||||
}
|
||||
|
||||
public void sendControlPanel(final VoiceChannel channel, final Member owner) {
|
||||
var limitText = channel.getUserLimit() == 0 ? "∞" : String.valueOf(channel.getUserLimit());
|
||||
var embed =
|
||||
new EmbedBuilder()
|
||||
.setTitle("🎛️ Управление каналом")
|
||||
.setDescription("Ваш личный голосовой канал")
|
||||
.addField("Владелец", owner.getAsMention(), true)
|
||||
.addField("Лимит", limitText, true)
|
||||
.setColor(Color.CYAN)
|
||||
.setFooter("Управляйте каналом с помощью кнопок ниже");
|
||||
var row =
|
||||
ActionRow.of(
|
||||
Button.primary("voice_lock:" + channel.getId(), "🔒 Замок"),
|
||||
Button.secondary("voice_limit:" + channel.getId(), "👥 Лимит"),
|
||||
Button.secondary("voice_rename:" + channel.getId(), "✏️ Название"),
|
||||
Button.secondary("voice_bitrate:" + channel.getId(), "🎵 Битрейт"),
|
||||
Button.danger("voice_delete:" + channel.getId(), "🗑️ Удалить"));
|
||||
channel.sendMessageEmbeds(embed.build()).addComponents(row).queue();
|
||||
}
|
||||
|
||||
public void saveUserPreset(final String userId, VoicePreset preset) {
|
||||
presetRepo.savePreset(userId, preset);
|
||||
LoggerDecorator.voice(logger, "Сохранён пресет для пользователя " + userId);
|
||||
channelActions.savePreset(userId, preset);
|
||||
}
|
||||
|
||||
public VoicePreset getUserPreset(final String userId) {
|
||||
return presetRepo.getPreset(userId) != null ? presetRepo.getPreset(userId) : new VoicePreset();
|
||||
return channelActions.getPreset(userId);
|
||||
}
|
||||
|
||||
public void applyUserPreset(final VoiceChannel channel, final String userId) {
|
||||
var preset = getUserPreset(userId);
|
||||
try {
|
||||
if (preset.getName() != null && !preset.getName().isEmpty()) {
|
||||
channel.getManager().setName(preset.getName()).queue();
|
||||
}
|
||||
if (preset.getLimit() >= 0) {
|
||||
channel.getManager().setUserLimit(preset.getLimit()).queue();
|
||||
}
|
||||
if (preset.getBitrate() > 0) {
|
||||
channel.getManager().setBitrate(preset.getBitrate()).queue();
|
||||
}
|
||||
if (preset.isLocked()) {
|
||||
var publicRole = channel.getGuild().getPublicRole();
|
||||
channel.upsertPermissionOverride(publicRole).deny(Permission.VOICE_CONNECT).queue();
|
||||
}
|
||||
LoggerDecorator.voice(
|
||||
logger, "Применён пресет пользователя " + userId + " к каналу " + channel.getId());
|
||||
} catch (Exception e) {
|
||||
LoggerDecorator.error(logger, "Ошибка при применении пресета", e);
|
||||
}
|
||||
channelActions.applyUserPreset(channel, userId);
|
||||
}
|
||||
|
||||
public void sendControlPanel(final VoiceChannel channel, final Member owner) {
|
||||
channelActions.sendControlPanel(channel, owner);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,62 @@
|
|||
package creator.loki.feature.voice.service;
|
||||
|
||||
import java.awt.Color;
|
||||
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
final class VoiceSetupEmbeds {
|
||||
private VoiceSetupEmbeds() {}
|
||||
|
||||
static MessageEmbed triggerSet(final String mention) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("✅ Канал-триггер установлен")
|
||||
.withDescription("Теперь при входе в " + mention + " будет создаваться личный канал")
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
}
|
||||
|
||||
static MessageEmbed categorySet(final String name) {
|
||||
return EmbedFactory.create()
|
||||
.withTitle("✅ Категория установлена")
|
||||
.withDescription("Личные каналы будут создаваться в категории: **" + name + "**")
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
}
|
||||
|
||||
static MessageEmbed info(
|
||||
final CommandContext context, final String triggerId, final String categoryId) {
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("🎤 Конфигурация системы личных каналов")
|
||||
.withColor(Color.BLUE);
|
||||
if (triggerId != null) {
|
||||
embed.addField("Канал-триггер", "<#" + triggerId + ">", false);
|
||||
} else {
|
||||
embed.addField("Канал-триггер", "❌ Не установлен", false);
|
||||
}
|
||||
if (categoryId != null) {
|
||||
var category = context.getGuild().getCategoryById(categoryId);
|
||||
embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false);
|
||||
} else {
|
||||
embed.addField("Категория", "❌ Не установлена", false);
|
||||
}
|
||||
return embed.build();
|
||||
}
|
||||
|
||||
static MessageEmbed help() {
|
||||
var desc =
|
||||
"**Доступные команды:**\n"
|
||||
+ "• `!voice-setup trigger` - `#войс` или числовой id канала\n"
|
||||
+ "• `!voice-setup category` - `#категория` или id\n"
|
||||
+ "• `!voice-setup info` - Показать текущую конфигурацию\n\n"
|
||||
+ "Текстовые каналы для управления создаются автоматически"
|
||||
+ " при создании войса.\nТолько для администраторов!";
|
||||
return EmbedFactory.create()
|
||||
.withTitle("🎤 Настройка голосовых каналов")
|
||||
.withDescription(desc)
|
||||
.withColor(Color.BLUE)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,10 @@
|
|||
package creator.loki.feature.voice.service;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.util.GuildChannelResolve;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
|
|
@ -79,14 +77,10 @@ public class VoiceSetupPrefixCommand implements Command {
|
|||
return;
|
||||
}
|
||||
configRepo.set("voice.trigger_channel_id", channel.getId());
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Канал-триггер установлен")
|
||||
.withDescription(
|
||||
"Теперь при входе в " + channel.getAsMention() + " будет создаваться личный канал")
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
context.getChannel().sendMessageEmbeds(embed).queue();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(VoiceSetupEmbeds.triggerSet(channel.getAsMention()))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleCategory(final CommandContext context, final List<String> args) {
|
||||
|
|
@ -107,53 +101,22 @@ public class VoiceSetupPrefixCommand implements Command {
|
|||
return;
|
||||
}
|
||||
configRepo.set("voice.category_id", category.getId());
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Категория установлена")
|
||||
.withDescription(
|
||||
"Личные каналы будут создаваться в категории: **" + category.getName() + "**")
|
||||
.withColor(Color.GREEN)
|
||||
.build();
|
||||
context.getChannel().sendMessageEmbeds(embed).queue();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(VoiceSetupEmbeds.categorySet(category.getName()))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void handleInfo(final CommandContext context) {
|
||||
var triggerId = configRepo.get("voice.trigger_channel_id");
|
||||
var categoryId = configRepo.get("voice.category_id");
|
||||
var embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle("🎤 Конфигурация системы личных каналов")
|
||||
.withColor(Color.BLUE);
|
||||
if (triggerId != null) {
|
||||
embed.addField("Канал-триггер", "<#" + triggerId + ">", false);
|
||||
} else {
|
||||
embed.addField("Канал-триггер", "❌ Не установлен", false);
|
||||
}
|
||||
if (categoryId != null) {
|
||||
var category = context.getGuild().getCategoryById(categoryId);
|
||||
embed.addField("Категория", category != null ? category.getName() : "❌ Не найдена", false);
|
||||
} else {
|
||||
embed.addField("Категория", "❌ Не установлена", false);
|
||||
}
|
||||
context.getChannel().sendMessageEmbeds(embed.build()).queue();
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(VoiceSetupEmbeds.info(context, triggerId, categoryId))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void sendHelp(final CommandContext context) {
|
||||
var desc =
|
||||
"**Доступные команды:**\n"
|
||||
+ "• `!voice-setup trigger` - `#войс` или числовой id канала\n"
|
||||
+ "• `!voice-setup category` - `#категория` или id\n"
|
||||
+ "• `!voice-setup info` - Показать текущую конфигурацию\n\n"
|
||||
+ "Текстовые каналы для управления создаются автоматически"
|
||||
+ " при создании войса.\nТолько для администраторов!";
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("🎤 Настройка голосовых каналов")
|
||||
.withDescription(desc)
|
||||
.withColor(Color.BLUE)
|
||||
.build())
|
||||
.queue();
|
||||
context.getChannel().sendMessageEmbeds(VoiceSetupEmbeds.help()).queue();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue