commit f501a71033355b2ddfb3fde0e998ea040cfb4dce Author: José Henrique Ivanchechen Date: Sat Dec 10 15:49:16 2022 -0300 initial 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