feat: TikTok download command + SOLID/DRY refactoring
- Add !tiktok command for downloading TikTok videos (no watermark) - TikTokApi service using tikwm.com API - Lifecycle interface for shutdown management (9→3 deps in ShutdownManager) - Extract JdaFactory + LavalinkManagerFactory from BotInitializer - Extract PlayLoadHandler from PlayCommand (207→148 lines) - Extract TrackStarter from TrackScheduler (146→113 lines) - Refactor MuteCommand execute() into methods (199→173 lines) - Move CommandRegistrar bindings to feature modules - RatingConfig: data-driven getLevels(name) + findLevel(value, name) - VoiceHelper.connectForPlayback() for voice connection retry - BotModule: 98→84 lines, BotInitializer: 162→135 lines
This commit is contained in:
parent
03d0ed6c43
commit
f93f6aafc8
65 changed files with 1599 additions and 666 deletions
|
|
@ -1,7 +1,5 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||
|
|
@ -9,6 +7,7 @@ import creator.loki.feature.counter.service.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.RadioBotListener;
|
||||
|
|
@ -17,11 +16,7 @@ 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.OnlineStatus;
|
||||
import net.dv8tion.jda.api.entities.Activity;
|
||||
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
|
@ -75,7 +70,7 @@ public class BotInitializer {
|
|||
JDA radioJda = null;
|
||||
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
||||
LavalinkManager radioLavalink =
|
||||
new LavalinkManager(config, queueRepository, config.getRadioToken());
|
||||
LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken());
|
||||
radioJda = createRadioJDA(radioLavalink);
|
||||
if (radioJda != null) {
|
||||
radioManager.setRadioJda(radioJda);
|
||||
|
|
@ -90,73 +85,51 @@ public class BotInitializer {
|
|||
|
||||
private JDA buildMainJDA() {
|
||||
var builder =
|
||||
createBuilder(
|
||||
JdaFactory.createBuilder(
|
||||
config.getMainToken(),
|
||||
config.getActivity(),
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_MEMBERS,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
if (lavalinkManager.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(lavalinkManager.getClient()));
|
||||
}
|
||||
builder.addEventListeners(listenerAggregator.getAll());
|
||||
return awaitJda(builder.build());
|
||||
return buildWithLavalink(builder, lavalinkManager);
|
||||
}
|
||||
|
||||
private JDA createMusicJDA() {
|
||||
if (config.getMusicToken() == null || config.getMusicToken().isBlank()) return null;
|
||||
LoggerDecorator.startup(logger, "Инициализация music бота...");
|
||||
LavalinkManager ml = new LavalinkManager(config, queueRepository, config.getMusicToken());
|
||||
LavalinkManager ml =
|
||||
LavalinkManagerFactory.create(config, queueRepository, config.getMusicToken());
|
||||
var builder =
|
||||
createBuilder(
|
||||
JdaFactory.createBuilder(
|
||||
config.getMusicToken(),
|
||||
"🎵 q!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
if (ml.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ml.getClient()));
|
||||
}
|
||||
builder.addEventListeners(
|
||||
new MusicBotListener(
|
||||
ml, queueRepository, new creator.loki.feature.music.core.PlayerManager(ml)));
|
||||
return awaitJda(builder.build());
|
||||
builder.addEventListeners(new MusicBotListener(ml, queueRepository, new PlayerManager(ml)));
|
||||
return buildWithLavalink(builder, ml);
|
||||
}
|
||||
|
||||
private JDA createRadioJDA(LavalinkManager radioLavalink) {
|
||||
if (config.getRadioToken() == null || config.getRadioToken().isBlank()) return null;
|
||||
LoggerDecorator.startup(logger, "Инициализация radio бота...");
|
||||
var builder =
|
||||
createBuilder(
|
||||
JdaFactory.createBuilder(
|
||||
config.getRadioToken(),
|
||||
"📻 r!play",
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT,
|
||||
GatewayIntent.GUILD_VOICE_STATES);
|
||||
if (radioLavalink.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(
|
||||
new JDAVoiceUpdateListener(radioLavalink.getClient()));
|
||||
}
|
||||
builder.addEventListeners(new RadioBotListener(radioManager));
|
||||
return awaitJda(builder.build());
|
||||
return buildWithLavalink(builder, radioLavalink);
|
||||
}
|
||||
|
||||
private static JDABuilder createBuilder(String token, String activity, GatewayIntent... intents) {
|
||||
return JDABuilder.createDefault(token)
|
||||
.setStatus(OnlineStatus.ONLINE)
|
||||
.setActivity(Activity.playing(activity))
|
||||
.enableIntents(List.of(intents))
|
||||
.enableCache(CacheFlag.VOICE_STATE);
|
||||
}
|
||||
|
||||
private static JDA awaitJda(JDA jda) {
|
||||
try {
|
||||
jda.awaitReady();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("JDA прерван во время ожидания готовности", e);
|
||||
private static JDA buildWithLavalink(JDABuilder builder, LavalinkManager ll) {
|
||||
if (ll.getClient() != null) {
|
||||
builder.setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(ll.getClient()));
|
||||
}
|
||||
return jda;
|
||||
return JdaFactory.awaitJda(builder.build());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,17 @@ public class BotJdaRegistry {
|
|||
private JDA music;
|
||||
private JDA radio;
|
||||
|
||||
public void setMain(JDA jda) { this.main = jda; }
|
||||
public void setMusic(JDA jda) { this.music = jda; }
|
||||
public void setRadio(JDA jda) { this.radio = jda; }
|
||||
public void setMain(JDA jda) {
|
||||
this.main = jda;
|
||||
}
|
||||
|
||||
public void setMusic(JDA jda) {
|
||||
this.music = jda;
|
||||
}
|
||||
|
||||
public void setRadio(JDA jda) {
|
||||
this.radio = jda;
|
||||
}
|
||||
|
||||
public JDA getByPersonality(String personality) {
|
||||
return switch (personality) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class EventDispatcher {
|
||||
public class EventDispatcher implements Lifecycle {
|
||||
private static final Logger logger = LoggerFactory.getLogger(EventDispatcher.class);
|
||||
private final BotConfig config;
|
||||
private final CommandRegistry registry;
|
||||
|
|
|
|||
32
src/main/java/creator/loki/core/JdaFactory.java
Normal file
32
src/main/java/creator/loki/core/JdaFactory.java
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.OnlineStatus;
|
||||
import net.dv8tion.jda.api.entities.Activity;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
import net.dv8tion.jda.api.utils.cache.CacheFlag;
|
||||
|
||||
public final class JdaFactory {
|
||||
private JdaFactory() {}
|
||||
|
||||
public static JDABuilder createBuilder(String token, String activity, GatewayIntent... intents) {
|
||||
return JDABuilder.createDefault(token)
|
||||
.setStatus(OnlineStatus.ONLINE)
|
||||
.setActivity(Activity.playing(activity))
|
||||
.enableIntents(List.of(intents))
|
||||
.enableCache(CacheFlag.VOICE_STATE);
|
||||
}
|
||||
|
||||
public static JDA awaitJda(JDA jda) {
|
||||
try {
|
||||
jda.awaitReady();
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException("JDA прерван во время ожидания готовности", e);
|
||||
}
|
||||
return jda;
|
||||
}
|
||||
}
|
||||
14
src/main/java/creator/loki/core/LavalinkManagerFactory.java
Normal file
14
src/main/java/creator/loki/core/LavalinkManagerFactory.java
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.repository.QueueRepository;
|
||||
|
||||
public final class LavalinkManagerFactory {
|
||||
private LavalinkManagerFactory() {}
|
||||
|
||||
public static LavalinkManager create(
|
||||
BotConfig config, QueueRepository queueRepository, String token) {
|
||||
return new LavalinkManager(config, queueRepository, token);
|
||||
}
|
||||
}
|
||||
22
src/main/java/creator/loki/core/Lifecycle.java
Normal file
22
src/main/java/creator/loki/core/Lifecycle.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package creator.loki.core;
|
||||
|
||||
/**
|
||||
* Интерфейс для компонентов, требующих корректного завершения работы.
|
||||
*
|
||||
* <p>Реализуется классами, которые управляют ресурсами (потоками, соединениями, планировщиками) и
|
||||
* должны быть корректно остановлены при завершении работы бота.
|
||||
*/
|
||||
public interface Lifecycle {
|
||||
|
||||
/**
|
||||
* Возвращает имя компонента для логирования при завершении работы.
|
||||
*
|
||||
* @return имя компонента (по умолчанию - простое имя класса)
|
||||
*/
|
||||
default String name() {
|
||||
return getClass().getSimpleName();
|
||||
}
|
||||
|
||||
/** Корректно завершает работу компонента, освобождая управляемые ресурсы. */
|
||||
void shutdown();
|
||||
}
|
||||
|
|
@ -1,16 +1,11 @@
|
|||
package creator.loki.core;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.core.security.CooldownManager;
|
||||
import creator.loki.core.security.RateLimiter;
|
||||
import creator.loki.database.Database;
|
||||
import creator.loki.feature.counter.service.MemberCounterListener;
|
||||
import creator.loki.feature.level.service.VoiceTracker;
|
||||
import creator.loki.feature.metrics.HealthHttpServer;
|
||||
import creator.loki.feature.moderation.MuteScheduler;
|
||||
import creator.loki.feature.radio.RadioManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
|
|
@ -21,34 +16,13 @@ import org.slf4j.LoggerFactory;
|
|||
public class ShutdownManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ShutdownManager.class);
|
||||
|
||||
private final HealthHttpServer healthHttpServer;
|
||||
private final CooldownManager cooldownManager;
|
||||
private final RateLimiter rateLimiter;
|
||||
private final VoiceTracker voiceTracker;
|
||||
private final MemberCounterListener memberCounterListener;
|
||||
private final MuteScheduler muteScheduler;
|
||||
private final EventDispatcher dispatcher;
|
||||
private final Set<Lifecycle> lifecycles;
|
||||
private final Database database;
|
||||
private final RadioManager radioManager;
|
||||
|
||||
@Inject
|
||||
public ShutdownManager(
|
||||
HealthHttpServer healthHttpServer,
|
||||
CooldownManager cooldownManager,
|
||||
RateLimiter rateLimiter,
|
||||
VoiceTracker voiceTracker,
|
||||
MemberCounterListener memberCounterListener,
|
||||
MuteScheduler muteScheduler,
|
||||
EventDispatcher dispatcher,
|
||||
Database database,
|
||||
RadioManager radioManager) {
|
||||
this.healthHttpServer = healthHttpServer;
|
||||
this.cooldownManager = cooldownManager;
|
||||
this.rateLimiter = rateLimiter;
|
||||
this.voiceTracker = voiceTracker;
|
||||
this.memberCounterListener = memberCounterListener;
|
||||
this.muteScheduler = muteScheduler;
|
||||
this.dispatcher = dispatcher;
|
||||
public ShutdownManager(Set<Lifecycle> lifecycles, Database database, RadioManager radioManager) {
|
||||
this.lifecycles = lifecycles;
|
||||
this.database = database;
|
||||
this.radioManager = radioManager;
|
||||
}
|
||||
|
|
@ -67,19 +41,15 @@ public class ShutdownManager {
|
|||
logger.info("========================================");
|
||||
|
||||
try {
|
||||
// Stop all radio playback
|
||||
if (cluster.hasRadioBot()) {
|
||||
radioManager.getRadioJda().getGuilds().forEach(g -> radioManager.stopRadio(g.getIdLong()));
|
||||
}
|
||||
|
||||
healthHttpServer.stop();
|
||||
LoggerDecorator.bot(logger, "Остановка менеджеров...");
|
||||
cooldownManager.shutdown();
|
||||
rateLimiter.shutdown();
|
||||
voiceTracker.shutdown();
|
||||
memberCounterListener.shutdown();
|
||||
muteScheduler.shutdown();
|
||||
dispatcher.shutdown();
|
||||
for (Lifecycle lifecycle : lifecycles) {
|
||||
LoggerDecorator.bot(logger, "Остановка " + lifecycle.name() + "...");
|
||||
lifecycle.shutdown();
|
||||
}
|
||||
|
||||
LoggerDecorator.database(logger, "Закрытие базы данных...");
|
||||
database.close();
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ import java.util.concurrent.Executors;
|
|||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CooldownManager {
|
||||
import creator.loki.core.Lifecycle;
|
||||
|
||||
public class CooldownManager implements Lifecycle {
|
||||
private final ConcurrentHashMap<String, Instant> cooldowns = new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService cleanupExecutor =
|
||||
Executors.newSingleThreadScheduledExecutor();
|
||||
|
|
|
|||
|
|
@ -5,12 +5,13 @@ import java.util.concurrent.Executors;
|
|||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Rate limiter для защиты от спама Использует алгоритм Token Bucket */
|
||||
public class RateLimiter {
|
||||
public class RateLimiter implements Lifecycle {
|
||||
private static final Logger logger = LoggerFactory.getLogger(RateLimiter.class);
|
||||
|
||||
// Лимиты по умолчанию
|
||||
|
|
|
|||
|
|
@ -8,11 +8,12 @@ import java.sql.Statement;
|
|||
|
||||
import com.zaxxer.hikari.HikariConfig;
|
||||
import com.zaxxer.hikari.HikariDataSource;
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class Database {
|
||||
public class Database implements Lifecycle {
|
||||
private static final Logger logger = LoggerFactory.getLogger(Database.class);
|
||||
private final String dbPath;
|
||||
private HikariDataSource dataSource;
|
||||
|
|
@ -105,4 +106,9 @@ public class Database {
|
|||
LoggerDecorator.database(logger, "Пул соединений закрыт");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,40 +3,33 @@ package creator.loki.di;
|
|||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.config.ConfigLoader;
|
||||
import creator.loki.core.EventDispatcher;
|
||||
import creator.loki.core.ListenerAggregator;
|
||||
import creator.loki.core.MessageCache;
|
||||
import creator.loki.core.ShutdownManager;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
import creator.loki.core.registry.CommandRegistry;
|
||||
import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||
import creator.loki.core.security.CooldownManager;
|
||||
import creator.loki.core.security.RateLimiter;
|
||||
import creator.loki.database.DatabaseModule;
|
||||
import creator.loki.feature.counter.CounterModule;
|
||||
import creator.loki.feature.economy.EconomyCommandRegistrar;
|
||||
import creator.loki.feature.economy.EconomyModule;
|
||||
import creator.loki.feature.entertainment.EntertainmentCommandRegistrar;
|
||||
import creator.loki.feature.image.ImageCommandRegistrar;
|
||||
import creator.loki.feature.entertainment.EntertainmentModule;
|
||||
import creator.loki.feature.image.ImageModule;
|
||||
import creator.loki.feature.level.LevelModule;
|
||||
import creator.loki.feature.level.commands.LevelCommandRegistrar;
|
||||
import creator.loki.feature.metrics.HealthHttpServer;
|
||||
import creator.loki.feature.metrics.MetricsCollector;
|
||||
import creator.loki.feature.metrics.MetricsModule;
|
||||
import creator.loki.feature.moderation.ModerationCommandRegistrar;
|
||||
import creator.loki.feature.moderation.ModerationModule;
|
||||
import creator.loki.feature.music.MusicCommandRegistrar;
|
||||
import creator.loki.feature.music.MusicModule;
|
||||
import creator.loki.feature.profile.ProfileModule;
|
||||
import creator.loki.feature.radio.RadioCommandRegistrar;
|
||||
import creator.loki.feature.radio.RadioModule;
|
||||
import creator.loki.feature.roast.RoastModule;
|
||||
import creator.loki.feature.role.RoleModule;
|
||||
import creator.loki.feature.utility.UtilityCommandRegistrar;
|
||||
import creator.loki.feature.tiktok.TikTokModule;
|
||||
import creator.loki.feature.utility.UtilityModule;
|
||||
import creator.loki.feature.voice.VoiceModule;
|
||||
import creator.loki.feature.welcome.WelcomeModule;
|
||||
|
||||
|
|
@ -67,17 +60,10 @@ public class BotModule extends AbstractModule {
|
|||
install(new RadioModule());
|
||||
install(new RoastModule());
|
||||
install(new ImageModule());
|
||||
|
||||
Multibinder<CommandRegistrar> commandBinder =
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class);
|
||||
commandBinder.addBinding().to(LevelCommandRegistrar.class);
|
||||
commandBinder.addBinding().to(ModerationCommandRegistrar.class);
|
||||
commandBinder.addBinding().to(EntertainmentCommandRegistrar.class);
|
||||
commandBinder.addBinding().to(UtilityCommandRegistrar.class);
|
||||
commandBinder.addBinding().to(MusicCommandRegistrar.class);
|
||||
commandBinder.addBinding().to(EconomyCommandRegistrar.class);
|
||||
commandBinder.addBinding().to(RadioCommandRegistrar.class);
|
||||
commandBinder.addBinding().to(ImageCommandRegistrar.class);
|
||||
install(new TikTokModule());
|
||||
install(new EntertainmentModule());
|
||||
install(new UtilityModule());
|
||||
install(new LifecycleModule());
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
28
src/main/java/creator/loki/di/LifecycleModule.java
Normal file
28
src/main/java/creator/loki/di/LifecycleModule.java
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package creator.loki.di;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.EventDispatcher;
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.core.security.CooldownManager;
|
||||
import creator.loki.core.security.RateLimiter;
|
||||
import creator.loki.database.Database;
|
||||
import creator.loki.feature.counter.service.MemberCounterListener;
|
||||
import creator.loki.feature.level.service.VoiceTracker;
|
||||
import creator.loki.feature.metrics.HealthHttpServer;
|
||||
import creator.loki.feature.moderation.MuteScheduler;
|
||||
|
||||
public class LifecycleModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
Multibinder<Lifecycle> binder = Multibinder.newSetBinder(binder(), Lifecycle.class);
|
||||
binder.addBinding().to(CooldownManager.class);
|
||||
binder.addBinding().to(RateLimiter.class);
|
||||
binder.addBinding().to(EventDispatcher.class);
|
||||
binder.addBinding().to(VoiceTracker.class);
|
||||
binder.addBinding().to(MemberCounterListener.class);
|
||||
binder.addBinding().to(MuteScheduler.class);
|
||||
binder.addBinding().to(HealthHttpServer.class);
|
||||
binder.addBinding().to(Database.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,8 +52,7 @@ public final class HelpCategories {
|
|||
Map.entry("Система", "Метрики и служебная информация для админов"),
|
||||
Map.entry("Настройка", "Настройка систем бота (только админы)"),
|
||||
Map.entry("Супер-Админ", "Сервисные команды владельца бота"),
|
||||
Map.entry(
|
||||
CommandCategories.NSFW_IMAGES, "Поиск изображений (только в NSFW каналах)"));
|
||||
Map.entry(CommandCategories.NSFW_IMAGES, "Поиск изображений (только в NSFW каналах)"));
|
||||
}
|
||||
|
||||
private HelpCategories() {}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import java.util.concurrent.ScheduledExecutorService;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.events.guild.member.GuildMemberJoinEvent;
|
||||
|
|
@ -14,7 +15,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/** Listener для счетчика участников */
|
||||
public class MemberCounterListener extends ListenerAdapter {
|
||||
public class MemberCounterListener extends ListenerAdapter implements Lifecycle {
|
||||
private static final Logger logger = LoggerFactory.getLogger(MemberCounterListener.class);
|
||||
private final CounterManager counterManager;
|
||||
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package creator.loki.feature.economy;
|
|||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
import creator.loki.core.security.CooldownManager;
|
||||
import creator.loki.database.Database;
|
||||
import creator.loki.feature.economy.repository.EconomyRepository;
|
||||
|
|
@ -16,7 +18,11 @@ import creator.loki.feature.economy.service.SlotService;
|
|||
public class EconomyModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {}
|
||||
protected void configure() {
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(EconomyCommandRegistrar.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -32,23 +32,27 @@ import creator.loki.feature.social.commands.ComplimentCommand;
|
|||
import creator.loki.feature.social.commands.PatCommand;
|
||||
import creator.loki.feature.social.commands.RoastCommand;
|
||||
import creator.loki.feature.social.commands.SocialCommand;
|
||||
import creator.loki.feature.tiktok.TikTokCommand;
|
||||
|
||||
public class EntertainmentCommandRegistrar implements CommandRegistrar {
|
||||
private final CooldownManager cooldownManager;
|
||||
private final RatingService ratingService;
|
||||
private final ShipPairRepository shipPairRepository;
|
||||
private final BotConfig config;
|
||||
private final TikTokCommand tikTokCommand;
|
||||
|
||||
@Inject
|
||||
public EntertainmentCommandRegistrar(
|
||||
CooldownManager cooldownManager,
|
||||
RatingService ratingService,
|
||||
ShipPairRepository shipPairRepository,
|
||||
BotConfig config) {
|
||||
BotConfig config,
|
||||
TikTokCommand tikTokCommand) {
|
||||
this.cooldownManager = cooldownManager;
|
||||
this.ratingService = ratingService;
|
||||
this.shipPairRepository = shipPairRepository;
|
||||
this.config = config;
|
||||
this.tikTokCommand = tikTokCommand;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -61,6 +65,7 @@ public class EntertainmentCommandRegistrar implements CommandRegistrar {
|
|||
registry.register(new RoastCommand());
|
||||
registry.register(new ComplimentCommand());
|
||||
registry.register(new GlazbobaCommand());
|
||||
registry.register(tikTokCommand);
|
||||
|
||||
registry.register(new IqCommand(ratingService));
|
||||
registry.register(new GayCommand(ratingService));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package creator.loki.feature.entertainment;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
|
||||
public class EntertainmentModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(EntertainmentCommandRegistrar.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -97,7 +97,8 @@ public class HelpCommand implements Command {
|
|||
boolean isNsfwChannel) {
|
||||
int totalCommands =
|
||||
commandsByCategory.entrySet().stream()
|
||||
.filter(e -> HelpCategories.isCategoryVisible(e.getKey(), isAdmin, isSuper, isNsfwChannel))
|
||||
.filter(
|
||||
e -> HelpCategories.isCategoryVisible(e.getKey(), isAdmin, isSuper, isNsfwChannel))
|
||||
.mapToInt(e -> e.getValue().size())
|
||||
.sum();
|
||||
|
||||
|
|
@ -106,8 +107,7 @@ public class HelpCommand implements Command {
|
|||
.sorted()
|
||||
.forEach(
|
||||
category -> {
|
||||
if (!HelpCategories.isCategoryVisible(
|
||||
category, isAdmin, isSuper, isNsfwChannel)) {
|
||||
if (!HelpCategories.isCategoryVisible(category, isAdmin, isSuper, isNsfwChannel)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
package creator.loki.feature.image;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||
import net.dv8tion.jda.api.components.buttons.Button;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
|
||||
public final class ImageEmbedHelper {
|
||||
|
||||
private static final String NAV_PREFIX = "img_nav:";
|
||||
|
|
@ -24,9 +24,8 @@ public final class ImageEmbedHelper {
|
|||
boolean descUsed = false;
|
||||
|
||||
for (String url : pageUrls) {
|
||||
var builder = EmbedFactory.create()
|
||||
.withTitle("🔞 " + session.site())
|
||||
.withColor(session.color());
|
||||
var builder =
|
||||
EmbedFactory.create().withTitle("🔞 " + session.site()).withColor(session.color());
|
||||
String ext = extension(url);
|
||||
boolean isVideo = isVideoExt(ext);
|
||||
if (!isVideo) {
|
||||
|
|
@ -57,10 +56,11 @@ public final class ImageEmbedHelper {
|
|||
int last = embeds.size() - 1;
|
||||
var lastUrl = pageUrls.get(last);
|
||||
var lastExt = extension(lastUrl);
|
||||
var lastBuilder = EmbedFactory.create()
|
||||
.withTitle("🔞 " + session.site())
|
||||
.withColor(session.color())
|
||||
.withFooter(footer);
|
||||
var lastBuilder =
|
||||
EmbedFactory.create()
|
||||
.withTitle("🔞 " + session.site())
|
||||
.withColor(session.color())
|
||||
.withFooter(footer);
|
||||
if (!isVideoExt(lastExt)) {
|
||||
lastBuilder.withImage(lastUrl);
|
||||
}
|
||||
|
|
@ -88,8 +88,7 @@ public final class ImageEmbedHelper {
|
|||
boolean noNext = !session.hasNext() && session.exhausted();
|
||||
return ActionRow.of(
|
||||
Button.primary(NAV_PREFIX + "prev", "◀ Назад").withDisabled(noPrev),
|
||||
Button.primary(NAV_PREFIX + "next", "Вперёд ▶").withDisabled(noNext)
|
||||
);
|
||||
Button.primary(NAV_PREFIX + "next", "Вперёд ▶").withDisabled(noNext));
|
||||
}
|
||||
|
||||
public static String formatTags(String tags) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package creator.loki.feature.image;
|
|||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
|
||||
public class ImageModule extends AbstractModule {
|
||||
|
||||
|
|
@ -11,6 +13,10 @@ public class ImageModule extends AbstractModule {
|
|||
protected void configure() {
|
||||
bind(ImageSessionManager.class).in(Singleton.class);
|
||||
bind(ImageNavigationHandler.class).in(Singleton.class);
|
||||
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(ImageCommandRegistrar.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package creator.loki.feature.image;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||
|
|
@ -7,8 +9,6 @@ import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Singleton
|
||||
public class ImageNavigationHandler extends ListenerAdapter {
|
||||
|
||||
|
|
@ -84,7 +84,8 @@ public class ImageNavigationHandler extends ListenerAdapter {
|
|||
|
||||
private void loadMore(ButtonInteractionEvent event, ImageSession session, String messageId) {
|
||||
int nextApiPage = session.apiPage() + 1;
|
||||
logger.debug("loadMore: site={} tags='{}' apiPage={}", session.site(), session.tags(), nextApiPage);
|
||||
logger.debug(
|
||||
"loadMore: site={} tags='{}' apiPage={}", session.site(), session.tags(), nextApiPage);
|
||||
|
||||
List<String> newUrls = fetchPage(session.site(), session.tags(), nextApiPage);
|
||||
logger.debug("loadMore: got {} urls", newUrls.size());
|
||||
|
|
@ -93,7 +94,8 @@ public class ImageNavigationHandler extends ListenerAdapter {
|
|||
ImageSession updated = session.withMoreUrls(newUrls);
|
||||
sessionManager.update(messageId, updated);
|
||||
|
||||
event.getHook()
|
||||
event
|
||||
.getHook()
|
||||
.editOriginalEmbeds(ImageEmbedHelper.buildEmbeds(updated))
|
||||
.setComponents(ImageEmbedHelper.buildActionRow(updated))
|
||||
.queue();
|
||||
|
|
@ -101,18 +103,19 @@ public class ImageNavigationHandler extends ListenerAdapter {
|
|||
|
||||
private List<String> fetchPage(String site, String tags, int apiPage) {
|
||||
return switch (site) {
|
||||
case "e621.net" -> imageService.fetchE621(tags, PAGE_FETCH, apiPage);
|
||||
case "rule34.xxx" -> imageService.fetchR34(tags, PAGE_FETCH, apiPage);
|
||||
case "e621.net" -> imageService.fetchE621(tags, PAGE_FETCH, apiPage);
|
||||
case "rule34.xxx" -> imageService.fetchR34(tags, PAGE_FETCH, apiPage);
|
||||
case "gelbooru.com" -> imageService.fetchGelbooru(tags, PAGE_FETCH, apiPage);
|
||||
case "yande.re" -> imageService.fetchYandere(tags, PAGE_FETCH, apiPage);
|
||||
default -> List.of();
|
||||
case "yande.re" -> imageService.fetchYandere(tags, PAGE_FETCH, apiPage);
|
||||
default -> List.of();
|
||||
};
|
||||
}
|
||||
|
||||
// ── Вспомогательное ──────────────────────────────────────────────────────────
|
||||
|
||||
private static void editMessage(ButtonInteractionEvent event, ImageSession session) {
|
||||
event.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||
event
|
||||
.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||
.queue();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
package creator.loki.feature.image;
|
||||
|
||||
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;
|
||||
|
|
@ -13,11 +18,6 @@ import okhttp3.Response;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Singleton
|
||||
public class ImageService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ImageService.class);
|
||||
|
|
@ -48,11 +48,7 @@ public class ImageService {
|
|||
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();
|
||||
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) {
|
||||
|
|
@ -91,8 +87,7 @@ public class ImageService {
|
|||
|
||||
public List<String> fetchR34(String tags, int limit, int page) {
|
||||
int capped = Math.min(limit, 100);
|
||||
String url =
|
||||
R34_API + "&tags=" + encodeTags(tags) + "&limit=" + capped + "&pid=" + (page - 1);
|
||||
String url = R34_API + "&tags=" + encodeTags(tags) + "&limit=" + capped + "&pid=" + (page - 1);
|
||||
if (!r34ApiKey.isEmpty() && !r34UserId.isEmpty()) {
|
||||
url += "&api_key=" + r34ApiKey + "&user_id=" + r34UserId;
|
||||
}
|
||||
|
|
@ -132,11 +127,8 @@ public class ImageService {
|
|||
}
|
||||
|
||||
private List<String> fetchBooruJson(String url, int limit) {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", "LokiBot/1.0 (by loki)")
|
||||
.get()
|
||||
.build();
|
||||
Request request =
|
||||
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) {
|
||||
|
|
@ -145,7 +137,10 @@ public class ImageService {
|
|||
}
|
||||
String body = response.body().string();
|
||||
if (body.isBlank() || !body.startsWith("[")) {
|
||||
logger.warn("Booru API returned non-JSON response (code {}): {}", response.code(), body.length() > 200 ? body.substring(0, 200) : body);
|
||||
logger.warn(
|
||||
"Booru API returned non-JSON response (code {}): {}",
|
||||
response.code(),
|
||||
body.length() > 200 ? body.substring(0, 200) : body);
|
||||
return List.of();
|
||||
}
|
||||
JsonArray posts = JsonParser.parseString(body).getAsJsonArray();
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import java.util.List;
|
|||
/**
|
||||
* Иммутабельная сессия навигации по изображениям.
|
||||
*
|
||||
* <p>exhausted = true означает, что API вернул пустой ответ на последнем запросе,
|
||||
* т.е. новых страниц больше нет — тогда кнопка «Вперёд» блокируется.
|
||||
* <p>exhausted = true означает, что API вернул пустой ответ на последнем запросе, т.е. новых
|
||||
* страниц больше нет — тогда кнопка «Вперёд» блокируется.
|
||||
*/
|
||||
public record ImageSession(
|
||||
List<String> urls,
|
||||
|
|
@ -37,8 +37,17 @@ public record ImageSession(
|
|||
long userId,
|
||||
long guildId) {
|
||||
return new ImageSession(
|
||||
urls, 0, pageSize, site, color, tags, userId, guildId,
|
||||
System.currentTimeMillis(), 1, false);
|
||||
urls,
|
||||
0,
|
||||
pageSize,
|
||||
site,
|
||||
color,
|
||||
tags,
|
||||
userId,
|
||||
guildId,
|
||||
System.currentTimeMillis(),
|
||||
1,
|
||||
false);
|
||||
}
|
||||
|
||||
// ── Навигация ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -66,25 +75,52 @@ public record ImageSession(
|
|||
|
||||
public ImageSession withPage(int newPage) {
|
||||
return new ImageSession(
|
||||
urls, newPage, pageSize, site, color, tags, userId, guildId,
|
||||
System.currentTimeMillis(), apiPage, exhausted);
|
||||
urls,
|
||||
newPage,
|
||||
pageSize,
|
||||
site,
|
||||
color,
|
||||
tags,
|
||||
userId,
|
||||
guildId,
|
||||
System.currentTimeMillis(),
|
||||
apiPage,
|
||||
exhausted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляем новые URL из следующей API-страницы и переходим на следующую
|
||||
* внутреннюю страницу. Если newUrls пустой — помечаем сессию как exhausted.
|
||||
* Добавляем новые URL из следующей API-страницы и переходим на следующую внутреннюю страницу.
|
||||
* Если newUrls пустой — помечаем сессию как exhausted.
|
||||
*/
|
||||
public ImageSession withMoreUrls(List<String> newUrls) {
|
||||
if (newUrls.isEmpty()) {
|
||||
// API больше ничего не отдал — остаёмся на месте, ставим флаг
|
||||
return new ImageSession(
|
||||
urls, page, pageSize, site, color, tags, userId, guildId,
|
||||
System.currentTimeMillis(), apiPage, true);
|
||||
urls,
|
||||
page,
|
||||
pageSize,
|
||||
site,
|
||||
color,
|
||||
tags,
|
||||
userId,
|
||||
guildId,
|
||||
System.currentTimeMillis(),
|
||||
apiPage,
|
||||
true);
|
||||
}
|
||||
List<String> combined = new ArrayList<>(urls);
|
||||
combined.addAll(newUrls);
|
||||
return new ImageSession(
|
||||
combined, page + 1, pageSize, site, color, tags, userId, guildId,
|
||||
System.currentTimeMillis(), apiPage + 1, false);
|
||||
combined,
|
||||
page + 1,
|
||||
pageSize,
|
||||
site,
|
||||
color,
|
||||
tags,
|
||||
userId,
|
||||
guildId,
|
||||
System.currentTimeMillis(),
|
||||
apiPage + 1,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package creator.loki.feature.image;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package creator.loki.feature.image.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
|
|
@ -9,24 +12,21 @@ import creator.loki.feature.image.ImageSession;
|
|||
import creator.loki.feature.image.ImageSessionManager;
|
||||
import net.dv8tion.jda.api.entities.channel.attribute.IAgeRestrictedChannel;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Общая база для команд поиска изображений.
|
||||
* Убирает дублирование: NSFW-проверка, parseArgs, buildEmbeds, buildButtons.
|
||||
* Общая база для команд поиска изображений. Убирает дублирование: NSFW-проверка, parseArgs,
|
||||
* buildEmbeds, buildButtons.
|
||||
*/
|
||||
public abstract class AbstractImageCommand implements Command {
|
||||
|
||||
protected static final int PAGE_SIZE = 10;
|
||||
protected static final int PAGE_SIZE = 10;
|
||||
protected static final int DEFAULT_FETCH = 30;
|
||||
protected static final int MAX_FETCH = 50;
|
||||
protected static final int MAX_FETCH = 50;
|
||||
|
||||
protected final ImageService imageService;
|
||||
protected final ImageSessionManager sessionManager;
|
||||
|
||||
protected AbstractImageCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||
this.imageService = imageService;
|
||||
this.imageService = imageService;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
|
|
@ -49,40 +49,54 @@ public abstract class AbstractImageCommand implements Command {
|
|||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
if (!isNsfwChannel(context)) {
|
||||
context.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error(
|
||||
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
ParseResult parsed = parseArgs(context.getArgs());
|
||||
String searchingMsg = "Ищу изображения на " + siteDisplayName()
|
||||
+ ImageEmbedHelper.tagsDesc(parsed.tags()) + "...";
|
||||
String searchingMsg =
|
||||
"Ищу изображения на "
|
||||
+ siteDisplayName()
|
||||
+ ImageEmbedHelper.tagsDesc(parsed.tags())
|
||||
+ "...";
|
||||
|
||||
context.getChannel()
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("🔍 Поиск", searchingMsg))
|
||||
.queue(msg -> {
|
||||
List<String> urls = fetchUrls(parsed.tags(), parsed.limit());
|
||||
if (urls.isEmpty()) {
|
||||
msg.editMessageEmbeds(EmbedTemplates.warning(
|
||||
"😔 Ничего не найдено",
|
||||
"По запросу" + ImageEmbedHelper.tagsDesc(parsed.tags()) + " ничего не найдено."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
.queue(
|
||||
msg -> {
|
||||
List<String> urls = fetchUrls(parsed.tags(), parsed.limit());
|
||||
if (urls.isEmpty()) {
|
||||
msg.editMessageEmbeds(
|
||||
EmbedTemplates.warning(
|
||||
"😔 Ничего не найдено",
|
||||
"По запросу"
|
||||
+ ImageEmbedHelper.tagsDesc(parsed.tags())
|
||||
+ " ничего не найдено."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
ImageSession session = ImageSession.create(
|
||||
urls, PAGE_SIZE, siteName(), siteColor(),
|
||||
parsed.tags(),
|
||||
context.getAuthor().getIdLong(),
|
||||
context.getGuild().getIdLong());
|
||||
sessionManager.put(msg.getId(), session);
|
||||
ImageSession session =
|
||||
ImageSession.create(
|
||||
urls,
|
||||
PAGE_SIZE,
|
||||
siteName(),
|
||||
siteColor(),
|
||||
parsed.tags(),
|
||||
context.getAuthor().getIdLong(),
|
||||
context.getGuild().getIdLong());
|
||||
sessionManager.put(msg.getId(), session);
|
||||
|
||||
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||
.queue();
|
||||
});
|
||||
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||
.queue();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Вспомогательное ──────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,26 +1,52 @@
|
|||
package creator.loki.feature.image.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.feature.image.ImageService;
|
||||
import creator.loki.feature.image.ImageSessionManager;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
public class E621Command extends AbstractImageCommand {
|
||||
|
||||
public E621Command(ImageService imageService, ImageSessionManager sessionManager) {
|
||||
super(imageService, sessionManager);
|
||||
}
|
||||
|
||||
@Override public String getName() { return "e621"; }
|
||||
@Override public String getDescription() { return "Поиск изображений на e621.net"; }
|
||||
@Override public String getUsage() { return "e621 <кол-во до 50> <теги>"; }
|
||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
||||
@Override
|
||||
public String getName() {
|
||||
return "e621";
|
||||
}
|
||||
|
||||
@Override protected String siteName() { return "e621.net"; }
|
||||
@Override protected String siteDisplayName() { return "e621"; }
|
||||
@Override protected Color siteColor() { return new Color(102, 45, 145); }
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Поиск изображений на e621.net";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "e621 <кол-во до 50> <теги>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.NSFW_IMAGES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteName() {
|
||||
return "e621.net";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteDisplayName() {
|
||||
return "e621";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Color siteColor() {
|
||||
return new Color(102, 45, 145);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> fetchUrls(String tags, int limit) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package creator.loki.feature.image.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
|
|
@ -8,23 +11,46 @@ import creator.loki.feature.image.ImageService;
|
|||
import creator.loki.feature.image.ImageSession;
|
||||
import creator.loki.feature.image.ImageSessionManager;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
public class E621RandomCommand extends AbstractImageCommand {
|
||||
|
||||
public E621RandomCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||
super(imageService, sessionManager);
|
||||
}
|
||||
|
||||
@Override public String getName() { return "e621-random"; }
|
||||
@Override public String getDescription() { return "Случайные изображения с e621.net"; }
|
||||
@Override public String getUsage() { return "e621-random <кол-во до 50>"; }
|
||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
||||
@Override
|
||||
public String getName() {
|
||||
return "e621-random";
|
||||
}
|
||||
|
||||
@Override protected String siteName() { return "e621.net"; }
|
||||
@Override protected String siteDisplayName() { return "e621 (случайные)"; }
|
||||
@Override protected Color siteColor() { return new Color(102, 45, 145); }
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Случайные изображения с e621.net";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "e621-random <кол-во до 50>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.NSFW_IMAGES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteName() {
|
||||
return "e621.net";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteDisplayName() {
|
||||
return "e621 (случайные)";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Color siteColor() {
|
||||
return new Color(102, 45, 145);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> fetchUrls(String tags, int limit) {
|
||||
|
|
@ -35,9 +61,11 @@ public class E621RandomCommand extends AbstractImageCommand {
|
|||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
if (!isNsfwChannel(context)) {
|
||||
context.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error(
|
||||
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"🔞 NSFW", "Эта команда доступна только в каналах с возрастным ограничением."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
|
@ -48,31 +76,40 @@ public class E621RandomCommand extends AbstractImageCommand {
|
|||
try {
|
||||
limit = Integer.parseInt(args.get(0));
|
||||
limit = Math.max(1, Math.min(limit, MAX_FETCH));
|
||||
} catch (NumberFormatException ignored) {}
|
||||
} catch (NumberFormatException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
final int finalLimit = limit;
|
||||
context.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.info("🔍 Случайные", "Ищу случайные изображения на e621..."))
|
||||
.queue(msg -> {
|
||||
List<String> urls = imageService.fetchE621("", finalLimit);
|
||||
if (urls.isEmpty()) {
|
||||
msg.editMessageEmbeds(EmbedTemplates.warning(
|
||||
"😔 Ничего не найдено", "Не удалось найти изображения."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info("🔍 Случайные", "Ищу случайные изображения на e621..."))
|
||||
.queue(
|
||||
msg -> {
|
||||
List<String> urls = imageService.fetchE621("", finalLimit);
|
||||
if (urls.isEmpty()) {
|
||||
msg.editMessageEmbeds(
|
||||
EmbedTemplates.warning(
|
||||
"😔 Ничего не найдено", "Не удалось найти изображения."))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
ImageSession session = ImageSession.create(
|
||||
urls, PAGE_SIZE, siteName(), siteColor(),
|
||||
"",
|
||||
context.getAuthor().getIdLong(),
|
||||
context.getGuild().getIdLong());
|
||||
sessionManager.put(msg.getId(), session);
|
||||
ImageSession session =
|
||||
ImageSession.create(
|
||||
urls,
|
||||
PAGE_SIZE,
|
||||
siteName(),
|
||||
siteColor(),
|
||||
"",
|
||||
context.getAuthor().getIdLong(),
|
||||
context.getGuild().getIdLong());
|
||||
sessionManager.put(msg.getId(), session);
|
||||
|
||||
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||
.queue();
|
||||
});
|
||||
msg.editMessageEmbeds(ImageEmbedHelper.buildEmbeds(session))
|
||||
.setComponents(ImageEmbedHelper.buildActionRow(session))
|
||||
.queue();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,26 +1,52 @@
|
|||
package creator.loki.feature.image.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.feature.image.ImageService;
|
||||
import creator.loki.feature.image.ImageSessionManager;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
public class GelbooruCommand extends AbstractImageCommand {
|
||||
|
||||
public GelbooruCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||
super(imageService, sessionManager);
|
||||
}
|
||||
|
||||
@Override public String getName() { return "gelbooru"; }
|
||||
@Override public String getDescription() { return "Поиск изображений на Gelbooru"; }
|
||||
@Override public String getUsage() { return "gelbooru <кол-во до 50> <теги>"; }
|
||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
||||
@Override
|
||||
public String getName() {
|
||||
return "gelbooru";
|
||||
}
|
||||
|
||||
@Override protected String siteName() { return "gelbooru.com"; }
|
||||
@Override protected String siteDisplayName() { return "Gelbooru"; }
|
||||
@Override protected Color siteColor() { return new Color(50, 100, 180); }
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Поиск изображений на Gelbooru";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "gelbooru <кол-во до 50> <теги>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.NSFW_IMAGES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteName() {
|
||||
return "gelbooru.com";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteDisplayName() {
|
||||
return "Gelbooru";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Color siteColor() {
|
||||
return new Color(50, 100, 180);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> fetchUrls(String tags, int limit) {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,52 @@
|
|||
package creator.loki.feature.image.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.feature.image.ImageService;
|
||||
import creator.loki.feature.image.ImageSessionManager;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
public class R34Command extends AbstractImageCommand {
|
||||
|
||||
public R34Command(ImageService imageService, ImageSessionManager sessionManager) {
|
||||
super(imageService, sessionManager);
|
||||
}
|
||||
|
||||
@Override public String getName() { return "r34"; }
|
||||
@Override public String getDescription() { return "Поиск изображений на Rule34"; }
|
||||
@Override public String getUsage() { return "r34 <кол-во до 50> <теги>"; }
|
||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
||||
@Override
|
||||
public String getName() {
|
||||
return "r34";
|
||||
}
|
||||
|
||||
@Override protected String siteName() { return "rule34.xxx"; }
|
||||
@Override protected String siteDisplayName() { return "Rule34"; }
|
||||
@Override protected Color siteColor() { return new Color(200, 100, 0); }
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Поиск изображений на Rule34";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "r34 <кол-во до 50> <теги>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.NSFW_IMAGES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteName() {
|
||||
return "rule34.xxx";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteDisplayName() {
|
||||
return "Rule34";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Color siteColor() {
|
||||
return new Color(200, 100, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> fetchUrls(String tags, int limit) {
|
||||
|
|
|
|||
|
|
@ -1,26 +1,52 @@
|
|||
package creator.loki.feature.image.commands;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.CommandCategories;
|
||||
import creator.loki.feature.image.ImageService;
|
||||
import creator.loki.feature.image.ImageSessionManager;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
|
||||
public class YandereCommand extends AbstractImageCommand {
|
||||
|
||||
public YandereCommand(ImageService imageService, ImageSessionManager sessionManager) {
|
||||
super(imageService, sessionManager);
|
||||
}
|
||||
|
||||
@Override public String getName() { return "yandere"; }
|
||||
@Override public String getDescription() { return "Поиск изображений на yande.re"; }
|
||||
@Override public String getUsage() { return "yandere <кол-во до 50> <теги>"; }
|
||||
@Override public String getCategory() { return CommandCategories.NSFW_IMAGES; }
|
||||
@Override
|
||||
public String getName() {
|
||||
return "yandere";
|
||||
}
|
||||
|
||||
@Override protected String siteName() { return "yande.re"; }
|
||||
@Override protected String siteDisplayName() { return "yande.re"; }
|
||||
@Override protected Color siteColor() { return new Color(220, 50, 120); }
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Поиск изображений на yande.re";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "yandere <кол-во до 50> <теги>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return CommandCategories.NSFW_IMAGES;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteName() {
|
||||
return "yande.re";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String siteDisplayName() {
|
||||
return "yande.re";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Color siteColor() {
|
||||
return new Color(220, 50, 120);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> fetchUrls(String tags, int limit) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,12 @@ package creator.loki.feature.level;
|
|||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
import creator.loki.database.Database;
|
||||
import creator.loki.feature.config.repository.ConfigRepository;
|
||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||
import creator.loki.feature.level.commands.LevelCommandRegistrar;
|
||||
import creator.loki.feature.level.commands.ResetLevelsCommand;
|
||||
import creator.loki.feature.level.domain.XpFormula;
|
||||
import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
|
||||
|
|
@ -24,6 +27,10 @@ public class LevelModule extends AbstractModule {
|
|||
protected void configure() {
|
||||
bind(VoiceTracker.class).in(Singleton.class);
|
||||
bind(ResetLevelsButtonHandler.class).in(Singleton.class);
|
||||
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(LevelCommandRegistrar.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import java.util.concurrent.TimeUnit;
|
|||
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.feature.economy.service.EconomyService;
|
||||
import creator.loki.feature.level.domain.LevelUpResult;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
|
@ -17,7 +18,7 @@ import org.slf4j.Logger;
|
|||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class VoiceTracker extends ListenerAdapter {
|
||||
public class VoiceTracker extends ListenerAdapter implements Lifecycle {
|
||||
private static final Logger logger = LoggerFactory.getLogger(VoiceTracker.class);
|
||||
private final LevelService levelService;
|
||||
private final EconomyService economyService;
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.google.inject.Singleton;
|
|||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -20,7 +21,7 @@ import org.slf4j.LoggerFactory;
|
|||
* BotConfig#getHealthHttpPort()} (0 = выкл).
|
||||
*/
|
||||
@Singleton
|
||||
public class HealthHttpServer {
|
||||
public class HealthHttpServer implements Lifecycle {
|
||||
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
|
||||
|
||||
private final BotConfig config;
|
||||
|
|
@ -106,7 +107,8 @@ public class HealthHttpServer {
|
|||
}
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
@Override
|
||||
public synchronized void shutdown() {
|
||||
if (server != null) {
|
||||
server.stop(1);
|
||||
server = null;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package creator.loki.feature.moderation;
|
|||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
import creator.loki.database.Database;
|
||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
||||
|
|
@ -15,7 +17,11 @@ import creator.loki.feature.moderation.repository.WarningRepository;
|
|||
public class ModerationModule extends AbstractModule {
|
||||
|
||||
@Override
|
||||
protected void configure() {}
|
||||
protected void configure() {
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(ModerationCommandRegistrar.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import java.util.concurrent.ScheduledFuture;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.feature.config.repository.GuildConfigRepository;
|
||||
import creator.loki.feature.moderation.repository.ActiveMuteRepository;
|
||||
import creator.loki.feature.moderation.repository.ModerationCaseRepository;
|
||||
|
|
@ -17,7 +18,7 @@ import net.dv8tion.jda.api.entities.Role;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MuteScheduler {
|
||||
public class MuteScheduler implements Lifecycle {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(MuteScheduler.class);
|
||||
|
||||
|
|
|
|||
|
|
@ -15,9 +15,13 @@ import creator.loki.feature.moderation.util.ModerationHelper;
|
|||
import creator.loki.util.MuteDurationParser;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.Permission;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
import net.dv8tion.jda.api.entities.Role;
|
||||
|
||||
public class MuteCommand implements Command {
|
||||
private static final int MAX_REASON_LENGTH = 500;
|
||||
private static final int MAX_LOG_REASON_LENGTH = 1000;
|
||||
|
||||
private final ModerationCaseRepository cases;
|
||||
private final ActiveMuteRepository activeMutes;
|
||||
private final ConfigRepository configRepo;
|
||||
|
|
@ -73,19 +77,49 @@ public class MuteCommand implements Command {
|
|||
err(ctx, "Укажи пользователя");
|
||||
return;
|
||||
}
|
||||
var t = mentioned.get(0);
|
||||
if (t.getId().equals(mod.getId())) {
|
||||
var target = mentioned.get(0);
|
||||
if (!validateMute(ctx, target, mod, g)) return;
|
||||
var parsed = parseDurationAndReason(ctx);
|
||||
if (parsed == null) return;
|
||||
Role muteRole;
|
||||
try {
|
||||
muteRole = muteRoleService.getOrCreateMuteRole(g);
|
||||
} catch (Exception e) {
|
||||
err(ctx, "Ошибка роли");
|
||||
return;
|
||||
}
|
||||
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(muteRole)) {
|
||||
err(ctx, "Подними роль бота выше роли **Мут**");
|
||||
return;
|
||||
}
|
||||
long caseId = createMuteCase(g, target, mod, parsed.dur, parsed.reason);
|
||||
if (caseId < 0) {
|
||||
err(ctx, "Ошибка записи кейса");
|
||||
return;
|
||||
}
|
||||
applyMuteRole(ctx, g, target, muteRole, caseId, parsed.dur, parsed.reason, mod);
|
||||
}
|
||||
|
||||
private boolean validateMute(
|
||||
CommandContext ctx, Member target, Member mod, net.dv8tion.jda.api.entities.Guild g) {
|
||||
if (target.getId().equals(mod.getId())) {
|
||||
err(ctx, "Нельзя замутить себя");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!mod.canInteract(t)) {
|
||||
if (!mod.canInteract(target)) {
|
||||
err(ctx, "Роль выше");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(t)) {
|
||||
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(target)) {
|
||||
err(ctx, "Бот не может");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private record ParsedDuration(MuteDurationParser.MuteDuration dur, String reason) {}
|
||||
|
||||
private ParsedDuration parseDurationAndReason(CommandContext ctx) {
|
||||
MuteDurationParser.MuteDuration dur = null;
|
||||
String durToken = null;
|
||||
for (String a : ctx.getArgs()) {
|
||||
|
|
@ -99,7 +133,7 @@ public class MuteCommand implements Command {
|
|||
}
|
||||
if (dur == null) {
|
||||
err(ctx, "Укажи длительность: `30s`, `5m`, `2h`, `1d` или `perm`");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
boolean past = false;
|
||||
var sb = new StringBuilder();
|
||||
|
|
@ -112,85 +146,113 @@ public class MuteCommand implements Command {
|
|||
if (!sb.isEmpty()) sb.append(' ');
|
||||
sb.append(a);
|
||||
}
|
||||
String reasonTmp = sb.toString().trim();
|
||||
String reason = reasonTmp.isEmpty() ? "Без причины" : reasonTmp;
|
||||
Role muteRole;
|
||||
try {
|
||||
muteRole = muteRoleService.getOrCreateMuteRole(g);
|
||||
} catch (Exception e) {
|
||||
err(ctx, "Ошибка роли");
|
||||
return;
|
||||
}
|
||||
if (!g.getMember(g.getJDA().getSelfUser()).canInteract(muteRole)) {
|
||||
err(ctx, "Подними роль бота выше роли **Мут**");
|
||||
return;
|
||||
}
|
||||
String tmp = sb.toString().trim();
|
||||
String reason = tmp.isEmpty() ? "Без причины" : tmp;
|
||||
return new ParsedDuration(dur, reason);
|
||||
}
|
||||
|
||||
private long createMuteCase(
|
||||
net.dv8tion.jda.api.entities.Guild g,
|
||||
Member target,
|
||||
Member mod,
|
||||
MuteDurationParser.MuteDuration dur,
|
||||
String reason) {
|
||||
boolean permanent = dur.permanent();
|
||||
long secs = dur.secondsTotal();
|
||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
||||
Long durSec = permanent ? null : secs;
|
||||
long caseId =
|
||||
cases.insert(
|
||||
g.getId(), "MUTE", t.getId(), mod.getId(), reason, durSec, permanent ? null : untilMs);
|
||||
if (caseId < 0) {
|
||||
err(ctx, "Ошибка записи кейса");
|
||||
return;
|
||||
g.getId(),
|
||||
"MUTE",
|
||||
target.getId(),
|
||||
mod.getId(),
|
||||
reason,
|
||||
durSec,
|
||||
permanent ? null : untilMs);
|
||||
if (caseId >= 0) {
|
||||
activeMutes.upsert(g.getId(), target.getId(), permanent ? 0L : untilMs, caseId);
|
||||
muteScheduler.cancelPending(g.getId(), target.getId());
|
||||
}
|
||||
activeMutes.upsert(g.getId(), t.getId(), permanent ? 0L : untilMs, caseId);
|
||||
muteScheduler.cancelPending(g.getId(), t.getId());
|
||||
g.addRoleToMember(t, muteRole)
|
||||
return caseId;
|
||||
}
|
||||
|
||||
private void applyMuteRole(
|
||||
CommandContext ctx,
|
||||
net.dv8tion.jda.api.entities.Guild g,
|
||||
Member target,
|
||||
Role muteRole,
|
||||
long caseId,
|
||||
MuteDurationParser.MuteDuration dur,
|
||||
String reason,
|
||||
Member mod) {
|
||||
boolean permanent = dur.permanent();
|
||||
long secs = dur.secondsTotal();
|
||||
long untilMs = permanent ? 0L : System.currentTimeMillis() + secs * 1000;
|
||||
g.addRoleToMember(target, muteRole)
|
||||
.queue(
|
||||
ok -> {
|
||||
muteScheduler.scheduleExpiry(g.getId(), t.getId(), permanent ? 0L : untilMs);
|
||||
muteScheduler.scheduleExpiry(g.getId(), target.getId(), permanent ? 0L : untilMs);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedFactory.create()
|
||||
.withTitle("✅ Мут выдан")
|
||||
.withColor(Color.GREEN)
|
||||
.withDescription(
|
||||
"**Кому:** "
|
||||
+ t.getAsMention()
|
||||
+ "\n**Кейс:** `"
|
||||
+ caseId
|
||||
+ "`"
|
||||
+ (permanent
|
||||
? "\n**Срок:** навсегда"
|
||||
: "\n**Срок:** <t:" + (untilMs / 1000) + ":R>")
|
||||
+ "\n**Причина:** "
|
||||
+ sub(reason, 500))
|
||||
.build())
|
||||
.sendMessageEmbeds(buildMuteConfirmEmbed(target, caseId, dur, reason, untilMs))
|
||||
.queue();
|
||||
var logId = configRepo.get("moderation.log_channel_id");
|
||||
if (logId != null) {
|
||||
var log = g.getTextChannelById(logId);
|
||||
if (log != null) {
|
||||
log.sendMessageEmbeds(
|
||||
new EmbedBuilder()
|
||||
.setTitle("🔇 Мут")
|
||||
.setColor(Color.DARK_GRAY)
|
||||
.addField(
|
||||
"Участник", t.getAsMention() + " (`" + t.getId() + "`)", false)
|
||||
.addField("Модератор", mod.getAsMention(), false)
|
||||
.addField("Кейс", "`" + caseId + "`", true)
|
||||
.addField(
|
||||
"Срок",
|
||||
permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>",
|
||||
true)
|
||||
.addField("Причина", sub(reason, 1000), false)
|
||||
.setTimestamp(OffsetDateTime.now())
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
}
|
||||
sendMuteLogEmbed(g, target, mod, caseId, dur, reason, untilMs);
|
||||
},
|
||||
err2 -> {
|
||||
activeMutes.delete(g.getId(), t.getId());
|
||||
activeMutes.delete(g.getId(), target.getId());
|
||||
err(ctx, "Ошибка выдачи роли");
|
||||
});
|
||||
}
|
||||
|
||||
private static String sub(String s, int max) {
|
||||
return ModerationHelper.trunc(s, max);
|
||||
private net.dv8tion.jda.api.entities.MessageEmbed buildMuteConfirmEmbed(
|
||||
Member target,
|
||||
long caseId,
|
||||
MuteDurationParser.MuteDuration dur,
|
||||
String reason,
|
||||
long untilMs) {
|
||||
boolean permanent = dur.permanent();
|
||||
return EmbedFactory.create()
|
||||
.withTitle("✅ Мут выдан")
|
||||
.withColor(Color.GREEN)
|
||||
.withDescription(
|
||||
"**Кому:** "
|
||||
+ target.getAsMention()
|
||||
+ "\n**Кейс:** `"
|
||||
+ caseId
|
||||
+ "`"
|
||||
+ (permanent
|
||||
? "\n**Срок:** навсегда"
|
||||
: "\n**Срок:** <t:" + (untilMs / 1000) + ":R>")
|
||||
+ "\n**Причина:** "
|
||||
+ ModerationHelper.trunc(reason, MAX_REASON_LENGTH))
|
||||
.build();
|
||||
}
|
||||
|
||||
private void sendMuteLogEmbed(
|
||||
net.dv8tion.jda.api.entities.Guild g,
|
||||
Member target,
|
||||
Member mod,
|
||||
long caseId,
|
||||
MuteDurationParser.MuteDuration dur,
|
||||
String reason,
|
||||
long untilMs) {
|
||||
var logId = configRepo.get("moderation.log_channel_id");
|
||||
if (logId == null) return;
|
||||
var log = g.getTextChannelById(logId);
|
||||
if (log == null) return;
|
||||
boolean permanent = dur.permanent();
|
||||
log.sendMessageEmbeds(
|
||||
new EmbedBuilder()
|
||||
.setTitle("🔇 Мут")
|
||||
.setColor(Color.DARK_GRAY)
|
||||
.addField("Участник", target.getAsMention() + " (`" + target.getId() + "`)", false)
|
||||
.addField("Модератор", mod.getAsMention(), false)
|
||||
.addField("Кейс", "`" + caseId + "`", true)
|
||||
.addField("Срок", permanent ? "Навсегда" : "<t:" + (untilMs / 1000) + ":F>", true)
|
||||
.addField("Причина", ModerationHelper.trunc(reason, MAX_LOG_REASON_LENGTH), false)
|
||||
.setTimestamp(OffsetDateTime.now())
|
||||
.build())
|
||||
.queue();
|
||||
}
|
||||
|
||||
private static void err(CommandContext ctx, String msg) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package creator.loki.feature.music;
|
|||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
|
||||
|
|
@ -10,5 +12,9 @@ public class MusicModule extends AbstractModule {
|
|||
protected void configure() {
|
||||
bind(LavalinkManager.class).in(Singleton.class);
|
||||
bind(PlayerManager.class).in(Singleton.class);
|
||||
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(MusicCommandRegistrar.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,7 +60,8 @@ public class MusicSlashCommands extends ListenerAdapter {
|
|||
.addOption(OptionType.USER, "user", "Цель", false),
|
||||
Commands.slash("clear", "Удалить сообщения")
|
||||
.addOption(OptionType.INTEGER, "amount", "Количество (1-100)", true)
|
||||
.addOption(OptionType.USER, "user", "Оставить только сообщения пользователя", false))
|
||||
.addOption(
|
||||
OptionType.USER, "user", "Оставить только сообщения пользователя", false))
|
||||
.queue();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ import java.util.Collections;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import dev.arbjerg.lavalink.client.player.LavalinkPlayer;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import dev.arbjerg.lavalink.protocol.v4.Message.EmittedEvent.TrackEndEvent.AudioTrackEndReason;
|
||||
|
|
@ -20,12 +18,14 @@ public class TrackScheduler {
|
|||
private static final Logger log = LoggerFactory.getLogger(TrackScheduler.class);
|
||||
private static final Consumer<Throwable> LOG_LAV = t -> log.warn("Lav: {}", t.getMessage());
|
||||
private final GuildMusicManager manager;
|
||||
private final TrackStarter trackStarter;
|
||||
final Queue<Track> queue = new LinkedList<>();
|
||||
private boolean repeating;
|
||||
public final FilterState filterState = new FilterState();
|
||||
|
||||
public TrackScheduler(GuildMusicManager manager) {
|
||||
this.manager = manager;
|
||||
this.trackStarter = new TrackStarter(manager.getExecutor());
|
||||
}
|
||||
|
||||
public void enqueue(Track track) {
|
||||
|
|
@ -103,13 +103,11 @@ public class TrackScheduler {
|
|||
|
||||
public void onTrackEnd(Track lastTrack, AudioTrackEndReason endReason) {
|
||||
if (endReason == AudioTrackEndReason.LOAD_FAILED) {
|
||||
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
||||
nextOrAutoLeave();
|
||||
handleLoadFailed();
|
||||
} else if (!endReason.getMayStartNext() && endReason != AudioTrackEndReason.FINISHED) {
|
||||
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
||||
manager.startAutoLeaveTimer();
|
||||
handleUnexpectedEnd(endReason);
|
||||
} else if (repeating && endReason == AudioTrackEndReason.FINISHED) {
|
||||
startTrack(lastTrack.makeClone());
|
||||
handleFinished(lastTrack);
|
||||
} else {
|
||||
nextOrAutoLeave();
|
||||
}
|
||||
|
|
@ -118,7 +116,21 @@ public class TrackScheduler {
|
|||
void startTrack(Track track) {
|
||||
manager.cancelAutoLeaveTimer();
|
||||
var link = manager.getLavalinkManager().getOrCreateLink(manager.getGuildId());
|
||||
sendTrackDelayed(link, track, 0);
|
||||
trackStarter.startWithRetry(link, track);
|
||||
}
|
||||
|
||||
private void handleLoadFailed() {
|
||||
LoggerDecorator.warn(log, "Load failed, skipping to next");
|
||||
nextOrAutoLeave();
|
||||
}
|
||||
|
||||
private void handleUnexpectedEnd(AudioTrackEndReason endReason) {
|
||||
LoggerDecorator.warn(log, "Track ended unexpectedly: " + endReason);
|
||||
manager.startAutoLeaveTimer();
|
||||
}
|
||||
|
||||
private void handleFinished(Track lastTrack) {
|
||||
startTrack(lastTrack.makeClone());
|
||||
}
|
||||
|
||||
private void nextOrAutoLeave() {
|
||||
|
|
@ -126,21 +138,4 @@ public class TrackScheduler {
|
|||
if (next != null) startTrack(next);
|
||||
else manager.startAutoLeaveTimer();
|
||||
}
|
||||
|
||||
private void scheduleRetry(Runnable r, long delayMs) {
|
||||
manager.getExecutor().schedule(r, delayMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void sendTrackDelayed(Link link, Track track, int attempt) {
|
||||
if (attempt > 5) return;
|
||||
if (link.getCachedPlayer() == null) {
|
||||
scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 300);
|
||||
return;
|
||||
}
|
||||
link.createOrUpdatePlayer()
|
||||
.setTrack(track)
|
||||
.setVolume(100)
|
||||
.subscribe(
|
||||
null, err -> scheduleRetry(() -> sendTrackDelayed(link, track, attempt + 1), 500));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
package creator.loki.feature.music.manage;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import dev.arbjerg.lavalink.client.Link;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
class TrackStarter {
|
||||
private static final Logger log = LoggerFactory.getLogger(TrackStarter.class);
|
||||
private static final int MAX_ATTEMPTS = 5;
|
||||
private static final long RETRY_DELAY_PLAYER_NULL_MS = 300;
|
||||
private static final long RETRY_DELAY_SUBSCRIBE_ERROR_MS = 500;
|
||||
|
||||
private final ScheduledExecutorService executor;
|
||||
|
||||
TrackStarter(ScheduledExecutorService executor) {
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a track on the given link with retry logic (up to {@value MAX_ATTEMPTS} attempts).
|
||||
*
|
||||
* @param link the Lavalink link to play the track on
|
||||
* @param track the track to start
|
||||
*/
|
||||
void startWithRetry(Link link, Track track) {
|
||||
startWithRetry(link, track, 0);
|
||||
}
|
||||
|
||||
private void startWithRetry(Link link, Track track, int attempt) {
|
||||
if (attempt > MAX_ATTEMPTS) return;
|
||||
if (link.getCachedPlayer() == null) {
|
||||
executor.schedule(
|
||||
() -> startWithRetry(link, track, attempt + 1),
|
||||
RETRY_DELAY_PLAYER_NULL_MS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
return;
|
||||
}
|
||||
link.createOrUpdatePlayer()
|
||||
.setTrack(track)
|
||||
.setVolume(100)
|
||||
.subscribe(
|
||||
null,
|
||||
err ->
|
||||
executor.schedule(
|
||||
() -> startWithRetry(link, track, attempt + 1),
|
||||
RETRY_DELAY_SUBSCRIBE_ERROR_MS,
|
||||
TimeUnit.MILLISECONDS));
|
||||
}
|
||||
}
|
||||
|
|
@ -11,12 +11,7 @@ import creator.loki.feature.music.core.PlayerManager;
|
|||
import creator.loki.feature.music.core.SearchManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.util.VoiceHelper;
|
||||
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
|
||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.SearchResult;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import dev.arbjerg.lavalink.client.player.TrackLoaded;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
|
|
@ -56,10 +51,7 @@ public class PlayCommand implements Command {
|
|||
if (guild == null) return;
|
||||
Member member = ctx.getAuthor();
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
||||
.queue();
|
||||
sendErr(ctx, "Зайди в голосовой канал сначала.");
|
||||
return;
|
||||
}
|
||||
var args = ctx.getArgs();
|
||||
|
|
@ -69,27 +61,43 @@ public class PlayCommand implements Command {
|
|||
}
|
||||
String first = args.get(0);
|
||||
if (SearchHandler.isNumeric(first)) {
|
||||
Track t = searchHandler.pickResult(guild.getIdLong(), Integer.parseInt(first) - 1);
|
||||
if (t == null) {
|
||||
sendErr(ctx, "Поиск устарел. Сделай новый: `!play <запрос>`");
|
||||
return;
|
||||
}
|
||||
playTrack(ctx, t);
|
||||
handlePickResult(ctx, first);
|
||||
return;
|
||||
}
|
||||
if (!resolveVoiceConnection(ctx)) return;
|
||||
var lm = playerManager.getLavalinkManager();
|
||||
if (!guild.getSelfMember().getVoiceState().inAudioChannel()) {
|
||||
var vc = member.getVoiceState().getChannel().asVoiceChannel();
|
||||
guild.getJDA().getDirectAudioController().connect(vc);
|
||||
if (!guild.getAudioManager().isConnected()) guild.getAudioManager().openAudioConnection(vc);
|
||||
var self = guild.getSelfMember();
|
||||
VoiceHelper.waitForCondition(() -> self.getVoiceState().inAudioChannel());
|
||||
if (!self.getVoiceState().inAudioChannel()) return;
|
||||
var link = lm.getOrCreateLink(guild.getIdLong());
|
||||
VoiceHelper.waitForCondition(() -> link.getCachedPlayer() != null);
|
||||
if (link.getCachedPlayer() == null) return;
|
||||
}
|
||||
var mm = lm.getMusicManager(guild.getIdLong());
|
||||
handlePlayQuery(ctx, args, first, lm, mm);
|
||||
}
|
||||
|
||||
private boolean resolveVoiceConnection(CommandContext ctx) {
|
||||
var guild = ctx.getGuild();
|
||||
var member = ctx.getAuthor();
|
||||
var lm = playerManager.getLavalinkManager();
|
||||
if (!VoiceHelper.connectForPlayback(guild, member, lm)) {
|
||||
sendErr(ctx, "Не удалось подключиться к голосовому каналу.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void handlePickResult(CommandContext ctx, String first) {
|
||||
var guild = ctx.getGuild();
|
||||
Track t = searchHandler.pickResult(guild.getIdLong(), Integer.parseInt(first) - 1);
|
||||
if (t == null) {
|
||||
sendErr(ctx, "Поиск устарел. Сделай новый: `!play <запрос>`");
|
||||
return;
|
||||
}
|
||||
if (!resolveVoiceConnection(ctx)) return;
|
||||
enqueueAndNotify(ctx, t);
|
||||
}
|
||||
|
||||
private void handlePlayQuery(
|
||||
CommandContext ctx,
|
||||
List<String> args,
|
||||
String first,
|
||||
LavalinkManager lm,
|
||||
GuildMusicManager mm) {
|
||||
boolean isSource = SOURCES.contains(first);
|
||||
String raw = String.join(" ", isSource ? args.subList(1, args.size()) : args);
|
||||
if (raw.isEmpty()) {
|
||||
|
|
@ -97,9 +105,9 @@ public class PlayCommand implements Command {
|
|||
return;
|
||||
}
|
||||
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
|
||||
String query = isUrl ? null : (first.equals("list") ? raw : raw);
|
||||
String query = isUrl ? null : raw;
|
||||
String id = buildQuery(first, raw);
|
||||
loadAndHandle(ctx, lm, guild.getIdLong(), mm, id, isUrl, query, raw);
|
||||
loadAndHandle(ctx, lm, ctx.getGuild().getIdLong(), mm, id, isUrl, query, raw);
|
||||
}
|
||||
|
||||
private void loadAndHandle(
|
||||
|
|
@ -111,64 +119,9 @@ public class PlayCommand implements Command {
|
|||
boolean isUrl,
|
||||
String query,
|
||||
String raw) {
|
||||
String noMatch =
|
||||
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
|
||||
lm.getOrCreateLink(guildId)
|
||||
.loadItem(id)
|
||||
.subscribe(
|
||||
new AbstractAudioLoadResultHandler() {
|
||||
@Override
|
||||
public void ontrackLoaded(TrackLoaded r) {
|
||||
if (isUrl) playTrack(ctx, r.getTrack());
|
||||
else showResults(ctx, query, mm, List.of(r.getTrack()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaylistLoaded(PlaylistLoaded r) {
|
||||
if (isUrl) {
|
||||
int c = 0;
|
||||
for (Track t : r.getTracks()) {
|
||||
mm.getScheduler().enqueue(t);
|
||||
c++;
|
||||
}
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Плейлист добавлен",
|
||||
"**" + r.getInfo().getName() + "** — " + c + " треков"))
|
||||
.queue();
|
||||
} else if (r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) {
|
||||
playTrack(ctx, r.getTracks().get(0));
|
||||
} else {
|
||||
showResults(ctx, query, mm, r.getTracks());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResultLoaded(SearchResult r) {
|
||||
if (r.getTracks().isEmpty()) {
|
||||
sendErr(ctx, noMatch);
|
||||
} else if (isUrl) {
|
||||
playTrack(ctx, r.getTracks().get(0));
|
||||
} else {
|
||||
showResults(ctx, query, mm, r.getTracks());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches() {
|
||||
sendErr(ctx, noMatch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(LoadFailed e) {
|
||||
sendErr(
|
||||
ctx,
|
||||
e.getException().getMessage() != null
|
||||
? e.getException().getMessage()
|
||||
: "Неизвестная ошибка");
|
||||
}
|
||||
});
|
||||
.subscribe(new PlayLoadHandler(ctx, searchHandler, mm, isUrl, query, raw));
|
||||
}
|
||||
|
||||
private static String buildQuery(String first, String raw) {
|
||||
|
|
@ -181,13 +134,7 @@ public class PlayCommand implements Command {
|
|||
return raw;
|
||||
}
|
||||
|
||||
private void showResults(CommandContext ctx, String q, GuildMusicManager m, List<Track> list) {
|
||||
if (q == null) return;
|
||||
searchHandler.storeResults(ctx.getGuild().getIdLong(), list);
|
||||
ctx.getChannel().sendMessageEmbeds(searchHandler.buildResultsEmbed(q, list).build()).queue();
|
||||
}
|
||||
|
||||
private void playTrack(CommandContext ctx, Track track) {
|
||||
private void enqueueAndNotify(CommandContext ctx, Track track) {
|
||||
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
||||
m.getScheduler().enqueue(track);
|
||||
ctx.getChannel()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,116 @@
|
|||
package creator.loki.feature.music.play;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
|
||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.SearchResult;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import dev.arbjerg.lavalink.client.player.TrackLoaded;
|
||||
|
||||
class PlayLoadHandler extends AbstractAudioLoadResultHandler {
|
||||
private final CommandContext ctx;
|
||||
private final SearchHandler searchHandler;
|
||||
private final GuildMusicManager mm;
|
||||
private final boolean isUrl;
|
||||
private final String query;
|
||||
private final String raw;
|
||||
|
||||
PlayLoadHandler(
|
||||
CommandContext ctx,
|
||||
SearchHandler searchHandler,
|
||||
GuildMusicManager mm,
|
||||
boolean isUrl,
|
||||
String query,
|
||||
String raw) {
|
||||
this.ctx = ctx;
|
||||
this.searchHandler = searchHandler;
|
||||
this.mm = mm;
|
||||
this.isUrl = isUrl;
|
||||
this.query = query;
|
||||
this.raw = raw;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ontrackLoaded(TrackLoaded r) {
|
||||
if (isUrl) enqueueAndNotify(r.getTrack());
|
||||
else showResults(List.of(r.getTrack()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlaylistLoaded(PlaylistLoaded r) {
|
||||
if (isUrl) {
|
||||
int count = 0;
|
||||
for (Track t : r.getTracks()) {
|
||||
mm.getScheduler().enqueue(t);
|
||||
count++;
|
||||
}
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Плейлист добавлен", "**" + r.getInfo().getName() + "** — " + count + " треков"))
|
||||
.queue();
|
||||
} else if (r.getTracks().size() <= 1 && !r.getTracks().isEmpty()) {
|
||||
enqueueAndNotify(r.getTracks().get(0));
|
||||
} else {
|
||||
showResults(r.getTracks());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSearchResultLoaded(SearchResult r) {
|
||||
String noMatch =
|
||||
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
|
||||
if (r.getTracks().isEmpty()) {
|
||||
sendErr(noMatch);
|
||||
} else if (isUrl) {
|
||||
enqueueAndNotify(r.getTracks().get(0));
|
||||
} else {
|
||||
showResults(r.getTracks());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noMatches() {
|
||||
String noMatch =
|
||||
isUrl ? "Ничего не найдено по ссылке." : "Ничего не найдено по запросу: " + raw;
|
||||
sendErr(noMatch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadFailed(LoadFailed e) {
|
||||
sendErr(
|
||||
e.getException().getMessage() != null
|
||||
? e.getException().getMessage()
|
||||
: "Неизвестная ошибка");
|
||||
}
|
||||
|
||||
private void enqueueAndNotify(Track track) {
|
||||
mm.getScheduler().enqueue(track);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.info(
|
||||
"Добавлено в очередь",
|
||||
"**"
|
||||
+ track.getInfo().getTitle()
|
||||
+ "**\n"
|
||||
+ SearchHandler.formatDuration(track.getInfo().getLength())))
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void showResults(List<Track> list) {
|
||||
if (query == null) return;
|
||||
searchHandler.storeResults(ctx.getGuild().getIdLong(), list);
|
||||
ctx.getChannel()
|
||||
.sendMessageEmbeds(searchHandler.buildResultsEmbed(query, list).build())
|
||||
.queue();
|
||||
}
|
||||
|
||||
private void sendErr(String msg) {
|
||||
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,16 @@ package creator.loki.feature.radio;
|
|||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Singleton;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
|
||||
public class RadioModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(RadioManager.class).in(Singleton.class);
|
||||
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(RadioCommandRegistrar.class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ public class IqCommand implements Command {
|
|||
iq = ratingService.getDeterministicValue(target.getId(), 123, 0, 140);
|
||||
}
|
||||
|
||||
RatingLevel level = RatingLevel.findLevel(iq, RatingConfig.getIqLevels());
|
||||
RatingLevel level = RatingConfig.findLevel(iq, "iq");
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ public class MustacheCommand implements Command {
|
|||
for (int i = 0; i < filled; i++) bar.append("🧔");
|
||||
for (int i = filled; i < 10; i++) bar.append("⬜");
|
||||
|
||||
RatingLevel level = RatingLevel.findLevel(mustacheLevel, RatingConfig.getMustacheLevels());
|
||||
RatingLevel level = RatingConfig.findLevel(mustacheLevel, "mustache");
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ public class PpCommand implements Command {
|
|||
for (int i = 0; i < size; i++) ppVisual.append("=");
|
||||
ppVisual.append("D");
|
||||
|
||||
RatingLevel level = RatingLevel.findLevel(size, RatingConfig.getPpLevels());
|
||||
RatingLevel level = RatingConfig.findLevel(size, "pp");
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
|
|
|
|||
|
|
@ -104,43 +104,59 @@ public class RatingConfig {
|
|||
LEVELS = Collections.unmodifiableMap(m);
|
||||
}
|
||||
|
||||
/** Data-driven accessor — единая точка доступа для всех рейтингов. */
|
||||
public static List<RatingLevel> getLevels(String ratingName) {
|
||||
var levels = LEVELS.get(ratingName);
|
||||
if (levels == null) {
|
||||
throw new IllegalArgumentException("Unknown rating: " + ratingName);
|
||||
}
|
||||
return levels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data-driven поиск уровня: `findLevel(value, "iq")` вместо `findLevel(value, getIqLevels())`.
|
||||
*/
|
||||
public static RatingLevel findLevel(int value, String ratingName) {
|
||||
return RatingLevel.findLevel(value, getLevels(ratingName));
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getMustacheLevels() {
|
||||
return LEVELS.get("mustache");
|
||||
return getLevels("mustache");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getIqLevels() {
|
||||
return LEVELS.get("iq");
|
||||
return getLevels("iq");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getFatLevels() {
|
||||
return LEVELS.get("fat");
|
||||
return getLevels("fat");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getPpLevels() {
|
||||
return LEVELS.get("pp");
|
||||
return getLevels("pp");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getBoykisserLevels() {
|
||||
return LEVELS.get("boykisser");
|
||||
return getLevels("boykisser");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getRizzLevels() {
|
||||
return LEVELS.get("rizz");
|
||||
return getLevels("rizz");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getLuckLevels() {
|
||||
return LEVELS.get("luck");
|
||||
return getLevels("luck");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getSimpLevels() {
|
||||
return LEVELS.get("simp");
|
||||
return getLevels("simp");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getVibeLevels() {
|
||||
return LEVELS.get("vibe");
|
||||
return getLevels("vibe");
|
||||
}
|
||||
|
||||
public static List<RatingLevel> getGlazeLevels() {
|
||||
return LEVELS.get("glaze");
|
||||
return getLevels("glaze");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ public class AiSessionManager {
|
|||
|
||||
public void addMessage(
|
||||
String channelId, String authorName, String authorId, String content, long timestamp) {
|
||||
AiSession session =
|
||||
sessions.computeIfAbsent(channelId, k -> new AiSession());
|
||||
AiSession session = sessions.computeIfAbsent(channelId, k -> new AiSession());
|
||||
session.addMessage(authorName, authorId, content, timestamp);
|
||||
}
|
||||
|
||||
|
|
@ -75,19 +74,13 @@ public class AiSessionManager {
|
|||
for (int i = start; i < messages.size(); i++) {
|
||||
SessionMessage m = messages.get(i);
|
||||
String label = (i == messages.size() - 1) ? "[now]" : "[history]";
|
||||
String formatted =
|
||||
label
|
||||
+ " "
|
||||
+ m.authorName
|
||||
+ "("
|
||||
+ m.authorId
|
||||
+ "): "
|
||||
+ m.content;
|
||||
String formatted = label + " " + m.authorName + "(" + m.authorId + "): " + m.content;
|
||||
result.add(new CachedMessage(formatted, "", m.timestamp));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private record SessionMessage(String authorName, String authorId, String content, long timestamp) {}
|
||||
private record SessionMessage(
|
||||
String authorName, String authorId, String content, long timestamp) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,11 +28,13 @@ public class AndrertListener extends ListenerAdapter {
|
|||
private final String allowedChannelId;
|
||||
private final ConcurrentHashMap<String, Long> trackedUsers = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Long> lastAiReplyTime = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, ScheduledFuture<?>> pendingPings = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, ScheduledFuture<?>> pendingPings =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
|
||||
|
||||
@Inject
|
||||
public AndrertListener(RoastService roastService, AiSessionManager sessionManager, BotConfig config) {
|
||||
public AndrertListener(
|
||||
RoastService roastService, AiSessionManager sessionManager, BotConfig config) {
|
||||
this.roastService = roastService;
|
||||
this.sessionManager = sessionManager;
|
||||
this.allowedChannelId = config.getAiChannelId();
|
||||
|
|
@ -102,7 +104,8 @@ public class AndrertListener extends ListenerAdapter {
|
|||
System.currentTimeMillis());
|
||||
|
||||
String targetName = event.getAuthor().getEffectiveName();
|
||||
List<CachedMessage> context = sessionManager.getContext(event.getChannel().getId(), CHANNEL_CONTEXT);
|
||||
List<CachedMessage> context =
|
||||
sessionManager.getContext(event.getChannel().getId(), CHANNEL_CONTEXT);
|
||||
|
||||
String response = roastService.generateResponse(context, targetName, userId);
|
||||
if (response == null || response.isBlank()) return;
|
||||
|
|
@ -145,12 +148,13 @@ public class AndrertListener extends ListenerAdapter {
|
|||
String[] lines = {
|
||||
"эй " + targetMention + ", ты чё замолчал? я не закончил",
|
||||
"куда пропал " + targetMention + "? аргументы закончились?",
|
||||
"ну " + targetMention + ", я жду. или скажешь что тебе нечего ответить?",
|
||||
"ну "
|
||||
+ targetMention
|
||||
+ ", я жду. или скажешь что тебе нечего ответить?",
|
||||
"тишина в эфире " + targetMention + ". всё, признал поражение?",
|
||||
targetMention + " слился. ожидаемо",
|
||||
};
|
||||
String msgText =
|
||||
lines[(int) (System.currentTimeMillis() % lines.length)];
|
||||
String msgText = lines[(int) (System.currentTimeMillis() % lines.length)];
|
||||
|
||||
channel
|
||||
.getHistory()
|
||||
|
|
@ -163,11 +167,12 @@ public class AndrertListener extends ListenerAdapter {
|
|||
if (m.getAuthor().isBot()) continue;
|
||||
String a = m.getAuthor().getEffectiveName();
|
||||
String c =
|
||||
m.getContentRaw()
|
||||
+ replyInfo(m)
|
||||
+ attachmentInfo(m);
|
||||
m.getContentRaw() + replyInfo(m) + attachmentInfo(m);
|
||||
sessionManager.addMessage(
|
||||
channelId, a, m.getAuthor().getId(), c,
|
||||
channelId,
|
||||
a,
|
||||
m.getAuthor().getId(),
|
||||
c,
|
||||
m.getTimeCreated().toEpochSecond() * 1000);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,8 +31,10 @@ public class DebateChannelListener extends ListenerAdapter {
|
|||
if (prev != null) return;
|
||||
seenMessages.entrySet().removeIf(e -> now - e.getValue() > 5000);
|
||||
|
||||
logger.info("debate msg ch={} a={} text={}",
|
||||
event.getChannel().getId(), event.getAuthor().getId(),
|
||||
logger.info(
|
||||
"debate msg ch={} a={} text={}",
|
||||
event.getChannel().getId(),
|
||||
event.getAuthor().getId(),
|
||||
event.getMessage().getContentRaw().replace("\n", " "));
|
||||
debateService.onHumanMessage(event);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,8 @@ public class DebateService {
|
|||
Files.isDirectory(Paths.get("/app/prompts"))
|
||||
? Paths.get("/app/prompts")
|
||||
: Paths.get("data/prompts");
|
||||
private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter DT_FMT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final long BOT_START_TIME = System.currentTimeMillis();
|
||||
private static final int CHANNEL_CONTEXT = 20;
|
||||
private static final String[] PERSONALITIES = {"arch", "windows", "gentoo"};
|
||||
|
|
@ -87,7 +88,8 @@ public class DebateService {
|
|||
if (debateId == null || debateId.isBlank()) return;
|
||||
if (!event.getChannel().getId().equals(debateId)) return;
|
||||
if (event.getAuthor().isBot()) return;
|
||||
if (event.getMessage().getContentRaw().isBlank() && event.getMessage().getAttachments().isEmpty()) return;
|
||||
if (event.getMessage().getContentRaw().isBlank()
|
||||
&& event.getMessage().getAttachments().isEmpty()) return;
|
||||
|
||||
TextChannel channel = event.getChannel().asTextChannel();
|
||||
ChannelState state = channels.computeIfAbsent(debateId, k -> new ChannelState());
|
||||
|
|
@ -97,8 +99,7 @@ public class DebateService {
|
|||
state.roundCount = 0;
|
||||
state.lastMessageId = event.getMessage().getId();
|
||||
|
||||
state.currentTask = scheduler.schedule(() ->
|
||||
doRound(channel, state), 0, TimeUnit.SECONDS);
|
||||
state.currentTask = scheduler.schedule(() -> doRound(channel, state), 0, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
private void doRound(TextChannel channel, ChannelState state) {
|
||||
|
|
@ -109,43 +110,57 @@ public class DebateService {
|
|||
state.cancel();
|
||||
state.roundDone = true;
|
||||
|
||||
channel.getHistory().retrievePast(CHANNEL_CONTEXT).queue(messages -> {
|
||||
boolean hasNewHuman = messages.stream().anyMatch(m ->
|
||||
!m.getAuthor().isBot() && m.getTimeCreated().toEpochSecond() * 1000 > state.lastHumanTime);
|
||||
if (hasNewHuman) {
|
||||
state.roundDone = false;
|
||||
state.lastHumanTime = now;
|
||||
state.lastPersonality = null;
|
||||
state.currentTask = scheduler.schedule(() ->
|
||||
doRound(channel, state), 0, TimeUnit.SECONDS);
|
||||
return;
|
||||
}
|
||||
channel
|
||||
.getHistory()
|
||||
.retrievePast(CHANNEL_CONTEXT)
|
||||
.queue(
|
||||
messages -> {
|
||||
boolean hasNewHuman =
|
||||
messages.stream()
|
||||
.anyMatch(
|
||||
m ->
|
||||
!m.getAuthor().isBot()
|
||||
&& m.getTimeCreated().toEpochSecond() * 1000
|
||||
> state.lastHumanTime);
|
||||
if (hasNewHuman) {
|
||||
state.roundDone = false;
|
||||
state.lastHumanTime = now;
|
||||
state.lastPersonality = null;
|
||||
state.currentTask =
|
||||
scheduler.schedule(() -> doRound(channel, state), 0, TimeUnit.SECONDS);
|
||||
return;
|
||||
}
|
||||
|
||||
List<CachedMessage> ctx = buildContext(messages);
|
||||
List<CachedMessage> ctx = buildContext(messages);
|
||||
|
||||
if (state.roundCount >= MAX_ROUNDS) return;
|
||||
state.roundCount++;
|
||||
if (state.roundCount >= MAX_ROUNDS) return;
|
||||
state.roundCount++;
|
||||
|
||||
String personality = pickPersonality(state.lastPersonality);
|
||||
if (personality == null) return;
|
||||
state.lastPersonality = personality;
|
||||
String personality = pickPersonality(state.lastPersonality);
|
||||
if (personality == null) return;
|
||||
state.lastPersonality = personality;
|
||||
|
||||
String response = generateResponse(personality, ctx);
|
||||
if (response == null || response.isBlank()) return;
|
||||
String response = generateResponse(personality, ctx);
|
||||
if (response == null || response.isBlank()) return;
|
||||
|
||||
JDA jda = jdaRegistry.getByPersonality(personality);
|
||||
TextChannel target = jda.getTextChannelById(debateId);
|
||||
if (target == null) return;
|
||||
String replyToId = state.lastMessageId;
|
||||
target.sendMessage(response)
|
||||
.setMessageReference(replyToId)
|
||||
.queue(m -> state.lastMessageId = m.getId());
|
||||
JDA jda = jdaRegistry.getByPersonality(personality);
|
||||
TextChannel target = jda.getTextChannelById(debateId);
|
||||
if (target == null) return;
|
||||
String replyToId = state.lastMessageId;
|
||||
target
|
||||
.sendMessage(response)
|
||||
.setMessageReference(replyToId)
|
||||
.queue(m -> state.lastMessageId = m.getId());
|
||||
|
||||
state.currentTask = scheduler.schedule(() -> {
|
||||
state.roundDone = false;
|
||||
doRound(channel, state);
|
||||
}, 0, TimeUnit.SECONDS);
|
||||
});
|
||||
state.currentTask =
|
||||
scheduler.schedule(
|
||||
() -> {
|
||||
state.roundDone = false;
|
||||
doRound(channel, state);
|
||||
},
|
||||
0,
|
||||
TimeUnit.SECONDS);
|
||||
});
|
||||
}
|
||||
|
||||
private String pickPersonality(String exclude) {
|
||||
|
|
@ -185,8 +200,16 @@ public class DebateService {
|
|||
|
||||
long uptimeSec = (System.currentTimeMillis() - BOT_START_TIME) / 1000;
|
||||
String now = LocalDateTime.now().format(DT_FMT);
|
||||
prompt += "\nтекущее время " + now + " uptime бота " + (uptimeSec / 3600) + "ч "
|
||||
+ ((uptimeSec % 3600) / 60) + "м " + (uptimeSec % 60) + "с";
|
||||
prompt +=
|
||||
"\nтекущее время "
|
||||
+ now
|
||||
+ " uptime бота "
|
||||
+ (uptimeSec / 3600)
|
||||
+ "ч "
|
||||
+ ((uptimeSec % 3600) / 60)
|
||||
+ "м "
|
||||
+ (uptimeSec % 60)
|
||||
+ "с";
|
||||
|
||||
StringBuilder userContent = new StringBuilder();
|
||||
for (CachedMessage m : ctx) {
|
||||
|
|
@ -196,7 +219,8 @@ public class DebateService {
|
|||
if (userContent.length() > config.getAiContextChars()) break;
|
||||
}
|
||||
String text = userContent.toString().strip();
|
||||
if (text.length() > config.getAiContextChars()) text = text.substring(0, config.getAiContextChars());
|
||||
if (text.length() > config.getAiContextChars())
|
||||
text = text.substring(0, config.getAiContextChars());
|
||||
|
||||
String wiki = wikipediaService.search(text);
|
||||
if (wiki != null) text += "\n\n[википедия]:\n" + wiki;
|
||||
|
|
@ -226,10 +250,11 @@ public class DebateService {
|
|||
name = msg.getAuthor().getEffectiveName();
|
||||
}
|
||||
String label = (i == messages.size() - 1) ? "[now]" : "[history]";
|
||||
result.add(new CachedMessage(
|
||||
label + " " + name + "(" + msg.getAuthor().getId() + "): " + msg.getContentRaw(),
|
||||
msg.getChannel().getName(),
|
||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
result.add(
|
||||
new CachedMessage(
|
||||
label + " " + name + "(" + msg.getAuthor().getId() + "): " + msg.getContentRaw(),
|
||||
msg.getChannel().getName(),
|
||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@ public class HelperListener extends ListenerAdapter {
|
|||
if (channelId == null || channelId.isBlank()) return;
|
||||
if (!event.getChannel().getId().equals(channelId)) return;
|
||||
if (event.getAuthor().isBot()) return;
|
||||
if (event.getMessage().getContentRaw().isBlank() && event.getMessage().getAttachments().isEmpty()) return;
|
||||
if (event.getMessage().getContentRaw().isBlank()
|
||||
&& event.getMessage().getAttachments().isEmpty()) return;
|
||||
|
||||
String raw = event.getMessage().getContentRaw();
|
||||
if (!raw.isBlank() && !LanguageFilter.isAllowed(raw)) return;
|
||||
|
|
@ -63,38 +64,53 @@ public class HelperListener extends ListenerAdapter {
|
|||
}
|
||||
|
||||
private void respond(MessageReceivedEvent event) {
|
||||
event.getChannel().getHistory().retrievePast(CHANNEL_CONTEXT).queue(messages -> {
|
||||
String targetName = event.getAuthor().getEffectiveName();
|
||||
List<CachedMessage> context = new ArrayList<>();
|
||||
for (int idx = 0; idx < messages.size(); idx++) {
|
||||
Message msg = messages.get(idx);
|
||||
boolean isSelf = msg.getAuthor().getId().equals(event.getJDA().getSelfUser().getId());
|
||||
String author = isSelf ? "ты" : msg.getAuthor().getEffectiveName();
|
||||
String authorId = msg.getAuthor().getId();
|
||||
event
|
||||
.getChannel()
|
||||
.getHistory()
|
||||
.retrievePast(CHANNEL_CONTEXT)
|
||||
.queue(
|
||||
messages -> {
|
||||
String targetName = event.getAuthor().getEffectiveName();
|
||||
List<CachedMessage> context = new ArrayList<>();
|
||||
for (int idx = 0; idx < messages.size(); idx++) {
|
||||
Message msg = messages.get(idx);
|
||||
boolean isSelf =
|
||||
msg.getAuthor().getId().equals(event.getJDA().getSelfUser().getId());
|
||||
String author = isSelf ? "ты" : msg.getAuthor().getEffectiveName();
|
||||
String authorId = msg.getAuthor().getId();
|
||||
|
||||
String label = (idx == 0) ? "[now]" : "[history]";
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(label).append(" ").append(author).append("(").append(authorId).append("): ")
|
||||
.append(msg.getContentRaw());
|
||||
line.append(attachmentInfo(msg));
|
||||
String label = (idx == 0) ? "[now]" : "[history]";
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(label)
|
||||
.append(" ")
|
||||
.append(author)
|
||||
.append("(")
|
||||
.append(authorId)
|
||||
.append("): ")
|
||||
.append(msg.getContentRaw());
|
||||
line.append(attachmentInfo(msg));
|
||||
|
||||
context.add(new CachedMessage(
|
||||
line.toString(),
|
||||
event.getChannel().getName(),
|
||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
java.util.Collections.reverse(context);
|
||||
context.add(
|
||||
new CachedMessage(
|
||||
line.toString(),
|
||||
event.getChannel().getName(),
|
||||
msg.getTimeCreated().toEpochSecond() * 1000));
|
||||
}
|
||||
java.util.Collections.reverse(context);
|
||||
|
||||
String response = roastService.generateResponse(context, targetName, null, helperPrompt);
|
||||
if (response == null || response.isBlank()) return;
|
||||
String response =
|
||||
roastService.generateResponse(context, targetName, null, helperPrompt);
|
||||
if (response == null || response.isBlank()) return;
|
||||
|
||||
sendReply(event, response);
|
||||
});
|
||||
sendReply(event, response);
|
||||
});
|
||||
}
|
||||
|
||||
private void sendReply(MessageReceivedEvent event, String text) {
|
||||
java.util.regex.Matcher m = java.util.regex.Pattern.compile(
|
||||
"\\[file:([^\\]]+)\\](.*?)\\[/file\\]", java.util.regex.Pattern.DOTALL).matcher(text);
|
||||
java.util.regex.Matcher m =
|
||||
java.util.regex.Pattern.compile(
|
||||
"\\[file:([^\\]]+)\\](.*?)\\[/file\\]", java.util.regex.Pattern.DOTALL)
|
||||
.matcher(text);
|
||||
if (!m.find()) {
|
||||
sendText(event, text);
|
||||
return;
|
||||
|
|
@ -105,16 +121,15 @@ public class HelperListener extends ListenerAdapter {
|
|||
String filename = m.group(1).strip();
|
||||
String content = m.group(2);
|
||||
m.appendReplacement(remaining, "");
|
||||
var data = net.dv8tion.jda.api.utils.FileUpload.fromData(
|
||||
content.getBytes(java.nio.charset.StandardCharsets.UTF_8), filename);
|
||||
var data =
|
||||
net.dv8tion.jda.api.utils.FileUpload.fromData(
|
||||
content.getBytes(java.nio.charset.StandardCharsets.UTF_8), filename);
|
||||
if (first) {
|
||||
String msg = remaining.toString().strip();
|
||||
event.getMessage().reply(msg == null ? "" : msg)
|
||||
.addFiles(data).queue();
|
||||
event.getMessage().reply(msg == null ? "" : msg).addFiles(data).queue();
|
||||
first = false;
|
||||
} else {
|
||||
event.getChannel().sendMessage(" ")
|
||||
.addFiles(data).queue();
|
||||
event.getChannel().sendMessage(" ").addFiles(data).queue();
|
||||
}
|
||||
} while (m.find());
|
||||
m.appendTail(remaining);
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import java.nio.file.Path;
|
|||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -28,17 +26,14 @@ public class KnowledgeService {
|
|||
private static final int RESULT_LIMIT = 3;
|
||||
private static final Set<String> STOP_WORDS =
|
||||
Set.of(
|
||||
"это", "как", "так", "что", "он", "она", "они", "мы", "вы", "ты", "я",
|
||||
"не", "да", "нет", "на", "в", "по", "для", "с", "у", "о", "от", "к",
|
||||
"из", "за", "над", "под", "об", "же", "ли", "бы", "а", "и", "но",
|
||||
"или", "то", "все", "его", "ее", "их", "еще", "уже", "когда",
|
||||
"the", "a", "an", "is", "are", "was", "were", "be", "been",
|
||||
"have", "has", "had", "do", "does", "did", "will", "would",
|
||||
"can", "could", "may", "might", "shall", "should", "to", "of",
|
||||
"in", "for", "with", "on", "at", "by", "from", "as", "into",
|
||||
"it", "its", "this", "that", "these", "those", "i", "you",
|
||||
"he", "she", "we", "they", "me", "him", "her", "us", "them",
|
||||
"and", "but", "or", "not", "no", "if", "so", "about");
|
||||
"это", "как", "так", "что", "он", "она", "они", "мы", "вы", "ты", "я", "не", "да", "нет",
|
||||
"на", "в", "по", "для", "с", "у", "о", "от", "к", "из", "за", "над", "под", "об", "же",
|
||||
"ли", "бы", "а", "и", "но", "или", "то", "все", "его", "ее", "их", "еще", "уже", "когда",
|
||||
"the", "a", "an", "is", "are", "was", "were", "be", "been", "have", "has", "had", "do",
|
||||
"does", "did", "will", "would", "can", "could", "may", "might", "shall", "should", "to",
|
||||
"of", "in", "for", "with", "on", "at", "by", "from", "as", "into", "it", "its", "this",
|
||||
"that", "these", "those", "i", "you", "he", "she", "we", "they", "me", "him", "her", "us",
|
||||
"them", "and", "but", "or", "not", "no", "if", "so", "about");
|
||||
|
||||
private final List<KnowledgeFile> files = new ArrayList<>();
|
||||
|
||||
|
|
@ -105,10 +100,8 @@ public class KnowledgeService {
|
|||
}
|
||||
|
||||
private String[] extractKeywords(String text) {
|
||||
String cleaned = text.toLowerCase()
|
||||
.replaceAll("[^a-zа-яё0-9\\s]", " ")
|
||||
.replaceAll("\\s+", " ")
|
||||
.strip();
|
||||
String cleaned =
|
||||
text.toLowerCase().replaceAll("[^a-zа-яё0-9\\s]", " ").replaceAll("\\s+", " ").strip();
|
||||
String[] all = cleaned.split(" ");
|
||||
return Arrays.stream(all)
|
||||
.filter(w -> w.length() > 2)
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ public class RoastService {
|
|||
Files.isDirectory(Paths.get("/app/prompts"))
|
||||
? Paths.get("/app/prompts")
|
||||
: Paths.get("data/prompts");
|
||||
private static final DateTimeFormatter DT_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final DateTimeFormatter DT_FMT =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
private static final long BOT_START_TIME = System.currentTimeMillis();
|
||||
|
||||
private final BotConfig config;
|
||||
|
|
@ -67,7 +68,9 @@ public class RoastService {
|
|||
long mins = (uptimeSec % 3600) / 60;
|
||||
long secs = uptimeSec % 60;
|
||||
String now = LocalDateTime.now().format(DT_FMT);
|
||||
return String.format("текущее время %s uptime бота %dч %dм %dс всего команд выполнено %d", now, hours, mins, secs, metricsCollector.getTotalCommands());
|
||||
return String.format(
|
||||
"текущее время %s uptime бота %dч %dм %dс всего команд выполнено %d",
|
||||
now, hours, mins, secs, metricsCollector.getTotalCommands());
|
||||
}
|
||||
|
||||
private String loadPrompt(String filename) {
|
||||
|
|
@ -171,7 +174,8 @@ public class RoastService {
|
|||
return deepSeekClient.callModel(systemPrompt, text, 0.9);
|
||||
}
|
||||
|
||||
public String generateResponse(List<CachedMessage> context, String targetName, String userId, String customPrompt) {
|
||||
public String generateResponse(
|
||||
List<CachedMessage> context, String targetName, String userId, String customPrompt) {
|
||||
String escapedName = escapeFormat(targetName != null ? targetName : "");
|
||||
String systemPrompt = customPrompt.formatted(escapedName, escapedName, escapedName);
|
||||
|
||||
|
|
|
|||
|
|
@ -25,11 +25,12 @@ public class WebSearchService {
|
|||
private final OkHttpClient httpClient;
|
||||
|
||||
public WebSearchService() {
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(8, TimeUnit.SECONDS)
|
||||
.readTimeout(12, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.build();
|
||||
this.httpClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(8, TimeUnit.SECONDS)
|
||||
.readTimeout(12, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
public String search(String query) {
|
||||
|
|
@ -68,13 +69,14 @@ public class WebSearchService {
|
|||
private List<SearchResult> searchDdg(String query) {
|
||||
List<SearchResult> results = new ArrayList<>();
|
||||
try {
|
||||
String url = "https://html.duckduckgo.com/html/?q="
|
||||
+ URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
.get()
|
||||
.build();
|
||||
String url =
|
||||
"https://html.duckduckgo.com/html/?q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||
Request req =
|
||||
new Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
.get()
|
||||
.build();
|
||||
try (Response resp = httpClient.newCall(req).execute()) {
|
||||
if (!resp.isSuccessful() || resp.body() == null) return results;
|
||||
String html = resp.body().string();
|
||||
|
|
@ -108,24 +110,28 @@ public class WebSearchService {
|
|||
if (amp > 0) enc = enc.substring(0, amp);
|
||||
return URLDecoder.decode(enc, StandardCharsets.UTF_8);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return ddgUrl;
|
||||
}
|
||||
|
||||
private String fetchPageText(String url) {
|
||||
try {
|
||||
Request req = new Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
.header("Accept", "text/html,application/xhtml+xml")
|
||||
.header("Accept-Language", "en,ru;q=0.9")
|
||||
.get()
|
||||
.build();
|
||||
Request req =
|
||||
new Request.Builder()
|
||||
.url(url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
|
||||
.header("Accept", "text/html,application/xhtml+xml")
|
||||
.header("Accept-Language", "en,ru;q=0.9")
|
||||
.get()
|
||||
.build();
|
||||
try (Response resp = httpClient.newCall(req).execute()) {
|
||||
if (!resp.isSuccessful() || resp.body() == null) return null;
|
||||
String html = resp.body().string();
|
||||
Document doc = Jsoup.parse(html);
|
||||
Element main = doc.selectFirst("main, article, .post-content, .entry-content, .readme, .markdown-body, #readme, .container-lg");
|
||||
Element main =
|
||||
doc.selectFirst(
|
||||
"main, article, .post-content, .entry-content, .readme, .markdown-body, #readme, .container-lg");
|
||||
String text;
|
||||
if (main != null) {
|
||||
main.select("script, style, nav, footer, header, .sidebar, aside").remove();
|
||||
|
|
|
|||
|
|
@ -17,17 +17,20 @@ import org.slf4j.LoggerFactory;
|
|||
@Singleton
|
||||
public class WikipediaService {
|
||||
private static final Logger logger = LoggerFactory.getLogger(WikipediaService.class);
|
||||
private static final String SEARCH_URL = "https://ru.wikipedia.org/w/api.php?action=query&list=search&srsearch=%s&format=json&srlimit=3&srprop=snippet";
|
||||
private static final String EXTRACT_URL = "https://ru.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=%s&format=json&exlimit=1";
|
||||
private static final String SEARCH_URL =
|
||||
"https://ru.wikipedia.org/w/api.php?action=query&list=search&srsearch=%s&format=json&srlimit=3&srprop=snippet";
|
||||
private static final String EXTRACT_URL =
|
||||
"https://ru.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&titles=%s&format=json&exlimit=1";
|
||||
private static final int MAX_RESULT_CHARS = 1500;
|
||||
|
||||
private final OkHttpClient httpClient;
|
||||
|
||||
public WikipediaService() {
|
||||
this.httpClient = new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.build();
|
||||
this.httpClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
public String search(String query) {
|
||||
|
|
@ -45,9 +48,8 @@ public class WikipediaService {
|
|||
for (int i = 0; i < Math.min(results.size(), 3); i++) {
|
||||
JsonObject r = results.get(i).getAsJsonObject();
|
||||
String title = r.get("title").getAsString();
|
||||
String snippet = r.get("snippet").getAsString()
|
||||
.replaceAll("<[^>]+>", "")
|
||||
.replaceAll("&[^;]+;", " ");
|
||||
String snippet =
|
||||
r.get("snippet").getAsString().replaceAll("<[^>]+>", "").replaceAll("&[^;]+;", " ");
|
||||
String pageContent = getExtract(title);
|
||||
sb.append("=== ").append(title).append(" ===\n");
|
||||
if (pageContent != null) {
|
||||
|
|
|
|||
130
src/main/java/creator/loki/feature/tiktok/TikTokApi.java
Normal file
130
src/main/java/creator/loki/feature/tiktok/TikTokApi.java
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
package creator.loki.feature.tiktok;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.inject.Inject;
|
||||
import com.google.inject.Singleton;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@Singleton
|
||||
public class TikTokApi {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TikTokApi.class);
|
||||
private static final String API_URL = "https://www.tikwm.com/api/";
|
||||
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||
|
||||
private final OkHttpClient client;
|
||||
|
||||
@Inject
|
||||
public TikTokApi() {
|
||||
this.client =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
public TikTokResult fetchVideo(String url) throws TikTokApiException {
|
||||
JsonObject requestBody = new JsonObject();
|
||||
requestBody.addProperty("url", url);
|
||||
|
||||
Request request =
|
||||
new Request.Builder()
|
||||
.url(API_URL)
|
||||
.post(RequestBody.create(requestBody.toString(), JSON))
|
||||
.build();
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
throw new TikTokApiException("Empty response from API");
|
||||
}
|
||||
|
||||
String responseStr = body.string();
|
||||
JsonObject json = JsonParser.parseString(responseStr).getAsJsonObject();
|
||||
|
||||
int code = json.has("code") ? json.get("code").getAsInt() : -1;
|
||||
if (code != 0) {
|
||||
String msg = json.has("msg") ? json.get("msg").getAsString() : "Unknown error";
|
||||
throw new TikTokApiException("API error: " + msg);
|
||||
}
|
||||
|
||||
JsonObject data = json.getAsJsonObject("data");
|
||||
if (data == null) {
|
||||
throw new TikTokApiException("No data in response");
|
||||
}
|
||||
|
||||
String videoUrl = data.has("play") ? data.get("play").getAsString() : null;
|
||||
String title = data.has("title") ? data.get("title").getAsString() : "";
|
||||
int duration = data.has("duration") ? data.get("duration").getAsInt() : 0;
|
||||
String cover = data.has("cover") ? data.get("cover").getAsString() : null;
|
||||
|
||||
JsonObject authorObj = data.has("author") ? data.getAsJsonObject("author") : null;
|
||||
String author =
|
||||
authorObj != null && authorObj.has("nickname")
|
||||
? authorObj.get("nickname").getAsString()
|
||||
: "Unknown";
|
||||
|
||||
if (videoUrl == null || videoUrl.isBlank()) {
|
||||
throw new TikTokApiException("No video URL in response");
|
||||
}
|
||||
|
||||
return new TikTokResult(videoUrl, title, author, duration, cover);
|
||||
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to fetch TikTok video", e);
|
||||
throw new TikTokApiException("Network error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public static class TikTokResult {
|
||||
private final String videoUrl;
|
||||
private final String title;
|
||||
private final String author;
|
||||
private final int duration;
|
||||
private final String cover;
|
||||
|
||||
public TikTokResult(String videoUrl, String title, String author, int duration, String cover) {
|
||||
this.videoUrl = videoUrl;
|
||||
this.title = title;
|
||||
this.author = author;
|
||||
this.duration = duration;
|
||||
this.cover = cover;
|
||||
}
|
||||
|
||||
public String getVideoUrl() {
|
||||
return videoUrl;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public int getDuration() {
|
||||
return duration;
|
||||
}
|
||||
|
||||
public String getCover() {
|
||||
return cover;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TikTokApiException extends Exception {
|
||||
public TikTokApiException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
163
src/main/java/creator/loki/feature/tiktok/TikTokCommand.java
Normal file
163
src/main/java/creator/loki/feature/tiktok/TikTokCommand.java
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
package creator.loki.feature.tiktok;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
import creator.loki.embed.EmbedFactory;
|
||||
import creator.loki.embed.EmbedTemplates;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import net.dv8tion.jda.api.utils.FileUpload;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class TikTokCommand implements Command {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TikTokCommand.class);
|
||||
private static final long MAX_FILE_SIZE = 8 * 1024 * 1024;
|
||||
|
||||
private final TikTokApi tikTokApi;
|
||||
private final OkHttpClient downloadClient;
|
||||
|
||||
@Inject
|
||||
public TikTokCommand(TikTokApi tikTokApi) {
|
||||
this.tikTokApi = tikTokApi;
|
||||
this.downloadClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
.readTimeout(60, TimeUnit.SECONDS)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "tiktok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "Скачать видео из TikTok";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "tiktok <url>";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCategory() {
|
||||
return "Развлечения";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(CommandContext context) {
|
||||
if (context.getArgs().isEmpty()) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Укажи ссылку на TikTok видео"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String url = context.getArgs().get(0);
|
||||
if (!url.contains("tiktok.com")) {
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Невалидная ссылка. Укажи ссылку на TikTok видео"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendMessage("🔍 Загружаю видео...")
|
||||
.queue(
|
||||
loadingMsg -> {
|
||||
Thread.startVirtualThread(
|
||||
() -> {
|
||||
try {
|
||||
TikTokApi.TikTokResult result = tikTokApi.fetchVideo(url);
|
||||
|
||||
byte[] videoBytes = downloadVideo(result.getVideoUrl());
|
||||
|
||||
if (videoBytes.length > MAX_FILE_SIZE) {
|
||||
loadingMsg
|
||||
.editMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Видео слишком большое (> 8MB)"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String durationStr = formatDuration(result.getDuration());
|
||||
|
||||
MessageEmbed embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle(truncate(result.getTitle(), 256))
|
||||
.withAuthor("@" + result.getAuthor())
|
||||
.withColor(new Color(0, 0, 0))
|
||||
.addField("Длительность", durationStr, true)
|
||||
.withThumbnail(result.getCover())
|
||||
.build();
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendFiles(FileUpload.fromData(videoBytes, "tiktok.mp4"))
|
||||
.setEmbeds(embed)
|
||||
.queue(
|
||||
success -> loadingMsg.delete().queue(),
|
||||
failure ->
|
||||
loadingMsg
|
||||
.editMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка", "Не удалось отправить видео"))
|
||||
.queue());
|
||||
|
||||
} catch (TikTokApi.TikTokApiException e) {
|
||||
loadingMsg
|
||||
.editMessageEmbeds(EmbedTemplates.error("Ошибка", e.getMessage()))
|
||||
.queue();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to download TikTok video", e);
|
||||
loadingMsg
|
||||
.editMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Ошибка загрузки видео"))
|
||||
.queue();
|
||||
} catch (Exception e) {
|
||||
logger.error("Unexpected error in TikTokCommand", e);
|
||||
loadingMsg
|
||||
.editMessageEmbeds(EmbedTemplates.error("Ошибка", "Произошла ошибка"))
|
||||
.queue();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private byte[] downloadVideo(String videoUrl) throws IOException {
|
||||
Request request = new Request.Builder().url(videoUrl).build();
|
||||
try (Response response = downloadClient.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
throw new IOException("Download failed: HTTP " + response.code());
|
||||
}
|
||||
InputStream is = response.body().byteStream();
|
||||
return is.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
||||
private String formatDuration(int seconds) {
|
||||
int min = seconds / 60;
|
||||
int sec = seconds % 60;
|
||||
return String.format("%d:%02d", min, sec);
|
||||
}
|
||||
|
||||
private String truncate(String str, int maxLen) {
|
||||
if (str == null) return "";
|
||||
return str.length() > maxLen ? str.substring(0, maxLen - 3) + "..." : str;
|
||||
}
|
||||
}
|
||||
17
src/main/java/creator/loki/feature/tiktok/TikTokModule.java
Normal file
17
src/main/java/creator/loki/feature/tiktok/TikTokModule.java
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
package creator.loki.feature.tiktok;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
|
||||
public class TikTokModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(TikTokApi.class).in(Singleton.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
TikTokCommand provideTikTokCommand(TikTokApi tikTokApi) {
|
||||
return new TikTokCommand(tikTokApi);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package creator.loki.feature.utility;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import creator.loki.core.registry.CommandRegistrar;
|
||||
|
||||
public class UtilityModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
.to(UtilityCommandRegistrar.class);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,10 @@ package creator.loki.util;
|
|||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import creator.loki.feature.music.core.LavalinkManager;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
import net.dv8tion.jda.api.entities.Member;
|
||||
|
||||
public final class VoiceHelper {
|
||||
private static final int RETRIES = 10;
|
||||
private static final long DELAY_MS = 200;
|
||||
|
|
@ -36,4 +40,22 @@ public final class VoiceHelper {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подключается к голосовому каналу для воспроизведения.
|
||||
*
|
||||
* @return true если подключение успешно, false если не удалось
|
||||
*/
|
||||
public static boolean connectForPlayback(Guild guild, Member member, LavalinkManager lm) {
|
||||
if (guild.getSelfMember().getVoiceState().inAudioChannel()) return true;
|
||||
var vc = member.getVoiceState().getChannel().asVoiceChannel();
|
||||
guild.getJDA().getDirectAudioController().connect(vc);
|
||||
if (!guild.getAudioManager().isConnected()) guild.getAudioManager().openAudioConnection(vc);
|
||||
var self = guild.getSelfMember();
|
||||
waitForCondition(() -> self.getVoiceState().inAudioChannel());
|
||||
if (!self.getVoiceState().inAudioChannel()) return false;
|
||||
var link = lm.getOrCreateLink(guild.getIdLong());
|
||||
waitForCondition(() -> link.getCachedPlayer() != null);
|
||||
return link.getCachedPlayer() != null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ class BotConfigTest {
|
|||
30,
|
||||
24,
|
||||
null,
|
||||
lavalink);
|
||||
lavalink,
|
||||
null,
|
||||
null);
|
||||
assertEquals("token123", config.getMainToken());
|
||||
assertEquals("token123", config.getToken());
|
||||
assertNull(config.getMusicToken());
|
||||
|
|
@ -45,7 +47,8 @@ class BotConfigTest {
|
|||
void handlesNullValues() {
|
||||
BotConfig config =
|
||||
new BotConfig(
|
||||
null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null, null);
|
||||
null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null,
|
||||
null, null, null);
|
||||
assertNull(config.getMainToken());
|
||||
assertNull(config.getMusicToken());
|
||||
assertNull(config.getRadioToken());
|
||||
|
|
@ -62,7 +65,8 @@ class BotConfigTest {
|
|||
LavalinkConfig lavalink = new LavalinkConfig("", 0, "", false);
|
||||
BotConfig config =
|
||||
new BotConfig(
|
||||
"", "", "", "", "", 8080, "", "", null, null, 10, 300, 3000, 30, 24, null, lavalink);
|
||||
"", "", "", "", "", 8080, "", "", null, null, 10, 300, 3000, 30, 24, null, lavalink,
|
||||
null, null);
|
||||
assertEquals("", config.getMainToken());
|
||||
assertEquals("", config.getMusicToken());
|
||||
assertEquals("", config.getRadioToken());
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import creator.loki.feature.roast.AndrertListener;
|
|||
import creator.loki.feature.roast.RoastService;
|
||||
import creator.loki.feature.role.CustomRoleManager;
|
||||
import creator.loki.feature.ship.repository.ShipPairRepository;
|
||||
import creator.loki.feature.tiktok.TikTokCommand;
|
||||
import creator.loki.feature.utility.UtilityCommandRegistrar;
|
||||
import creator.loki.feature.voice.service.VoiceChannelManager;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
|
@ -102,12 +103,19 @@ class SmokeCommandRegistrationTest {
|
|||
mock(GuildConfigRepository.class));
|
||||
moderationRegistrar.register(registry);
|
||||
|
||||
var tikTokCommand = mock(TikTokCommand.class);
|
||||
lenient().when(tikTokCommand.getName()).thenReturn("tiktok");
|
||||
lenient().when(tikTokCommand.getDescription()).thenReturn("Скачать видео из TikTok");
|
||||
lenient().when(tikTokCommand.getUsage()).thenReturn("tiktok <url>");
|
||||
lenient().when(tikTokCommand.getCategory()).thenReturn("Развлечения");
|
||||
|
||||
var entertainmentRegistrar =
|
||||
new EntertainmentCommandRegistrar(
|
||||
mock(CooldownManager.class),
|
||||
mock(RatingService.class),
|
||||
mock(ShipPairRepository.class),
|
||||
mock(BotConfig.class));
|
||||
mock(BotConfig.class),
|
||||
tikTokCommand);
|
||||
entertainmentRegistrar.register(registry);
|
||||
|
||||
var utilityRegistrar =
|
||||
|
|
|
|||
|
|
@ -40,7 +40,9 @@ class LavalinkManagerTest {
|
|||
30,
|
||||
24,
|
||||
null,
|
||||
lavalinkConfig);
|
||||
lavalinkConfig,
|
||||
null,
|
||||
null);
|
||||
|
||||
var manager = new LavalinkManager(config, queueRepository);
|
||||
|
||||
|
|
@ -68,7 +70,9 @@ class LavalinkManagerTest {
|
|||
30,
|
||||
24,
|
||||
null,
|
||||
lavalinkConfig);
|
||||
lavalinkConfig,
|
||||
null,
|
||||
null);
|
||||
var manager = new LavalinkManager(config, queueRepository);
|
||||
|
||||
var first = manager.getMusicManager(123L);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue