From f501a71033355b2ddfb3fde0e998ea040cfb4dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Henrique=20Ivanchechen?= Date: Sat, 10 Dec 2022 15:49:16 -0300 Subject: [PATCH] initial --- .gitignore | 3 + Client.cs | 88 ++++++++++++++++++ CommandHandler.cs | 59 ++++++++++++ Commands.cs | 33 +++++++ KasinoBot.csproj | 15 +++ KasinoBot.sln | 25 +++++ Music.cs | 164 +++++++++++++++++++++++++++++++++ PlayerController.cs | 80 ++++++++++++++++ Program.cs | 15 +++ Properties/launchSettings.json | 10 ++ 10 files changed, 492 insertions(+) create mode 100644 .gitignore create mode 100644 Client.cs create mode 100644 CommandHandler.cs create mode 100644 Commands.cs create mode 100644 KasinoBot.csproj create mode 100644 KasinoBot.sln create mode 100644 Music.cs create mode 100644 PlayerController.cs create mode 100644 Program.cs create mode 100644 Properties/launchSettings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93f0f8e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vs* +bin/ +obj/ diff --git a/Client.cs b/Client.cs new file mode 100644 index 0000000..2f680a3 --- /dev/null +++ b/Client.cs @@ -0,0 +1,88 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KasinoBot +{ + public class Client + { + private static DiscordSocketClient? client; + private CommandService? commandService; + private ServiceProvider? services; + + private async Task MessageReceived (SocketMessage rawMessage) + { + if (commandService == null || services == null) + return; + + if (rawMessage is not SocketUserMessage message) + return; + + if (message.Source != MessageSource.User) + return; + + var argPos = 0; + if (!message.HasCharPrefix('!', ref argPos)) + return; + + var context = new SocketCommandContext(client, message); + await commandService.ExecuteAsync(context, argPos, services); + } + + public async Task Initialize() + { + Console.WriteLine("Initializing..."); + + using (services = GetServices()) + { + // Initialize Discord Socket Client and Command Service + client = services.GetRequiredService(); + commandService = services.GetRequiredService(); + + // add log handlers + client.Log += LogAsync; + client.MessageReceived += MessageReceived; + client.Ready += ReadyAsync; + + services.GetRequiredService().Log += LogAsync; + + // Start Discord Socket Client + await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("TOKEN")); + await client.StartAsync(); + + // Initialize command handler + await services.GetRequiredService().InitializeAsync(); + + // Hang there! + await Task.Delay(Timeout.Infinite); + } + } + + private ServiceProvider GetServices() + { + return new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton() + .BuildServiceProvider(); + } + + private Task ReadyAsync() + { + Console.WriteLine($"{client?.CurrentUser} is connected!"); + return Task.CompletedTask; + } + + private Task LogAsync(LogMessage arg) + { + Console.WriteLine(arg.ToString()); + return Task.CompletedTask; + } + } +} diff --git a/CommandHandler.cs b/CommandHandler.cs new file mode 100644 index 0000000..7e31a1d --- /dev/null +++ b/CommandHandler.cs @@ -0,0 +1,59 @@ +using Discord; +using Discord.Commands; +using Discord.WebSocket; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Reflection; +using System.Threading.Tasks; + +namespace KasinoBot +{ + public class CommandHandler + { + private readonly CommandService _commands; + private readonly DiscordSocketClient _discord; + private readonly IServiceProvider _services; + + public CommandHandler(IServiceProvider services) + { + _commands = services.GetRequiredService(); + _discord = services.GetRequiredService(); + _services = services; + + _commands.CommandExecuted += CommandExecutedAsync; + _discord.MessageReceived += MessageReceivedAsync; + } + + public async Task InitializeAsync() + { + await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services); + } + + public async Task MessageReceivedAsync(SocketMessage rawMessage) + { + if (!(rawMessage is SocketUserMessage message)) + return; + if (message.Source != MessageSource.User) + return; + + var argPos = 0; + if (!message.HasCharPrefix('~', ref argPos)) + return; + + var context = new SocketCommandContext(_discord, message); + await _commands.ExecuteAsync(context, argPos, _services); + } + + public async Task CommandExecutedAsync(Optional command, ICommandContext context, IResult result) + { + if (!command.IsSpecified) + return; + + if (result.IsSuccess) + return; + + Console.WriteLine($"[{context.User.Username} | {context.Guild.Name}] Command error: {result}"); + await context.Channel.SendMessageAsync($"Command error: {result}"); + } + } +} \ No newline at end of file diff --git a/Commands.cs b/Commands.cs new file mode 100644 index 0000000..8dc30b2 --- /dev/null +++ b/Commands.cs @@ -0,0 +1,33 @@ +using Discord.Commands; + +namespace KasinoBot +{ + public class Commands : ModuleBase + { + private PlayerController playerController = PlayerController.Instance; + + [Command("ping")] + [Alias("pong", "hello")] + public Task PingAsync() + => ReplyAsync("pong!"); + + [Command("play", RunMode = RunMode.Async)] + public async Task PlayAsync([Remainder] string musicString) + { + await playerController.Play(musicString, Context); + } + + [Command("stop", RunMode = RunMode.Async)] + public async Task StopAsync() + { + await playerController.Stop(Context); + } + + [Command("skip", RunMode = RunMode.Async)] + public async Task SkipAsync() + { + await playerController.Skip(Context); + } + } +} + diff --git a/KasinoBot.csproj b/KasinoBot.csproj new file mode 100644 index 0000000..c34bd55 --- /dev/null +++ b/KasinoBot.csproj @@ -0,0 +1,15 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + diff --git a/KasinoBot.sln b/KasinoBot.sln new file mode 100644 index 0000000..85280c9 --- /dev/null +++ b/KasinoBot.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "KasinoBot", "KasinoBot.csproj", "{A13F32A9-2011-467D-BEFA-92FD5329B422}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A13F32A9-2011-467D-BEFA-92FD5329B422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A13F32A9-2011-467D-BEFA-92FD5329B422}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A13F32A9-2011-467D-BEFA-92FD5329B422}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A13F32A9-2011-467D-BEFA-92FD5329B422}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1F467E8A-0721-4DE4-B46C-08F58DD91926} + EndGlobalSection +EndGlobal diff --git a/Music.cs b/Music.cs new file mode 100644 index 0000000..72a0255 --- /dev/null +++ b/Music.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Discord; +using Discord.Audio; +using Discord.WebSocket; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace KasinoBot +{ + public class Music + { + private IAudioClient AudioClient { get; set; } + private ISocketMessageChannel TextChannel { get; set; } + private Queue Queue { get; } + private Stream? CurrentMusicStream { get; set; } + private Process? CurrentFFmpeg { get; set; } + private AudioOutStream? CurrentOuAudio { get; set; } + + public Music(SocketGuild guild, ISocketMessageChannel channel, IAudioClient audioClient) + { + Queue = new Queue(); + TextChannel = channel; + AudioClient = audioClient; + } + + public async Task Play(string query, SocketUser user, SocketUserMessage message) + { + if (message.Channel != null) + TextChannel = message.Channel; + + MusicInfo musicInfo = new MusicInfo(query, user); + //await message.AddReactionAsync(Emoji.Parse("👌")); + await TextChannel.SendMessageAsync($"Enqueued **\"{musicInfo.Title}\"** *({musicInfo.Duration})*"); + + Queue.Enqueue(musicInfo); + if (Queue.Count == 1) + await PlayNextMusic(); + } + + public async Task Stop() + { + Queue.Clear(); + if (CurrentMusicStream != null) + CurrentMusicStream.Close(); + await Task.Delay(10); + } + + public async Task Skip() + { + Console.WriteLine("Skipping"); + if (CurrentMusicStream != null) + CurrentMusicStream.Close(); + await Task.Delay(10); + } + + private async Task PlayNextMusic() + { + if (Queue.Count == 0) + return; + + try + { + var musicInfo = Queue.Peek(); + + using (var input = AudioClient.CreatePCMStream(AudioApplication.Music)) + using (CurrentFFmpeg = CreateAudioStream(musicInfo.MediaURL)) + using (CurrentMusicStream = CurrentFFmpeg.StandardOutput.BaseStream) + { + try + { + await CurrentMusicStream.CopyToAsync(input); + } + catch (Exception ex) + { + Console.WriteLine("Error CopyToAsync: " + ex.ToString()); + } + finally + { + Queue.Dequeue(); + await input.FlushAsync(); + } + } + } + catch (Exception e) + { + await TextChannel.SendMessageAsync($"Error: {e.Message}"); + Console.WriteLine(e); + } + + await PlayNextMusic(); + } + + private Process CreateAudioStream(string path) + { + var p = Process.Start(new ProcessStartInfo + { + FileName = "ffmpeg", + Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1", + UseShellExecute = false, + RedirectStandardOutput = true, + }); + + if (p == null) + throw new Exception("Could not start ffmpeg. Please make sure ffmpeg is available."); + else if (p.HasExited) + throw new Exception("ffmpeg exited with code " + p.ExitCode); + + return p; + } + } + + public class MusicInfo + { + public string Query { get; } + public SocketUser User { get; } + public string Title { get; set; } + public string Duration { get; set; } + public string MediaURL { get; set; } + public string YoutubeURL { get; set; } + public MusicInfo(string query, SocketUser user) + { + Query = query; + User = user; + + var p = Process.Start(new ProcessStartInfo + { + FileName = "youtube-dl", + Arguments = $"-f bestaudio --dump-json --default-search \"ytsearch\" -g \"{this.Query}\"", + UseShellExecute = false, + RedirectStandardOutput = true, + }); + + if (p == null) + throw new Exception("Could not start youtube-dl. Please make sure youtube-dl is available."); + else if (p.HasExited) + throw new Exception("youtube-dl exited with code " + p.ExitCode); + + var output = p.StandardOutput.ReadToEnd(); + p.WaitForExit(); + + try + { + JToken json = JObject.Parse(output.Split('\n')[1]); + if (json == null) + throw new Exception("Could not parse youtube-dl output."); + + int duration = int.Parse(json["duration"].ToString()); + this.MediaURL = output.Split('\n')[0]; + this.Title = json["title"].ToString(); + this.YoutubeURL = json["webpage_url"].ToString(); + this.Duration = $"{Math.Floor((float)duration / 60)}m {duration % 60}s"; + } + catch (Exception e) + { + throw new Exception("Error parsing information from Youtube-DL: " + e.Message); + } + } + } +} diff --git a/PlayerController.cs b/PlayerController.cs new file mode 100644 index 0000000..491ac73 --- /dev/null +++ b/PlayerController.cs @@ -0,0 +1,80 @@ +using Discord.Audio; +using Discord.Commands; +using Discord.WebSocket; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace KasinoBot +{ + public class PlayerController + { + private static PlayerController instance; + public static PlayerController Instance + { + get + { + if (instance == null) + instance = new PlayerController(); + + return instance; + } + } + + private static Dictionary guildMusic = new Dictionary(); + private static Dictionary clientAudios = new Dictionary(); + + private PlayerController() { } + + public async Task Play(string musicInfo, SocketCommandContext context) + { + var guild = context.Guild; + var user = context.User; + var message = context.Message; + if (guildMusic.ContainsKey(guild)) + { + await guildMusic[guild].Play(musicInfo, user, message); + } + else + { + Music musicPlayer; + IAudioClient? audioClient; + if (!clientAudios.TryGetValue(guild, out audioClient)) + { + SocketVoiceChannel? voiceChannel = + guild.Channels.Select(x => x as SocketVoiceChannel).Where(x => x?.Users.Contains(user) ?? false).FirstOrDefault(); + + if (voiceChannel == null) + { + await context.Channel.SendMessageAsync("You need to be in a voice channel to play music."); + return; + } + + audioClient = await voiceChannel.ConnectAsync(); + clientAudios.Add(guild, audioClient); + } + + musicPlayer = new Music(guild, context.Channel, audioClient); + + guildMusic.Add(guild, musicPlayer); + + await guildMusic[guild].Play(musicInfo, user, message); + } + } + + public async Task Stop(SocketCommandContext context) + { + if (guildMusic.ContainsKey(context.Guild)) + await guildMusic[context.Guild].Stop(); + } + + public async Task Skip(SocketCommandContext context) + { + if (guildMusic.ContainsKey(context.Guild)) + await guildMusic[context.Guild].Skip(); + } + + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 0000000..b295358 --- /dev/null +++ b/Program.cs @@ -0,0 +1,15 @@ + +namespace KasinoBot +{ + class Program + { + + static void Main(string[] args) + { + Client client = new Client(); + client.Initialize().Wait(); + } + + } +} + diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 0000000..7f1b49d --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "KasinoBot": { + "commandName": "Project", + "environmentVariables": { + "TOKEN": "ODg3Mzc0OTczNjA0MzU2MTA2.YUDOWA.tI--BKjpUHBHdiHZLjNNaZxniec" + } + } + } +} \ No newline at end of file