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.metrics.HealthHttpServer;
|
||||||
import creator.loki.feature.moderation.MuteScheduler;
|
import creator.loki.feature.moderation.MuteScheduler;
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
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.music.repository.QueueRepository;
|
||||||
import creator.loki.feature.radio.listener.RadioBotListener;
|
|
||||||
import creator.loki.feature.radio.service.RadioManager;
|
import creator.loki.feature.radio.service.RadioManager;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener;
|
|
||||||
import net.dv8tion.jda.api.JDA;
|
import net.dv8tion.jda.api.JDA;
|
||||||
import net.dv8tion.jda.api.JDABuilder;
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
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;
|
JDA radioJda = null;
|
||||||
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
||||||
sleep(3000);
|
sleep(3000);
|
||||||
LavalinkManager radioLavalink =
|
LavalinkManager radioLavalink =
|
||||||
LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken());
|
LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken());
|
||||||
radioJda = createRadioJDA(radioLavalink);
|
radioJda = AuxiliaryBotFactory.createRadioJDA(config, radioLavalink, radioManager);
|
||||||
if (radioJda != null) {
|
if (radioJda != null) {
|
||||||
radioManager.setRadioJda(radioJda);
|
radioManager.setRadioJda(radioJda);
|
||||||
radioManager.setLavalinkManager(radioLavalink);
|
radioManager.setLavalinkManager(radioLavalink);
|
||||||
|
|
@ -105,43 +101,10 @@ public class BotInitializer {
|
||||||
return buildWithLavalink(builder, lavalinkManager);
|
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) {
|
private static JDA buildWithLavalink(final JDABuilder builder, final LavalinkManager ll) {
|
||||||
if (ll.getClient() != null) {
|
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());
|
return JdaFactory.awaitJda(builder.build());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -98,54 +98,4 @@ public class RateLimiter implements Lifecycle {
|
||||||
}
|
}
|
||||||
LoggerDecorator.success(logger, "RateLimiter завершён");
|
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;
|
package creator.loki.feature.admin.commands;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.feature.rating.repository.RatingsRepository;
|
import creator.loki.feature.rating.repository.RatingsRepository;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -51,7 +48,7 @@ public class GenerateRatingsCommand implements Command {
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(
|
||||||
errorEmbed(
|
RatingsEmbeds.error(
|
||||||
"❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота."))
|
"❌ Недостаточно прав", "Эта команда доступна только супер-администратору бота."))
|
||||||
.queue();
|
.queue();
|
||||||
return;
|
return;
|
||||||
|
|
@ -64,7 +61,7 @@ public class GenerateRatingsCommand implements Command {
|
||||||
context
|
context
|
||||||
.getChannel()
|
.getChannel()
|
||||||
.sendMessageEmbeds(
|
.sendMessageEmbeds(
|
||||||
warnEmbed(
|
RatingsEmbeds.warn(
|
||||||
"⏳ Генерация рейтингов...",
|
"⏳ Генерация рейтингов...",
|
||||||
"Начинаю генерацию всех rating значений для участников сервера..."))
|
"Начинаю генерацию всех rating значений для участников сервера..."))
|
||||||
.complete();
|
.complete();
|
||||||
|
|
@ -83,7 +80,9 @@ public class GenerateRatingsCommand implements Command {
|
||||||
}
|
}
|
||||||
generatedCount++;
|
generatedCount++;
|
||||||
}
|
}
|
||||||
initial.editMessageEmbeds(successEmbed(context, memberCount, generatedCount)).queue();
|
initial
|
||||||
|
.editMessageEmbeds(RatingsEmbeds.success(context, memberCount, generatedCount))
|
||||||
|
.queue();
|
||||||
logger.info(
|
logger.info(
|
||||||
"Супер-админ {} сгенерировал рейтинги для {} участников",
|
"Супер-админ {} сгенерировал рейтинги для {} участников",
|
||||||
context.getAuthor().getUser().getName(),
|
context.getAuthor().getUser().getName(),
|
||||||
|
|
@ -92,7 +91,8 @@ public class GenerateRatingsCommand implements Command {
|
||||||
logger.error("Ошибка при генерации рейтингов", e);
|
logger.error("Ошибка при генерации рейтингов", e);
|
||||||
initial
|
initial
|
||||||
.editMessageEmbeds(
|
.editMessageEmbeds(
|
||||||
errorEmbed("❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage()))
|
RatingsEmbeds.error(
|
||||||
|
"❌ Ошибка", "Произошла ошибка при генерации рейтингов: " + e.getMessage()))
|
||||||
.queue();
|
.queue();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -116,41 +116,4 @@ public class GenerateRatingsCommand implements Command {
|
||||||
};
|
};
|
||||||
ratingsRepository.setRating(guildId, userId, ratingType, value);
|
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.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
|
||||||
|
|
||||||
import creator.loki.feature.economy.repository.EconomyRepository;
|
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
public class BlackjackService {
|
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 EconomyRepository repository;
|
||||||
private final ConcurrentHashMap<String, ActiveBlackjack> activeGames = new ConcurrentHashMap<>();
|
private final ConcurrentHashMap<String, ActiveBlackjack> activeGames = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
|
@ -38,11 +29,13 @@ public class BlackjackService {
|
||||||
|
|
||||||
repository.save(account.withCash(account.getCash() - bet));
|
repository.save(account.withCash(account.getCash() - bet));
|
||||||
|
|
||||||
var playerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
|
var playerCards =
|
||||||
var dealerCards = new ArrayList<>(List.of(drawCard(), drawCard()));
|
new ArrayList<>(List.of(BlackjackEngine.drawCard(), BlackjackEngine.drawCard()));
|
||||||
|
var dealerCards =
|
||||||
|
new ArrayList<>(List.of(BlackjackEngine.drawCard(), BlackjackEngine.drawCard()));
|
||||||
|
|
||||||
var playerSum = calculateSum(playerCards);
|
var playerSum = BlackjackEngine.calculateSum(playerCards);
|
||||||
var dealerSum = calculateSum(dealerCards);
|
var dealerSum = BlackjackEngine.calculateSum(dealerCards);
|
||||||
var playerBj = playerSum == 21;
|
var playerBj = playerSum == 21;
|
||||||
var dealerBj = dealerSum == 21;
|
var dealerBj = dealerSum == 21;
|
||||||
|
|
||||||
|
|
@ -62,8 +55,8 @@ public class BlackjackService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
game.playerCards().add(drawCard());
|
game.playerCards().add(BlackjackEngine.drawCard());
|
||||||
var sum = calculateSum(game.playerCards());
|
var sum = BlackjackEngine.calculateSum(game.playerCards());
|
||||||
|
|
||||||
var over = sum > 21;
|
var over = sum > 21;
|
||||||
if (over) {
|
if (over) {
|
||||||
|
|
@ -74,7 +67,7 @@ public class BlackjackService {
|
||||||
game.playerCards(),
|
game.playerCards(),
|
||||||
game.dealerCards(),
|
game.dealerCards(),
|
||||||
sum,
|
sum,
|
||||||
calculateSum(game.dealerCards()),
|
BlackjackEngine.calculateSum(game.dealerCards()),
|
||||||
game.bet(),
|
game.bet(),
|
||||||
over,
|
over,
|
||||||
0);
|
0);
|
||||||
|
|
@ -87,12 +80,12 @@ public class BlackjackService {
|
||||||
}
|
}
|
||||||
|
|
||||||
var dealerCards = new ArrayList<>(game.dealerCards());
|
var dealerCards = new ArrayList<>(game.dealerCards());
|
||||||
while (calculateSum(dealerCards) < 16) {
|
while (BlackjackEngine.calculateSum(dealerCards) < 16) {
|
||||||
dealerCards.add(drawCard());
|
dealerCards.add(BlackjackEngine.drawCard());
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerSum = calculateSum(game.playerCards());
|
var playerSum = BlackjackEngine.calculateSum(game.playerCards());
|
||||||
var dealerSum = calculateSum(dealerCards);
|
var dealerSum = BlackjackEngine.calculateSum(dealerCards);
|
||||||
var payout = dealerSum > 21 || playerSum > dealerSum ? game.bet() * 2 : 0;
|
var payout = dealerSum > 21 || playerSum > dealerSum ? game.bet() * 2 : 0;
|
||||||
|
|
||||||
creditUser(userId, payout);
|
creditUser(userId, payout);
|
||||||
|
|
@ -109,30 +102,6 @@ public class BlackjackService {
|
||||||
repository.save(account.withCash(account.getCash() + amount));
|
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(
|
private BlackjackState buildState(
|
||||||
final List<String> playerCards,
|
final List<String> playerCards,
|
||||||
final List<String> dealerCards,
|
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;
|
package creator.loki.feature.image.service;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
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.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import creator.loki.config.BotConfig;
|
import creator.loki.config.BotConfig;
|
||||||
|
|
@ -21,7 +16,6 @@ import org.slf4j.LoggerFactory;
|
||||||
@Singleton
|
@Singleton
|
||||||
public class ImageService {
|
public class ImageService {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(ImageService.class);
|
private static final Logger logger = LoggerFactory.getLogger(ImageService.class);
|
||||||
private static final String E621_API = "https://e621.net/posts.json";
|
|
||||||
|
|
||||||
private final OkHttpClient client;
|
private final OkHttpClient client;
|
||||||
|
|
||||||
|
|
@ -40,37 +34,13 @@ public class ImageService {
|
||||||
|
|
||||||
public List<String> fetchE621(String tags, int limit, int page) {
|
public List<String> fetchE621(String tags, int limit, int page) {
|
||||||
int capped = Math.min(limit, 100);
|
int capped = Math.min(limit, 100);
|
||||||
String url = E621_API + "?tags=" + encodeTags(tags) + "&limit=" + capped + "&page=" + page;
|
String url = ImageUrls.e621(BooruParser.encodeTags(tags), capped, page);
|
||||||
Request request =
|
try (Response response = call(url)) {
|
||||||
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
|
||||||
|
|
||||||
try (Response response = client.newCall(request).execute()) {
|
|
||||||
if (!response.isSuccessful() || response.body() == null) {
|
if (!response.isSuccessful() || response.body() == null) {
|
||||||
logger.error("e621 API error: {} {}", response.code(), response.message());
|
logger.error("e621 API error: {} {}", response.code(), response.message());
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
String body = response.body().string();
|
return BooruParser.parseE621(response.body().string(), 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;
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Error fetching from e621", e);
|
logger.error("Error fetching from e621", e);
|
||||||
return List.of();
|
return List.of();
|
||||||
|
|
@ -83,13 +53,7 @@ public class ImageService {
|
||||||
|
|
||||||
public List<String> fetchGelbooru(String tags, int limit, int page) {
|
public List<String> fetchGelbooru(String tags, int limit, int page) {
|
||||||
int capped = Math.min(limit, 100);
|
int capped = Math.min(limit, 100);
|
||||||
String url =
|
String url = ImageUrls.gelbooru(BooruParser.encodeTags(tags), capped, page);
|
||||||
"https://gelbooru.com/index.php?page=dapi&s=post&q=index&json=1&tags="
|
|
||||||
+ encodeTags(tags)
|
|
||||||
+ "&limit="
|
|
||||||
+ capped
|
|
||||||
+ "&pid="
|
|
||||||
+ (page - 1);
|
|
||||||
return fetchBooruJson(url, limit);
|
return fetchBooruJson(url, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,62 +63,26 @@ public class ImageService {
|
||||||
|
|
||||||
public List<String> fetchYandere(String tags, int limit, int page) {
|
public List<String> fetchYandere(String tags, int limit, int page) {
|
||||||
int capped = Math.min(limit, 100);
|
int capped = Math.min(limit, 100);
|
||||||
String url =
|
String url = ImageUrls.yandere(BooruParser.encodeTags(tags), capped, page);
|
||||||
"https://yande.re/post.json?tags="
|
|
||||||
+ encodeTags(tags)
|
|
||||||
+ "&limit="
|
|
||||||
+ capped
|
|
||||||
+ "&page="
|
|
||||||
+ page;
|
|
||||||
return fetchBooruJson(url, limit);
|
return fetchBooruJson(url, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> fetchBooruJson(final String url, final int limit) {
|
private List<String> fetchBooruJson(final String url, final int limit) {
|
||||||
Request request =
|
try (Response response = call(url)) {
|
||||||
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
|
||||||
|
|
||||||
try (Response response = client.newCall(request).execute()) {
|
|
||||||
if (!response.isSuccessful() || response.body() == null) {
|
if (!response.isSuccessful() || response.body() == null) {
|
||||||
logger.error("Booru API error: {} {}", response.code(), response.message());
|
logger.error("Booru API error: {} {}", response.code(), response.message());
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return parseBooruResponse(response.body().string(), response.code(), limit);
|
return BooruParser.parseBooru(response.body().string(), response.code(), limit);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Error fetching from booru", e);
|
logger.error("Error fetching from booru", e);
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<String> parseBooruResponse(final String body, final int code, final int limit) {
|
private Response call(final String url) throws IOException {
|
||||||
if (body.isBlank() || !body.startsWith("[")) {
|
Request request =
|
||||||
logger.warn(
|
new Request.Builder().url(url).header("User-Agent", "LokiBot/1.0 (by loki)").get().build();
|
||||||
"Booru API returned non-JSON response (code {}): {}",
|
return client.newCall(request).execute();
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
package creator.loki.feature.serverstats;
|
||||||
|
|
||||||
import java.awt.Color;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.awt.Font;
|
|
||||||
import java.awt.Graphics2D;
|
|
||||||
import java.awt.RenderingHints;
|
|
||||||
import java.awt.image.BufferedImage;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
|
||||||
|
|
||||||
import creator.loki.feature.counter.repository.CounterStatsRepository;
|
import creator.loki.feature.counter.repository.CounterStatsRepository;
|
||||||
import creator.loki.feature.counter.service.format.DailyStats;
|
import creator.loki.feature.counter.service.format.DailyStats;
|
||||||
import net.dv8tion.jda.api.OnlineStatus;
|
import net.dv8tion.jda.api.OnlineStatus;
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
|
||||||
public class ServerStatsService {
|
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 CounterStatsRepository counterStatsRepository;
|
||||||
|
private final StatsChartRenderer chartRenderer = new StatsChartRenderer();
|
||||||
|
|
||||||
public ServerStatsService(CounterStatsRepository counterStatsRepository) {
|
public ServerStatsService(CounterStatsRepository counterStatsRepository) {
|
||||||
this.counterStatsRepository = counterStatsRepository;
|
this.counterStatsRepository = counterStatsRepository;
|
||||||
|
|
@ -39,80 +22,7 @@ public class ServerStatsService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] generateChart(List<DailyStats> history) throws Exception {
|
public byte[] generateChart(List<DailyStats> history) throws Exception {
|
||||||
var image = new BufferedImage(CHART_WIDTH, CHART_HEIGHT, BufferedImage.TYPE_INT_RGB);
|
return chartRenderer.render(history);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] generateCsv(final Guild guild, final List<DailyStats> history) {
|
public byte[] generateCsv(final Guild guild, final List<DailyStats> history) {
|
||||||
|
|
@ -142,7 +52,7 @@ public class ServerStatsService {
|
||||||
var header =
|
var header =
|
||||||
"server_name,server_id,owner,created_at,total_members,online,bots,text_channels,voice_channels,roles\n";
|
"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)
|
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) {
|
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.feature.voice.service.VoiceChannelManager;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import net.dv8tion.jda.api.Permission;
|
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.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
import net.dv8tion.jda.api.modals.Modal;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
@ -96,57 +92,15 @@ public class VoiceButtonHandler extends ListenerAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLimit(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
private void handleLimit(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
||||||
TextInput limitInput =
|
event.replyModal(VoiceModals.limit(channel)).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleRename(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
private void handleRename(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
||||||
TextInput nameInput =
|
event.replyModal(VoiceModals.rename(channel)).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleBitrate(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
private void handleBitrate(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
||||||
TextInput bitrateInput =
|
event.replyModal(VoiceModals.bitrate(channel)).queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleDelete(final ButtonInteractionEvent event, final VoiceChannel channel) {
|
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;
|
package creator.loki.feature.voice.service;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
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.VoiceChannelRepository;
|
||||||
import creator.loki.feature.voice.repository.VoicePresetRepository;
|
import creator.loki.feature.voice.repository.VoicePresetRepository;
|
||||||
import creator.loki.util.LoggerDecorator;
|
import creator.loki.util.LoggerDecorator;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.Permission;
|
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.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
import net.dv8tion.jda.api.entities.channel.concrete.VoiceChannel;
|
||||||
|
|
@ -25,7 +21,7 @@ public class VoiceChannelManager {
|
||||||
Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT,
|
Permission.MANAGE_CHANNEL, Permission.VOICE_CONNECT,
|
||||||
Permission.VOICE_MOVE_OTHERS, Permission.VOICE_MUTE_OTHERS);
|
Permission.VOICE_MOVE_OTHERS, Permission.VOICE_MUTE_OTHERS);
|
||||||
private final VoiceChannelRepository channelRepo;
|
private final VoiceChannelRepository channelRepo;
|
||||||
private final VoicePresetRepository presetRepo;
|
private final VoiceChannelActions channelActions;
|
||||||
private String triggerChannelId;
|
private String triggerChannelId;
|
||||||
private String categoryId;
|
private String categoryId;
|
||||||
|
|
||||||
|
|
@ -34,7 +30,7 @@ public class VoiceChannelManager {
|
||||||
VoicePresetRepository presetRepo,
|
VoicePresetRepository presetRepo,
|
||||||
final ConfigRepository configRepo) {
|
final ConfigRepository configRepo) {
|
||||||
this.channelRepo = channelRepo;
|
this.channelRepo = channelRepo;
|
||||||
this.presetRepo = presetRepo;
|
this.channelActions = new VoiceChannelActions(presetRepo);
|
||||||
this.triggerChannelId = configRepo.get("voice.trigger_channel_id");
|
this.triggerChannelId = configRepo.get("voice.trigger_channel_id");
|
||||||
this.categoryId = configRepo.get("voice.category_id");
|
this.categoryId = configRepo.get("voice.category_id");
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +47,7 @@ public class VoiceChannelManager {
|
||||||
logger,
|
logger,
|
||||||
"Создан личный канал %s для пользователя %s"
|
"Создан личный канал %s для пользователя %s"
|
||||||
.formatted(channel.getId(), member.getEffectiveName()));
|
.formatted(channel.getId(), member.getEffectiveName()));
|
||||||
applyUserPreset(channel, member.getId());
|
channelActions.applyUserPreset(channel, member.getId());
|
||||||
return channel;
|
return channel;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LoggerDecorator.error(logger, "Ошибка при создании личного канала", e);
|
LoggerDecorator.error(logger, "Ошибка при создании личного канала", e);
|
||||||
|
|
@ -107,55 +103,19 @@ public class VoiceChannelManager {
|
||||||
saveUserPreset(userId, preset);
|
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) {
|
public void saveUserPreset(final String userId, VoicePreset preset) {
|
||||||
presetRepo.savePreset(userId, preset);
|
channelActions.savePreset(userId, preset);
|
||||||
LoggerDecorator.voice(logger, "Сохранён пресет для пользователя " + userId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public VoicePreset getUserPreset(final String userId) {
|
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) {
|
public void applyUserPreset(final VoiceChannel channel, final String userId) {
|
||||||
var preset = getUserPreset(userId);
|
channelActions.applyUserPreset(channel, 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
package creator.loki.feature.voice.service;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandCategories;
|
import creator.loki.command.CommandCategories;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedFactory;
|
|
||||||
import creator.loki.feature.config.repository.ConfigRepository;
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
import creator.loki.util.GuildChannelResolve;
|
import creator.loki.util.GuildChannelResolve;
|
||||||
import net.dv8tion.jda.api.Permission;
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
|
@ -79,14 +77,10 @@ public class VoiceSetupPrefixCommand implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
configRepo.set("voice.trigger_channel_id", channel.getId());
|
configRepo.set("voice.trigger_channel_id", channel.getId());
|
||||||
var embed =
|
context
|
||||||
EmbedFactory.create()
|
.getChannel()
|
||||||
.withTitle("✅ Канал-триггер установлен")
|
.sendMessageEmbeds(VoiceSetupEmbeds.triggerSet(channel.getAsMention()))
|
||||||
.withDescription(
|
.queue();
|
||||||
"Теперь при входе в " + channel.getAsMention() + " будет создаваться личный канал")
|
|
||||||
.withColor(Color.GREEN)
|
|
||||||
.build();
|
|
||||||
context.getChannel().sendMessageEmbeds(embed).queue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCategory(final CommandContext context, final List<String> args) {
|
private void handleCategory(final CommandContext context, final List<String> args) {
|
||||||
|
|
@ -107,53 +101,22 @@ public class VoiceSetupPrefixCommand implements Command {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
configRepo.set("voice.category_id", category.getId());
|
configRepo.set("voice.category_id", category.getId());
|
||||||
var embed =
|
context
|
||||||
EmbedFactory.create()
|
.getChannel()
|
||||||
.withTitle("✅ Категория установлена")
|
.sendMessageEmbeds(VoiceSetupEmbeds.categorySet(category.getName()))
|
||||||
.withDescription(
|
.queue();
|
||||||
"Личные каналы будут создаваться в категории: **" + category.getName() + "**")
|
|
||||||
.withColor(Color.GREEN)
|
|
||||||
.build();
|
|
||||||
context.getChannel().sendMessageEmbeds(embed).queue();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleInfo(final CommandContext context) {
|
private void handleInfo(final CommandContext context) {
|
||||||
var triggerId = configRepo.get("voice.trigger_channel_id");
|
var triggerId = configRepo.get("voice.trigger_channel_id");
|
||||||
var categoryId = configRepo.get("voice.category_id");
|
var categoryId = configRepo.get("voice.category_id");
|
||||||
var embed =
|
context
|
||||||
EmbedFactory.create()
|
.getChannel()
|
||||||
.withTitle("🎤 Конфигурация системы личных каналов")
|
.sendMessageEmbeds(VoiceSetupEmbeds.info(context, triggerId, categoryId))
|
||||||
.withColor(Color.BLUE);
|
.queue();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendHelp(final CommandContext context) {
|
private void sendHelp(final CommandContext context) {
|
||||||
var desc =
|
context.getChannel().sendMessageEmbeds(VoiceSetupEmbeds.help()).queue();
|
||||||
"**Доступные команды:**\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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue