diff --git a/README.md b/README.md index a599525..50105db 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,8 @@ Alternatively, you can use `Discord4JUtils.leave(gatewayClient, guildId);` as th ## Examples The following examples are minimal implementations but show how the library works. - Java examples - - JDA: [link](src/test/java/JavaJDAExample.java) + - JDA (simple): [link](src/test/java/JavaJDAExample.java) + - JDA (more complex example): [link](testbot/src/main/java/me/duncte123/testbot/Main.java) - Kotlin examples - JDA: [link](src/test/kotlin/testScript.kt) - Discord4J: [link](src/test/kotlin/d4jTestScript.kt) diff --git a/build.gradle.kts b/build.gradle.kts index 07b5fbe..591baba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -35,20 +35,22 @@ group = "dev.arbjerg" version = gitVersion val archivesBaseName = "lavalink-client" +allprojects { + repositories { + mavenCentral() + maven("https://maven.lavalink.dev/releases") + maven("https://maven.lavalink.dev/snapshots") + maven("https://maven.topi.wtf/releases") + // Note to self: jitpack always comes last + maven("https://jitpack.io") + } +} + java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } -repositories { - mavenCentral() - maven("https://maven.lavalink.dev/releases") - maven("https://maven.lavalink.dev/snapshots") - maven("https://maven.topi.wtf/releases") - // Note to self: jitpack always comes last - maven("https://jitpack.io") -} - dependencies { // package libraries api(kotlin("stdlib")) diff --git a/settings.gradle.kts b/settings.gradle.kts index d792e33..d10f5f1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,6 +2,8 @@ rootProject.name = "lavalink-client" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") +include(":testbot") + dependencyResolutionManagement { versionCatalogs { create("libs") { diff --git a/src/main/kotlin/dev/arbjerg/lavalink/client/protocol/FilterBuilder.kt b/src/main/kotlin/dev/arbjerg/lavalink/client/protocol/FilterBuilder.kt new file mode 100644 index 0000000..991daa4 --- /dev/null +++ b/src/main/kotlin/dev/arbjerg/lavalink/client/protocol/FilterBuilder.kt @@ -0,0 +1,85 @@ +package dev.arbjerg.lavalink.client.protocol + +import dev.arbjerg.lavalink.internal.toJsonElement +import dev.arbjerg.lavalink.protocol.v4.* +import kotlinx.serialization.json.JsonElement + +class FilterBuilder { + private var volume: Omissible = Omissible.Omitted() + private var equalizer: Omissible> = Omissible.Omitted() + private var karaoke: Omissible = Omissible.Omitted() + private var timescale: Omissible = Omissible.Omitted() + private var tremolo: Omissible = Omissible.Omitted() + private var vibrato: Omissible = Omissible.Omitted() + private var distortion: Omissible = Omissible.Omitted() + private var rotation: Omissible = Omissible.Omitted() + private var channelMix: Omissible = Omissible.Omitted() + private var lowPass: Omissible = Omissible.Omitted() + private var pluginFilters: MutableMap = mutableMapOf() + + fun setVolume(volume: Float) = apply { + this.volume = volume.toOmissible() + } + + fun setEqualizer(equalizer: List) = apply { + this.equalizer = equalizer.toOmissible() + } + + fun setKaraoke(karaoke: Karaoke?) = apply { + this.karaoke = Omissible.of(karaoke) + } + + fun setTimescale(timescale: Timescale?) = apply { + this.timescale = Omissible.of(timescale) + } + + fun setTremolo(tremolo: Tremolo?) = apply { + this.tremolo = Omissible.of(tremolo) + } + + fun setVibrato(vibrato: Vibrato?) = apply { + this.vibrato = Omissible.of(vibrato) + } + + fun setDistortion(distortion: Distortion?) = apply { + this.distortion = Omissible.of(distortion) + } + + fun setRotation(rotation: Rotation?) = apply { + this.rotation = Omissible.of(rotation) + } + + fun setChannelMix(channelMix: ChannelMix?) = apply { + this.channelMix = Omissible.of(channelMix) + } + + fun setLowPass(lowPass: LowPass?) = apply { + this.lowPass = Omissible.of(lowPass) + } + + fun setPluginFilter(name: String, filter: Any) = apply { + pluginFilters[name] = toJsonElement(filter) + } + + fun setPluginFilter(name: String, filter: JsonElement) = apply { + pluginFilters[name] = filter + } + + fun removePluginFilter(name: String) = apply { + pluginFilters.remove(name) + } + + fun build() = Filters( + volume, + equalizer, + karaoke, + timescale, + tremolo, + vibrato, + distortion, + rotation, + channelMix, + lowPass, + pluginFilters + ) +} diff --git a/testbot/build.gradle.kts b/testbot/build.gradle.kts new file mode 100644 index 0000000..ab18a4e --- /dev/null +++ b/testbot/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + java + application +} + +group = "me.duncte123" +version = "1.0-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +dependencies { + // Include the lavalink client + implementation(projects.lavalinkClient) + + // other libs such as a discord client and a logger + implementation(libs.jda) + implementation(libs.logger.impl) +} diff --git a/testbot/src/main/java/me/duncte123/testbot/AudioLoader.java b/testbot/src/main/java/me/duncte123/testbot/AudioLoader.java new file mode 100644 index 0000000..92c9bce --- /dev/null +++ b/testbot/src/main/java/me/duncte123/testbot/AudioLoader.java @@ -0,0 +1,77 @@ +package me.duncte123.testbot; + +import dev.arbjerg.lavalink.client.AbstractAudioLoadResultHandler; +import dev.arbjerg.lavalink.client.Link; +import dev.arbjerg.lavalink.client.protocol.*; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class AudioLoader extends AbstractAudioLoadResultHandler { + private final Link link; + private final SlashCommandInteractionEvent event; + + public AudioLoader(Link link, SlashCommandInteractionEvent event) { + this.link = link; + this.event = event; + } + + @Override + public void ontrackLoaded(@NotNull TrackLoaded result) { + final Track track = result.getTrack(); + + // Inner class at the end of this file + var userData = new MyUserData(event.getUser().getIdLong()); + + track.setUserData(userData); + + link.createOrUpdatePlayer() + .setTrack(track) + .setVolume(35) + .subscribe((player) -> { + final Track playingTrack = player.getTrack(); + final var trackTitle = playingTrack.getInfo().getTitle(); + final MyUserData customData = playingTrack.getUserData(MyUserData.class); + + event.getHook().sendMessage("Now playing: " + trackTitle + "\nRequested by: <@" + customData.requester() + '>').queue(); + }); + } + + @Override + public void onPlaylistLoaded(@NotNull PlaylistLoaded result) { + final int trackCount = result.getTracks().size(); + event.getHook() + .sendMessage("This playlist has " + trackCount + " tracks!") + .queue(); + } + + @Override + public void onSearchResultLoaded(@NotNull SearchResult result) { + final List tracks = result.getTracks(); + + if (tracks.isEmpty()) { + event.getHook().sendMessage("No tracks found!").queue(); + return; + } + + final Track firstTrack = tracks.get(0); + + // This is a different way of updating the player! Choose your preference! + // This method will also create a player if there is not one in the server yet + link.updatePlayer((update) -> update.setTrack(firstTrack).setVolume(35)) + .subscribe((ignored) -> { + event.getHook().sendMessage("Now playing: " + firstTrack.getInfo().getTitle()).queue(); + }); + } + + @Override + public void noMatches() { + event.getHook().sendMessage("No matches found for your input!").queue(); + } + + @Override + public void loadFailed(@NotNull LoadFailed result) { + event.getHook().sendMessage("Failed to load track! " + result.getException().getMessage()).queue(); + } +} diff --git a/testbot/src/main/java/me/duncte123/testbot/JDAListener.java b/testbot/src/main/java/me/duncte123/testbot/JDAListener.java new file mode 100644 index 0000000..7801aa2 --- /dev/null +++ b/testbot/src/main/java/me/duncte123/testbot/JDAListener.java @@ -0,0 +1,168 @@ +package me.duncte123.testbot; + +import dev.arbjerg.lavalink.client.LavalinkClient; +import dev.arbjerg.lavalink.client.Link; +import dev.arbjerg.lavalink.client.protocol.FilterBuilder; +import dev.arbjerg.lavalink.protocol.v4.Karaoke; +import net.dv8tion.jda.api.entities.Guild; +import net.dv8tion.jda.api.entities.GuildVoiceState; +import net.dv8tion.jda.api.entities.Member; +import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; +import net.dv8tion.jda.api.events.session.ReadyEvent; +import net.dv8tion.jda.api.hooks.ListenerAdapter; +import net.dv8tion.jda.api.interactions.commands.OptionType; +import net.dv8tion.jda.api.interactions.commands.build.Commands; +import net.dv8tion.jda.api.interactions.commands.build.SubcommandData; +import okhttp3.ResponseBody; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +public class JDAListener extends ListenerAdapter { + private static final Logger LOG = LoggerFactory.getLogger(JDAListener.class); + + private final LavalinkClient client; + + public JDAListener(LavalinkClient client) { + this.client = client; + } + + @Override + public void onReady(@NotNull ReadyEvent event) { + LOG.info(event.getJDA().getSelfUser().getAsTag() + " is ready!"); + + event.getJDA().updateCommands() + .addCommands( + Commands.slash("custom-request", "Testing custom requests"), + Commands.slash("join", "Join the voice channel you are in."), + Commands.slash("leave", "Leaves the vc"), + Commands.slash("stop", "Stops the current track"), + Commands.slash("pause", "Pause or unpause the plauer"), + Commands.slash("play", "Play a song") + .addOption( + OptionType.STRING, + "identifier", + "The identifier of the song you want to play", + true + ), + Commands.slash("karaoke", "Turn karaoke on or off") + .addSubcommands( + new SubcommandData("on", "Turn karaoke on"), + new SubcommandData("off", "Turn karaoke on") + ) + ) + .queue(); + } + + @Override + public void onSlashCommandInteraction(@NotNull SlashCommandInteractionEvent event) { + switch (event.getFullCommandName()) { + case "join": + joinHelper(event); + break; + case "stop": + this.client.getLink(event.getGuild().getIdLong()) + .updatePlayer( + (update) -> update.setTrack(null).setPaused(false) + ) + .subscribe((__) -> { + event.reply("Stopped the current track").queue(); + }); + break; + case "leave": + event.getJDA().getDirectAudioController().disconnect(event.getGuild()); + event.reply("Leaving your channel!").queue(); + break; + case "pause": + this.client.getLink(event.getGuild().getIdLong()) + .getPlayer() + .flatMap((player) -> player.setPaused(!player.getPaused()).asMono()) + .subscribe((player) -> { + event.reply("Player has been " + (player.getPaused() ? "paused" : "resumed") + "!").queue(); + }); + break; + case "karaoke on": { + final Guild guild = event.getGuild(); + final long guildId = guild.getIdLong(); + final Link link = this.client.getLink(guildId); + + link.createOrUpdatePlayer() + .setFilters( + new FilterBuilder() + .setKaraoke( + new Karaoke() + ) + .build() + ) + .subscribe(); + break; + } + case "karaoke off": { + final Guild guild = event.getGuild(); + final long guildId = guild.getIdLong(); + final Link link = this.client.getLink(guildId); + + link.createOrUpdatePlayer() + .setFilters( + new FilterBuilder() + .setKaraoke(null) + .build() + ) + .subscribe(); + break; + } + case "play": { + final Guild guild = event.getGuild(); + + // We are already connected, go ahead and play + if (guild.getSelfMember().getVoiceState().inAudioChannel()) { + event.deferReply(false).queue(); + } else { + // Connect to VC first + joinHelper(event); + } + + final String identifier = event.getOption("identifier").getAsString(); + final long guildId = guild.getIdLong(); + final Link link = this.client.getLink(guildId); + + link.loadItem(identifier).subscribe(new AudioLoader(link, event)); + + break; + } + case "custom-request": { + final Link link = this.client.getLink(event.getGuild().getIdLong()); + + link.getNode().customRequest( + (builder) -> builder.get().path("/version").header("Accept", "text/plain") + ).subscribe((response) -> { + try (ResponseBody body = response.body()) { + final String bodyText = body.string(); + + event.reply("Response from version endpoint (with custom request): " + bodyText).queue(); + } catch (IOException e) { + event.reply("Something went wrong! " + e.getMessage()).queue(); + } + }); + break; + } + default: + event.reply("Unknown command???").queue(); + break; + } + } + + // Makes sure that the bot is in a voice channel! + private void joinHelper(SlashCommandInteractionEvent event) { + final Member member = event.getMember(); + final GuildVoiceState memberVoiceState = member.getVoiceState(); + + if (memberVoiceState.inAudioChannel()) { + event.getJDA().getDirectAudioController().connect(memberVoiceState.getChannel()); + } + + event.reply("Joining your channel!").queue(); + } +} diff --git a/testbot/src/main/java/me/duncte123/testbot/Main.java b/testbot/src/main/java/me/duncte123/testbot/Main.java new file mode 100644 index 0000000..713974e --- /dev/null +++ b/testbot/src/main/java/me/duncte123/testbot/Main.java @@ -0,0 +1,103 @@ +package me.duncte123.testbot; + +import dev.arbjerg.lavalink.client.*; +import dev.arbjerg.lavalink.client.loadbalancing.RegionGroup; +import dev.arbjerg.lavalink.client.loadbalancing.builtin.VoiceRegionPenaltyProvider; +import dev.arbjerg.lavalink.libraries.jda.JDAVoiceUpdateListener; +import net.dv8tion.jda.api.JDABuilder; +import net.dv8tion.jda.api.requests.GatewayIntent; +import net.dv8tion.jda.api.utils.cache.CacheFlag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.List; + +public class Main { + private static final Logger LOG = LoggerFactory.getLogger(Main.class); + + public static void main(String[] args) throws InterruptedException { + final var token = System.getenv("BOT_TOKEN"); + final LavalinkClient client = new LavalinkClient(Helpers.getUserIdFromToken(token)); + + client.getLoadBalancer().addPenaltyProvider(new VoiceRegionPenaltyProvider()); + + registerLavalinkListeners(client); + registerLavalinkNodes(client); + + JDABuilder.createDefault(token) + .setVoiceDispatchInterceptor(new JDAVoiceUpdateListener(client)) + .enableIntents(GatewayIntent.GUILD_VOICE_STATES) + .enableCache(CacheFlag.VOICE_STATE) + .addEventListeners(new JDAListener(client)) + .build() + .awaitReady(); + } + + + + private static void registerLavalinkNodes(LavalinkClient client) { + List.of( + /*client.addNode( + "Testnode", + URI.create("ws://localhost:2333"), + "youshallnotpass", + RegionGroup.EUROPE + ),*/ + + client.addNode( + "Mac-mini", + URI.create("ws://192.168.1.139:2333"), + "youshallnotpass", + RegionGroup.US + ) + ).forEach((node) -> { + node.on(TrackStartEvent.class).subscribe((event) -> { + final LavalinkNode node1 = event.getNode(); + + LOG.info( + "{}: track started: {}", + node1.getName(), + event.getTrack().getInfo() + ); + }); + }); + } + + private static void registerLavalinkListeners(LavalinkClient client) { + client.on(ReadyEvent.class).subscribe((event) -> { + final LavalinkNode node = event.getNode(); + + LOG.info( + "Node '{}' is ready, session id is '{}'!", + node.getName(), + event.getSessionId() + ); + }); + + client.on(StatsEvent.class).subscribe((event) -> { + final LavalinkNode node = event.getNode(); + + LOG.info( + "Node '{}' has stats, current players: {}/{}", + node.getName(), + event.getPlayingPlayers(), + event.getPlayers() + ); + }); + + client.on(EmittedEvent.class).subscribe((event) -> { + if (event instanceof TrackStartEvent) { + LOG.info("Is a track start event!"); + } + + final var node = event.getNode(); + + LOG.info( + "Node '{}' emitted event: {}", + node.getName(), + event + ); + }); + } +} diff --git a/testbot/src/main/java/me/duncte123/testbot/MyUserData.java b/testbot/src/main/java/me/duncte123/testbot/MyUserData.java new file mode 100644 index 0000000..9ef8748 --- /dev/null +++ b/testbot/src/main/java/me/duncte123/testbot/MyUserData.java @@ -0,0 +1,4 @@ +package me.duncte123.testbot; + +public record MyUserData(long requester) { +}