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:
loki5512344 2026-08-19 12:00:27 +02:00
parent 1483c5cee0
commit e5e727b6c4
Signed by: boba
GPG key ID: 253067914055423B
19 changed files with 653 additions and 505 deletions

View 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());
}
}

View file

@ -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());
}

View file

@ -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;
}
}
}

View 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;
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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");
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}