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.economy.FishMessageListener;
|
||||
import creator.loki.feature.help.HelpMenuHandler;
|
||||
import creator.loki.feature.reactroles.ReactionRoleListener;
|
||||
import creator.loki.feature.image.ImageNavigationHandler;
|
||||
import creator.loki.feature.leaderboard.LeaderboardButtonListener;
|
||||
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.moderation.commands.utils.ClearSlashCommands;
|
||||
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.listener.PlaySelectHandler;
|
||||
import creator.loki.feature.ticket.TicketListener;
|
||||
import creator.loki.feature.roast.AndrertListener;
|
||||
import creator.loki.feature.roast.HelperListener;
|
||||
import creator.loki.feature.voice.handler.VoiceButtonHandler;
|
||||
|
|
@ -43,13 +46,16 @@ public class ListenerAggregator {
|
|||
ResetLevelsButtonHandler resetLevelsButtonHandler,
|
||||
LeaderboardButtonListener leaderboardButtonListener,
|
||||
MessageCache messageCache,
|
||||
MusicSearchButtonHandler musicSearchButtonHandler,
|
||||
MusicSlashCommands musicSlashCommands,
|
||||
PlaySelectHandler playSelectHandler,
|
||||
FishMessageListener fishMessageListener,
|
||||
AndrertListener andrertListener,
|
||||
ClearSlashCommands clearSlashCommands,
|
||||
HelperListener helperListener,
|
||||
ImageNavigationHandler imageNavigationHandler) {
|
||||
ImageNavigationHandler imageNavigationHandler,
|
||||
SetupSlashHandler setupSlashHandler,
|
||||
TicketListener ticketListener,
|
||||
ReactionRoleListener reactionRoleListener) {
|
||||
this.listeners =
|
||||
new Object[] {
|
||||
readyListener,
|
||||
|
|
@ -66,13 +72,16 @@ public class ListenerAggregator {
|
|||
resetLevelsButtonHandler,
|
||||
leaderboardButtonListener,
|
||||
messageCache,
|
||||
musicSearchButtonHandler,
|
||||
musicSlashCommands,
|
||||
playSelectHandler,
|
||||
fishMessageListener,
|
||||
andrertListener,
|
||||
clearSlashCommands,
|
||||
helperListener,
|
||||
imageNavigationHandler
|
||||
imageNavigationHandler,
|
||||
setupSlashHandler,
|
||||
ticketListener,
|
||||
reactionRoleListener
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,27 @@ public final class FeatureTables {
|
|||
left_count 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 =
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS ratings (
|
||||
|
|
@ -123,7 +144,9 @@ public final class FeatureTables {
|
|||
database.execute(VOICE_CHANNELS);
|
||||
database.execute(VOICE_PRESETS);
|
||||
database.execute(COUNTER_STATS);
|
||||
database.execute(TICKETS);
|
||||
database.execute(RATINGS);
|
||||
database.execute(REACTION_ROLES);
|
||||
createIndexes(database);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import creator.loki.core.registry.PrefixCommandRegistrar;
|
|||
import creator.loki.core.security.CooldownManager;
|
||||
import creator.loki.core.security.RateLimiter;
|
||||
import creator.loki.database.DatabaseModule;
|
||||
import creator.loki.feature.config.ConfigModule;
|
||||
import creator.loki.feature.counter.CounterModule;
|
||||
import creator.loki.feature.economy.EconomyModule;
|
||||
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.role.RoleModule;
|
||||
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.voice.VoiceModule;
|
||||
import creator.loki.feature.welcome.WelcomeModule;
|
||||
|
|
@ -47,6 +50,7 @@ public class BotModule extends AbstractModule {
|
|||
bind(PrefixCommandRegistrar.class).in(Singleton.class);
|
||||
|
||||
install(new DatabaseModule());
|
||||
install(new ConfigModule());
|
||||
install(new VoiceModule());
|
||||
install(new MusicModule());
|
||||
install(new LevelModule());
|
||||
|
|
@ -62,7 +66,9 @@ public class BotModule extends AbstractModule {
|
|||
install(new ImageModule());
|
||||
install(new TikTokModule());
|
||||
install(new EntertainmentModule());
|
||||
install(new TicketModule());
|
||||
install(new UtilityModule());
|
||||
install(new ReactionRolesModule());
|
||||
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.core.PlayerManager;
|
||||
import creator.loki.feature.music.core.SearchManager;
|
||||
import creator.loki.feature.music.play.MusicSearchSessionManager;
|
||||
import creator.loki.feature.music.play.PlayCommand;
|
||||
|
||||
public class MusicCommandRegistrar implements CommandRegistrar {
|
||||
private final PlayerManager playerManager;
|
||||
private final SearchManager searchManager;
|
||||
private final MusicSearchSessionManager sessionManager;
|
||||
|
||||
@Inject
|
||||
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager, MusicSearchSessionManager sessionManager) {
|
||||
public MusicCommandRegistrar(PlayerManager playerManager, SearchManager searchManager) {
|
||||
this.playerManager = playerManager;
|
||||
this.searchManager = searchManager;
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
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.stop(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.feature.music.core.LavalinkManager;
|
||||
import creator.loki.feature.music.core.PlayerManager;
|
||||
import creator.loki.feature.music.play.MusicSearchSessionManager;
|
||||
|
||||
public class MusicModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(LavalinkManager.class).in(Singleton.class);
|
||||
bind(PlayerManager.class).in(Singleton.class);
|
||||
bind(MusicSearchSessionManager.class).in(Singleton.class);
|
||||
|
||||
Multibinder.newSetBinder(binder(), CommandRegistrar.class)
|
||||
.addBinding()
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ public class MusicBotControlHandler {
|
|||
}
|
||||
try {
|
||||
var vol = Math.max(0, Math.min(100, Integer.parseInt(args.get(0))));
|
||||
if (link != null && link.getCachedPlayer() != null)
|
||||
if (link != null)
|
||||
link.createOrUpdatePlayer()
|
||||
.setVolume(vol)
|
||||
.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) {
|
||||
if (reqVoice(ch, member) == null) return;
|
||||
if (args.isEmpty()) {
|
||||
send(ch, err("Укажи URL или запрос: `q!play <название>`"));
|
||||
send(ch, err("Укажи URL или запрос: `m!play <название>`"));
|
||||
return;
|
||||
}
|
||||
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) {
|
||||
if (attempt > MAX_ATTEMPTS) return;
|
||||
if (link.getCachedPlayer() == null) {
|
||||
executor.schedule(
|
||||
() -> startWithRetry(link, track, attempt + 1),
|
||||
RETRY_DELAY_PLAYER_NULL_MS,
|
||||
TimeUnit.MILLISECONDS);
|
||||
return;
|
||||
}
|
||||
link.createOrUpdatePlayer()
|
||||
.setTrack(track)
|
||||
.setVolume(100)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
import java.awt.Color;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import creator.loki.command.Command;
|
||||
import creator.loki.command.CommandContext;
|
||||
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.SearchManager;
|
||||
import creator.loki.feature.music.manage.GuildMusicManager;
|
||||
import creator.loki.util.VoiceHelper;
|
||||
import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler;
|
||||
import dev.arbjerg.lavalink.client.player.LoadFailed;
|
||||
import dev.arbjerg.lavalink.client.player.PlaylistLoaded;
|
||||
import dev.arbjerg.lavalink.client.player.SearchResult;
|
||||
import dev.arbjerg.lavalink.client.player.Track;
|
||||
import dev.arbjerg.lavalink.client.player.TrackLoaded;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.Guild;
|
||||
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 {
|
||||
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 SearchHandler searchHandler;
|
||||
private final MusicSearchSessionManager sessionManager;
|
||||
|
||||
public PlayCommand(PlayerManager playerManager, SearchManager searchManager, MusicSearchSessionManager sessionManager) {
|
||||
public PlayCommand(PlayerManager playerManager, SearchManager searchManager) {
|
||||
this.playerManager = playerManager;
|
||||
this.searchHandler = new SearchHandler(searchManager);
|
||||
this.sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -46,7 +37,7 @@ public class PlayCommand implements Command {
|
|||
|
||||
@Override
|
||||
public String getUsage() {
|
||||
return "play <URL> | play <номер> | play <запрос>";
|
||||
return "play [yt|sc|list] <запрос> | play <номер>";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -59,7 +50,10 @@ public class PlayCommand implements Command {
|
|||
Guild guild = ctx.getGuild();
|
||||
if (guild == null) return;
|
||||
Member member = ctx.getAuthor();
|
||||
if (member == null) return;
|
||||
if (member == null || !member.getVoiceState().inAudioChannel()) {
|
||||
sendErr(ctx, "Зайди в голосовой канал сначала.");
|
||||
return;
|
||||
}
|
||||
var args = ctx.getArgs();
|
||||
if (args.isEmpty()) {
|
||||
sendErr(ctx, "Укажи URL, запрос или номер: `" + getUsage() + "`");
|
||||
|
|
@ -70,86 +64,10 @@ public class PlayCommand implements Command {
|
|||
handlePickResult(ctx, first);
|
||||
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;
|
||||
var lm = playerManager.getLavalinkManager();
|
||||
long guildId = ctx.getGuild().getIdLong();
|
||||
lm.getOrCreateLink(guildId)
|
||||
.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);
|
||||
}
|
||||
});
|
||||
var mm = lm.getMusicManager(guild.getIdLong());
|
||||
handlePlayQuery(ctx, args, first, lm, mm);
|
||||
}
|
||||
|
||||
private boolean resolveVoiceConnection(CommandContext ctx) {
|
||||
|
|
@ -174,6 +92,47 @@ public class PlayCommand implements Command {
|
|||
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) {
|
||||
var m = playerManager.getLavalinkManager().getMusicManager(ctx.getGuild().getIdLong());
|
||||
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;
|
||||
|
||||
public class SearchHandler {
|
||||
private static final int SHOW_RESULTS = 10;
|
||||
private static final int SHOW_RESULTS = 5;
|
||||
|
||||
private final SearchManager searchManager;
|
||||
|
||||
|
|
@ -31,8 +31,7 @@ public class SearchHandler {
|
|||
searchManager.clear(guildId);
|
||||
}
|
||||
|
||||
public EmbedBuilder buildResultsEmbed(
|
||||
String query, List<Track> tracks, String source, String prefix, String commandName) {
|
||||
public EmbedBuilder buildResultsEmbed(String query, List<Track> tracks) {
|
||||
int show = Math.min(SHOW_RESULTS, tracks.size());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < show; i++) {
|
||||
|
|
@ -45,16 +44,10 @@ public class SearchHandler {
|
|||
.append(formatDuration(t.getInfo().getLength()))
|
||||
.append("\n");
|
||||
}
|
||||
String pickHint = "Выбери `" + prefix + commandName + " 1`–`" + show + "`";
|
||||
String sourceLabel = switch (source) {
|
||||
case "sc" -> "SoundCloud";
|
||||
case "ytm" -> "YouTube Music";
|
||||
default -> "YouTube";
|
||||
};
|
||||
return new EmbedBuilder()
|
||||
.setColor(0x00AAFF)
|
||||
.setTitle("Результаты поиска: " + query + " на " + sourceLabel)
|
||||
.setDescription(sb + "\n" + pickHint)
|
||||
.setTitle("Результаты поиска: " + query)
|
||||
.setDescription(sb + "\nВыбери `!" + "play" + " 1`–`" + show + "`")
|
||||
.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