diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6dd4c0e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+appsettings.*.json
+.env*
+
+.vs
+.vscode
+bin
+obj
\ No newline at end of file
diff --git a/Kasbot.csproj b/Kasbot.csproj
new file mode 100644
index 0000000..18b178e
--- /dev/null
+++ b/Kasbot.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/Kasbot.sln b/Kasbot.sln
new file mode 100644
index 0000000..83cd07c
--- /dev/null
+++ b/Kasbot.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.4.33213.308
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kasbot", "Kasbot.csproj", "{70A0CD18-5914-4104-A1A1-C531B96FCC20}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {70A0CD18-5914-4104-A1A1-C531B96FCC20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {70A0CD18-5914-4104-A1A1-C531B96FCC20}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {70A0CD18-5914-4104-A1A1-C531B96FCC20}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {70A0CD18-5914-4104-A1A1-C531B96FCC20}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {65C82F99-E6D5-41DC-AA40-7B12A66657BF}
+ EndGlobalSection
+EndGlobal
diff --git a/Modules/PublicModule.cs b/Modules/PublicModule.cs
new file mode 100644
index 0000000..086e554
--- /dev/null
+++ b/Modules/PublicModule.cs
@@ -0,0 +1,64 @@
+using Discord;
+using Discord.Audio;
+using Discord.Commands;
+using Kasbot.Services;
+using NAudio.Wave;
+using TextCommandFramework.Services;
+using YoutubeExplode;
+
+namespace TextCommandFramework.Modules
+{
+ public class PublicModule : ModuleBase
+ {
+ public PictureService PictureService { get; set; }
+ public PlayerService PlayerService { get; set; }
+
+ [Command("ping")]
+ [Alias("pong", "hello")]
+ public Task PingAsync()
+ => ReplyAsync("pong!");
+
+ [Command("cat")]
+ public async Task CatAsync()
+ {
+ var stream = await PictureService.GetCatPictureAsync();
+ stream.Seek(0, SeekOrigin.Begin);
+ await Context.Channel.SendFileAsync(stream, "cat.png");
+ }
+
+ [Command("echo")]
+ public Task EchoAsync([Remainder] string text)
+ => ReplyAsync('\u200B' + text);
+
+ [Command("play", RunMode = RunMode.Async)]
+ public async Task PlayAsync([Remainder] string text)
+ {
+ var user = Context.User;
+ if (user.IsBot) return;
+
+ string youtubeUrl = text;
+ IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel;
+ if (channel is null)
+ {
+ await Context.Channel.SendMessageAsync("You need to be in a voice channel to use this command.");
+ return;
+ }
+
+ await PlayerService.Play(Context, text);
+ }
+
+ [Command("stop", RunMode = RunMode.Async)]
+ public async Task StopAsync()
+ {
+ var user = Context.User;
+ if (user.IsBot) return;
+
+ await PlayerService.Stop(Context);
+ }
+
+ [Command("guild_only")]
+ [RequireContext(ContextType.Guild, ErrorMessage = "Sorry, this command must be ran from within a server, not a DM!")]
+ public Task GuildOnlyCommand()
+ => ReplyAsync("Nothing to see here!");
+ }
+}
diff --git a/Program.cs b/Program.cs
new file mode 100644
index 0000000..d4a7b57
--- /dev/null
+++ b/Program.cs
@@ -0,0 +1,66 @@
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Kasbot.Services;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using TextCommandFramework.Services;
+
+namespace TextCommandFramework
+{
+ class Program
+ {
+ static void Main(string[] args)
+ {
+ new Program().MainAsync().GetAwaiter().GetResult();
+ }
+
+ public async Task MainAsync()
+ {
+ using (var services = ConfigureServices())
+ {
+ var token = Environment.GetEnvironmentVariable("TOKEN");
+
+ if (token == null)
+ {
+ throw new Exception("Discord Bot Token was not found.");
+ }
+
+ var client = services.GetRequiredService();
+
+ client.Log += LogAsync;
+ services.GetRequiredService().Log += LogAsync;
+
+ await client.LoginAsync(TokenType.Bot, token);
+ await client.StartAsync();
+
+ await services.GetRequiredService().InitializeAsync();
+
+ await Task.Delay(Timeout.Infinite);
+ }
+ }
+
+ private Task LogAsync(LogMessage log)
+ {
+ Console.WriteLine(log.ToString());
+
+ return Task.CompletedTask;
+ }
+
+ private ServiceProvider ConfigureServices()
+ {
+ return new ServiceCollection()
+ .AddSingleton(new DiscordSocketConfig
+ {
+ GatewayIntents = GatewayIntents.AllUnprivileged | GatewayIntents.MessageContent
+ })
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .BuildServiceProvider();
+ }
+ }
+}
diff --git a/Services/CommandHandlingService.cs b/Services/CommandHandlingService.cs
new file mode 100644
index 0000000..5709c27
--- /dev/null
+++ b/Services/CommandHandlingService.cs
@@ -0,0 +1,60 @@
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+
+namespace TextCommandFramework.Services
+{
+ public class CommandHandlingService
+ {
+ private readonly CommandService _commands;
+ private readonly DiscordSocketClient _discord;
+ private readonly IServiceProvider _services;
+
+ public CommandHandlingService(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;
+ var prefix = "!";
+
+ //Check if the message sent has the specified prefix
+ if (!message.HasStringPrefix(prefix, 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;
+
+ await context.Channel.SendMessageAsync($"error: {result}");
+ }
+ }
+}
diff --git a/Services/PictureService.cs b/Services/PictureService.cs
new file mode 100644
index 0000000..5c8e1dd
--- /dev/null
+++ b/Services/PictureService.cs
@@ -0,0 +1,20 @@
+using System.IO;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace TextCommandFramework.Services
+{
+ public class PictureService
+ {
+ private readonly HttpClient _http;
+
+ public PictureService(HttpClient http)
+ => _http = http;
+
+ public async Task GetCatPictureAsync()
+ {
+ var resp = await _http.GetAsync("https://cataas.com/cat");
+ return await resp.Content.ReadAsStreamAsync();
+ }
+ }
+}
diff --git a/Services/PlayerService.cs b/Services/PlayerService.cs
new file mode 100644
index 0000000..5852e6a
--- /dev/null
+++ b/Services/PlayerService.cs
@@ -0,0 +1,87 @@
+using Discord;
+using Discord.Audio;
+using Discord.Commands;
+using Discord.WebSocket;
+using NAudio.Wave;
+using YoutubeExplode;
+
+namespace Kasbot.Services
+{
+ public class PlayerService
+ {
+ public Dictionary Clients { get; set; }
+
+ public PlayerService()
+ {
+ Clients = new Dictionary();
+ }
+
+ private async Task DownloadAudioFromYoutube(string youtubeUrl)
+ {
+ var youtube = new YoutubeClient();
+ var videoId = await youtube.Search.GetVideosAsync(youtubeUrl).FirstOrDefaultAsync();
+ var streamInfoSet = await youtube.Videos.Streams.GetManifestAsync(videoId.Id);
+ var highestAudioStreamInfo = streamInfoSet.GetAudioStreams().OrderByDescending(s => s.Bitrate).FirstOrDefault();
+ var streamVideo = await youtube.Videos.Streams.GetAsync(highestAudioStreamInfo);
+ var memoryStream = new MemoryStream();
+ await streamVideo.CopyToAsync(memoryStream);
+ memoryStream.Position = 0;
+ return memoryStream;
+ }
+
+ public async Task Play(SocketCommandContext Context, string arguments)
+ {
+ IVoiceChannel channel = (Context.User as IVoiceState).VoiceChannel;
+
+ var audioStream = await DownloadAudioFromYoutube(arguments);
+ if (audioStream is null)
+ {
+ await Context.Channel.SendMessageAsync("Failed to download audio from YouTube.");
+ return;
+ }
+
+ var audioClient = await channel.ConnectAsync();
+ using (var mp3Reader = new StreamMediaFoundationReader(audioStream))
+ {
+ var audioOut = audioClient.CreatePCMStream(AudioApplication.Music);
+ await audioClient.SetSpeakingAsync(true);
+
+ var media = new Media
+ {
+ AudioClient = audioClient,
+ Message = Context.Message,
+ Name = "",
+ AudioOutStream = audioOut,
+ };
+ Clients.Add(Context.Guild, media);
+
+ await mp3Reader.CopyToAsync(audioOut);
+ await audioClient.SetSpeakingAsync(false);
+ }
+ }
+
+ public async Task Stop(SocketCommandContext Context)
+ {
+ if (!Clients.ContainsKey(Context.Guild))
+ return;
+
+ var media = Clients[Context.Guild];
+ Clients.Remove(Context.Guild);
+
+ await Context.Message.DeleteAsync();
+ await media.Message.DeleteAsync();
+ await media.AudioOutStream.DisposeAsync();
+ await media.AudioOutStream.ClearAsync(new CancellationToken());
+ await media.AudioClient.StopAsync();
+ }
+
+ }
+
+ public class Media
+ {
+ public string Name { get; set; }
+ public IAudioClient AudioClient { get; set; }
+ public AudioOutStream AudioOutStream { get; set; }
+ public SocketUserMessage Message { get; set; }
+ }
+}