feat: setup dashboard, reaction roles, tickets; fix music modifiers & search UX
- /setup slash command with interactive dashboard (voice/counter/logs/levelroles) - Reaction roles via !reactionrole command - Ticket system via !ticketsetup with create/close/delete buttons - Fixed TrackStarter chicken-egg bug (music player never created) - Fixed music modifiers: m!volume, m!bassboost, m!nightcore, m!vaporwave - Replaced !play <number> with StringSelectMenu (dropdown) for picking tracks - Fixed SoundCloud search (!play sc <query>) - Changed music bot prefix: q! -> m!
This commit is contained in:
parent
102cec3b56
commit
ff9d3462f6
27 changed files with 1505 additions and 433 deletions
|
|
@ -6,6 +6,7 @@ import creator.loki.bootstrap.ReadyListener;
|
||||||
import creator.loki.feature.counter.service.MemberCounterListener;
|
import creator.loki.feature.counter.service.MemberCounterListener;
|
||||||
import creator.loki.feature.economy.FishMessageListener;
|
import creator.loki.feature.economy.FishMessageListener;
|
||||||
import creator.loki.feature.help.HelpMenuHandler;
|
import creator.loki.feature.help.HelpMenuHandler;
|
||||||
|
import creator.loki.feature.reactroles.ReactionRoleListener;
|
||||||
import creator.loki.feature.image.ImageNavigationHandler;
|
import creator.loki.feature.image.ImageNavigationHandler;
|
||||||
import creator.loki.feature.leaderboard.LeaderboardButtonListener;
|
import creator.loki.feature.leaderboard.LeaderboardButtonListener;
|
||||||
import creator.loki.feature.level.listener.MessageListener;
|
import creator.loki.feature.level.listener.MessageListener;
|
||||||
|
|
@ -13,8 +14,10 @@ import creator.loki.feature.level.listener.ResetLevelsButtonHandler;
|
||||||
import creator.loki.feature.level.service.VoiceTracker;
|
import creator.loki.feature.level.service.VoiceTracker;
|
||||||
import creator.loki.feature.moderation.commands.utils.ClearSlashCommands;
|
import creator.loki.feature.moderation.commands.utils.ClearSlashCommands;
|
||||||
import creator.loki.feature.moderation.log.ModerationLogger;
|
import creator.loki.feature.moderation.log.ModerationLogger;
|
||||||
import creator.loki.feature.music.listener.MusicSearchButtonHandler;
|
import creator.loki.feature.config.SetupSlashHandler;
|
||||||
import creator.loki.feature.music.MusicSlashCommands;
|
import creator.loki.feature.music.MusicSlashCommands;
|
||||||
|
import creator.loki.feature.music.listener.PlaySelectHandler;
|
||||||
|
import creator.loki.feature.ticket.TicketListener;
|
||||||
import creator.loki.feature.roast.AndrertListener;
|
import creator.loki.feature.roast.AndrertListener;
|
||||||
import creator.loki.feature.roast.HelperListener;
|
import creator.loki.feature.roast.HelperListener;
|
||||||
import creator.loki.feature.voice.handler.VoiceButtonHandler;
|
import creator.loki.feature.voice.handler.VoiceButtonHandler;
|
||||||
|
|
@ -43,13 +46,16 @@ public class ListenerAggregator {
|
||||||
ResetLevelsButtonHandler resetLevelsButtonHandler,
|
ResetLevelsButtonHandler resetLevelsButtonHandler,
|
||||||
LeaderboardButtonListener leaderboardButtonListener,
|
LeaderboardButtonListener leaderboardButtonListener,
|
||||||
MessageCache messageCache,
|
MessageCache messageCache,
|
||||||
MusicSearchButtonHandler musicSearchButtonHandler,
|
|
||||||
MusicSlashCommands musicSlashCommands,
|
MusicSlashCommands musicSlashCommands,
|
||||||
|
PlaySelectHandler playSelectHandler,
|
||||||
FishMessageListener fishMessageListener,
|
FishMessageListener fishMessageListener,
|
||||||
AndrertListener andrertListener,
|
AndrertListener andrertListener,
|
||||||
ClearSlashCommands clearSlashCommands,
|
ClearSlashCommands clearSlashCommands,
|
||||||
HelperListener helperListener,
|
HelperListener helperListener,
|
||||||
ImageNavigationHandler imageNavigationHandler) {
|
ImageNavigationHandler imageNavigationHandler,
|
||||||
|
SetupSlashHandler setupSlashHandler,
|
||||||
|
TicketListener ticketListener,
|
||||||
|
ReactionRoleListener reactionRoleListener) {
|
||||||
this.listeners =
|
this.listeners =
|
||||||
new Object[] {
|
new Object[] {
|
||||||
readyListener,
|
readyListener,
|
||||||
|
|
@ -66,13 +72,16 @@ public class ListenerAggregator {
|
||||||
resetLevelsButtonHandler,
|
resetLevelsButtonHandler,
|
||||||
leaderboardButtonListener,
|
leaderboardButtonListener,
|
||||||
messageCache,
|
messageCache,
|
||||||
musicSearchButtonHandler,
|
|
||||||
musicSlashCommands,
|
musicSlashCommands,
|
||||||
|
playSelectHandler,
|
||||||
fishMessageListener,
|
fishMessageListener,
|
||||||
andrertListener,
|
andrertListener,
|
||||||
clearSlashCommands,
|
clearSlashCommands,
|
||||||
helperListener,
|
helperListener,
|
||||||
imageNavigationHandler
|
imageNavigationHandler,
|
||||||
|
setupSlashHandler,
|
||||||
|
ticketListener,
|
||||||
|
reactionRoleListener
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,6 +103,27 @@ public final class FeatureTables {
|
||||||
left_count INTEGER DEFAULT 0,
|
left_count INTEGER DEFAULT 0,
|
||||||
difference INTEGER DEFAULT 0
|
difference INTEGER DEFAULT 0
|
||||||
)""";
|
)""";
|
||||||
|
private static final String REACTION_ROLES =
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS reaction_roles (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
channel_id TEXT NOT NULL,
|
||||||
|
message_id TEXT NOT NULL,
|
||||||
|
emoji TEXT NOT NULL,
|
||||||
|
role_id TEXT NOT NULL,
|
||||||
|
UNIQUE(message_id, emoji)
|
||||||
|
)""";
|
||||||
|
private static final String TICKETS =
|
||||||
|
"""
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (
|
||||||
|
channel_id TEXT PRIMARY KEY,
|
||||||
|
guild_id TEXT NOT NULL,
|
||||||
|
creator_id TEXT NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
closed INTEGER DEFAULT 0,
|
||||||
|
claimed_by TEXT
|
||||||
|
)""";
|
||||||
private static final String RATINGS =
|
private static final String RATINGS =
|
||||||
"""
|
"""
|
||||||
CREATE TABLE IF NOT EXISTS ratings (
|
CREATE TABLE IF NOT EXISTS ratings (
|
||||||
|
|
@ -123,7 +144,9 @@ public final class FeatureTables {
|
||||||
database.execute(VOICE_CHANNELS);
|
database.execute(VOICE_CHANNELS);
|
||||||
database.execute(VOICE_PRESETS);
|
database.execute(VOICE_PRESETS);
|
||||||
database.execute(COUNTER_STATS);
|
database.execute(COUNTER_STATS);
|
||||||
|
database.execute(TICKETS);
|
||||||
database.execute(RATINGS);
|
database.execute(RATINGS);
|
||||||
|
database.execute(REACTION_ROLES);
|
||||||
createIndexes(database);
|
createIndexes(database);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import creator.loki.core.registry.PrefixCommandRegistrar;
|
||||||
import creator.loki.core.security.CooldownManager;
|
import creator.loki.core.security.CooldownManager;
|
||||||
import creator.loki.core.security.RateLimiter;
|
import creator.loki.core.security.RateLimiter;
|
||||||
import creator.loki.database.DatabaseModule;
|
import creator.loki.database.DatabaseModule;
|
||||||
|
import creator.loki.feature.config.ConfigModule;
|
||||||
import creator.loki.feature.counter.CounterModule;
|
import creator.loki.feature.counter.CounterModule;
|
||||||
import creator.loki.feature.economy.EconomyModule;
|
import creator.loki.feature.economy.EconomyModule;
|
||||||
import creator.loki.feature.entertainment.EntertainmentModule;
|
import creator.loki.feature.entertainment.EntertainmentModule;
|
||||||
|
|
@ -29,6 +30,8 @@ import creator.loki.feature.radio.RadioModule;
|
||||||
import creator.loki.feature.roast.RoastModule;
|
import creator.loki.feature.roast.RoastModule;
|
||||||
import creator.loki.feature.role.RoleModule;
|
import creator.loki.feature.role.RoleModule;
|
||||||
import creator.loki.feature.tiktok.TikTokModule;
|
import creator.loki.feature.tiktok.TikTokModule;
|
||||||
|
import creator.loki.feature.ticket.TicketModule;
|
||||||
|
import creator.loki.feature.reactroles.ReactionRolesModule;
|
||||||
import creator.loki.feature.utility.UtilityModule;
|
import creator.loki.feature.utility.UtilityModule;
|
||||||
import creator.loki.feature.voice.VoiceModule;
|
import creator.loki.feature.voice.VoiceModule;
|
||||||
import creator.loki.feature.welcome.WelcomeModule;
|
import creator.loki.feature.welcome.WelcomeModule;
|
||||||
|
|
@ -47,6 +50,7 @@ public class BotModule extends AbstractModule {
|
||||||
bind(PrefixCommandRegistrar.class).in(Singleton.class);
|
bind(PrefixCommandRegistrar.class).in(Singleton.class);
|
||||||
|
|
||||||
install(new DatabaseModule());
|
install(new DatabaseModule());
|
||||||
|
install(new ConfigModule());
|
||||||
install(new VoiceModule());
|
install(new VoiceModule());
|
||||||
install(new MusicModule());
|
install(new MusicModule());
|
||||||
install(new LevelModule());
|
install(new LevelModule());
|
||||||
|
|
@ -62,7 +66,9 @@ public class BotModule extends AbstractModule {
|
||||||
install(new ImageModule());
|
install(new ImageModule());
|
||||||
install(new TikTokModule());
|
install(new TikTokModule());
|
||||||
install(new EntertainmentModule());
|
install(new EntertainmentModule());
|
||||||
|
install(new TicketModule());
|
||||||
install(new UtilityModule());
|
install(new UtilityModule());
|
||||||
|
install(new ReactionRolesModule());
|
||||||
install(new LifecycleModule());
|
install(new LifecycleModule());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
src/main/java/creator/loki/feature/config/ConfigModule.java
Normal file
11
src/main/java/creator/loki/feature/config/ConfigModule.java
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package creator.loki.feature.config;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
public class ConfigModule extends AbstractModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(SetupSlashHandler.class).in(Singleton.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
432
src/main/java/creator/loki/feature/config/SetupSlashHandler.java
Normal file
432
src/main/java/creator/loki/feature/config/SetupSlashHandler.java
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
package creator.loki.feature.config;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.feature.config.repository.ConfigRepository;
|
||||||
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.channel.ChannelType;
|
||||||
|
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||||
|
import net.dv8tion.jda.api.components.buttons.Button;
|
||||||
|
import net.dv8tion.jda.api.components.label.Label;
|
||||||
|
import net.dv8tion.jda.api.components.selections.EntitySelectMenu;
|
||||||
|
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||||
|
import net.dv8tion.jda.api.components.textinput.TextInput;
|
||||||
|
import net.dv8tion.jda.api.components.textinput.TextInputStyle;
|
||||||
|
import net.dv8tion.jda.api.events.guild.GuildReadyEvent;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.ModalInteractionEvent;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.component.EntitySelectInteractionEvent;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
import net.dv8tion.jda.api.interactions.InteractionHook;
|
||||||
|
import net.dv8tion.jda.api.interactions.commands.build.Commands;
|
||||||
|
import net.dv8tion.jda.api.modals.Modal;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class SetupSlashHandler extends ListenerAdapter {
|
||||||
|
private static final Color BLURPLE = new Color(88, 101, 242);
|
||||||
|
|
||||||
|
private final ConfigRepository configRepo;
|
||||||
|
private volatile boolean registered = false;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public SetupSlashHandler(ConfigRepository configRepo) {
|
||||||
|
this.configRepo = configRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGuildReady(@NotNull GuildReadyEvent event) {
|
||||||
|
if (registered) return;
|
||||||
|
registered = true;
|
||||||
|
event.getJDA().updateCommands()
|
||||||
|
.addCommands(Commands.slash("setup", "Панель управления сервером"))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) {
|
||||||
|
if (!event.getName().equals("setup")) return;
|
||||||
|
event.deferReply().queue();
|
||||||
|
showDashboard(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onButtonInteraction(@NotNull ButtonInteractionEvent event) {
|
||||||
|
String id = event.getComponentId();
|
||||||
|
if (!id.startsWith("setup_")) return;
|
||||||
|
event.deferEdit().queue();
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case "setup_voice" -> showVoicePanel(event.getGuild(), event.getHook());
|
||||||
|
case "setup_counter" -> showCounterPanel(event.getGuild(), event.getHook());
|
||||||
|
case "setup_logs" -> showLoggingPanel(event.getGuild(), event.getHook());
|
||||||
|
case "setup_levelroles" -> showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||||
|
case "setup_back" -> showDashboard(event.getGuild(), event.getHook());
|
||||||
|
case "setup_counter_format" -> openCounterFormatModal(event);
|
||||||
|
case "setup_levelrole_add" -> openLevelRoleAddModal(event);
|
||||||
|
case "setup_levelrole_remove" -> openLevelRoleRemoveSelect(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onEntitySelectInteraction(@NotNull EntitySelectInteractionEvent event) {
|
||||||
|
String id = event.getComponentId();
|
||||||
|
if (!id.startsWith("setup_")) return;
|
||||||
|
event.deferEdit().queue();
|
||||||
|
|
||||||
|
String value = event.getValues().get(0).getId();
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case "setup_voice_trigger" -> {
|
||||||
|
configRepo.set("voice.trigger_channel_id", value);
|
||||||
|
showVoicePanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
case "setup_voice_category" -> {
|
||||||
|
configRepo.set("voice.category_id", value);
|
||||||
|
showVoicePanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
case "setup_counter_channel" -> {
|
||||||
|
configRepo.set("counter.channel_id", value);
|
||||||
|
updateCounterChannelName(event.getGuild());
|
||||||
|
showCounterPanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
case "setup_logs_moderation" -> {
|
||||||
|
configRepo.set("moderation.log_channel_id", value);
|
||||||
|
showLoggingPanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
case "setup_logs_levelup" -> {
|
||||||
|
configRepo.set("levelup.notification_channel_id", value);
|
||||||
|
showLoggingPanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStringSelectInteraction(@NotNull StringSelectInteractionEvent event) {
|
||||||
|
String id = event.getComponentId();
|
||||||
|
if (!id.startsWith("setup_")) return;
|
||||||
|
event.deferEdit().queue();
|
||||||
|
|
||||||
|
if (id.equals("setup_levelrole_remove_select")) {
|
||||||
|
String selected = event.getValues().get(0);
|
||||||
|
configRepo.delete("level_role_" + selected);
|
||||||
|
showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onModalInteraction(@NotNull ModalInteractionEvent event) {
|
||||||
|
String id = event.getModalId();
|
||||||
|
if (!id.startsWith("setup_")) return;
|
||||||
|
event.deferEdit().queue();
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case "setup_counter_format_modal" -> {
|
||||||
|
String format = event.getValue("format").getAsString();
|
||||||
|
configRepo.set("counter.format", format);
|
||||||
|
updateCounterChannelName(event.getGuild());
|
||||||
|
showCounterPanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
case "setup_levelrole_add_modal" -> {
|
||||||
|
String levelStr = event.getValue("level").getAsString();
|
||||||
|
String role = event.getValue("role").getAsString();
|
||||||
|
configRepo.set("level_role_" + levelStr, role);
|
||||||
|
showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDashboard(Guild guild, InteractionHook hook) {
|
||||||
|
hook.editOriginalEmbeds(buildDashboardEmbed(guild).build())
|
||||||
|
.setComponents(buildDashboardButtons())
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showVoicePanel(Guild guild, InteractionHook hook) {
|
||||||
|
EmbedBuilder embed = new EmbedBuilder()
|
||||||
|
.setTitle("🎤 Настройка голосовых каналов")
|
||||||
|
.setColor(BLURPLE);
|
||||||
|
|
||||||
|
String triggerId = configRepo.get("voice.trigger_channel_id");
|
||||||
|
if (triggerId != null) {
|
||||||
|
embed.addField("Канал-триггер", "<#" + triggerId + ">", false);
|
||||||
|
} else {
|
||||||
|
embed.addField("Канал-триггер", "❌ Не настроено", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String categoryId = configRepo.get("voice.category_id");
|
||||||
|
if (categoryId != null) {
|
||||||
|
var cat = guild.getCategoryById(categoryId);
|
||||||
|
embed.addField("Категория", cat != null ? cat.getName() : "❌ Не найдена", false);
|
||||||
|
} else {
|
||||||
|
embed.addField("Категория", "❌ Не настроено", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
hook.editOriginalEmbeds(embed.build())
|
||||||
|
.setComponents(
|
||||||
|
ActionRow.of(EntitySelectMenu.create("setup_voice_trigger", EntitySelectMenu.SelectTarget.CHANNEL)
|
||||||
|
.setChannelTypes(ChannelType.VOICE)
|
||||||
|
.setPlaceholder("Выбери канал-триггер")
|
||||||
|
.build()),
|
||||||
|
ActionRow.of(EntitySelectMenu.create("setup_voice_category", EntitySelectMenu.SelectTarget.CHANNEL)
|
||||||
|
.setChannelTypes(ChannelType.CATEGORY)
|
||||||
|
.setPlaceholder("Выбери категорию")
|
||||||
|
.build()),
|
||||||
|
ActionRow.of(Button.secondary("setup_back", "🔙 Назад")))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showCounterPanel(Guild guild, InteractionHook hook) {
|
||||||
|
EmbedBuilder embed = new EmbedBuilder()
|
||||||
|
.setTitle("📊 Настройка счётчика участников")
|
||||||
|
.setColor(BLURPLE);
|
||||||
|
|
||||||
|
String channelId = configRepo.get("counter.channel_id");
|
||||||
|
if (channelId != null) {
|
||||||
|
embed.addField("Канал", "<#" + channelId + ">", false);
|
||||||
|
} else {
|
||||||
|
embed.addField("Канал", "❌ Не настроено", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String format = configRepo.get("counter.format");
|
||||||
|
embed.addField("Формат", format != null ? "`" + format + "`" : "❌ Не настроено", false);
|
||||||
|
|
||||||
|
hook.editOriginalEmbeds(embed.build())
|
||||||
|
.setComponents(
|
||||||
|
ActionRow.of(EntitySelectMenu.create("setup_counter_channel", EntitySelectMenu.SelectTarget.CHANNEL)
|
||||||
|
.setChannelTypes(ChannelType.VOICE)
|
||||||
|
.setPlaceholder("Выбери канал")
|
||||||
|
.build()),
|
||||||
|
ActionRow.of(
|
||||||
|
Button.secondary("setup_counter_format", "✏️ Формат"),
|
||||||
|
Button.secondary("setup_back", "🔙 Назад")))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLoggingPanel(Guild guild, InteractionHook hook) {
|
||||||
|
EmbedBuilder embed = new EmbedBuilder()
|
||||||
|
.setTitle("📝 Настройка логирования")
|
||||||
|
.setColor(BLURPLE);
|
||||||
|
|
||||||
|
String modId = configRepo.get("moderation.log_channel_id");
|
||||||
|
embed.addField("Логи модерации", modId != null ? "<#" + modId + ">" : "❌ Не настроено", false);
|
||||||
|
|
||||||
|
String lvlId = configRepo.get("levelup.notification_channel_id");
|
||||||
|
embed.addField("Уведомления level-up", lvlId != null ? "<#" + lvlId + ">" : "❌ Не настроено", false);
|
||||||
|
|
||||||
|
hook.editOriginalEmbeds(embed.build())
|
||||||
|
.setComponents(
|
||||||
|
ActionRow.of(EntitySelectMenu.create("setup_logs_moderation", EntitySelectMenu.SelectTarget.CHANNEL)
|
||||||
|
.setChannelTypes(ChannelType.TEXT)
|
||||||
|
.setPlaceholder("Канал логов модерации")
|
||||||
|
.build()),
|
||||||
|
ActionRow.of(EntitySelectMenu.create("setup_logs_levelup", EntitySelectMenu.SelectTarget.CHANNEL)
|
||||||
|
.setChannelTypes(ChannelType.TEXT)
|
||||||
|
.setPlaceholder("Канал level-up уведомлений")
|
||||||
|
.build()),
|
||||||
|
ActionRow.of(Button.secondary("setup_back", "🔙 Назад")))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showLevelRolesPanel(Guild guild, InteractionHook hook) {
|
||||||
|
Map<String, String> allConfig = configRepo.getAll();
|
||||||
|
var levelRoles = allConfig.entrySet().stream()
|
||||||
|
.filter(e -> e.getKey().startsWith("level_role_"))
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
e -> Integer.parseInt(e.getKey().substring("level_role_".length())),
|
||||||
|
Map.Entry::getValue));
|
||||||
|
|
||||||
|
EmbedBuilder embed = new EmbedBuilder()
|
||||||
|
.setTitle("🏆 Роли за уровни")
|
||||||
|
.setColor(BLURPLE);
|
||||||
|
|
||||||
|
if (levelRoles.isEmpty()) {
|
||||||
|
embed.setDescription("❌ Роли не настроены");
|
||||||
|
} else {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
levelRoles.entrySet().stream()
|
||||||
|
.sorted(Map.Entry.comparingByKey())
|
||||||
|
.forEach(e -> sb.append("**Уровень ").append(e.getKey()).append(":** `").append(e.getValue()).append("`\n"));
|
||||||
|
embed.setDescription(sb.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttons = new java.util.ArrayList<Button>();
|
||||||
|
buttons.add(Button.success("setup_levelrole_add", "➕ Добавить"));
|
||||||
|
if (!levelRoles.isEmpty()) {
|
||||||
|
buttons.add(Button.danger("setup_levelrole_remove", "➖ Удалить"));
|
||||||
|
}
|
||||||
|
buttons.add(Button.secondary("setup_back", "🔙 Назад"));
|
||||||
|
|
||||||
|
hook.editOriginalEmbeds(embed.build())
|
||||||
|
.setComponents(ActionRow.of(buttons))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openCounterFormatModal(ButtonInteractionEvent event) {
|
||||||
|
String currentFormat = configRepo.get("counter.format");
|
||||||
|
if (currentFormat == null) {
|
||||||
|
currentFormat = "【👥】Участники: {count}";
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput formatInput = TextInput.create("format", TextInputStyle.SHORT)
|
||||||
|
.setValue(currentFormat)
|
||||||
|
.setPlaceholder("【👥】Участники: {count}")
|
||||||
|
.setRequired(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Modal modal = Modal.create("setup_counter_format_modal", "✏️ Формат счётчика")
|
||||||
|
.addComponents(Label.of("Формат строки", formatInput))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
event.replyModal(modal).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openLevelRoleAddModal(ButtonInteractionEvent event) {
|
||||||
|
TextInput levelInput = TextInput.create("level", TextInputStyle.SHORT)
|
||||||
|
.setPlaceholder("10")
|
||||||
|
.setRequired(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
TextInput roleInput = TextInput.create("role", TextInputStyle.SHORT)
|
||||||
|
.setPlaceholder("Название роли")
|
||||||
|
.setRequired(true)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
Modal modal = Modal.create("setup_levelrole_add_modal", "➕ Добавить роль за уровень")
|
||||||
|
.addComponents(Label.of("Уровень", levelInput), Label.of("Название роли", roleInput))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
event.replyModal(modal).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openLevelRoleRemoveSelect(ButtonInteractionEvent event) {
|
||||||
|
Map<String, String> allConfig = configRepo.getAll();
|
||||||
|
var options = allConfig.entrySet().stream()
|
||||||
|
.filter(e -> e.getKey().startsWith("level_role_"))
|
||||||
|
.map(e -> {
|
||||||
|
String level = e.getKey().substring("level_role_".length());
|
||||||
|
return "Уровень " + level + " — " + e.getValue();
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (options.isEmpty()) {
|
||||||
|
showLevelRolesPanel(event.getGuild(), event.getHook());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringSelectMenu.Builder menuBuilder = StringSelectMenu.create("setup_levelrole_remove_select")
|
||||||
|
.setPlaceholder("Выбери роль для удаления");
|
||||||
|
|
||||||
|
var levelKeys = allConfig.keySet().stream()
|
||||||
|
.filter(k -> k.startsWith("level_role_"))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
for (String key : levelKeys) {
|
||||||
|
String level = key.substring("level_role_".length());
|
||||||
|
String roleName = allConfig.get(key);
|
||||||
|
String label = "Уровень " + level + " — " + roleName;
|
||||||
|
menuBuilder.addOption(label, level);
|
||||||
|
}
|
||||||
|
|
||||||
|
event.getHook().editOriginalComponents(
|
||||||
|
ActionRow.of(menuBuilder.build()),
|
||||||
|
ActionRow.of(Button.secondary("setup_back", "🔙 Назад"))).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private EmbedBuilder buildDashboardEmbed(Guild guild) {
|
||||||
|
EmbedBuilder embed = new EmbedBuilder()
|
||||||
|
.setTitle("🎛 Панель управления сервером")
|
||||||
|
.setColor(BLURPLE)
|
||||||
|
.setFooter("Выбери категорию для настройки");
|
||||||
|
|
||||||
|
String triggerId = configRepo.get("voice.trigger_channel_id");
|
||||||
|
String categoryId = configRepo.get("voice.category_id");
|
||||||
|
if (triggerId != null || categoryId != null) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (triggerId != null) {
|
||||||
|
sb.append("Триггер: <#").append(triggerId).append(">");
|
||||||
|
} else {
|
||||||
|
sb.append("Триггер: ❌ Не настроено");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
if (categoryId != null) {
|
||||||
|
var cat = guild.getCategoryById(categoryId);
|
||||||
|
sb.append("Категория: ").append(cat != null ? cat.getName() : "❌ Не найдена");
|
||||||
|
} else {
|
||||||
|
sb.append("Категория: ❌ Не настроено");
|
||||||
|
}
|
||||||
|
embed.addField("🎤 Голосовые каналы", sb.toString(), false);
|
||||||
|
} else {
|
||||||
|
embed.addField("🎤 Голосовые каналы", "❌ Не настроено", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String counterChannelId = configRepo.get("counter.channel_id");
|
||||||
|
String counterFormat = configRepo.get("counter.format");
|
||||||
|
if (counterChannelId != null) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("Канал: <#").append(counterChannelId).append(">");
|
||||||
|
if (counterFormat != null) {
|
||||||
|
sb.append("\nФормат: `").append(counterFormat).append("`");
|
||||||
|
}
|
||||||
|
embed.addField("📊 Счётчик", sb.toString(), false);
|
||||||
|
} else {
|
||||||
|
embed.addField("📊 Счётчик", "❌ Не настроено", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String modLogId = configRepo.get("moderation.log_channel_id");
|
||||||
|
String levelupLogId = configRepo.get("levelup.notification_channel_id");
|
||||||
|
if (modLogId != null || levelupLogId != null) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
if (modLogId != null) {
|
||||||
|
sb.append("Модерация: <#").append(modLogId).append(">");
|
||||||
|
} else {
|
||||||
|
sb.append("Модерация: ❌ Не настроено");
|
||||||
|
}
|
||||||
|
sb.append("\n");
|
||||||
|
if (levelupLogId != null) {
|
||||||
|
sb.append("Level-up: <#").append(levelupLogId).append(">");
|
||||||
|
} else {
|
||||||
|
sb.append("Level-up: ❌ Не настроено");
|
||||||
|
}
|
||||||
|
embed.addField("📝 Логи", sb.toString(), false);
|
||||||
|
} else {
|
||||||
|
embed.addField("📝 Логи", "❌ Не настроено", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> allConfig = configRepo.getAll();
|
||||||
|
long levelRoleCount = allConfig.keySet().stream().filter(k -> k.startsWith("level_role_")).count();
|
||||||
|
embed.addField("🏆 Роли за уровни", "Настроено ролей: **" + levelRoleCount + "**", false);
|
||||||
|
|
||||||
|
return embed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ActionRow[] buildDashboardButtons() {
|
||||||
|
return new ActionRow[] {
|
||||||
|
ActionRow.of(
|
||||||
|
Button.secondary("setup_voice", "🎤 Голосовые"),
|
||||||
|
Button.secondary("setup_counter", "📊 Счётчик"),
|
||||||
|
Button.secondary("setup_logs", "📝 Логи"),
|
||||||
|
Button.secondary("setup_levelroles", "🏆 Роли"))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCounterChannelName(Guild guild) {
|
||||||
|
if (guild == null) return;
|
||||||
|
String channelId = configRepo.get("counter.channel_id");
|
||||||
|
String format = configRepo.get("counter.format");
|
||||||
|
if (channelId != null && format != null) {
|
||||||
|
var channel = guild.getVoiceChannelById(channelId);
|
||||||
|
if (channel != null) {
|
||||||
|
int count = guild.getMemberCount();
|
||||||
|
channel.getManager().setName(format.replace("{count}", String.valueOf(count))).queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,24 +8,21 @@ import creator.loki.feature.music.commands.PlayerCommands;
|
||||||
import creator.loki.feature.music.commands.QueueCommands;
|
import creator.loki.feature.music.commands.QueueCommands;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
import creator.loki.feature.music.play.MusicSearchSessionManager;
|
|
||||||
import creator.loki.feature.music.play.PlayCommand;
|
import creator.loki.feature.music.play.PlayCommand;
|
||||||
|
|
||||||
public class MusicCommandRegistrar implements CommandRegistrar {
|
public class MusicCommandRegistrar implements CommandRegistrar {
|
||||||
private final PlayerManager playerManager;
|
private final PlayerManager playerManager;
|
||||||
private final SearchManager searchManager;
|
private final SearchManager searchManager;
|
||||||
private final MusicSearchSessionManager sessionManager;
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager, MusicSearchSessionManager sessionManager) {
|
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager) {
|
||||||
this.playerManager = playerManager;
|
this.playerManager = playerManager;
|
||||||
this.searchManager = searchManager;
|
this.searchManager = searchManager;
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void register(CommandRegistry registry) {
|
public void register(CommandRegistry registry) {
|
||||||
registry.register(new PlayCommand(playerManager, searchManager, sessionManager));
|
registry.register(new PlayCommand(playerManager, searchManager));
|
||||||
registry.register(PlayerCommands.skip(playerManager));
|
registry.register(PlayerCommands.skip(playerManager));
|
||||||
registry.register(PlayerCommands.stop(playerManager));
|
registry.register(PlayerCommands.stop(playerManager));
|
||||||
registry.register(PlayerCommands.leave(playerManager));
|
registry.register(PlayerCommands.leave(playerManager));
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,12 @@ import com.google.inject.multibindings.Multibinder;
|
||||||
import creator.loki.core.registry.CommandRegistrar;
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.play.MusicSearchSessionManager;
|
|
||||||
|
|
||||||
public class MusicModule extends AbstractModule {
|
public class MusicModule extends AbstractModule {
|
||||||
@Override
|
@Override
|
||||||
protected void configure() {
|
protected void configure() {
|
||||||
bind(LavalinkManager.class).in(Singleton.class);
|
bind(LavalinkManager.class).in(Singleton.class);
|
||||||
bind(PlayerManager.class).in(Singleton.class);
|
bind(PlayerManager.class).in(Singleton.class);
|
||||||
bind(MusicSearchSessionManager.class).in(Singleton.class);
|
|
||||||
|
|
||||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
.addBinding()
|
.addBinding()
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ public class MusicBotControlHandler {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
||||||
if (link != null && link.getCachedPlayer() != null)
|
if (link != null)
|
||||||
link.createOrUpdatePlayer()
|
link.createOrUpdatePlayer()
|
||||||
.setVolume(vol)
|
.setVolume(vol)
|
||||||
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()));
|
.subscribe(null, err -> log.warn("Lavalink: {}", err.getMessage()));
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ public class MusicBotPlayHandler {
|
||||||
public void handlePlay(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
public void handlePlay(MessageChannel ch, Guild guild, Member member, List<String> args) {
|
||||||
if (reqVoice(ch, member) == null) return;
|
if (reqVoice(ch, member) == null) return;
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
send(ch, err("Укажи URL или запрос: `q!play <название>`"));
|
send(ch, err("Укажи URL или запрос: `m!play <название>`"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
connect(guild, member, lavalinkManager);
|
connect(guild, member, lavalinkManager);
|
||||||
|
|
|
||||||
|
|
@ -1,242 +0,0 @@
|
||||||
package creator.loki.feature.music.listener;
|
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.google.inject.Inject;
|
|
||||||
import com.google.inject.Singleton;
|
|
||||||
import creator.loki.embed.EmbedTemplates;
|
|
||||||
import creator.loki.feature.music.core.LavalinkManager;
|
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
|
||||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
|
||||||
import creator.loki.feature.music.play.MusicSearchSession;
|
|
||||||
import creator.loki.feature.music.play.MusicSearchSessionManager;
|
|
||||||
import creator.loki.feature.music.play.SearchHandler;
|
|
||||||
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.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
|
||||||
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
|
||||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
|
||||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
|
||||||
import net.dv8tion.jda.api.components.buttons.Button;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
public class MusicSearchButtonHandler extends ListenerAdapter {
|
|
||||||
private static final Color BRAND = new Color(88, 101, 242);
|
|
||||||
private static final int MAX_RESULTS = 10;
|
|
||||||
|
|
||||||
private final MusicSearchSessionManager sessionManager;
|
|
||||||
private final LavalinkManager lavalinkManager;
|
|
||||||
private final SearchHandler searchHandler;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public MusicSearchButtonHandler(
|
|
||||||
MusicSearchSessionManager sessionManager,
|
|
||||||
LavalinkManager lavalinkManager,
|
|
||||||
SearchManager searchManager) {
|
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
this.lavalinkManager = lavalinkManager;
|
|
||||||
this.searchHandler = new SearchHandler(searchManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onButtonInteraction(@NotNull ButtonInteractionEvent event) {
|
|
||||||
String id = event.getComponentId();
|
|
||||||
if (id.startsWith("ms_platform_")) {
|
|
||||||
handlePlatform(event, id.replace("ms_platform_", ""));
|
|
||||||
} else if (id.startsWith("ms_pick_")) {
|
|
||||||
handlePick(event, id.replace("ms_pick_", ""));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePlatform(ButtonInteractionEvent event, String source) {
|
|
||||||
String messageId = event.getMessageId();
|
|
||||||
MusicSearchSession session = sessionManager.get(messageId);
|
|
||||||
if (session == null) {
|
|
||||||
event
|
|
||||||
.replyEmbeds(
|
|
||||||
EmbedTemplates.error(
|
|
||||||
"Поиск устарел", "Сделай новый поиск: `" + getPrefix() + "play <запрос>`"))
|
|
||||||
.setEphemeral(true)
|
|
||||||
.queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.deferEdit().queue();
|
|
||||||
|
|
||||||
String query = session.getQuery();
|
|
||||||
String searchId = switch (source) {
|
|
||||||
case "sc" -> "scsearch:" + query;
|
|
||||||
case "ytm" -> "ytmsearch:" + query;
|
|
||||||
default -> "ytsearch:" + query;
|
|
||||||
};
|
|
||||||
|
|
||||||
event.getHook()
|
|
||||||
.editOriginalEmbeds(
|
|
||||||
new EmbedBuilder()
|
|
||||||
.setColor(BRAND)
|
|
||||||
.setTitle("🔍 Ищу: " + query)
|
|
||||||
.setDescription("Поиск на " + sourceLabel(source) + "...")
|
|
||||||
.build())
|
|
||||||
.setComponents()
|
|
||||||
.queue();
|
|
||||||
|
|
||||||
lavalinkManager
|
|
||||||
.getOrCreateLink(session.getGuildId())
|
|
||||||
.loadItem(searchId)
|
|
||||||
.subscribe(
|
|
||||||
new AbstractAudioLoadResultHandler() {
|
|
||||||
@Override
|
|
||||||
public void onSearchResultLoaded(SearchResult result) {
|
|
||||||
if (result.getTracks().isEmpty()) {
|
|
||||||
noMatch();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showResults(event, result.getTracks(), query, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void ontrackLoaded(TrackLoaded result) {
|
|
||||||
showResults(event, List.of(result.getTrack()), query, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlaylistLoaded(PlaylistLoaded result) {
|
|
||||||
showResults(event, result.getTracks(), query, source);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void noMatches() {
|
|
||||||
noMatch();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadFailed(LoadFailed e) {
|
|
||||||
String msg =
|
|
||||||
e.getException().getMessage() != null
|
|
||||||
? e.getException().getMessage()
|
|
||||||
: "Неизвестная ошибка";
|
|
||||||
event
|
|
||||||
.getHook()
|
|
||||||
.editOriginalEmbeds(EmbedTemplates.error("Ошибка загрузки", msg))
|
|
||||||
.queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void noMatch() {
|
|
||||||
event
|
|
||||||
.getHook()
|
|
||||||
.editOriginalEmbeds(
|
|
||||||
EmbedTemplates.error("Не найдено", "Ничего не найдено: " + query))
|
|
||||||
.queue();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showResults(
|
|
||||||
ButtonInteractionEvent event, List<Track> tracks, String query, String source) {
|
|
||||||
String messageId = event.getMessageId();
|
|
||||||
MusicSearchSession session = sessionManager.get(messageId);
|
|
||||||
if (session == null) return;
|
|
||||||
|
|
||||||
int show = Math.min(MAX_RESULTS, tracks.size());
|
|
||||||
List<Track> display = tracks.subList(0, show);
|
|
||||||
session.setResults(tracks);
|
|
||||||
|
|
||||||
EmbedBuilder embed = searchHandler.buildResultsEmbed(
|
|
||||||
query, display, source, getPrefix(), "play");
|
|
||||||
|
|
||||||
var rows = buildNumberButtons(show);
|
|
||||||
event.getHook().editOriginalEmbeds(embed.build()).setComponents(rows).queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handlePick(ButtonInteractionEvent event, String indexStr) {
|
|
||||||
int index = Integer.parseInt(indexStr);
|
|
||||||
String messageId = event.getMessageId();
|
|
||||||
MusicSearchSession session = sessionManager.get(messageId);
|
|
||||||
if (session == null) {
|
|
||||||
event
|
|
||||||
.replyEmbeds(
|
|
||||||
EmbedTemplates.error(
|
|
||||||
"Поиск устарел", "Сделай новый поиск: `" + getPrefix() + "play <запрос>`"))
|
|
||||||
.setEphemeral(true)
|
|
||||||
.queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Track> tracks = session.getResults();
|
|
||||||
if (tracks == null || index < 0 || index >= tracks.size()) {
|
|
||||||
event
|
|
||||||
.replyEmbeds(EmbedTemplates.error("Ошибка", "Результаты поиска устарели."))
|
|
||||||
.setEphemeral(true)
|
|
||||||
.queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Track track = tracks.get(index);
|
|
||||||
|
|
||||||
Member member = event.getMember();
|
|
||||||
Guild guild = event.getGuild();
|
|
||||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
|
||||||
event
|
|
||||||
.replyEmbeds(
|
|
||||||
EmbedTemplates.error("Голосовой канал", "Зайди в голосовой канал сначала."))
|
|
||||||
.setEphemeral(true)
|
|
||||||
.queue();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.deferEdit().queue();
|
|
||||||
|
|
||||||
VoiceHelper.connectForPlayback(guild, member, lavalinkManager);
|
|
||||||
GuildMusicManager mm = lavalinkManager.getMusicManager(session.getGuildId());
|
|
||||||
mm.getScheduler().enqueue(track);
|
|
||||||
|
|
||||||
String duration = SearchHandler.formatDuration(track.getInfo().getLength());
|
|
||||||
event
|
|
||||||
.getHook()
|
|
||||||
.editOriginalEmbeds(
|
|
||||||
EmbedTemplates.info(
|
|
||||||
"Добавлено в очередь",
|
|
||||||
"**" + track.getInfo().getTitle() + "**\n" + duration))
|
|
||||||
.setComponents()
|
|
||||||
.queue();
|
|
||||||
|
|
||||||
sessionManager.remove(messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static List<ActionRow> buildNumberButtons(int count) {
|
|
||||||
int rows = (count + 4) / 5;
|
|
||||||
var list = new java.util.ArrayList<ActionRow>();
|
|
||||||
for (int r = 0; r < rows; r++) {
|
|
||||||
var builder = new java.util.ArrayList<Button>();
|
|
||||||
for (int c = 0; c < 5; c++) {
|
|
||||||
int idx = r * 5 + c;
|
|
||||||
if (idx >= count) break;
|
|
||||||
builder.add(Button.primary("ms_pick_" + idx, String.valueOf(idx + 1)));
|
|
||||||
}
|
|
||||||
list.add(ActionRow.of(builder));
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static String sourceLabel(String source) {
|
|
||||||
return switch (source) {
|
|
||||||
case "sc" -> "SoundCloud";
|
|
||||||
case "ytm" -> "YouTube Music";
|
|
||||||
default -> "YouTube";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getPrefix() {
|
|
||||||
return lavalinkManager.getConfig().getPrefix();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package creator.loki.feature.music.listener;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
|
import creator.loki.feature.music.play.SearchHandler;
|
||||||
|
import creator.loki.util.VoiceHelper;
|
||||||
|
import dev.arbjerg.lavalink.client.player.Track;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.component.StringSelectInteractionEvent;
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class PlaySelectHandler extends ListenerAdapter {
|
||||||
|
private final PlayerManager playerManager;
|
||||||
|
private final SearchManager searchManager;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PlaySelectHandler(PlayerManager playerManager, SearchManager searchManager) {
|
||||||
|
this.playerManager = playerManager;
|
||||||
|
this.searchManager = searchManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStringSelectInteraction(@NotNull StringSelectInteractionEvent event) {
|
||||||
|
if (!event.getComponentId().equals("play_select")) return;
|
||||||
|
|
||||||
|
var guild = event.getGuild();
|
||||||
|
var member = event.getMember();
|
||||||
|
if (guild == null || member == null) return;
|
||||||
|
|
||||||
|
if (!member.getVoiceState().inAudioChannel()) {
|
||||||
|
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Зайди в голосовой канал сначала."))
|
||||||
|
.setEphemeral(true)
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = Integer.parseInt(event.getValues().get(0));
|
||||||
|
Track track = searchManager.pick(guild.getIdLong(), index);
|
||||||
|
if (track == null) {
|
||||||
|
event.replyEmbeds(EmbedTemplates.error("Ошибка", "Поиск устарел"))
|
||||||
|
.setEphemeral(true)
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.deferEdit().queue();
|
||||||
|
|
||||||
|
VoiceHelper.connectForPlayback(guild, member, playerManager.getLavalinkManager());
|
||||||
|
playerManager.getMusicManager(guild.getIdLong()).getScheduler().enqueue(track);
|
||||||
|
|
||||||
|
event.getHook()
|
||||||
|
.editOriginalEmbeds(
|
||||||
|
EmbedTemplates.info(
|
||||||
|
"Добавлено в очередь",
|
||||||
|
"**" + track.getInfo().getTitle() + "**\n" + SearchHandler.formatDuration(track.getInfo().getLength())))
|
||||||
|
.setComponents()
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,13 +32,6 @@ class TrackStarter {
|
||||||
|
|
||||||
private void startWithRetry(Link link, Track track, int attempt) {
|
private void startWithRetry(Link link, Track track, int attempt) {
|
||||||
if (attempt > MAX_ATTEMPTS) return;
|
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()
|
link.createOrUpdatePlayer()
|
||||||
.setTrack(track)
|
.setTrack(track)
|
||||||
.setVolume(100)
|
.setVolume(100)
|
||||||
|
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
package creator.loki.feature.music.play;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import dev.arbjerg.lavalink.client.player.Track;
|
|
||||||
|
|
||||||
public class MusicSearchSession {
|
|
||||||
private final long guildId;
|
|
||||||
private final long userId;
|
|
||||||
private final String query;
|
|
||||||
private List<Track> results;
|
|
||||||
|
|
||||||
public MusicSearchSession(long guildId, long userId, String query) {
|
|
||||||
this.guildId = guildId;
|
|
||||||
this.userId = userId;
|
|
||||||
this.query = query;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getGuildId() {
|
|
||||||
return guildId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getUserId() {
|
|
||||||
return userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getQuery() {
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Track> getResults() {
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setResults(List<Track> results) {
|
|
||||||
this.results = results;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
package creator.loki.feature.music.play;
|
|
||||||
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import com.github.benmanes.caffeine.cache.Cache;
|
|
||||||
import com.github.benmanes.caffeine.cache.Caffeine;
|
|
||||||
|
|
||||||
public class MusicSearchSessionManager {
|
|
||||||
private final Cache<String, MusicSearchSession> sessions;
|
|
||||||
|
|
||||||
public MusicSearchSessionManager() {
|
|
||||||
this.sessions =
|
|
||||||
Caffeine.newBuilder().maximumSize(100).expireAfterWrite(3, TimeUnit.MINUTES).build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void put(String messageId, MusicSearchSession session) {
|
|
||||||
sessions.put(messageId, session);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MusicSearchSession get(String messageId) {
|
|
||||||
return sessions.getIfPresent(messageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void remove(String messageId) {
|
|
||||||
sessions.invalidate(messageId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +1,28 @@
|
||||||
package creator.loki.feature.music.play;
|
package creator.loki.feature.music.play;
|
||||||
|
|
||||||
import java.awt.Color;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import creator.loki.command.Command;
|
import creator.loki.command.Command;
|
||||||
import creator.loki.command.CommandContext;
|
import creator.loki.command.CommandContext;
|
||||||
import creator.loki.embed.EmbedTemplates;
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
import creator.loki.feature.music.core.LavalinkManager;
|
||||||
import creator.loki.feature.music.core.PlayerManager;
|
import creator.loki.feature.music.core.PlayerManager;
|
||||||
import creator.loki.feature.music.core.SearchManager;
|
import creator.loki.feature.music.core.SearchManager;
|
||||||
|
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||||
import creator.loki.util.VoiceHelper;
|
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.Track;
|
||||||
import dev.arbjerg.lavalink.client.player.TrackLoaded;
|
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
|
||||||
import net.dv8tion.jda.api.entities.Guild;
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
import net.dv8tion.jda.api.entities.Member;
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
|
||||||
import net.dv8tion.jda.api.components.buttons.Button;
|
|
||||||
|
|
||||||
public class PlayCommand implements Command {
|
public class PlayCommand implements Command {
|
||||||
private static final Color BRAND = new Color(88, 101, 242);
|
private static final Set<String> SOURCES = Set.of("yt", "sc", "list");
|
||||||
|
|
||||||
private final PlayerManager playerManager;
|
private final PlayerManager playerManager;
|
||||||
private final SearchHandler searchHandler;
|
private final SearchHandler searchHandler;
|
||||||
private final MusicSearchSessionManager sessionManager;
|
|
||||||
|
|
||||||
public PlayCommand(PlayerManager playerManager, SearchManager searchManager, MusicSearchSessionManager sessionManager) {
|
public PlayCommand(PlayerManager playerManager, SearchManager searchManager) {
|
||||||
this.playerManager = playerManager;
|
this.playerManager = playerManager;
|
||||||
this.searchHandler = new SearchHandler(searchManager);
|
this.searchHandler = new SearchHandler(searchManager);
|
||||||
this.sessionManager = sessionManager;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -46,7 +37,7 @@ public class PlayCommand implements Command {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getUsage() {
|
public String getUsage() {
|
||||||
return "play <URL> | play <номер> | play <запрос>";
|
return "play [yt|sc|list] <запрос> | play <номер>";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -59,7 +50,10 @@ public class PlayCommand implements Command {
|
||||||
Guild guild = ctx.getGuild();
|
Guild guild = ctx.getGuild();
|
||||||
if (guild == null) return;
|
if (guild == null) return;
|
||||||
Member member = ctx.getAuthor();
|
Member member = ctx.getAuthor();
|
||||||
if (member == null) return;
|
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||||
|
sendErr(ctx, "Зайди в голосовой канал сначала.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
var args = ctx.getArgs();
|
var args = ctx.getArgs();
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
|
sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
|
||||||
|
|
@ -70,86 +64,10 @@ public class PlayCommand implements Command {
|
||||||
handlePickResult(ctx, first);
|
handlePickResult(ctx, first);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (first.startsWith("http://") || first.startsWith("https://")) {
|
|
||||||
handleUrl(ctx, first);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
showPlatformSelection(ctx, first);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showPlatformSelection(CommandContext ctx, String query) {
|
|
||||||
Guild guild = ctx.getGuild();
|
|
||||||
Member member = ctx.getAuthor();
|
|
||||||
var lm = playerManager.getLavalinkManager();
|
|
||||||
|
|
||||||
var embed = new EmbedBuilder()
|
|
||||||
.setColor(BRAND)
|
|
||||||
.setTitle("Выбери платформу")
|
|
||||||
.setDescription("Результаты поиска для: **" + query + "**\nВыбери платформу для поиска:")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
var row = ActionRow.of(
|
|
||||||
Button.primary("ms_platform_yt", "🇾 YouTube"),
|
|
||||||
Button.primary("ms_platform_sc", "🎵 SoundCloud"),
|
|
||||||
Button.primary("ms_platform_ytm", "▶️ YouTube Music"));
|
|
||||||
|
|
||||||
var message = ctx.getChannel()
|
|
||||||
.sendMessageEmbeds(embed)
|
|
||||||
.addComponents(row)
|
|
||||||
.complete();
|
|
||||||
|
|
||||||
var session = new MusicSearchSession(guild.getIdLong(), member.getIdLong(), query);
|
|
||||||
sessionManager.put(message.getId(), session);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleUrl(CommandContext ctx, String url) {
|
|
||||||
if (!resolveVoiceConnection(ctx)) return;
|
if (!resolveVoiceConnection(ctx)) return;
|
||||||
var lm = playerManager.getLavalinkManager();
|
var lm = playerManager.getLavalinkManager();
|
||||||
long guildId = ctx.getGuild().getIdLong();
|
var mm = lm.getMusicManager(guild.getIdLong());
|
||||||
lm.getOrCreateLink(guildId)
|
handlePlayQuery(ctx, args, first, lm, mm);
|
||||||
.loadItem(url)
|
|
||||||
.subscribe(new AbstractAudioLoadResultHandler() {
|
|
||||||
@Override
|
|
||||||
public void ontrackLoaded(TrackLoaded result) {
|
|
||||||
enqueueAndNotify(ctx, result.getTrack());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSearchResultLoaded(SearchResult result) {
|
|
||||||
if (result.getTracks().isEmpty()) {
|
|
||||||
sendErr(ctx, "Ничего не найдено.");
|
|
||||||
} else {
|
|
||||||
enqueueAndNotify(ctx, result.getTracks().get(0));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPlaylistLoaded(PlaylistLoaded result) {
|
|
||||||
var mm = lm.getMusicManager(guildId);
|
|
||||||
for (Track t : result.getTracks()) {
|
|
||||||
mm.getScheduler().enqueue(t);
|
|
||||||
}
|
|
||||||
ctx.getChannel()
|
|
||||||
.sendMessageEmbeds(
|
|
||||||
EmbedTemplates.info(
|
|
||||||
"Плейлист добавлен",
|
|
||||||
"**" + result.getInfo().getName() + "** — " + result.getTracks().size() + " треков"))
|
|
||||||
.queue();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void noMatches() {
|
|
||||||
sendErr(ctx, "Ничего не найдено.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadFailed(LoadFailed e) {
|
|
||||||
String msg = e.getException().getMessage() != null
|
|
||||||
? e.getException().getMessage()
|
|
||||||
: "Ошибка загрузки";
|
|
||||||
sendErr(ctx, msg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean resolveVoiceConnection(CommandContext ctx) {
|
private boolean resolveVoiceConnection(CommandContext ctx) {
|
||||||
|
|
@ -174,6 +92,47 @@ public class PlayCommand implements Command {
|
||||||
enqueueAndNotify(ctx, t);
|
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()) {
|
||||||
|
sendErr(ctx, "Укажи запрос после источника.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
boolean isUrl = raw.startsWith("http://") || raw.startsWith("https://");
|
||||||
|
String query = isUrl ? null : raw;
|
||||||
|
String id = buildQuery(first, raw);
|
||||||
|
loadAndHandle(ctx, lm, ctx.getGuild().getIdLong(), mm, id, isUrl, query, raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAndHandle(
|
||||||
|
CommandContext ctx,
|
||||||
|
LavalinkManager lm,
|
||||||
|
long guildId,
|
||||||
|
GuildMusicManager mm,
|
||||||
|
String id,
|
||||||
|
boolean isUrl,
|
||||||
|
String query,
|
||||||
|
String raw) {
|
||||||
|
lm.getOrCreateLink(guildId)
|
||||||
|
.loadItem(id)
|
||||||
|
.subscribe(new PlayLoadHandler(ctx, searchHandler, mm, isUrl, query, raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String buildQuery(String first, String raw) {
|
||||||
|
if (raw.startsWith("http://") || raw.startsWith("https://")) return raw;
|
||||||
|
if (raw.startsWith("ytsearch:")
|
||||||
|
|| raw.startsWith("ytmsearch:")
|
||||||
|
|| raw.startsWith("scsearch:")) return raw;
|
||||||
|
if ("sc".equals(first)) return "scsearch:" + raw;
|
||||||
|
return "ytsearch:" + raw;
|
||||||
|
}
|
||||||
|
|
||||||
private void enqueueAndNotify(CommandContext ctx, Track track) {
|
private void enqueueAndNotify(CommandContext ctx, Track track) {
|
||||||
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
||||||
m.getScheduler().enqueue(track);
|
m.getScheduler().enqueue(track);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,135 @@
|
||||||
|
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;
|
||||||
|
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||||
|
import net.dv8tion.jda.api.components.selections.StringSelectMenu;
|
||||||
|
|
||||||
|
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);
|
||||||
|
int show = Math.min(25, list.size());
|
||||||
|
var builder = StringSelectMenu.create("play_select")
|
||||||
|
.setPlaceholder("Выбери трек...");
|
||||||
|
for (int i = 0; i < show; i++) {
|
||||||
|
Track t = list.get(i);
|
||||||
|
builder.addOption(
|
||||||
|
truncate(t.getInfo().getTitle(), 100),
|
||||||
|
String.valueOf(i),
|
||||||
|
truncate(t.getInfo().getAuthor(), 100));
|
||||||
|
}
|
||||||
|
ctx.getChannel()
|
||||||
|
.sendMessageEmbeds(searchHandler.buildResultsEmbed(query, list).build())
|
||||||
|
.addComponents(ActionRow.of(builder.build()))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendErr(String msg) {
|
||||||
|
ctx.getChannel().sendMessageEmbeds(EmbedTemplates.error("Ошибка", msg)).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String truncate(String s, int max) {
|
||||||
|
if (s == null) return "";
|
||||||
|
if (s.length() <= max) return s;
|
||||||
|
return s.substring(0, max - 1) + "…";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ import dev.arbjerg.lavalink.client.player.Track;
|
||||||
import net.dv8tion.jda.api.EmbedBuilder;
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
|
||||||
public class SearchHandler {
|
public class SearchHandler {
|
||||||
private static final int SHOW_RESULTS = 10;
|
private static final int SHOW_RESULTS = 5;
|
||||||
|
|
||||||
private final SearchManager searchManager;
|
private final SearchManager searchManager;
|
||||||
|
|
||||||
|
|
@ -31,8 +31,7 @@ public class SearchHandler {
|
||||||
searchManager.clear(guildId);
|
searchManager.clear(guildId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedBuilder buildResultsEmbed(
|
public EmbedBuilder buildResultsEmbed(String query, List<Track> tracks) {
|
||||||
String query, List<Track> tracks, String source, String prefix, String commandName) {
|
|
||||||
int show = Math.min(SHOW_RESULTS, tracks.size());
|
int show = Math.min(SHOW_RESULTS, tracks.size());
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (int i = 0; i < show; i++) {
|
for (int i = 0; i < show; i++) {
|
||||||
|
|
@ -45,16 +44,10 @@ public class SearchHandler {
|
||||||
.append(formatDuration(t.getInfo().getLength()))
|
.append(formatDuration(t.getInfo().getLength()))
|
||||||
.append("\n");
|
.append("\n");
|
||||||
}
|
}
|
||||||
String pickHint = "Выбери `" + prefix + commandName + " 1`–`" + show + "`";
|
|
||||||
String sourceLabel = switch (source) {
|
|
||||||
case "sc" -> "SoundCloud";
|
|
||||||
case "ytm" -> "YouTube Music";
|
|
||||||
default -> "YouTube";
|
|
||||||
};
|
|
||||||
return new EmbedBuilder()
|
return new EmbedBuilder()
|
||||||
.setColor(0x00AAFF)
|
.setColor(0x00AAFF)
|
||||||
.setTitle("Результаты поиска: " + query + " на " + sourceLabel)
|
.setTitle("Результаты поиска: " + query)
|
||||||
.setDescription(sb + "\n" + pickHint)
|
.setDescription(sb + "\nВыбери `!" + "play" + " 1`–`" + show + "`")
|
||||||
.setFooter("Результаты доступны 2 минуты");
|
.setFooter("Результаты доступны 2 минуты");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,156 @@
|
||||||
|
package creator.loki.feature.reactroles;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import creator.loki.embed.EmbedTemplates;
|
||||||
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
|
||||||
|
public class ReactionRoleCommand implements Command {
|
||||||
|
private final ReactionRoleRepository repository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ReactionRoleCommand(ReactionRoleRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "reactionrole";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Управление реакциями-ролями";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "reactionrole setup <messageId> <emoji> <role> | remove <messageId> <emoji> | list [messageId]";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Настройка";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
if (!context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.error("Нет доступа", "Требуются права администратора"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var guild = context.getGuild();
|
||||||
|
if (guild == null) {
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.error("Ошибка", "Команда доступна только на сервере"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = context.getArgs();
|
||||||
|
if (args.isEmpty()) {
|
||||||
|
sendHelp(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subcommand = args.get(0).toLowerCase();
|
||||||
|
switch (subcommand) {
|
||||||
|
case "setup" -> handleSetup(context, guild, args);
|
||||||
|
case "remove" -> handleRemove(context, args);
|
||||||
|
case "list" -> handleList(context, guild, args);
|
||||||
|
default -> sendHelp(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSetup(CommandContext context, net.dv8tion.jda.api.entities.Guild guild, List<String> args) {
|
||||||
|
if (args.size() < 4) {
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.error("Неверное использование",
|
||||||
|
"Используйте: `reactionrole setup <messageId> <emoji> <role>`"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String messageId = args.get(1);
|
||||||
|
String emoji = args.get(2);
|
||||||
|
String roleInput = args.get(3);
|
||||||
|
|
||||||
|
String roleId = roleInput.replaceAll("<@&|>", "");
|
||||||
|
var role = guild.getRoleById(roleId);
|
||||||
|
if (role == null) {
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.error("Роль не найдена",
|
||||||
|
"Убедитесь, что роль существует и указан ID или упоминание"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.save(guild.getId(), context.getChannel().getId(), messageId, emoji, roleId);
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.success("Reaction-role добавлена",
|
||||||
|
"Роль " + role.getAsMention() + " будет выдаваться при реакции " + emoji))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRemove(CommandContext context, List<String> args) {
|
||||||
|
if (args.size() < 3) {
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.error("Неверное использование",
|
||||||
|
"Используйте: `reactionrole remove <messageId> <emoji>`"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String messageId = args.get(1);
|
||||||
|
String emoji = args.get(2);
|
||||||
|
|
||||||
|
repository.deleteByMessageAndEmoji(messageId, emoji);
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.success("Reaction-role удалена",
|
||||||
|
"Связь реакции " + emoji + " на сообщении " + messageId + " удалена"))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleList(CommandContext context, net.dv8tion.jda.api.entities.Guild guild, List<String> args) {
|
||||||
|
List<ReactionRoleRepository.ReactionRole> bindings;
|
||||||
|
if (args.size() >= 2) {
|
||||||
|
bindings = repository.findByMessage(args.get(1));
|
||||||
|
} else {
|
||||||
|
bindings = repository.findByGuild(guild.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindings.isEmpty()) {
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.info("Список пуст", "Reaction-роли не найдены"))
|
||||||
|
.queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (var b : bindings) {
|
||||||
|
var role = guild.getRoleById(b.roleId());
|
||||||
|
String roleName = role != null ? role.getAsMention() : b.roleId();
|
||||||
|
sb.append("• `").append(b.messageId()).append("` — ").append(b.emoji()).append(" → ").append(roleName).append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.info("Reaction-роли (" + bindings.size() + ")", sb.toString()))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendHelp(CommandContext context) {
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(EmbedTemplates.info("Reaction Role",
|
||||||
|
"`reactionrole setup <messageId> <emoji> <role>` — добавить связь\n"
|
||||||
|
+ "`reactionrole remove <messageId> <emoji>` — удалить связь\n"
|
||||||
|
+ "`reactionrole list [messageId]` — список связей"))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
package creator.loki.feature.reactroles;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
import creator.loki.util.LoggerDecorator;
|
||||||
|
import net.dv8tion.jda.api.events.message.react.MessageReactionAddEvent;
|
||||||
|
import net.dv8tion.jda.api.events.message.react.MessageReactionRemoveEvent;
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class ReactionRoleListener extends ListenerAdapter {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ReactionRoleListener.class);
|
||||||
|
|
||||||
|
private final ReactionRoleRepository repository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ReactionRoleListener(ReactionRoleRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessageReactionAdd(@NotNull MessageReactionAddEvent event) {
|
||||||
|
if (event.getUser() == null || event.getUser().isBot()) return;
|
||||||
|
if (event.getGuild() == null) return;
|
||||||
|
|
||||||
|
var emoji = event.getEmoji().getAsReactionCode();
|
||||||
|
var messageId = event.getMessageId();
|
||||||
|
|
||||||
|
var bindings = repository.findByMessage(messageId);
|
||||||
|
var binding = bindings.stream().filter(b -> b.emoji().equals(emoji)).findFirst();
|
||||||
|
if (binding.isEmpty()) return;
|
||||||
|
|
||||||
|
var role = event.getGuild().getRoleById(binding.get().roleId());
|
||||||
|
if (role == null) {
|
||||||
|
LoggerDecorator.warn(logger, "Роль " + binding.get().roleId() + " не найдена для reaction-role");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var member = event.getMember();
|
||||||
|
if (member == null) return;
|
||||||
|
|
||||||
|
event.getGuild().addRoleToMember(member, role).queue(
|
||||||
|
ok -> {},
|
||||||
|
error -> LoggerDecorator.error(logger, "Ошибка выдачи роли " + binding.get().roleId(), error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessageReactionRemove(@NotNull MessageReactionRemoveEvent event) {
|
||||||
|
if (event.getUser() == null || event.getUser().isBot()) return;
|
||||||
|
if (event.getGuild() == null) return;
|
||||||
|
|
||||||
|
var emoji = event.getEmoji().getAsReactionCode();
|
||||||
|
var messageId = event.getMessageId();
|
||||||
|
|
||||||
|
var bindings = repository.findByMessage(messageId);
|
||||||
|
var binding = bindings.stream().filter(b -> b.emoji().equals(emoji)).findFirst();
|
||||||
|
if (binding.isEmpty()) return;
|
||||||
|
|
||||||
|
var role = event.getGuild().getRoleById(binding.get().roleId());
|
||||||
|
if (role == null) {
|
||||||
|
LoggerDecorator.warn(logger, "Роль " + binding.get().roleId() + " не найдена для reaction-role");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.getGuild().removeRoleFromMember(event.getUser(), role).queue(
|
||||||
|
ok -> {},
|
||||||
|
error -> LoggerDecorator.error(logger, "Ошибка снятия роли " + binding.get().roleId(), error)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
package creator.loki.feature.reactroles;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class ReactionRoleRepository {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(ReactionRoleRepository.class);
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ReactionRoleRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void save(String guildId, String channelId, String messageId, String emoji, String roleId) {
|
||||||
|
String sql =
|
||||||
|
"""
|
||||||
|
INSERT OR REPLACE INTO reaction_roles (guild_id, channel_id, message_id, emoji, role_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, guildId);
|
||||||
|
stmt.setString(2, channelId);
|
||||||
|
stmt.setString(3, messageId);
|
||||||
|
stmt.setString(4, emoji);
|
||||||
|
stmt.setString(5, roleId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка сохранения reaction-role", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ReactionRole> findByMessage(String messageId) {
|
||||||
|
String sql = "SELECT id, guild_id, channel_id, message_id, emoji, role_id FROM reaction_roles WHERE message_id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, messageId);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
List<ReactionRole> results = new ArrayList<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
results.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка поиска reaction-roles по сообщению {}", messageId, e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ReactionRole> findByGuild(String guildId) {
|
||||||
|
String sql = "SELECT id, guild_id, channel_id, message_id, emoji, role_id FROM reaction_roles WHERE guild_id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, guildId);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
List<ReactionRole> results = new ArrayList<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
results.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка поиска reaction-roles по гильдии {}", guildId, e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void delete(int id) {
|
||||||
|
String sql = "DELETE FROM reaction_roles WHERE id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setInt(1, id);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка удаления reaction-role {}", id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteByMessageAndEmoji(String messageId, String emoji) {
|
||||||
|
String sql = "DELETE FROM reaction_roles WHERE message_id = ? AND emoji = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, messageId);
|
||||||
|
stmt.setString(2, emoji);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка удаления reaction-role {} {}", messageId, emoji, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReactionRole mapRow(ResultSet rs) throws SQLException {
|
||||||
|
return new ReactionRole(
|
||||||
|
rs.getInt("id"),
|
||||||
|
rs.getString("guild_id"),
|
||||||
|
rs.getString("channel_id"),
|
||||||
|
rs.getString("message_id"),
|
||||||
|
rs.getString("emoji"),
|
||||||
|
rs.getString("role_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ReactionRole(int id, String guildId, String channelId, String messageId, String emoji, String roleId) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package creator.loki.feature.reactroles;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
import creator.loki.core.registry.CommandRegistry;
|
||||||
|
|
||||||
|
public class ReactionRolesCommandRegistrar implements CommandRegistrar {
|
||||||
|
private final ReactionRoleRepository repository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public ReactionRolesCommandRegistrar(ReactionRoleRepository repository) {
|
||||||
|
this.repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(CommandRegistry registry) {
|
||||||
|
registry.register(new ReactionRoleCommand(repository));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package creator.loki.feature.reactroles;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
|
||||||
|
public class ReactionRolesModule extends AbstractModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(ReactionRoleRepository.class).in(Singleton.class);
|
||||||
|
bind(ReactionRoleListener.class).in(Singleton.class);
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(ReactionRolesCommandRegistrar.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
package creator.loki.feature.ticket;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
import creator.loki.core.registry.CommandRegistry;
|
||||||
|
|
||||||
|
public class TicketCommandRegistrar implements CommandRegistrar {
|
||||||
|
private final TicketSetupCommand ticketSetupCommand;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TicketCommandRegistrar(TicketSetupCommand ticketSetupCommand) {
|
||||||
|
this.ticketSetupCommand = ticketSetupCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void register(CommandRegistry registry) {
|
||||||
|
registry.register(ticketSetupCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
157
src/main/java/creator/loki/feature/ticket/TicketListener.java
Normal file
157
src/main/java/creator/loki/feature/ticket/TicketListener.java
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
package creator.loki.feature.ticket;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.feature.ticket.TicketRepository.Ticket;
|
||||||
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
import net.dv8tion.jda.api.entities.Guild;
|
||||||
|
import net.dv8tion.jda.api.entities.Member;
|
||||||
|
import net.dv8tion.jda.api.entities.channel.concrete.Category;
|
||||||
|
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||||
|
import net.dv8tion.jda.api.events.interaction.component.ButtonInteractionEvent;
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
import net.dv8tion.jda.api.managers.channel.concrete.TextChannelManager;
|
||||||
|
import net.dv8tion.jda.api.requests.restaction.ChannelAction;
|
||||||
|
import net.dv8tion.jda.api.components.buttons.Button;
|
||||||
|
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class TicketListener extends ListenerAdapter {
|
||||||
|
private final TicketRepository ticketRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TicketListener(TicketRepository ticketRepository) {
|
||||||
|
this.ticketRepository = ticketRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onButtonInteraction(ButtonInteractionEvent event) {
|
||||||
|
String buttonId = event.getComponentId();
|
||||||
|
if (!buttonId.startsWith("ticket_")) return;
|
||||||
|
|
||||||
|
switch (buttonId) {
|
||||||
|
case "ticket_create" -> handleCreate(event);
|
||||||
|
case "ticket_close" -> handleClose(event);
|
||||||
|
case "ticket_delete" -> handleDelete(event);
|
||||||
|
default -> { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleCreate(ButtonInteractionEvent event) {
|
||||||
|
Guild guild = event.getGuild();
|
||||||
|
if (guild == null) return;
|
||||||
|
|
||||||
|
Member member = event.getMember();
|
||||||
|
if (member == null) return;
|
||||||
|
|
||||||
|
String username = member.getUser().getName();
|
||||||
|
String channelName = "ticket-" + username;
|
||||||
|
|
||||||
|
Category category = findTicketCategory(guild);
|
||||||
|
|
||||||
|
ChannelAction<TextChannel> action = guild.createTextChannel(channelName);
|
||||||
|
|
||||||
|
if (category != null) {
|
||||||
|
action = action.setParent(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
action.queue(channel -> {
|
||||||
|
TextChannelManager manager = channel.getManager();
|
||||||
|
|
||||||
|
manager.putPermissionOverride(guild.getPublicRole(), null, EnumSet.of(Permission.VIEW_CHANNEL)).queue();
|
||||||
|
|
||||||
|
manager.putPermissionOverride(member, EnumSet.of(Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND, Permission.MESSAGE_HISTORY), null).queue();
|
||||||
|
|
||||||
|
manager.putPermissionOverride(guild.getSelfMember(), EnumSet.of(Permission.VIEW_CHANNEL, Permission.MESSAGE_SEND, Permission.MESSAGE_MANAGE, Permission.MESSAGE_HISTORY), null).queue();
|
||||||
|
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.setColor(Color.CYAN)
|
||||||
|
.setTitle("🎫 Поддержка")
|
||||||
|
.setDescription("Опишите вашу проблему, администраторы скоро ответят.")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
channel.sendMessageEmbeds(embed)
|
||||||
|
.addComponents(ActionRow.of(Button.danger("ticket_close", "🔒 Закрыть")))
|
||||||
|
.queue();
|
||||||
|
|
||||||
|
ticketRepository.createTicket(channel.getId(), guild.getId(), member.getId());
|
||||||
|
|
||||||
|
event.reply("🎫 Создан тикет: " + channel.getAsMention())
|
||||||
|
.setEphemeral(true)
|
||||||
|
.queue();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleClose(ButtonInteractionEvent event) {
|
||||||
|
Guild guild = event.getGuild();
|
||||||
|
if (guild == null) return;
|
||||||
|
|
||||||
|
Member member = event.getMember();
|
||||||
|
if (member == null) return;
|
||||||
|
|
||||||
|
TextChannel channel = event.getChannel().asTextChannel();
|
||||||
|
Ticket ticket = ticketRepository.findByChannel(channel.getId());
|
||||||
|
|
||||||
|
if (ticket == null) {
|
||||||
|
event.reply("❌ Тикет не найден в базе данных.").setEphemeral(true).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isCreator = ticket.creatorId().equals(member.getId());
|
||||||
|
boolean isAdmin = member.hasPermission(Permission.MANAGE_CHANNEL);
|
||||||
|
|
||||||
|
if (!isCreator && !isAdmin) {
|
||||||
|
event.reply("❌ У вас нет прав для закрытия этого тикета.").setEphemeral(true).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ticketRepository.closeTicket(channel.getId());
|
||||||
|
|
||||||
|
Member ticketCreator = guild.getMemberById(ticket.creatorId());
|
||||||
|
if (ticketCreator != null) {
|
||||||
|
channel.upsertPermissionOverride(ticketCreator)
|
||||||
|
.deny(Permission.MESSAGE_SEND)
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
event.getMessage().editMessageEmbeds(
|
||||||
|
new EmbedBuilder()
|
||||||
|
.setColor(Color.GRAY)
|
||||||
|
.setTitle("✅ Тикет закрыт")
|
||||||
|
.setDescription("Тикет закрыт пользователем " + member.getAsMention())
|
||||||
|
.build()
|
||||||
|
).setComponents(ActionRow.of(Button.danger("ticket_delete", "🗑 Удалить канал"))).queue();
|
||||||
|
|
||||||
|
event.reply("✅ Тикет закрыт.").setEphemeral(true).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleDelete(ButtonInteractionEvent event) {
|
||||||
|
Member member = event.getMember();
|
||||||
|
if (member == null || !member.hasPermission(Permission.MANAGE_CHANNEL)) {
|
||||||
|
event.reply("❌ У вас нет прав на удаление каналов.").setEphemeral(true).queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TextChannel channel = event.getChannel().asTextChannel();
|
||||||
|
ticketRepository.deleteTicket(channel.getId());
|
||||||
|
channel.delete().queue();
|
||||||
|
|
||||||
|
event.reply("🗑 Канал будет удалён.").setEphemeral(true).queue();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Category findTicketCategory(Guild guild) {
|
||||||
|
for (Category category : guild.getCategories()) {
|
||||||
|
if (category.getName().equalsIgnoreCase("Tickets")) {
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!guild.getCategories().isEmpty()) {
|
||||||
|
return guild.getCategories().get(0);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main/java/creator/loki/feature/ticket/TicketModule.java
Normal file
18
src/main/java/creator/loki/feature/ticket/TicketModule.java
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
package creator.loki.feature.ticket;
|
||||||
|
|
||||||
|
import com.google.inject.AbstractModule;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import com.google.inject.multibindings.Multibinder;
|
||||||
|
import creator.loki.core.registry.CommandRegistrar;
|
||||||
|
|
||||||
|
public class TicketModule extends AbstractModule {
|
||||||
|
@Override
|
||||||
|
protected void configure() {
|
||||||
|
bind(TicketRepository.class).in(Singleton.class);
|
||||||
|
bind(TicketListener.class).in(Singleton.class);
|
||||||
|
|
||||||
|
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||||
|
.addBinding()
|
||||||
|
.to(TicketCommandRegistrar.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
121
src/main/java/creator/loki/feature/ticket/TicketRepository.java
Normal file
121
src/main/java/creator/loki/feature/ticket/TicketRepository.java
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
package creator.loki.feature.ticket;
|
||||||
|
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.google.inject.Singleton;
|
||||||
|
import creator.loki.database.Database;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
public class TicketRepository {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(TicketRepository.class);
|
||||||
|
private final Database database;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TicketRepository(Database database) {
|
||||||
|
this.database = database;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void createTicket(String channelId, String guildId, String creatorId) {
|
||||||
|
String sql =
|
||||||
|
"""
|
||||||
|
INSERT INTO tickets (channel_id, guild_id, creator_id, created_at, closed, claimed_by)
|
||||||
|
VALUES (?, ?, ?, ?, 0, NULL)
|
||||||
|
""";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, channelId);
|
||||||
|
stmt.setString(2, guildId);
|
||||||
|
stmt.setString(3, creatorId);
|
||||||
|
stmt.setLong(4, System.currentTimeMillis());
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка создания тикета {}", channelId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void closeTicket(String channelId) {
|
||||||
|
String sql = "UPDATE tickets SET closed = 1 WHERE channel_id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, channelId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка закрытия тикета {}", channelId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteTicket(String channelId) {
|
||||||
|
String sql = "DELETE FROM tickets WHERE channel_id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, channelId);
|
||||||
|
stmt.executeUpdate();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка удаления тикета {}", channelId, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Ticket> findOpenByGuild(String guildId) {
|
||||||
|
String sql =
|
||||||
|
"SELECT channel_id, guild_id, creator_id, created_at, closed, claimed_by FROM tickets WHERE guild_id = ? AND closed = 0";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, guildId);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
List<Ticket> results = new ArrayList<>();
|
||||||
|
while (rs.next()) {
|
||||||
|
results.add(mapRow(rs));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка поиска открытых тикетов для гильдии {}", guildId, e);
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Ticket findByChannel(String channelId) {
|
||||||
|
String sql =
|
||||||
|
"SELECT channel_id, guild_id, creator_id, created_at, closed, claimed_by FROM tickets WHERE channel_id = ?";
|
||||||
|
|
||||||
|
try (PreparedStatement stmt = database.prepare(sql)) {
|
||||||
|
stmt.setString(1, channelId);
|
||||||
|
|
||||||
|
try (ResultSet rs = stmt.executeQuery()) {
|
||||||
|
if (rs.next()) {
|
||||||
|
return mapRow(rs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Ошибка поиска тикета {}", channelId, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Ticket mapRow(ResultSet rs) throws SQLException {
|
||||||
|
return new Ticket(
|
||||||
|
rs.getString("channel_id"),
|
||||||
|
rs.getString("guild_id"),
|
||||||
|
rs.getString("creator_id"),
|
||||||
|
rs.getLong("created_at"),
|
||||||
|
rs.getBoolean("closed"),
|
||||||
|
rs.getString("claimed_by"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public record Ticket(
|
||||||
|
String channelId,
|
||||||
|
String guildId,
|
||||||
|
String creatorId,
|
||||||
|
long createdAt,
|
||||||
|
boolean closed,
|
||||||
|
String claimedBy) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
package creator.loki.feature.ticket;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import creator.loki.command.Command;
|
||||||
|
import creator.loki.command.CommandContext;
|
||||||
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
import net.dv8tion.jda.api.Permission;
|
||||||
|
import net.dv8tion.jda.api.components.buttons.Button;
|
||||||
|
import net.dv8tion.jda.api.components.actionrow.ActionRow;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
|
||||||
|
public class TicketSetupCommand implements Command {
|
||||||
|
private final TicketRepository ticketRepository;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public TicketSetupCommand(TicketRepository ticketRepository) {
|
||||||
|
this.ticketRepository = ticketRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() {
|
||||||
|
return "ticketsetup";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getDescription() {
|
||||||
|
return "Создать панель создания тикетов в текущем канале";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getUsage() {
|
||||||
|
return "ticketsetup";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getCategory() {
|
||||||
|
return "Настройка";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(CommandContext context) {
|
||||||
|
if (!context.getAuthor().hasPermission(Permission.ADMINISTRATOR)) {
|
||||||
|
context.getChannel().sendMessage("❌ Только администраторы могут использовать эту команду.").queue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var embed = new EmbedBuilder()
|
||||||
|
.setColor(Color.CYAN)
|
||||||
|
.setTitle("🎫 Поддержка")
|
||||||
|
.setDescription("Нажми кнопку, чтобы создать тикет")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
context.getChannel()
|
||||||
|
.sendMessageEmbeds(embed)
|
||||||
|
.addComponents(ActionRow.of(Button.success("ticket_create", "🎫 Создать тикет")))
|
||||||
|
.queue();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue