diff --git a/Bot/Handler/MessageReceivedHandler.cs b/Bot/Handler/MessageReceivedHandler.cs index ca1518b..a3cb230 100644 --- a/Bot/Handler/MessageReceivedHandler.cs +++ b/Bot/Handler/MessageReceivedHandler.cs @@ -1,4 +1,7 @@ +using Discord.Commands; using Lunaris2.Handler.GoodByeCommand; +using Lunaris2.Handler.MusicPlayer.JoinCommand; +using Lunaris2.Handler.MusicPlayer.PlayCommand; using Lunaris2.Notification; using Lunaris2.SlashCommand; using MediatR; @@ -17,6 +20,12 @@ public class MessageReceivedHandler(ISender mediator) : INotificationHandler option.Name == optionName)?.Value.ToString() ?? string.Empty; + } + } +} \ No newline at end of file diff --git a/Bot/Handler/MusicPlayer/JoinCommand/JoinHandler.cs b/Bot/Handler/MusicPlayer/JoinCommand/JoinHandler.cs new file mode 100644 index 0000000..a31786d --- /dev/null +++ b/Bot/Handler/MusicPlayer/JoinCommand/JoinHandler.cs @@ -0,0 +1,34 @@ +using Discord; +using Discord.WebSocket; +using MediatR; +using Victoria.Node; + +namespace Lunaris2.Handler.MusicPlayer.JoinCommand; + +public record JoinCommand(SocketSlashCommand Message) : IRequest; + +public class JoinHandler : IRequestHandler +{ + private readonly LavaNode _lavaNode; + private readonly DiscordSocketClient _client; + + public JoinHandler(LavaNode lavaNode, DiscordSocketClient client) + { + _lavaNode = lavaNode; + _client = client; + } + + public async Task Handle(JoinCommand command, CancellationToken cancellationToken) + { + var context = command.Message; + + await _lavaNode.EnsureConnected(); + + if (_lavaNode.HasPlayer(context.GetGuild(_client))) { + await context.RespondAsync("I'm already connected to a voice channel!"); + return; + } + + await context.JoinVoiceChannel(_lavaNode); + } +} \ No newline at end of file diff --git a/Bot/Handler/MusicPlayer/PlayCommand/PlayHandler.cs b/Bot/Handler/MusicPlayer/PlayCommand/PlayHandler.cs new file mode 100644 index 0000000..52b92d2 --- /dev/null +++ b/Bot/Handler/MusicPlayer/PlayCommand/PlayHandler.cs @@ -0,0 +1,110 @@ +using Discord; +using Discord.WebSocket; +using Lunaris2.SlashCommand; +using MediatR; +using Victoria.Node; +using Victoria.Node.EventArgs; +using Victoria.Player; +using Victoria.Responses.Search; + +namespace Lunaris2.Handler.MusicPlayer.PlayCommand; + +public record PlayCommand(SocketSlashCommand Message) : IRequest; + +public class PlayHandler : IRequestHandler +{ + private readonly LavaNode _lavaNode; + private readonly DiscordSocketClient _client; + + public PlayHandler(LavaNode lavaNode, DiscordSocketClient client) + { + _lavaNode = lavaNode; + _client = client; + } + + public async Task Handle(PlayCommand command, CancellationToken cancellationToken) + { + var context = command.Message; + + await _lavaNode.EnsureConnected(); + + var songName = context.GetOptionValueByName(Option.Input); + + if (string.IsNullOrWhiteSpace(songName)) { + await context.RespondAsync("Please provide search terms."); + return; + } + + var player = await GetPlayer(context); + + if (player == null) + return; + + var searchResponse = await _lavaNode.SearchAsync( + Uri.IsWellFormedUriString(songName, UriKind.Absolute) + ? SearchType.Direct + : SearchType.YouTube, songName); + + if (!await HandleSearchResponse(searchResponse, player, context, songName)) + return; + + await PlayTrack(player); + + _lavaNode.OnTrackEnd += OnTrackEnd; + } + + private async Task OnTrackEnd(TrackEndEventArg, LavaTrack> arg) + { + var player = arg.Player; + if (!player.Vueue.TryDequeue(out var nextTrack)) + { + await player.TextChannel.SendMessageAsync("Queue completed!"); + return; + } + + await player.PlayAsync(nextTrack); + } + + private async Task PlayTrack(LavaPlayer player) + { + if (player.PlayerState is PlayerState.Playing or PlayerState.Paused) { + return; + } + + player.Vueue.TryDequeue(out var lavaTrack); + await player.PlayAsync(lavaTrack); + } + + private async Task?> GetPlayer(SocketSlashCommand context) + { + var voiceState = context.User as IVoiceState; + + if (voiceState?.VoiceChannel != null) + return await _lavaNode.JoinAsync(voiceState.VoiceChannel, context.Channel as ITextChannel); + + await context.RespondAsync("You must be connected to a voice channel!"); + return null; + } + + private async Task HandleSearchResponse(SearchResponse searchResponse, LavaPlayer player, SocketSlashCommand context, string songName) + { + if (searchResponse.Status is SearchStatus.LoadFailed or SearchStatus.NoMatches) { + await context.RespondAsync($"I wasn't able to find anything for `{songName}`."); + return false; + } + + if (!string.IsNullOrWhiteSpace(searchResponse.Playlist.Name)) { + player.Vueue.Enqueue(searchResponse.Tracks); + + await context.RespondAsync($"Enqueued {searchResponse.Tracks.Count} songs."); + } + else { + var track = searchResponse.Tracks.FirstOrDefault()!; + player.Vueue.Enqueue(track); + + await context.RespondAsync($"Enqueued {track?.Title}"); + } + + return true; + } +} \ No newline at end of file diff --git a/Bot/Lunaris2.csproj b/Bot/Lunaris2.csproj index e42eb32..66b7e4f 100644 --- a/Bot/Lunaris2.csproj +++ b/Bot/Lunaris2.csproj @@ -8,16 +8,17 @@ - - - - - + + + + + + diff --git a/Bot/Notification/DiscordEventListener.cs b/Bot/Notification/DiscordEventListener.cs index a975031..6c4bcde 100644 --- a/Bot/Notification/DiscordEventListener.cs +++ b/Bot/Notification/DiscordEventListener.cs @@ -24,8 +24,8 @@ public class DiscordEventListener(DiscordSocketClient client, IServiceScopeFacto await Task.CompletedTask; } - private Task OnMessageReceivedAsync(SocketSlashCommand arg) + private async Task OnMessageReceivedAsync(SocketSlashCommand arg) { - return Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); + await Mediator.Publish(new MessageReceivedNotification(arg), _cancellationToken); } } \ No newline at end of file diff --git a/Bot/Program.cs b/Bot/Program.cs index 71bfdfa..fa70cf1 100644 --- a/Bot/Program.cs +++ b/Bot/Program.cs @@ -8,11 +8,15 @@ using Lunaris2.SlashCommand; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Victoria; +using Victoria.Node; +using RunMode = Discord.Commands.RunMode; namespace Lunaris2 { public class Program { + public static LavaNode _lavaNode; public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); @@ -27,18 +31,34 @@ namespace Lunaris2 GatewayIntents = GatewayIntents.All }; + var commandServiceConfig = new CommandServiceConfig{ DefaultRunMode = RunMode.Async }; + var client = new DiscordSocketClient(config); - var commands = new CommandService(); + var commands = new CommandService(commandServiceConfig); var configuration = new ConfigurationBuilder() .SetBasePath(AppContext.BaseDirectory) .AddJsonFile("appsettings.json") .Build(); - + services.AddSingleton(client) .AddSingleton(commands) .AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())) .AddSingleton() - .AddSingleton(x => new InteractionService(x.GetRequiredService())); + .AddSingleton(x => new InteractionService(x.GetRequiredService())) + .AddLavaNode(x => + { + x.SelfDeaf = false; + x.Hostname = "lavalink.devamop.in"; + x.Port = 80; + x.Authorization = "DevamOP"; + }) + // .AddLavaNode(x => + // { + // x.SelfDeaf = false; + // x.Hostname = "localhost"; + // x.Port = 2333; + // }) + .AddSingleton(); client.Ready += () => Client_Ready(client); client.Log += Log; @@ -52,6 +72,8 @@ namespace Lunaris2 .StartAsync() .GetAwaiter() .GetResult(); + + _lavaNode = services.BuildServiceProvider().GetRequiredService(); var listener = services .BuildServiceProvider() @@ -63,10 +85,10 @@ namespace Lunaris2 .GetResult(); }); - private static Task Client_Ready(DiscordSocketClient client) + private static async Task Client_Ready(DiscordSocketClient client) { + await _lavaNode.ConnectAsync(); client.RegisterCommands(); - return Task.CompletedTask; } private static Task Log(LogMessage arg) diff --git a/Bot/SlashCommand/Command.cs b/Bot/SlashCommand/Command.cs index eead49a..a466184 100644 --- a/Bot/SlashCommand/Command.cs +++ b/Bot/SlashCommand/Command.cs @@ -1,5 +1,12 @@ +using Discord; + namespace Lunaris2.SlashCommand; +public static class Option +{ + public const string Input = "input"; +} + public static class Command { public static class Hello @@ -14,6 +21,29 @@ public static class Command public const string Description = "Say goodbye to the bot!"; } + public static class Join + { + public const string Name = "join"; + public const string Description = "Join the voice channel!"; + } + + public static class Play + { + public const string Name = "play"; + public const string Description = "Play a song!"; + + public static readonly List Options = new() + { + new SlashCommandOptionBuilder + { + Name = "input", + Description = "The song you want to play", + Type = ApplicationCommandOptionType.String, + IsRequired = true + }, + }; + } + public static string[] GetAllCommands() { return typeof(Command) diff --git a/Bot/SlashCommand/SlashCommandBuilder.cs b/Bot/SlashCommand/SlashCommandBuilder.cs index 460fb9a..94ddc7d 100644 --- a/Bot/SlashCommand/SlashCommandBuilder.cs +++ b/Bot/SlashCommand/SlashCommandBuilder.cs @@ -1,13 +1,15 @@ +using Discord; using Discord.Net; using Discord.WebSocket; using Newtonsoft.Json; namespace Lunaris2.SlashCommand; -public class SlashCommandBuilder(string commandName, string commandDescription) +public class SlashCommandBuilder(string commandName, string commandDescription, List commandOptions = null) { private string CommandName { get; set; } = commandName; private string CommandDescription { get; set; } = commandDescription; + private List CommandOptions { get; set; } public async Task CreateSlashCommand(DiscordSocketClient client) { @@ -20,6 +22,8 @@ public class SlashCommandBuilder(string commandName, string commandDescription) var globalCommand = new Discord.SlashCommandBuilder(); globalCommand.WithName(CommandName); globalCommand.WithDescription(CommandDescription); + + commandOptions.ForEach(option => globalCommand.AddOption(option)); try { @@ -46,12 +50,12 @@ public class SlashCommandBuilder(string commandName, string commandDescription) } } - private async Task CommandExists(IEnumerable registeredCommands) + private Task CommandExists(IEnumerable registeredCommands) { if (!registeredCommands.Any(command => command.Name == CommandName && command.Description == CommandDescription)) - return false; + return Task.FromResult(false); Console.WriteLine($"Command {CommandName} already exists."); - return true; + return Task.FromResult(true); } } \ No newline at end of file diff --git a/Bot/SlashCommand/SlashCommandRegistration.cs b/Bot/SlashCommand/SlashCommandRegistration.cs index dca90bc..9c602ef 100644 --- a/Bot/SlashCommand/SlashCommandRegistration.cs +++ b/Bot/SlashCommand/SlashCommandRegistration.cs @@ -1,3 +1,4 @@ +using Discord; using Discord.WebSocket; namespace Lunaris2.SlashCommand; @@ -8,11 +9,13 @@ public static class SlashCommandRegistration { RegisterCommand(client, Command.Hello.Name, Command.Hello.Description); RegisterCommand(client, Command.Goodbye.Name, Command.Goodbye.Description); + RegisterCommand(client, Command.Join.Name, Command.Join.Description); + RegisterCommand(client, Command.Play.Name, Command.Play.Description, Command.Play.Options); } - private static void RegisterCommand(DiscordSocketClient client, string commandName, string commandDescription) + private static void RegisterCommand(DiscordSocketClient client, string commandName, string commandDescription, List commandOptions = null) { - var command = new SlashCommandBuilder(commandName, commandDescription); + var command = new SlashCommandBuilder(commandName, commandDescription, commandOptions); _ = command.CreateSlashCommand(client); } } diff --git a/application.yml b/application.yml index 40a77a0..4a97aca 100644 --- a/application.yml +++ b/application.yml @@ -16,7 +16,6 @@ lavalink: # defaultPluginRepository: "https://maven.lavalink.dev/releases" # optional, defaults to the Lavalink release repository # defaultPluginSnapshotRepository: "https://maven.lavalink.dev/snapshots" # optional, defaults to the Lavalink snapshot repository server: - password: "youshallnotpass" sources: youtube: true bandcamp: true diff --git a/docker-compose.yml b/docker-compose.yml index fb75e7e..b6eda8f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: lavalink: # pin the image version to Lavalink v4 - image: ghcr.io/lavalink-devs/lavalink:4 + image: ghcr.io/lavalink-devs/lavalink:3.7.10 container_name: lavalink restart: unless-stopped environment: diff --git a/plugins/lavasearch-plugin-1.0.0.jar b/plugins-x/lavasearch-plugin-1.0.0.jar similarity index 100% rename from plugins/lavasearch-plugin-1.0.0.jar rename to plugins-x/lavasearch-plugin-1.0.0.jar diff --git a/plugins/skybot-lavalink-plugin-1.6.3.jar b/plugins-x/skybot-lavalink-plugin-1.6.3.jar similarity index 100% rename from plugins/skybot-lavalink-plugin-1.6.3.jar rename to plugins-x/skybot-lavalink-plugin-1.6.3.jar diff --git a/plugins/sponsorblock-plugin-3.0.0.jar b/plugins-x/sponsorblock-plugin-3.0.0.jar similarity index 100% rename from plugins/sponsorblock-plugin-3.0.0.jar rename to plugins-x/sponsorblock-plugin-3.0.0.jar diff --git a/plugins/lavasrc-plugin-3.2.10.jar b/plugins/lavasrc-plugin-3.2.10.jar new file mode 100644 index 0000000..5c8a7d7 Binary files /dev/null and b/plugins/lavasrc-plugin-3.2.10.jar differ